package common import ( "context" "database/sql" "errors" "fmt" "path/filepath" "strconv" "strings" "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/allocationunits" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rabbitmq" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook" "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" "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/os/gtime" ) type sCommonBatchExecutionLog struct{} type batchExecutionLogSummaryBatch struct { CreatedDept int64 `orm:"created_dept"` TargetDeptId int64 `orm:"target_dept_id"` SourceChannel string `orm:"source_channel"` ExecutionMode int `orm:"execution_mode"` Id int64 `orm:"id"` BatchNo string `orm:"batch_no"` BatchType int `orm:"batch_type"` SourceBatchNo string `orm:"source_batch_no"` SourceType *int `orm:"source_type"` BatchAsyncStatus int `orm:"batch_async_status"` RunResult int `orm:"run_result"` FailReason string `orm:"fail_reason"` TotalCount int `orm:"total_count"` SourceOssId int64 `orm:"source_oss_id"` ResultOssId int64 `orm:"result_oss_id"` BatchInfo *gjson.Json `orm:"batch_info"` StartTime *gtime.Time `orm:"start_time"` EndTime *gtime.Time `orm:"end_time"` } func NewCommonBatchExecutionLog() *sCommonBatchExecutionLog { return &sCommonBatchExecutionLog{} } func init() { service.RegisterCommonBatchExecutionLog(NewCommonBatchExecutionLog()) } func (s *sCommonBatchExecutionLog) batchModel(ctx context.Context, option ...*handler.Option) *gdb.Model { if len(option) == 0 { option = append(option, &handler.Option{ FilterTenant: true, FilterAuth: true, }) } return handler.Model(dao.BatchMain.Ctx(ctx), option...) } func (s *sCommonBatchExecutionLog) detailModel(ctx context.Context, option ...*handler.Option) *gdb.Model { if len(option) == 0 { option = append(option, &handler.Option{ FilterTenant: true, FilterAuth: true, }) } return handler.Model(dao.BatchDetail.Ctx(ctx), option...) } func (s *sCommonBatchExecutionLog) downloadModel(ctx context.Context, option ...*handler.Option) *gdb.Model { if len(option) == 0 { option = append(option, &handler.Option{ FilterTenant: true, FilterAuth: false, }) } return handler.Model(dao.TResourceBatchDownload.Ctx(ctx), option...) } func (s *sCommonBatchExecutionLog) ossModel(ctx context.Context, option ...*handler.Option) *gdb.Model { if len(option) == 0 { option = append(option, &handler.Option{ FilterTenant: true, FilterAuth: false, }) } return handler.Model(dao.SysOss.Ctx(ctx), option...) } // exportTaskModel 导出任务表;导出批次的结果文件只登记在这里。 func (s *sCommonBatchExecutionLog) exportTaskModel(ctx context.Context, option ...*handler.Option) *gdb.Model { if len(option) == 0 { option = append(option, &handler.Option{ FilterTenant: true, FilterAuth: false, }) } return handler.Model(dao.TExportTask.Ctx(ctx), option...) } func (s *sCommonBatchExecutionLog) List(ctx context.Context, in *model.BatchExecutionLogListParam) (list []*model.BatchExecutionLogListModel, totalCount int, err error) { mod := s.batchModel(ctx). Fields(model.BatchExecutionLogListModel{}) if in.BatchNo != "" { mod = mod.Where(dao.BatchMain.Columns().BatchNo+" LIKE ?", "%"+in.BatchNo+"%") } if keyword := strings.TrimSpace(in.Keyword); keyword != "" { like := "%" + keyword + "%" mod = mod.Where( "("+dao.BatchMain.Columns().BatchNo+" LIKE ? OR "+dao.BatchMain.Columns().OrderSn+" LIKE ? OR "+dao.BatchMain.Columns().FailReason+" LIKE ?)", like, like, like, ) } if in.BatchAsyncStatus != nil { mod = mod.Where(dao.BatchMain.Columns().BatchAsyncStatus, *in.BatchAsyncStatus) } if in.SourceType != nil { mod = mod.Where(dao.BatchMain.Columns().SourceType, *in.SourceType) } if in.SourceChannel != "" { mod = mod.Where(dao.BatchMain.Columns().SourceChannel, in.SourceChannel) } if in.ExecutionMode != nil { mod = mod.Where(dao.BatchMain.Columns().ExecutionMode, *in.ExecutionMode) } mod = mod.Where( fmt.Sprintf("NOT (%s = ? AND COALESCE(%s, '') <> '' AND %s <> %s)", dao.BatchMain.Columns().BatchType, dao.BatchMain.Columns().SourceBatchNo, dao.BatchMain.Columns().SourceBatchNo, dao.BatchMain.Columns().BatchNo), consts.BatchTypeAllocate, ) mod = executionLogBatchTypeModel(mod, dao.BatchMain.Columns().BatchType, dao.BatchMain.Columns().BatchInfo, in.BatchType) if len(in.CreatedAt) == 2 { in.CreatedAt[1] = in.CreatedAt[1].EndOfDay() mod = mod.WhereBetween(dao.BatchMain.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) } mod = mod.Page(in.Page, in.PageSize). OrderDesc(dao.BatchMain.Columns().Id). Hook(hook.MemberSummary) if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { err = gerror.Wrap(err, "获取执行日志列表失败,请稍后重试!") return } fields := []*allocationunits.Fields{} for _, item := range list { item.UnknownCount = batchExecutionLogUnknownCount(item.UnitInfo) item.RestartPhase, item.RestartResumeAt = batchRestartPhase(item.UnitInfo) if item.BatchType == consts.BatchTypeAllocate { item.Fields = allocationunits.Resolve(item.UnitInfo, item.CreatedDept, item.StoredTargetDeptId) fields = append(fields, &item.Fields) } } if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), fields); err != nil { return nil, 0, err } return } func (s *sCommonBatchExecutionLog) Summary(ctx context.Context, in *model.BatchExecutionLogDetailParam) (res *model.BatchExecutionLogDetailModel, err error) { if in == nil || in.BatchNo == "" { return nil, gerror.New("批次号不能为空") } batch := &batchExecutionLogSummaryBatch{} mod := executionLogBatchTypeModel( s.batchModel(ctx).Where(dao.BatchMain.Columns().BatchNo, in.BatchNo), dao.BatchMain.Columns().BatchType, dao.BatchMain.Columns().BatchInfo, nil, ) if err = mod.Scan(batch); err != nil { return nil, gerror.Wrap(err, "获取执行日志详情失败,请稍后重试!") } if batch.Id == 0 { return nil, gerror.New("执行日志不存在") } if isAllocateChildBatch(batch.BatchType, batch.BatchNo, batch.SourceBatchNo) { return nil, gerror.New("执行日志不存在") } res = &model.BatchExecutionLogDetailModel{ SourceChannel: batch.SourceChannel, ExecutionMode: batch.ExecutionMode, Id: batch.Id, BatchNo: batch.BatchNo, BatchType: batch.BatchType, SourceType: batch.SourceType, BatchAsyncStatus: batch.BatchAsyncStatus, RunResult: batch.RunResult, FailReason: batch.FailReason, TotalCount: batch.TotalCount, UnknownCount: batchExecutionLogUnknownCount(batch.BatchInfo), StartTime: batch.StartTime, EndTime: batch.EndTime, } res.RestartPhase, res.RestartResumeAt = batchRestartPhase(batch.BatchInfo) if batch.BatchType == consts.BatchTypeAllocate { res.Fields = allocationunits.Resolve(batch.BatchInfo, batch.CreatedDept, batch.TargetDeptId) if err := allocationunits.Fill(ctx, contexts.GetTenantId(ctx), []*allocationunits.Fields{&res.Fields}); err != nil { return nil, err } } if sourceFile, err := s.getSourceFile(ctx, batch.SourceOssId, batch.BatchInfo); err != nil { return nil, err } else { res.SourceFile = sourceFile } // 导出任务的成品文件落在 t_export_task,不写 t_resource_batch_download,须分开取。 var resultFile *model.BatchExecutionLogResultFileModel if batch.BatchType == consts.BatchTypeExport { resultFile, err = s.getExportResultFile(ctx, batch.BatchNo, batch.ResultOssId) } else { resultFile, err = s.getResultFile(ctx, batch.BatchNo, batch.ResultOssId, batch.BatchInfo) } if err != nil { return nil, err } res.ResultFile = resultFile return res, nil } func (s *sCommonBatchExecutionLog) DetailList(ctx context.Context, in *model.BatchDetailListParam) (list []*model.BatchDetailListModel, totalCount int, err error) { if in == nil || in.BatchNo == "" { return nil, 0, gerror.New("批次号不能为空") } batch, err := s.getDetailBatch(ctx, in.BatchNo, in.BatchType) if err != nil { return nil, 0, err } columns := dao.BatchDetail.Columns() detailBatchType := batch.BatchType mod := s.detailModel(ctx). Fields( columns.Id, columns.BatchNo, columns.BatchType, columns.DetailStatus, columns.RowNo, columns.ItemId, columns.ErrorReason, columns.Snapshot, columns.TenantId, columns.CreatedDept, columns.CreatedBy, ). Where(columns.BatchType, detailBatchType) if isAllocateParentBatch(batch) { childBatches := s.batchModel(ctx). Fields(dao.BatchMain.Columns().BatchNo). Where(dao.BatchMain.Columns().BatchType, consts.BatchTypeAllocate). Where(dao.BatchMain.Columns().SourceBatchNo, batch.BatchNo). Where(dao.BatchMain.Columns().SourceBatchNo + " <> " + dao.BatchMain.Columns().BatchNo) mod = mod.Where("("+columns.BatchNo+" = ? OR "+columns.BatchNo+" IN (?))", batch.BatchNo, childBatches) } else { mod = mod.Where(columns.BatchNo, batch.BatchNo) } if in.DetailStatus != nil { mod = mod.Where(columns.DetailStatus, *in.DetailStatus) } if in.Iccid != "" { mod = mod.Where(columns.ItemId+" LIKE ?", "%"+in.Iccid+"%") } if keyword := strings.TrimSpace(in.Keyword); keyword != "" { like := "%" + keyword + "%" mod = mod.Where("("+columns.ItemId+" LIKE ? OR "+columns.ErrorReason+" LIKE ?)", like, like) } var details []*entity.BatchDetail mod = orderBatchCards(mod.Page(in.Page, in.PageSize), batch.BatchType) if err = mod.ScanAndCount(&details, &totalCount, false); err != nil { return nil, 0, gerror.Wrap(err, "获取批次执行明细列表失败,请稍后重试") } list = make([]*model.BatchDetailListModel, 0, len(details)) displayRowStart := 0 if in.Page > 0 && in.PageSize > 0 { displayRowStart = (in.Page - 1) * in.PageSize } for index, detail := range details { iccid, msisdn, imsi := batchExecutionLogDetailSnapshotCards(detail) item := &model.BatchDetailListModel{ RowNo: displayRowStart + index + 1, Iccid: iccid, Msisdn: msisdn, Imsi: imsi, Imei: batchExecutionLogSnapshotValue(detail, "IMEI", "imei", "rawFields.IMEI"), ChanKey: batchExecutionLogSnapshotValue(detail, "chanKey", "chan_key", "rawFields.chanKey", "rawFields.chan_key", "rawFields.通道编号", "rawFields.通道编码", "rawFields.通道编号*"), CardType: batchExecutionLogSnapshotValue(detail, "cardType", "rawFields.cardType", "rawFields.卡类型"), UpPkgCode: batchExecutionLogSnapshotValue(detail, "upPkgCode", "rawFields.upPkgCode", "rawFields.上游套餐编码", "rawFields.上游套餐"), SuppliersCode: batchExecutionLogSnapshotValue(detail, "suppliersCode", "rawFields.suppliersCode", "rawFields.供应商编码", "rawFields.供应商"), UpstreamActivationTime: batchExecutionLogSnapshotValue(detail, "upstreamActivationTime", "rawFields.upstreamActivationTime", "rawFields.上游激活时间", "rawFields.上游给出激活时间"), InsertToPlugEnabled: batchExecutionLogSnapshotValue(detail, "insertToPlugEnabled", "rawFields.insertToPlugEnabled", "rawFields.是否贴片转插拔"), CarrierTrafficCutoffEnabled: batchExecutionLogSnapshotValue(detail, "carrierTrafficCutoffEnabled", "rawFields.carrierTrafficCutoffEnabled", "rawFields.是否运营商达量断网"), AgreementExpiryDate: batchExecutionLogSnapshotValue(detail, "agreementExpiryDate", "rawFields.agreementExpiryDate", "rawFields.协议到期时间", "rawFields.协议期到期时间"), HasTestPeriod: batchExecutionLogSnapshotValue(detail, "hasTestPeriod", "rawFields.hasTestPeriod", "rawFields.是否有测试期"), TestPeriodDays: batchExecutionLogSnapshotValue(detail, "testPeriodDays", "rawFields.testPeriodDays", "rawFields.测试期天数"), TestPeriodLatestExpiryDate: batchExecutionLogSnapshotValue(detail, "testPeriodLatestExpiryDate", "rawFields.testPeriodLatestExpiryDate", "rawFields.测试期最晚过期日期"), HasSilentPeriod: batchExecutionLogSnapshotValue(detail, "hasSilentPeriod", "rawFields.hasSilentPeriod", "rawFields.是否有沉默期"), SilentPeriodDays: batchExecutionLogSnapshotValue(detail, "silentPeriodDays", "rawFields.silentPeriodDays", "rawFields.沉默期天数"), SilentPeriodLatestExpiryDate: batchExecutionLogSnapshotValue(detail, "silentPeriodLatestExpiryDate", "rawFields.silentPeriodLatestExpiryDate", "rawFields.沉默期最晚过期日期"), DetailStatus: detail.DetailStatus, } if batch.BatchType == consts.BatchTypeAllocate { parentUnits := allocationunits.Resolve(batch.BatchInfo, batch.CreatedDept, batch.TargetDeptId) item.Fields = allocationunits.Resolve(detail.Snapshot, parentUnits.SourceDeptId, parentUnits.TargetDeptId) if item.SourceDeptName == "" && item.SourceDeptId == parentUnits.SourceDeptId { item.SourceDeptName = parentUnits.SourceDeptName } if item.TargetDeptName == "" && item.TargetDeptId == parentUnits.TargetDeptId { item.TargetDeptName = parentUnits.TargetDeptName } } if detail.ErrorReason != "" { item.Error = &model.BatchDetailErrorModel{ Reason: detail.ErrorReason, } } list = append(list, item) } fields := []*allocationunits.Fields{} for _, item := range list { if batch.BatchType == consts.BatchTypeAllocate { fields = append(fields, &item.Fields) } } if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), fields); err != nil { return nil, 0, err } return } // dataReviewQueryMessage 通知 Worker 对既有 operationId 做一次只读补查。 type dataReviewQueryMessage struct { BatchNo string `json:"batchNo"` RequestedBy string `json:"requestedBy,omitempty"` } var publishDataReviewQuery = rabbitmq.Publish // SyncReview 手动同步待核查批次的上游执行结果。 // 只重查既有受理号(jobId),不重发办理请求、不清零自动补查计数; // 上游仍无结论的明细继续保持待核查。 func (s *sCommonBatchExecutionLog) SyncReview(ctx context.Context, in *model.BatchExecutionLogSyncReviewParam) (res *model.BatchExecutionLogSyncReviewModel, err error) { if in == nil { in = &model.BatchExecutionLogSyncReviewParam{} } batchNo := strings.TrimSpace(in.BatchNo) reason := strings.TrimSpace(in.Reason) if batchNo == "" { return nil, gerror.New("批次编号不能为空") } columns := dao.BatchMain.Columns() batch := &entity.BatchMain{} // 沿用执行日志列表的可见性口径(部门权限过滤)。 if err = s.batchModel(ctx). Fields(columns.Id, columns.BatchNo, columns.BatchType, columns.BatchAsyncStatus, columns.RunResult, columns.BatchInfo). Where(columns.BatchNo, batchNo). Scan(batch); err != nil { return nil, gerror.Wrap(err, "查询批次信息失败") } if batch.Id == 0 { return nil, gerror.New("批次不存在或无权限") } if batch.BatchAsyncStatus != consts.BatchAsyncStatusReview { return nil, gerror.New("仅待核查批次支持手动同步上游结果") } unknownCount := batchExecutionLogUnknownCount(batch.BatchInfo) if unknownCount <= 0 { return nil, gerror.New("该批次没有待核查明细") } message := dataReviewQueryMessage{BatchNo: batch.BatchNo, RequestedBy: contexts.GetUserName(ctx)} if err = publishDataReviewQuery(ctx, consts.QueueDataReviewQuery, message); err != nil { g.Log().Errorf(ctx, "手动同步上游结果消息发布失败, batchNo=%s, tenantId=%s, userId=%d, reason=%s, err=%+v", batch.BatchNo, contexts.GetTenantId(ctx), contexts.GetUserId(ctx), reason, err) return nil, gerror.Wrap(err, "发布上游结果同步消息失败") } g.Log().Infof(ctx, "手动同步上游结果消息已发布, batchNo=%s, batchType=%d, unknownCount=%d, tenantId=%s, userId=%d, reason=%s", batch.BatchNo, batch.BatchType, unknownCount, contexts.GetTenantId(ctx), contexts.GetUserId(ctx), reason) return &model.BatchExecutionLogSyncReviewModel{ BatchNo: batch.BatchNo, BatchType: batch.BatchType, BatchAsyncStatus: batch.BatchAsyncStatus, UnknownCount: unknownCount, Queued: true, }, nil } func (s *sCommonBatchExecutionLog) getDetailBatch(ctx context.Context, batchNo string, batchType *int) (*entity.BatchMain, error) { if batchNo == "" { return nil, gerror.New("批次号不能为空") } columns := dao.BatchMain.Columns() batch := &entity.BatchMain{} mod := s.batchModel(ctx). Fields( columns.Id, columns.BatchNo, columns.BatchType, columns.SourceType, columns.SourceBatchNo, columns.BatchInfo, columns.CreatedDept, columns.TargetDeptId, ). Where(columns.BatchNo, batchNo) mod = executionLogBatchTypeModel(mod, columns.BatchType, columns.BatchInfo, batchType) if err := mod.Scan(batch); err != nil { return nil, gerror.Wrap(err, "获取批次信息失败,请稍后重试") } if batch.Id == 0 { return nil, gerror.New("批次不存在") } if isAllocateChildBatch(batch.BatchType, batch.BatchNo, batch.SourceBatchNo) { return nil, gerror.New("批次不存在") } if !executionLogBatchTypeMatches(batch.BatchType, batch.BatchInfo, batchType) { return nil, gerror.New("批次类型不匹配") } return batch, nil } func executionLogBatchTypeModel(mod *gdb.Model, batchTypeColumn, batchInfoColumn string, requested *int) *gdb.Model { if requested == nil { return mod } if *requested == consts.BatchTypePrepare { return mod.Where(batchTypeColumn, consts.BatchTypePrepare) } if *requested == consts.BatchTypeAllocate { return mod.Where( fmt.Sprintf("(%s = ? OR (%s = ? AND JSON_UNQUOTE(JSON_EXTRACT(%s, '$.targetBatchType')) = ?))", batchTypeColumn, batchTypeColumn, batchInfoColumn), consts.BatchTypeAllocate, consts.BatchTypePrepare, strconv.Itoa(*requested), ) } return mod.Where( fmt.Sprintf("(%s = ? OR (%s = ? AND JSON_UNQUOTE(JSON_EXTRACT(%s, '$.targetBatchType')) = ?))", batchTypeColumn, batchTypeColumn, batchInfoColumn), *requested, consts.BatchTypePrepare, strconv.Itoa(*requested), ) } func executionLogBatchTypeMatches(actual int, info *gjson.Json, requested *int) bool { if requested == nil { return true } if actual == *requested { return true } return actual == consts.BatchTypePrepare && info != nil && info.Get("targetBatchType").Int() == *requested } func isAllocateParentBatch(batch *entity.BatchMain) bool { if batch == nil || batch.BatchType != consts.BatchTypeAllocate || isAllocateChildBatch(batch.BatchType, batch.BatchNo, batch.SourceBatchNo) { return false } return true } func isAllocateChildBatch(batchType int, batchNo, sourceBatchNo string) bool { return batchType == consts.BatchTypeAllocate && strings.TrimSpace(sourceBatchNo) != "" && strings.TrimSpace(sourceBatchNo) != strings.TrimSpace(batchNo) } func batchExecutionLogDetailSnapshotCards(detail *entity.BatchDetail) (iccid, msisdn, imsi string) { if detail == nil { return } if detail.Snapshot != nil { iccid = strings.TrimSpace(detail.Snapshot.Get("ICCID").String()) msisdn = strings.TrimSpace(detail.Snapshot.Get("MSISDN").String()) imsi = strings.TrimSpace(detail.Snapshot.Get("IMSI").String()) if iccid == "" { iccid = strings.TrimSpace(detail.Snapshot.Get("iccid").String()) } if msisdn == "" { msisdn = strings.TrimSpace(detail.Snapshot.Get("msisdn").String()) } if imsi == "" { imsi = strings.TrimSpace(detail.Snapshot.Get("imsi").String()) } } if iccid == "" { iccid = detail.ItemId } return } func batchExecutionLogSnapshotValue(detail *entity.BatchDetail, paths ...string) string { if detail == nil || detail.Snapshot == nil { return "" } for _, path := range paths { value := strings.TrimSpace(detail.Snapshot.Get(path).String()) if value != "" { return value } } return "" } func (s *sCommonBatchExecutionLog) getSourceFile(ctx context.Context, sourceOssId int64, batchInfo *gjson.Json) (*model.BatchExecutionLogFileModel, error) { ossId := sourceOssId if ossId == 0 { ossId = legacySourceOssId(batchInfo) } if ossId == 0 { return nil, nil } oss := &entity.SysOss{} if err := s.ossModel(ctx). Where(dao.SysOss.Columns().OssId, ossId). Scan(oss); err != nil { return nil, gerror.Wrap(err, "获取原始文件信息失败,请稍后重试!") } if oss.OssId == 0 { return nil, nil } return &model.BatchExecutionLogFileModel{ OssId: oss.OssId, Url: batchExecutionLogFileURL(ctx, oss.Url, oss.Path), }, nil } func (s *sCommonBatchExecutionLog) getResultFile(ctx context.Context, batchNo string, resultOssId int64, batchInfo *gjson.Json) (*model.BatchExecutionLogResultFileModel, error) { download := &entity.TResourceBatchDownload{} // 出库/划拨/导出等批次不写 t_resource_batch_download,查不到记录按「无结果文件」处理, // 不能当异常抛给页面(否则详情弹窗整体报 sql: no rows in result set)。 if err := s.downloadModel(ctx). Where(dao.TResourceBatchDownload.Columns().BatchNo, batchNo). OrderDesc(dao.TResourceBatchDownload.Columns().Id). Limit(1). Scan(download); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, gerror.Wrap(err, "获取结果文件信息失败,请稍后重试!") } ossId := resultOssId if ossId == 0 && download.OssId > 0 { ossId = download.OssId } if ossId == 0 { ossId = legacyResultOssId(batchInfo) } if download.Id == 0 && ossId == 0 { return nil, nil } url := batchExecutionLogFileURL(ctx, download.FileUrl, download.FilePath) originalName := "" if ossId > 0 { oss := &entity.SysOss{} if scanErr := s.ossModel(ctx). Where(dao.SysOss.Columns().OssId, ossId). Scan(oss); scanErr != nil && !errors.Is(scanErr, sql.ErrNoRows) { return nil, gerror.Wrap(scanErr, "获取结果文件OSS信息失败,请稍后重试!") } if oss.OssId > 0 { originalName = oss.OriginalName if ossUrl := batchExecutionLogFileURL(ctx, oss.Url, oss.Path); ossUrl != "" { url = ossUrl } } } return &model.BatchExecutionLogResultFileModel{ DownloadId: download.Id, OssId: ossId, Url: url, OriginalName: originalName, DownloadStatus: resultDownloadStatus(download), }, nil } // getExportResultFile 只暴露任务 ID;文件必须经下载中心重新校验本人、权限和有效期。 func (s *sCommonBatchExecutionLog) getExportResultFile(ctx context.Context, taskNo string, _ int64) (*model.BatchExecutionLogResultFileModel, error) { task := &entity.TExportTask{} if err := s.exportTaskModel(ctx). Where(dao.TExportTask.Columns().TaskNo, taskNo). Where(dao.TExportTask.Columns().OwnerUserId, contexts.GetUserId(ctx)). Scan(task); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, gerror.Wrap(err, "获取结果文件信息失败,请稍后重试!") } if task.Id <= 0 || task.DeletedTime != nil || task.Status != consts.DownloadStatusGenerated || task.OssId <= 0 || (task.ExpireTime != nil && task.ExpireTime.Before(gtime.Now())) || !hasExportPermission(ctx, task.PermCode) { return nil, nil } return &model.BatchExecutionLogResultFileModel{ ExportTaskId: task.Id, OriginalName: task.FileName, DownloadStatus: consts.DownloadStatusGenerated, }, nil } func batchExecutionLogUnknownCount(batchInfo *gjson.Json) int { if batchInfo == nil { return 0 } return batchInfo.Get("apnUnknownCount").Int() } // batchRestartPhase 从 batch_info 取出重启阶段与计划复机时间。 // 重启批次停机完成后要等待若干分钟再复机,这段时间主状态仍是处理中, // 明细却已落终态;前端据此显示「等待复机」,避免误以为卡住或已办结。 func batchRestartPhase(batchInfo *gjson.Json) (phase, resumeAt string) { if batchInfo == nil { return "", "" } return strings.TrimSpace(batchInfo.Get("restart.phase").String()), strings.TrimSpace(batchInfo.Get("restart.resumeAt").String()) } func legacySourceOssId(batchInfo *gjson.Json) int64 { if batchInfo == nil { return 0 } for _, path := range []string{"importFileOssId", "source.importFileOssId", "source.inputFileOssId"} { if ossId := batchInfo.Get(path).Int64(); ossId > 0 { return ossId } } return 0 } func legacyResultOssId(batchInfo *gjson.Json) int64 { if batchInfo == nil { return 0 } return batchInfo.Get("resultFileOssId").Int64() } func resultDownloadStatus(download *entity.TResourceBatchDownload) int { if download != nil && download.Id > 0 { return download.DownloadStatus } return consts.DownloadStatusGenerated } func batchExecutionLogFileURL(ctx context.Context, storedURL string, storedPath string) string { if url := normalizeBatchLocalUploadURL(ctx, storedURL); url != "" { return url } if url := normalizeBatchLocalUploadURL(ctx, storedPath); url != "" { return url } return strings.TrimSpace(storedURL) } func normalizeBatchLocalUploadURL(ctx context.Context, value string) string { value = strings.TrimSpace(value) if value == "" { return "" } lower := strings.ToLower(value) if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") { return value } normalized := filepath.ToSlash(filepath.Clean(value)) if normalized == "/upload" || strings.HasPrefix(normalized, "/upload/") { return normalized } if configType, err := service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssUrlPath); err == nil && configType != nil { configValue := strings.TrimSpace(configType.ConfigValue) if configValue != "" { if strings.Contains(configValue, "|") { split := strings.SplitN(configValue, "|", 2) from := strings.TrimSpace(filepath.ToSlash(split[0])) to := strings.TrimSpace(filepath.ToSlash(split[1])) if suffix, ok := batchLocalUploadMappedSuffix(normalized, from); ok { return batchJoinUploadURL(to, suffix) } } else if suffix := batchLocalUploadRelativePath(normalized); suffix != "" { return batchJoinUploadURL(configValue, suffix) } } } if suffix := batchLocalUploadRelativePath(normalized); suffix != "" { return batchJoinUploadURL("/upload", suffix) } if strings.HasPrefix(normalized, "/") { return normalized } return "" } func batchLocalUploadMappedSuffix(filePath string, from string) (string, bool) { from = strings.Trim(strings.TrimSpace(filepath.ToSlash(from)), "/") if from == "" { return "", false } normalized := strings.TrimSpace(filepath.ToSlash(filePath)) marker := "/" + from + "/" if idx := strings.Index(normalized, marker); idx >= 0 { return strings.TrimPrefix(normalized[idx+len(marker):], "/"), true } prefix := from + "/" if strings.HasPrefix(normalized, prefix) { return strings.TrimPrefix(normalized[len(prefix):], "/"), true } if strings.Trim(normalized, "/") == from { return "", true } return "", false } func batchLocalUploadRelativePath(filePath string) string { normalized := filepath.ToSlash(filepath.Clean(strings.TrimSpace(filePath))) for _, marker := range []string{ "resource/upload/", "/resource/upload/", "/app/resource/upload/", "/app/upload/", "upload/", "/upload/", } { if idx := strings.Index(normalized, marker); idx >= 0 { return strings.TrimPrefix(normalized[idx+len(marker):], "/") } } return "" } func batchJoinUploadURL(base string, suffix string) string { base = strings.TrimRight(strings.TrimSpace(filepath.ToSlash(base)), "/") suffix = strings.TrimLeft(strings.TrimSpace(filepath.ToSlash(suffix)), "/") if base == "" { base = "/upload" } if suffix == "" { return base } return base + "/" + suffix }