Files
iotlcm/admin-svr/server/internal/logic/common/batch_execution_log.go

765 lines
28 KiB
Go

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
}