342 lines
11 KiB
Go
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
|
|
}
|