package common import ( "context" "strconv" "strings" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/gtime" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/exportcenter" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" ) type sCommonDownloadCenter struct{} func init() { service.RegisterCommonDownloadCenter(NewCommonDownloadCenter()) } func NewCommonDownloadCenter() *sCommonDownloadCenter { return &sCommonDownloadCenter{} } // List 下载中心列表:K2 仅本人 + K1 业务权限过滤 func (s *sCommonDownloadCenter) List(ctx context.Context, in *model.DownloadCenterListParam) (list []*model.DownloadCenterListModel, totalCount int, err error) { user := contexts.GetUser(ctx) if user == nil || user.ID <= 0 { return nil, 0, gerror.New("登录状态已失效,请重新登录") } mod := dao.TExportTask.Ctx(ctx). Fields(model.DownloadCenterListModel{}). Where(dao.TExportTask.Columns().OwnerUserId, user.ID) mod = applyExportPermissionFilter(ctx, mod) if keyword := strings.TrimSpace(in.Keyword); keyword != "" { like := "%" + keyword + "%" mod = mod.Where( "("+dao.TExportTask.Columns().TaskNo+" LIKE ? OR "+ dao.TExportTask.Columns().FileName+" LIKE ? OR "+ dao.TExportTask.Columns().BizName+" LIKE ?)", like, like, like, ) } if in.BizKey != "" { mod = mod.Where(dao.TExportTask.Columns().BizKey, in.BizKey) } if statusIds := parseStatusIds(in.Status); len(statusIds) > 0 { mod = mod.WhereIn(dao.TExportTask.Columns().Status, statusIds) } if len(in.CreatedAt) == 2 { in.CreatedAt[1] = in.CreatedAt[1].EndOfDay() mod = mod.WhereBetween(dao.TExportTask.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) } err = mod.Page(in.Page, in.PageSize). OrderDesc(dao.TExportTask.Columns().Id). ScanAndCount(&list, &totalCount, false) if err != nil { return nil, 0, gerror.Wrap(err, "获取导出任务列表失败,请稍后重试") } if list == nil { list = []*model.DownloadCenterListModel{} } return } // Recent 右上角全局入口:最近任务与进行中/已完成数量 func (s *sCommonDownloadCenter) Recent(ctx context.Context, in *model.DownloadCenterRecentParam) (res *model.DownloadCenterRecentModel, err error) { user := contexts.GetUser(ctx) if user == nil || user.ID <= 0 { return nil, gerror.New("登录状态已失效,请重新登录") } limit := in.Limit if limit <= 0 || limit > 20 { limit = 5 } items := make([]*model.DownloadCenterListModel, 0, limit) mod := dao.TExportTask.Ctx(ctx). WhereNull(dao.TExportTask.Columns().DeletedTime). Fields(model.DownloadCenterListModel{}). Where(dao.TExportTask.Columns().OwnerUserId, user.ID) mod = applyExportPermissionFilter(ctx, mod) if err = mod.OrderDesc(dao.TExportTask.Columns().Id).Limit(limit).Scan(&items); err != nil { return nil, gerror.Wrap(err, "获取最近导出任务失败") } running, err := s.countByStatus(ctx, user.ID, []int{consts.DownloadStatusPending, consts.DownloadStatusRunning}) if err != nil { return nil, err } finished, err := s.countByStatus(ctx, user.ID, []int{consts.DownloadStatusGenerated}) if err != nil { return nil, err } return &model.DownloadCenterRecentModel{ Items: items, RunningCount: running, FinishedCount: finished, }, nil } func (s *sCommonDownloadCenter) countByStatus(ctx context.Context, userId int64, status []int) (int, error) { mod := dao.TExportTask.Ctx(ctx). WhereNull(dao.TExportTask.Columns().DeletedTime). Where(dao.TExportTask.Columns().OwnerUserId, userId). WhereIn(dao.TExportTask.Columns().Status, status) mod = applyExportPermissionFilter(ctx, mod) count, err := mod.Count() if err != nil { return 0, gerror.Wrap(err, "统计导出任务数量失败") } return count, nil } // Download 下载导出文件:重新校验 K1+K2,然后直接写文件流 func (s *sCommonDownloadCenter) Download(ctx context.Context, in *model.DownloadCenterTaskParam) (err error) { task, err := s.loadOwnedTask(ctx, in.TaskId) if err != nil { return err } if !hasExportPermission(ctx, task.PermCode) { return gerror.New("你的导出权限已变更,无法下载该文件") } if task.DeletedTime != nil { return gerror.New("文件已删除或已过期清理,请重新导出") } if task.Status != consts.DownloadStatusGenerated { return gerror.New("文件尚未生成完成,请稍后再试") } if task.ExpireTime == nil || !task.ExpireTime.After(gtime.Now()) { return gerror.New("文件已过期,请重新导出") } if task.OssId <= 0 { return gerror.New("文件不存在") } oss := &entity.SysOss{} if err = dao.SysOss.Ctx(ctx).Where(dao.SysOss.Columns().OssId, task.OssId).Scan(oss); err != nil { return gerror.Wrap(err, "获取文件信息失败") } if oss.OssId <= 0 { return gerror.New("文件不存在或已被清理") } return service.SysOss().DownloadLocal(ctx, oss, func() error { columns := dao.TExportTask.Columns() // 先计算有效期再递增次数,避免 MySQL 从左到右赋值影响首次下载判断。 affected, updateErr := dao.TExportTask.Ctx(ctx). Data("expire_time = CASE WHEN download_count = 0 THEN LEAST(expire_time, DATE_ADD(NOW(), INTERVAL 1 HOUR)) ELSE expire_time END, last_download_time = NOW(), download_count = download_count + 1"). Where(columns.Id, task.Id). WhereNull(columns.DeletedTime). Where(columns.OwnerUserId, task.OwnerUserId). Where(columns.Status, consts.DownloadStatusGenerated). Where(columns.OssId, oss.OssId). Where(columns.ExpireTime + " > NOW()"). UpdateAndGetAffected() if updateErr != nil { return gerror.Wrap(updateErr, "更新下载有效期失败,请稍后重试") } if affected != 1 { return gerror.New("文件已过期或状态已变更,请刷新后重试") } return nil }) } // Remove 物理清理文件与OSS记录,软删除并保留导出任务历史 func (s *sCommonDownloadCenter) Remove(ctx context.Context, in *model.DownloadCenterTaskParam) (err error) { task, err := s.loadOwnedTask(ctx, in.TaskId) if err != nil { return err } if task.Status == consts.DownloadStatusRunning { return gerror.New("文件正在生成中,暂时不能删除") } if !hasExportPermission(ctx, task.PermCode) { return gerror.New("你的导出权限已变更,无法删除该任务") } userId := contexts.GetUserId(ctx) _, err = exportcenter.RemoveTaskFile(ctx, task.Id, "user", func(locked *entity.TExportTask) error { if locked.OwnerUserId != userId { return gerror.New("无权访问该导出任务") } if locked.Status == consts.DownloadStatusRunning { return gerror.New("任务已开始生成,暂时不能删除") } if !hasExportPermission(ctx, locked.PermCode) { return gerror.New("你的导出权限已变更,无法删除该任务") } return nil }) if err != nil { return gerror.Wrap(err, "删除导出任务失败") } return nil } // loadOwnedTask 取任务并校验归属(K2:仅本人) func (s *sCommonDownloadCenter) loadOwnedTask(ctx context.Context, taskId int64) (*entity.TExportTask, error) { if taskId <= 0 { return nil, gerror.New("任务ID不能为空") } task := &entity.TExportTask{} if err := dao.TExportTask.Ctx(ctx). Where(dao.TExportTask.Columns().Id, taskId). Scan(task); err != nil { return nil, gerror.Wrap(err, "获取导出任务失败") } if task.Id <= 0 { return nil, gerror.New("导出任务不存在") } user := contexts.GetUser(ctx) if user == nil || user.ID <= 0 { return nil, gerror.New("登录状态已失效,请重新登录") } if task.OwnerUserId != user.ID { return nil, gerror.New("无权访问该导出任务") } return task, nil } // applyExportPermissionFilter K1:只显示「我仍持有对应导出权限」的任务 func applyExportPermissionFilter(ctx context.Context, mod *gdb.Model) *gdb.Model { if contexts.IsSuperAdmin(ctx) { return mod } codes := exportPermissionCodes(contexts.GetAccessCodeList(ctx)) if len(codes) == 0 { return mod.Where("1 = 0") } exact := make([]string, 0, len(codes)) prefixes := make([]string, 0, len(codes)) for _, code := range codes { if strings.HasSuffix(code, "*") { prefixes = append(prefixes, strings.TrimSuffix(code, "*")) continue } exact = append(exact, code) } conditions := make([]string, 0, len(exact)+len(prefixes)) args := make([]interface{}, 0, len(exact)+len(prefixes)) column := dao.TExportTask.Columns().PermCode if len(exact) > 0 { conditions = append(conditions, column+" IN (?)") args = append(args, exact) } for _, prefix := range prefixes { conditions = append(conditions, column+" LIKE ?") args = append(args, prefix+"%") } if len(conditions) == 0 { return mod.Where("1 = 0") } return mod.Where("("+strings.Join(conditions, " OR ")+")", args...) } // hasExportPermission 下载/删除时的二次校验 func hasExportPermission(ctx context.Context, required string) bool { if contexts.IsSuperAdmin(ctx) { return true } if strings.TrimSpace(required) == "" { // 提交时接口未声明权限码时,退化为只校验归属,避免历史任务全部不可用 return true } for _, code := range contexts.GetAccessCodeList(ctx) { if matchExportCode(code, required) { return true } } return false } func matchExportCode(pattern, code string) bool { if pattern == code { return true } if strings.HasSuffix(pattern, "*") { return strings.HasPrefix(code, strings.TrimSuffix(pattern, "*")) } return false } // exportPermissionCodes 取「可用于判断导出任务可见性」的权限码。 // // 历史实现只认 :export 结尾,但导出任务的 perm_code 是提交时声明的权限码, // 不同业务可能声明为 :view / :download / :list(例如卡基础信息查询、月历史用量查询 // 把查询与导出合并成一个 :view 码)。只认 :export 会让这些任务对提交人自己也不可见。 // // 放宽到常见后缀不影响安全:真正下载时 hasExportPermission 会拿任务记录的 perm_code // 再校验一次,用户失去权限后即使看得到记录也拿不到文件。 func exportPermissionCodes(list []string) []string { suffixes := []string{":export", ":view", ":download", ":list"} out := make([]string, 0, len(list)) for _, code := range list { if strings.HasSuffix(code, "*") { out = append(out, code) continue } for _, suffix := range suffixes { if strings.HasSuffix(code, suffix) { out = append(out, code) break } } } return out } // parseStatusIds 解析逗号分隔的状态筛选 func parseStatusIds(raw string) []int { raw = strings.TrimSpace(raw) if raw == "" { return nil } ids := make([]int, 0, 4) for _, part := range strings.Split(raw, ",") { part = strings.TrimSpace(part) if part == "" { continue } if v, err := strconv.Atoi(part); err == nil { ids = append(ids, v) } } return ids }