package executionlog import ( "context" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" "regexp" "slices" "strings" "unicode/utf8" "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/allocationunits" "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchmetadata" "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" ) // 办理结果只允许发起人查看;通用批次日志沿用控制台的数据范围。 type ownerOnlyKey struct{} func scope(ctx context.Context) (*gdb.Model, error) { if contexts.GetUserId(ctx) <= 0 || strings.TrimSpace(contexts.GetTenantId(ctx)) == "" { return nil, gerror.New("请先登录") } return scopedModel(ctx, dao.BatchMain.Ctx(ctx)) } func scopedModel(ctx context.Context, mod *gdb.Model) (*gdb.Model, error) { user, tenant := contexts.GetUserId(ctx), strings.TrimSpace(contexts.GetTenantId(ctx)) if user <= 0 || tenant == "" { return nil, gerror.New("请先登录") } mod = mod.Where("tenant_id", tenant) if ownerOnly, _ := ctx.Value(ownerOnlyKey{}).(bool); ownerOnly { return mod.Where("created_by", user), nil } if contexts.IsSuperAdmin(ctx) { return mod, nil } // 菜单范围由认证中间件读取当前数据库后写入,优先于登录时的角色快照。 roleId := gconv.Int64(contexts.GetDataValue(ctx, "currentRoleId")) dataScope := gconv.String(contexts.GetDataValue(ctx, "currentRoleDataScope")) if roleId > 0 && dataScope != "" && len(contexts.GetRoles(ctx)) == 0 { identity := *contexts.GetUser(ctx) identity.Roles = []model.Role{{RoleId: roleId, DataScope: dataScope}} copied := *contexts.Get(ctx) copied.User = &identity ctx = contexts.Set(ctx, &copied) mod = mod.Ctx(ctx) } if len(contexts.GetRoles(ctx)) == 0 { return mod.Where("1 = 0"), nil } validScopes := []string{ consts.SysRoleDataScopeAll, consts.SysRoleDataScopeCustom, consts.SysRoleDataScopeDept, consts.SysRoleDataScopeDeptAndBelow, consts.SysRoleDataScopePersonal, consts.SysRoleDataScopeDeptAndBelowOrPersonal, consts.SysRoleDataScopeOrgAndNextLevel, consts.SysRoleDataScopeOrgNextLevel, } if roleId > 0 && dataScope != "" { if !slices.Contains(validScopes, dataScope) { return mod.Where("1 = 0"), nil } } else { for _, role := range contexts.GetRoles(ctx) { if !slices.Contains(validScopes, role.DataScope) { return mod.Where("1 = 0"), nil } } } // 先生成创建部门/创建人的过滤条件,再选择返回字段,避免 DTO 缺少权限字段导致漏过滤。 return handler.FilterAuth(mod.Fields("created_dept", "created_by")), nil } func dbError(ctx context.Context, err error) error { g.Log().Error(ctx, "execution log query failed", err) return gerror.New("执行日志查询失败,请稍后重试") } func paging(page, size int) (int, int) { if page < 1 { page = 1 } if size < 1 { size = 20 } if size > 100 { size = 100 } return page, size } func normalize(item *model.AppExecutionLogItem) { item.SourceChannel = batchmetadata.Channel(item.SourceChannel) if !batchmetadata.ValidMode(item.ExecutionMode) { item.ExecutionMode = batchmetadata.Unknown } item.FailReason = safeReason(item.FailReason) } // Do not expose internal network addresses, credentials or stack traces to mobile clients. var publicErrorCode = regexp.MustCompile("^[A-Za-z0-9_.-]{1,64}$") func safeReason(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } lower := strings.ToLower(value) for _, marker := range []string{"token", "secret", "password", "authorization", "api_key", "apikey", "http:", "https:", "rpc", "sql", "tcp", "panic", "goroutine", "stack", "/src/", "/app/", "dial ", "127.0.", "192." + "168.", "10.", "172.", "sk-", "mysql", "redis"} { if strings.Contains(lower, marker) { return "执行失败,请凭批次号联系客服核查" } } if strings.ContainsAny(value, "\r\n") || utf8.RuneCountInString(value) > 180 { return "执行失败,请凭批次号联系客服核查" } return value } func List(ctx context.Context, in *model.AppExecutionLogListParam) ([]*model.AppExecutionLogItem, int, error) { mod, err := scope(ctx) if err != nil { return nil, 0, err } if in == nil { in = &model.AppExecutionLogListParam{} } c := dao.BatchMain.Columns() mod = mod.Fields(model.AppExecutionLogItem{}) if n := strings.TrimSpace(in.BatchNo); n != "" { mod = mod.Where(c.BatchNo, n) } if in.SourceChannel != "" { mod = mod.Where(c.SourceChannel, in.SourceChannel) } if in.ExecutionMode != nil { mod = mod.Where(c.ExecutionMode, *in.ExecutionMode) } if in.SourceType != nil { mod = mod.Where(c.SourceType, *in.SourceType) } if in.BatchAsyncStatus != nil { mod = mod.Where(c.BatchAsyncStatus, *in.BatchAsyncStatus) } // Split allocation children are execution details, not a second user submission. // Allocation children are execution details of the parent submission. The // source_batch_no relation is authoritative; batch_info is only descriptive. mod = mod.Where("NOT (batch_type = ? AND COALESCE(source_batch_no, '') <> '' AND source_batch_no <> batch_no)", consts.BatchTypeAllocate) page, size := paging(in.Page, in.PageSize) items := make([]*model.AppExecutionLogItem, 0) total := 0 if err = mod.Page(page, size).OrderDesc(c.Id).ScanAndCount(&items, &total, false); err != nil { return nil, 0, dbError(ctx, err) } unitFields := []*allocationunits.Fields{} for _, item := range items { normalize(item) if item.BatchType == consts.BatchTypeAllocate { item.Fields = allocationunits.Resolve(item.UnitInfo, item.StoredSourceDeptId, item.StoredTargetDeptId) unitFields = append(unitFields, &item.Fields) } } if err := allocationunits.Fill(ctx, contexts.GetTenantId(ctx), unitFields); err != nil { return nil, 0, dbError(ctx, err) } return items, total, nil } func ensureAllocateParent(ctx context.Context, batchNo string) error { mod, err := scope(ctx) if err != nil { return err } columns := dao.BatchMain.Columns() count, err := mod.Where(columns.BatchNo, strings.TrimSpace(batchNo)).Where("(batch_type = ? OR (batch_type = ? AND JSON_EXTRACT(batch_info, '$.targetBatchType') = ?))", consts.BatchTypeAllocate, consts.BatchTypePrepare, consts.BatchTypeAllocate).Where("COALESCE(source_batch_no, '') = ''").Count() if err != nil { return dbError(ctx, err) } if count != 1 { return gerror.New("划拨批次不存在或无权查看") } return nil } func AllocateSummary(ctx context.Context, batchNo string) (*model.AppExecutionLogItem, error) { ctx = context.WithValue(ctx, ownerOnlyKey{}, true) if err := ensureAllocateParent(ctx, batchNo); err != nil { return nil, err } return Summary(ctx, batchNo) } func AllocateDetails(ctx context.Context, in *model.AppExecutionLogDetailParam) ([]*model.AppExecutionLogDetail, int, error) { ctx = context.WithValue(ctx, ownerOnlyKey{}, true) if in == nil { return nil, 0, gerror.New("批次号不能为空") } if err := ensureAllocateParent(ctx, in.BatchNo); err != nil { return nil, 0, err } return Details(ctx, in) } func Summary(ctx context.Context, batchNo string) (*model.AppExecutionLogItem, error) { mod, err := scope(ctx) if err != nil { return nil, err } if strings.TrimSpace(batchNo) == "" { return nil, gerror.New("批次号不能为空") } var item *model.AppExecutionLogItem if err = mod.Fields(model.AppExecutionLogItem{}). Where(dao.BatchMain.Columns().BatchNo, batchNo). Where("NOT (batch_type = ? AND COALESCE(source_batch_no, '') <> '' AND source_batch_no <> batch_no)", consts.BatchTypeAllocate). Scan(&item); err != nil { return nil, dbError(ctx, err) } if item == nil { return nil, gerror.New("执行日志不存在或无权查看") } normalize(item) if item.BatchType == consts.BatchTypeAllocate { item.Fields = allocationunits.Resolve(item.UnitInfo, item.StoredSourceDeptId, item.StoredTargetDeptId) if err := allocationunits.Fill(ctx, contexts.GetTenantId(ctx), []*allocationunits.Fields{&item.Fields}); err != nil { return nil, dbError(ctx, err) } } return item, nil } func Details(ctx context.Context, in *model.AppExecutionLogDetailParam) ([]*model.AppExecutionLogDetail, int, error) { if in == nil { return nil, 0, gerror.New("批次号不能为空") } parent, err := Summary(ctx, in.BatchNo) if err != nil { return nil, 0, err } c := dao.BatchDetail.Columns() mod, err := scopedModel(ctx, dao.BatchDetail.Ctx(ctx)) if err != nil { return nil, 0, err } mod = mod.Fields(model.AppExecutionLogDetail{}) if parent.BatchType == consts.BatchTypeAllocate { // 先按当前菜单的数据范围解析子批次,再用批次号索引查询明细。 var children []struct { BatchNo string `orm:"batch_no"` } mainColumns := dao.BatchMain.Columns() childrenMod, err := scope(ctx) if err != nil { return nil, 0, err } if err := childrenMod.Fields(mainColumns.BatchNo). Where(mainColumns.SourceBatchNo, in.BatchNo).Where(mainColumns.BatchType, parent.BatchType). Where(mainColumns.BatchNo+" <> ?", in.BatchNo).Scan(&children); err != nil { return nil, 0, dbError(ctx, err) } batchNos := []string{in.BatchNo} for _, child := range children { batchNos = append(batchNos, child.BatchNo) } mod = mod.WhereIn(c.BatchNo, batchNos) } else { mod = mod.Where(c.BatchNo, in.BatchNo) } if in.DetailStatus != nil { mod = mod.Where(c.DetailStatus, *in.DetailStatus) } page, size := paging(in.Page, in.PageSize) items := make([]*model.AppExecutionLogDetail, 0) total := 0 if err = mod.OrderAsc(c.Id).Page(page, size).ScanAndCount(&items, &total, false); err != nil { return nil, 0, dbError(ctx, err) } unitFields := []*allocationunits.Fields{} for index, item := range items { if parent.BatchType == consts.BatchTypeAllocate { item.Fields = allocationunits.Resolve(item.UnitInfo, parent.SourceDeptId, parent.TargetDeptId) if item.SourceDeptName == "" && item.SourceDeptId == parent.SourceDeptId { item.SourceDeptName = parent.SourceDeptName } if item.TargetDeptName == "" && item.TargetDeptId == parent.TargetDeptId { item.TargetDeptName = parent.TargetDeptName } unitFields = append(unitFields, &item.Fields) } item.RowNo = (page-1)*size + index + 1 item.ErrorReason = safeReason(item.ErrorReason) if item.ErrorCode != "" && !publicErrorCode.MatchString(item.ErrorCode) { item.ErrorCode = "EXECUTION_FAILED" } } if err := allocationunits.Fill(ctx, contexts.GetTenantId(ctx), unitFields); err != nil { return nil, 0, dbError(ctx, err) } return items, total, nil }