package common import ( "context" "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/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/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...) } 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 { 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, StartTime: batch.StartTime, EndTime: batch.EndTime, } 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 } if resultFile, err := s.getResultFile(ctx, batch.BatchNo, batch.ResultOssId, batch.BatchInfo); err != nil { return nil, err } else { 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 = mod.Page(in.Page, in.PageSize).OrderAsc(columns.RowNo) 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 } 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{} if err := s.downloadModel(ctx). Where(dao.TResourceBatchDownload.Columns().BatchNo, batchNo). OrderDesc(dao.TResourceBatchDownload.Columns().Id). Limit(1). Scan(download); err != nil { 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) if ossId > 0 { oss := &entity.SysOss{} if err := s.ossModel(ctx). Where(dao.SysOss.Columns().OssId, ossId). Scan(oss); err != nil { return nil, gerror.Wrap(err, "获取结果文件OSS信息失败,请稍后重试!") } if oss.OssId > 0 { if ossUrl := batchExecutionLogFileURL(ctx, oss.Url, oss.Path); ossUrl != "" { url = ossUrl } } } return &model.BatchExecutionLogResultFileModel{ DownloadId: download.Id, OssId: ossId, Url: url, DownloadStatus: resultDownloadStatus(download), }, nil } 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 }