sync: 同步 v1.0.4 并脱敏配置与镜像
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user