sync: 同步 v1.0.4 并脱敏配置与镜像

This commit is contained in:
MiFaZhan
2026-10-29 14:36:49 +08:00
parent 743e8e6e6e
commit 2206a393f7
516 changed files with 18119 additions and 2253 deletions
@@ -0,0 +1,173 @@
package exportcenter
import (
"context"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"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/batchimport"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
)
func snapshot(ctx context.Context) (*model.ExportIdentitySnapshot, error) {
user := contexts.GetUser(ctx)
if user == nil || user.ID <= 0 {
return nil, gerror.New("登录状态已失效,请重新登录")
}
snap := &model.ExportIdentitySnapshot{
UserId: user.ID,
DeptId: user.DeptId,
TenantId: user.TenantId,
Roles: user.Roles,
AccessCodeList: user.AccessCodeList,
}
if v := contexts.GetDataValue(ctx, "currentRoleId"); v != nil {
snap.CurrentRoleId = gconv.String(v)
}
if v := contexts.GetDataValue(ctx, "currentRoleDataScope"); v != nil {
snap.CurrentRoleDataScope = gconv.String(v)
}
return snap, nil
}
// Submit 提交一次导出任务:预校验行数、落任务与执行日志、投递后台执行。
func Submit(ctx context.Context, req *Request) (res *SubmitResult, err error) {
if req == nil || req.Tags == nil || req.Fetch == nil {
return nil, gerror.New("导出配置不完整")
}
if req.BizKey == "" || req.BizName == "" {
return nil, gerror.New("导出业务标识不能为空")
}
// 提交前占用执行/排队容量,避免高峰期无限创建等待执行的 goroutine。
select {
case slots <- struct{}{}:
default:
return nil, gerror.New("导出任务排队已满,请稍后重试")
}
ownershipTransferred := false
defer func() {
if !ownershipTransferred {
<-slots
}
}()
snap, err := snapshot(ctx)
if err != nil {
return nil, failSubmit(ctx, "snapshot", req.BizKey, err)
}
// 预校验:只取 1 行,拿到筛选条件下的总行数。
// 业务侧可能返回 -1 表示「提交阶段还无法确定条数」(如按号段/按公司圈卡,
// 需要后台实际查库才知道命中多少),此时放行,最终条数以实际写出为准。
// total=0 也放行:空结果同样要产出文件(至少带表头与失败原因列),
// 让用户能拿到「查了但没命中」的凭据,而不是只看到一句报错。
_, total, err := req.Fetch(ctx, 1, 1)
if err != nil {
return nil, failSubmit(ctx, "count", req.BizKey, gerror.Wrap(err, "统计导出数据失败"))
}
if total > MaxRows {
return nil, gerror.Newf("本次导出 %d 行,超过单次上限 %d 行,请缩小筛选范围", total, MaxRows)
}
// 未知条数不写入任务记录,避免 -1 出现在列表与执行日志里。
recordedTotal := total
if recordedTotal < 0 {
recordedTotal = 0
}
taskNo, err := batchimport.NextBatchNo(consts.BatchNoPrefixExport)
if err != nil {
return nil, failSubmit(ctx, "batch_no", req.BizKey, gerror.Wrap(err, "生成任务编号失败"))
}
// 列清单直接影响导出内容,留痕便于排查「导出列与页面不一致」。
if len(req.Columns) == 0 {
req.Columns = columnsFromRequest(ctx)
}
g.Log().Infof(ctx, "[exportcenter] 提交导出 biz=%s user=%d columns=%d", req.BizKey, snap.UserId, len(req.Columns))
now := time.Now()
expire := now.AddDate(0, 0, ExpireDays)
fileName := taskNo + ".xlsx"
batchInfo := gjson.New(g.Map{
"exportBizKey": req.BizKey,
"exportBizName": req.BizName,
"permCode": req.PermCode,
})
var taskId int64
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
id, insertErr := dao.TExportTask.Ctx(ctx).TX(tx).Data(g.Map{
dao.TExportTask.Columns().TaskNo: taskNo,
dao.TExportTask.Columns().BizKey: req.BizKey,
dao.TExportTask.Columns().BizName: req.BizName,
dao.TExportTask.Columns().PermCode: req.PermCode,
dao.TExportTask.Columns().QueryParams: gjson.New(req.QueryParam),
dao.TExportTask.Columns().IdentitySnapshot: gjson.New(snap),
dao.TExportTask.Columns().Status: consts.DownloadStatusPending,
dao.TExportTask.Columns().TotalCount: recordedTotal,
dao.TExportTask.Columns().FileName: fileName,
dao.TExportTask.Columns().OwnerUserId: snap.UserId,
dao.TExportTask.Columns().DeptId: snap.DeptId,
dao.TExportTask.Columns().TenantId: snap.TenantId,
dao.TExportTask.Columns().CreatedDept: snap.DeptId,
dao.TExportTask.Columns().CreatedBy: snap.UserId,
dao.TExportTask.Columns().CreatedAt: now,
dao.TExportTask.Columns().ExpireTime: expire,
}).InsertAndGetId()
if insertErr != nil {
return gerror.Wrap(insertErr, "创建导出任务失败")
}
taskId = id
_, insertErr = dao.BatchMain.Ctx(ctx).TX(tx).Data(g.Map{
dao.BatchMain.Columns().BatchNo: taskNo,
dao.BatchMain.Columns().BatchType: consts.BatchTypeExport,
dao.BatchMain.Columns().BatchStatus: consts.BatchStatusNormal,
dao.BatchMain.Columns().BatchAsyncStatus: consts.BatchAsyncStatusCreated,
dao.BatchMain.Columns().SourceChannel: batchmetadata.Console,
dao.BatchMain.Columns().ExecutionMode: batchmetadata.Bulk,
dao.BatchMain.Columns().RunResult: consts.BatchRunResultPending,
dao.BatchMain.Columns().TotalCount: recordedTotal,
dao.BatchMain.Columns().BatchInfo: batchInfo,
dao.BatchMain.Columns().TenantId: snap.TenantId,
dao.BatchMain.Columns().CreatedDept: snap.DeptId,
// dept_id 必须与 created_dept 一致。
// FilterAuth 对 batch_main 优先用 dept_id 过滤,留空(NUL)会让提交人自己也看不到
// 这条执行日志(只有全部数据权限的管理员能看到),表现为「导出了但日志里没有」。
dao.BatchMain.Columns().DeptId: snap.DeptId,
dao.BatchMain.Columns().CreatedBy: snap.UserId,
dao.BatchMain.Columns().CreatedAt: now,
}).Insert()
if insertErr != nil {
return gerror.Wrap(insertErr, "写导出执行日志失败")
}
return nil
})
if err != nil {
return nil, failSubmit(ctx, "persist", req.BizKey, err)
}
go run(taskNo, req)
ownershipTransferred = true
return &SubmitResult{TaskId: taskId, TaskNo: taskNo, Total: total}, nil
}
// failSubmit 统一记录提交失败的环节与原因。
// 框架会把未带错误码的 error 统一转成「内部服务器错误」,真实原因只能从日志看,
// 所以这里必须把 step 打出来,否则线上无法定位。
func failSubmit(ctx context.Context, step, bizKey string, err error) error {
g.Log().Errorf(ctx, "[exportcenter] 提交导出失败 step=%s biz=%s err=%+v", step, bizKey, err)
return err
}