Softhub软件下载站实战开发(八):编写软件后台管理

Softhub软件下载站实战开发(八):编写软件后台管理

在上一篇我们集成了MinIO文件存储功能后,本文将深入讲解如何构建软件管理后台的核心功能模块,实现软件及其资源的高效管理。

一、软件管理模块

1. 软件实体模型
type DsSoftwareInfo struct {
	Id              uint        `orm:"id"  json:"id" description:"id"`    
	CategoryId      uint        `orm:"category_id"  json:"categoryId" description:"分类id"`          
	SoftwareName    string      `orm:"software_name"  json:"softwareName" description:"软件名称"`      
	OfficialWebsite string      `orm:"official_website"  json:"officialWebsite" description:"官网地址"` 
	Icon            string      `orm:"icon"  json:"icon" description:"图标"`                          
	Detail          string      `orm:"detail"  json:"detail" description:"软件详情"`                   
	Remark          string      `orm:"remark"  json:"remark" description:"备注"`                     
	CreatedBy       uint        `orm:"created_by"  json:"createdBy" description:"创建人"`            
	UpdatedBy       uint        `orm:"updated_by"  json:"updatedBy" description:"更新人"`             
	CreatedAt       *gtime.Time `orm:"created_at"  json:"createdAt" description:"创建时间"`            
	UpdatedAt       *gtime.Time `orm:"updated_at"  json:"updatedAt" description:"更新时间"`             
	PlatformIds     []uint      `json:"platformIds" description:"平台ID列表"`
}

2. 核心功能实现

新增软件(支持多平台)
func (s sDsSoftware) Add(ctx context.Context, req *api.DsSoftwareAddReq) (err error) {
	err = g.Try(ctx, func(ctx context.Context) {
		var software *model.DsSoftwareInfo
		dao.DsSoftware.Ctx(ctx).Where(fmt.Sprintf("%s=?", dao.DsSoftware.Columns().SoftwareName), req.SoftwareName).Scan(&software)
		if software != nil {
			liberr.ErrIsNil(ctx, fmt.Errorf("软件名称%s已经存在", req.SoftwareName))
		}

		// 查询分类是否存在
		var categoryInfo *model.DsCategoryInfo
		dao.DsCategory.Ctx(ctx).Where(fmt.Sprintf("%s=?", dao.DsCategory.Columns().Id), req.CategoryId).Scan(&categoryInfo)
		if categoryInfo == nil {
			liberr.ErrIsNil(ctx, fmt.Errorf("分类id%d不存在", req.CategoryId))
		}

		// 开启事务
		err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
			// 添加软件
			result, err := dao.DsSoftware.Ctx(ctx).Insert(do.DsSoftware{
				CategoryId:      req.CategoryId,      // 分类id
				SoftwareName:    req.SoftwareName,    // 软件名称
				OfficialWebsite: req.OfficialWebsite, // 官网地址
				Icon:            req.Icon,            // 图标
				Detail:          req.Detail,          // 软件详情
				Remark:          req.Remark,          // 备注
				CreatedBy:       SystemS.Context().GetUserId(ctx),
				UpdatedBy:       SystemS.Context().GetUserId(ctx),
			})
			liberr.ErrIsNil(ctx, err, "新增软件表失败")

			// 获取新插入的软件ID
			softwareId, err := result.LastInsertId()
			liberr.ErrIsNil(ctx, err, "获取软件ID失败")

			// 添加软件平台关系
			for _, platformId := range req.PlatformIds {
				// 查询平台是否存在
				var platformInfo *model.DsPlatformInfo
				dao.DsPlatform.Ctx(ctx).Where(fmt.Sprintf("%s=?", dao.DsPlatform.Columns().Id), platformId).Scan(&platformInfo)
				if platformInfo == nil {
					liberr.ErrIsNil(ctx, fmt.Errorf("平台id%d不存在", platformId))
				}

				// 添加平台关系
				_, err = dao.DsSoftwarePlatform.Ctx(ctx).Insert(do.DsSoftwarePlatform{
					SoftwareId: uint(softwareId), // 软件id
					PlatformId: int(platformId),  // 平台id
				})
				liberr.ErrIsNil(ctx, err, "新增软件平台关系失败")
			}

			return nil
		})
		liberr.ErrIsNil(ctx, err, "新增软件失败")
	})
	return
}
软件列表查询(带平台信息)

由于是面向个人,循环调用还能接受。实际业务中应该放到redis中查询,等功能全部完全这些细节都应该需要优化。

func (s sDsSoftware) List(ctx context.Context, req *api.DsSoftwareListReq) (total interface{}, dsSoftwareList []*model.DsSoftwareInfo, err error) {
	err = g.Try(ctx, func(ctx context.Context) {
		m := dao.DsSoftware.Ctx(ctx)
		if req.CategoryId != 0 {
			m = m.Where(dao.DsSoftware.Columns().CategoryId+" = ", req.CategoryId)
		}
		if req.SoftwareName != "" {
			// like
			m = m.Where(dao.DsSoftware.Columns().SoftwareName+" like ?", "%"+req.SoftwareName+"%")
		}
		total, err = m.Count()
		liberr.ErrIsNil(ctx, err, "获取软件列表失败")
		orderBy := req.OrderBy
		if orderBy == "" {
			orderBy = "created_at desc"
		}
		err = m.Page(req.PageNum, req.PageSize).Order(orderBy).Scan(&dsSoftwareList)
		liberr.ErrIsNil(ctx, err, "获取软件表列表失败")

		// 获取每个软件的平台ID列表
		for _, software := range dsSoftwareList {
			g.Log().Debug(ctx, "开始获取软件平台关系,软件ID:", software.Id)

			// 构建查询
			query := dao.DsSoftwarePlatform.Ctx(ctx).
				Where(dao.DsSoftwarePlatform.Columns().SoftwareId, software.Id)

			var platformRelations []*model.DsSoftwarePlatformInfo
			err = query.Scan(&platformRelations)
			if err != nil {
				g.Log().Error(ctx, "获取软件平台关系失败:", err)
				continue
			}

			g.Log().Debug(ctx, "获取到的平台关系:", gjson.MustEncodeString(platformRelations))

			// 从关系中提取平台ID
			platformIds := make([]uint, 0, len(platformRelations))
			for _, relation := range platformRelations {
				g.Log().Debug(ctx, "处理平台关系:", gjson.MustEncodeString(relation))
				platformIds = append(platformIds, uint(relation.PlatformId))
			}

			g.Log().Debug(ctx, "最终的平台ID列表:", platformIds)
			software.PlatformIds = platformIds
		}
	})
	return
}

二、软件资源管理模块

1. 资源实体模型

type DsSoftwareResourceInfo struct {
	Id            uint   `orm:"id"  json:"id"`                        
	SoftwareId    uint   `orm:"software_id"  json:"softwareId"`       
	ResourceName  string `orm:"resource_name"  json:"resourceName"`    
	OriginName    string `orm:"origin_name"  json:"originName"`      
	ResourceUrl   string `orm:"resource_url"  json:"resourceUrl"`     
	Size          string `orm:"size"  json:"size"`                    
	Md5           string `orm:"md5"  json:"md5"`                     
	Version       string `orm:"version"  json:"version"`             
	DownloadCount uint   `orm:"download_count"  json:"downloadCount"` 
	Default       uint   `orm:"default"  json:"default"`              
	Remark        string `orm:"remark"  json:"remark"`                
}

2. 软件上传

2.1 分片上传流程
用户(浏览器) 前端(Vue3) 后端(GoFrame) MinIO存储 选择大文件上传 发送初始化请求(文件名/大小/软件ID) 生成唯一UploadID 创建临时目录 返回UploadID和分片数量 分割文件(每片5MB) 发送分片数据(UploadID, 分片索引) 保存分片到临时目录 返回上传成功 loop [每个分片] 发送合并请求(UploadID, 资源信息) 合并临时分片文件 计算文件MD5 上传合并后的文件 返回存储成功 保存资源到数据库 返回资源ID和URL 显示上传成功 清理临时文件(后台任务) 用户(浏览器) 前端(Vue3) 后端(GoFrame) MinIO存储
2.2 分片上传优势
  1. 大文件支持
    • 突破单文件上传大小限制
    • 支持GB级超大文件上传
  2. 断点续传
上传中断
查询已上传分片
继续上传剩余分片
合并完整文件
  1. 网络优化
    • 并行上传多个分片(加速上传)
    • 失败分片单独重试(避免整体重传)
2.3 分片上传后端实现

1.初始化分片上传

func (s *sDsSoftwareResource) InitChunkUpload(ctx context.Context, req *api.ChunkInitReq) (res *api.ChunkInitRes, err error) {
	err = g.Try(ctx, func(ctx context.Context) {
		// 检查软件是否存在
		software, err := dao.DsSoftware.Ctx(ctx).Where(dao.DsSoftware.Columns().Id, req.SoftwareId).One()
		liberr.ErrIsNil(ctx, err, "获取软件信息失败")
		if software == nil {
			liberr.ErrIsNil(ctx, fmt.Errorf("软件不存在"))
		}

		// 生成上传ID
		uploadId := gconv.String(gtime.TimestampNano())

		// 创建临时目录
		tempDir := gfile.Join(gfile.Temp(), "upload", fmt.Sprintf("%d_%s", req.SoftwareId, uploadId))
		if err := gfile.Mkdir(tempDir); err != nil {
			liberr.ErrIsNil(ctx, err, "创建临时目录失败")
		}

		res = &api.ChunkInitRes{
			UploadId: uploadId,
		}
	})
	return
}

2.上传分片

func (s *sDsSoftwareResource) UploadChunk(ctx context.Context, req *api.ChunkUploadReq) (res *api.ChunkUploadRes, err error) {
	err = g.Try(ctx, func(ctx context.Context) {
		// 验证上传会话
		tempDir := gfile.Join(gfile.Temp(), "upload", fmt.Sprintf("%d_%s", req.SoftwareId, req.UploadId))
		if !gfile.Exists(tempDir) {
			liberr.ErrIsNil(ctx, fmt.Errorf("无效的上传会话"))
		}

		// 保存分片
		chunkFile := gfile.Join(tempDir, fmt.Sprintf("chunk_%d", req.ChunkIndex))
		file, err := req.File.Open()
		if err != nil {
			liberr.ErrIsNil(ctx, err, "打开分片文件失败")
		}
		defer file.Close()

		// 读取文件内容
		fileBytes, err := io.ReadAll(file)
		if err != nil {
			liberr.ErrIsNil(ctx, err, "读取分片文件失败")
		}

		// 保存分片
		if err := gfile.PutBytes(chunkFile, fileBytes); err != nil {
			liberr.ErrIsNil(ctx, err, "保存分片失败")
		}

		res = &api.ChunkUploadRes{
			Success: true,
		}
	})
	return
}

3.合并分片

// 合并分片
func (s *sDsSoftwareResource) MergeChunks(ctx context.Context, req *api.ChunkMergeReq) (res *api.ChunkMergeRes, err error) {
	err = g.Try(ctx, func(ctx context.Context) {
		// 验证上传会话
		tempDir := gfile.Join(gfile.Temp(), "upload", fmt.Sprintf("%d_%s", req.SoftwareId, req.UploadId))
		g.Log().Debug(ctx, "临时目录路径:", tempDir)
		if !gfile.Exists(tempDir) {
			liberr.ErrIsNil(ctx, fmt.Errorf("无效的上传会话"))
		}

		// 获取分片文件列表
		chunkFiles, err := gfile.ScanDir(tempDir, "chunk_*")
		liberr.ErrIsNil(ctx, err, "获取分片文件列表失败")
		g.Log().Debug(ctx, "分片文件列表:", chunkFiles)

		// 合并分片
		mergedFile := gfile.Join(tempDir, "merged")
		g.Log().Debug(ctx, "合并文件路径:", mergedFile)
		outFile, err := gfile.OpenFile(mergedFile, os.O_CREATE|os.O_WRONLY, 0644)
		liberr.ErrIsNil(ctx, err, "创建合并文件失败")

		for _, chunkFile := range chunkFiles {
			chunkData := gfile.GetBytes(chunkFile)
			if _, err := outFile.Write(chunkData); err != nil {
				liberr.ErrIsNil(ctx, err, "写入合并文件失败")
			}
		}

		// 关闭写入文件句柄
		outFile.Close()

		// 计算MD5
		md5, err := gmd5.EncryptFile(mergedFile)
		liberr.ErrIsNil(ctx, err, "计算MD5失败")

		// 上传到MinIO
		drive := storage.MinioDrive{}
		// 构建存储路径:software/year/month/day/md5.softhub
		now := gtime.Now()
		objectName := fmt.Sprintf("software/%d/%02d/%02d/%s.softhub",
			now.Year(),
			now.Month(),
			now.Day(),
			md5)
		g.Log().Debug(ctx, "MinIO对象名称:", objectName)

		// 获取文件信息
		fileInfo, err := os.Stat(mergedFile)
		liberr.ErrIsNil(ctx, err, "获取文件信息失败")
		g.Log().Debug(ctx, "文件信息:", map[string]interface{}{
			"name": fileInfo.Name(),
			"size": fileInfo.Size(),
			"mode": fileInfo.Mode(),
		})

		// 打开文件用于上传
		file, err := os.Open(mergedFile)
		liberr.ErrIsNil(ctx, err, "打开文件失败")

		// 直接使用MinIO客户端上传
		client, err := drive.GetClient()
		liberr.ErrIsNil(ctx, err, "获取MinIO客户端失败")

		opts := minio.PutObjectOptions{
			ContentType: "application/octet-stream",
		}

		_, err = client.PutObject(ctx, config.MINIO_BUCKET, objectName, file, fileInfo.Size(), opts)
		liberr.ErrIsNil(ctx, err, "上传到MinIO失败")

		// 保存资源信息
		_, err = dao.DsSoftwareResource.Ctx(ctx).Insert(do.DsSoftwareResource{
			SoftwareId:    req.SoftwareId,                // 软件id
			ResourceName:  req.ResourceName,              // 软件名称
			OriginName:    req.ResourceName,              // 原始名称
			Version:       req.Version,                   // 版本
			Md5:           md5,                           // md5
			DownloadCount: 0,                             // 下载次数
			Size:          gconv.String(fileInfo.Size()), // 大小
			ResourceUrl:   objectName,                    // 资源路径
			Default:       false,                         // 是否默认
			Remark:        req.Remark,                    // 备注
			CreatedBy:     SystemS.Context().GetUserId(ctx),
			UpdatedBy:     SystemS.Context().GetUserId(ctx),
		})
		liberr.ErrIsNil(ctx, err, "保存资源信息失败")

		// 获取新创建的资源ID
		var resource *model.DsSoftwareResourceInfo
		err = dao.DsSoftwareResource.Ctx(ctx).Where(dao.DsSoftwareResource.Columns().ResourceName, req.ResourceName).Scan(&resource)
		liberr.ErrIsNil(ctx, err, "获取资源信息失败")

		res = &api.ChunkMergeRes{
			ResourceId: resource.Id,
			FileUrl:    fmt.Sprintf("/minio/software/%s", objectName),
		}
		outFile.Close()
		file.Close()
		if err := os.RemoveAll(tempDir); err != nil {
			g.Log().Error(ctx, "强制删除临时目录失败:", tempDir, err)
		}
	})
	return
}

3.资源下载

func (c *dsSoftwareResourceController) Download(ctx context.Context, req *api.DsSoftwareResourceDownloadReq) (res *api.DsSoftwareResourceDownloadRes, err error) {
	res = new(api.DsSoftwareResourceDownloadRes)
	info, err := service.DsSoftwareResource().GetById(ctx, req.Id)
	if err != nil {
		return
	}
	resourceUrl := info.ResourceUrl
	fileName := info.ResourceName
	drive := storage.MinioDrive{}
	client, _ := drive.GetClient()
	// 获取对象
	obj, err := client.GetObject(ctx, config.MINIO_BUCKET, resourceUrl, minio.GetObjectOptions{})
	if err != nil {
		liberr.ErrIsNil(ctx, err, err.Error())
	}
	defer obj.Close()
	// count+1
	_, err = service.DsSoftwareResource().AddCount(ctx, req.Id)

	writer := g.RequestFromCtx(ctx).Response.ResponseWriter
	writer.Header().Set("access-control-expose-headers", "Content-Disposition")
	writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename="+fileName))
	// 流式传输对象到响应
	_, err = io.Copy(writer, obj)
	return
}

五、总结

通过本篇文章,我们实现了:

✅ 完整的软件生命周期管理
✅ 多平台关联支持
✅ 大文件分片上传技术
✅ 资源下载功能

软件管理
新增软件
编辑软件
删除软件
软件列表
资源管理
上传资源
版本管理
下载统计
设置默认版本

softhub系列往期文章

  1. Softhub软件下载站实战开发(一):项目总览
  2. Softhub软件下载站实战开发(二):项目基础框架搭建
  3. Softhub软件下载站实战开发(三):平台管理模块实战
  4. Softhub软件下载站实战开发(四):代码生成器设计与实现
  5. Softhub软件下载站实战开发(五):分类模块实现
  6. Softhub软件下载站实战开发(六):软件配置面板实现
  7. Softhub软件下载站实战开发(七):集成MinIO实现文件存储功能

你可能感兴趣的:(softHub,go)