Files
iotlcm/server/internal/logic/common/download_center.go
T
2026-10-29 14:36:49 +08:00

342 lines
11 KiB
Go

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
}