// Package gen // @Link https://github.com/xiujiecn/xiu-admin // @Copyright Copyright (c) 2026 LiXiujie // @Author Lxj // @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE // @AutoGenerate Version package common import ( "context" "fmt" "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/batchmetadata" "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" "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/do" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/excel" "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/os/gctx" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" ) type sCommonBatchMain struct{} const restartPhaseWaitingResume = "waiting_resume" type batchExecuteMessage struct { BatchNo string `json:"batchNo"` BatchType int `json:"batchType"` } var publishBatchExecuteMessage = rabbitmq.Publish func NewCommonBatchMain() *sCommonBatchMain { return &sCommonBatchMain{} } func (s *sCommonBatchMain) getRequeueBatch(ctx context.Context, batchNo string) (*entity.BatchMain, error) { columns := dao.BatchMain.Columns() batch := &entity.BatchMain{} err := s.Model(ctx). Fields( columns.Id, columns.BatchNo, columns.BatchType, columns.BatchAsyncStatus, columns.RunResult, columns.BatchInfo, columns.TenantId, columns.DeptId, columns.CreatedDept, columns.CreatedBy, ). Where(columns.BatchNo, batchNo). Where(columns.BatchType+" <> ?", consts.BatchTypePrepare). Scan(batch) if err != nil { return nil, gerror.Wrap(err, "查询批次信息失败") } if batch.Id == 0 { return nil, gerror.New("批次不存在或无权限") } return batch, nil } func validateRequeueBatch(batch *entity.BatchMain) error { if batch == nil { return gerror.New("批次不存在") } switch batch.BatchAsyncStatus { case consts.BatchAsyncStatusPending: return nil case consts.BatchAsyncStatusProcessing: return validateProcessingRequeueBatch(batch) case consts.BatchAsyncStatusFailed, consts.BatchAsyncStatusSuccess, consts.BatchAsyncStatusPartialSuccess: return gerror.New("终态批次不支持安全重投") default: return gerror.Newf("当前批次状态不支持安全重投,status=%d", batch.BatchAsyncStatus) } } func validateProcessingRequeueBatch(batch *entity.BatchMain) error { if batch.BatchType != consts.BatchTypeRestart { return gerror.New("处理中批次不支持安全重投,避免重复调用运营商") } if restartPhase(batch) != restartPhaseWaitingResume { return gerror.New("RST 批次当前不在等待复机阶段,不能安全重投") } resumeAt := restartResumeAt(batch) if resumeAt == nil { return gerror.New("RST 批次缺少计划复机时间,不能安全重投") } if resumeAt.After(gtime.Now()) { return gerror.Newf("未到计划复机时间,resumeAt=%s", resumeAt.String()) } return nil } func restartPhase(batch *entity.BatchMain) string { if batch == nil || batch.BatchInfo == nil { return "" } return strings.TrimSpace(batch.BatchInfo.Get("restart.phase").String()) } func restartResumeAt(batch *entity.BatchMain) *gtime.Time { if batch == nil || batch.BatchInfo == nil { return nil } value := strings.TrimSpace(batch.BatchInfo.Get("restart.resumeAt").String()) if value == "" { return nil } parsed := gtime.NewFromStr(value) if parsed == nil || parsed.IsZero() { return nil } return parsed } func init() { service.RegisterCommonBatchMain(NewCommonBatchMain()) } // Model 批次主表ORM模型 func (s *sCommonBatchMain) Model(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 *sCommonBatchMain) 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...) } // List 获取批次主表列表 func (s *sCommonBatchMain) List(ctx context.Context, in *model.BatchMainListParam) (list []*model.BatchMainListModel, totalCount int, err error) { mod := s.Model(ctx) // 字段过滤 mod = mod.Fields(model.BatchMainListModel{}) // 查询主键ID if in.Id > 0 { mod = mod.Where(dao.BatchMain.Columns().Id, in.Id) } // 查询批次编号 if in.BatchNo != "" { mod = mod.Where(dao.BatchMain.Columns().BatchNo+" LIKE ?", "%"+in.BatchNo+"%") } // 查询订单编号 if in.OrderSn != "" { mod = mod.Where(dao.BatchMain.Columns().OrderSn+" LIKE ?", "%"+in.OrderSn+"%") } // 查询批次类型 枚举 mod = visibleBatchTypeModel(mod, dao.BatchMain.Columns().BatchType, in.BatchType) // 查询创建时间 if len(in.CreatedAt) == 2 { in.CreatedAt[1] = in.CreatedAt[1].EndOfDay() } if len(in.CreatedAt) == 2 { mod = mod.WhereBetween(dao.BatchMain.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1]) } // 分页 mod = mod.Page(in.Page, in.PageSize) // 排序 mod = mod.OrderDesc(dao.BatchMain.Columns().Id) // 操作人摘要信息 mod = mod.Hook(hook.MemberSummary) // 查询数据 if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { err = gerror.Wrap(err, "获取批次主表列表失败,请稍后重试!") return } units := make([]*allocationunits.Fields, 0, len(list)) for _, item := range list { if item.BatchType == consts.BatchTypeAllocate { fields := allocationunits.Resolve(item.BatchInfo, item.CreatedDept, item.TargetDeptId) units = append(units, &fields) } } if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), units); err != nil { return nil, 0, err } unitIndex := 0 for _, item := range list { if item.BatchType == consts.BatchTypeAllocate { fields := units[unitIndex] item.SourceDeptId, item.SourceDeptName = fields.SourceDeptId, fields.SourceDeptName item.TargetDeptId, item.TargetDeptName, item.TargetDeptMode = fields.TargetDeptId, fields.TargetDeptName, fields.TargetDeptMode unitIndex++ } } return } func (s *sCommonBatchMain) SuccessList(ctx context.Context, in *model.BatchMainSuccessListParam) (list []*model.BatchMainSuccessListModel, totalCount int, err error) { if in == nil { in = &model.BatchMainSuccessListParam{} } columns := dao.BatchMain.Columns() mod := s.Model(ctx). Fields( columns.BatchInfo, columns.BatchNo, columns.BatchType, columns.StartTime+" AS batch_time", columns.TotalCount, columns.CreatedDept, columns.TargetDeptId, ). Where(columns.RunResult, consts.BatchRunResultSuccess). WhereGT(columns.TotalCount, 0) var requestedBatchType *int if in.BatchType != 0 { requestedBatchType = &in.BatchType } mod = visibleBatchTypeModel(mod, columns.BatchType, requestedBatchType) if in.BatchNo != "" { mod = mod.Where(columns.BatchNo+" LIKE ?", "%"+in.BatchNo+"%") } if len(in.Time) == 2 { in.Time[1] = in.Time[1].EndOfDay() mod = mod.WhereBetween(columns.StartTime, in.Time[0], in.Time[1]) } mod = mod.Page(in.Page, in.PageSize).OrderDesc(columns.Id) if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { return nil, 0, gerror.Wrap(err, "获取成功批次列表失败,请稍后重试") } if err = fillInboundBatchStockCounts(ctx, list); err != nil { return nil, 0, err } units := make([]*allocationunits.Fields, 0, len(list)) for _, item := range list { if item.BatchType == consts.BatchTypeAllocate { item.Fields = allocationunits.Resolve(item.BatchInfo, item.StoredSourceDeptId, item.StoredTargetDeptId) units = append(units, &item.Fields) } } if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), units); err != nil { return nil, 0, err } return } func fillInboundBatchStockCounts(ctx context.Context, list []*model.BatchMainSuccessListModel) error { if len(list) == 0 { return nil } batchNos := make([]string, 0, len(list)) for _, item := range list { if item == nil || item.BatchType != consts.BatchTypeInbound || strings.TrimSpace(item.BatchNo) == "" { continue } batchNos = append(batchNos, item.BatchNo) } if len(batchNos) == 0 { return nil } type stockCountRow struct { BatchNo string `orm:"batch_no"` StockCount int `orm:"stock_count"` } var rows []*stockCountRow stockColumns := dao.TSimCardStock.Columns() if err := dao.TSimCardStock.Ctx(ctx). Fields(stockColumns.BatchNo, "COUNT(1) AS stock_count"). Where(stockColumns.TenantId, contexts.GetTenantId(ctx)). WhereIn(stockColumns.BatchNo, batchNos). Group(stockColumns.BatchNo). Scan(&rows); err != nil { return gerror.Wrap(err, "统计入库批次在库数量失败") } stockCountMap := make(map[string]int, len(rows)) for _, row := range rows { if row != nil { stockCountMap[row.BatchNo] = row.StockCount } } for _, item := range list { if item != nil && item.BatchType == consts.BatchTypeInbound { item.StockCount = stockCountMap[item.BatchNo] } } return nil } type batchDetailQuery struct { BatchNo string BatchType *int DetailStatus *int Iccid string Msisdn string CurrentStockOnly bool Page int PageSize int } func (s *sCommonBatchMain) ListCards(ctx context.Context, in *model.BatchCardListParam) (list []*model.BatchCardListModel, totalCount int, err error) { if in == nil || in.BatchNo == "" { return nil, 0, gerror.New("批次号不能为空") } batch, err := s.getVisibleBatch(ctx, in.BatchNo, in.BatchType) if err != nil { return nil, 0, err } details, totalCount, err := s.listBatchDetails(ctx, batch, batchDetailQuery{ BatchNo: in.BatchNo, BatchType: in.BatchType, Iccid: in.Iccid, Msisdn: in.Msisdn, CurrentStockOnly: in.CurrentStockOnly, Page: in.Page, PageSize: in.PageSize, }) if err != nil { return nil, 0, err } list = make([]*model.BatchCardListModel, 0, len(details)) displayRowStart := 0 if in.Page > 0 && in.PageSize > 0 { displayRowStart = (in.Page - 1) * in.PageSize } units := make([]*allocationunits.Fields, 0, len(details)) parentUnits := allocationunits.Resolve(batch.BatchInfo, batch.CreatedDept, batch.TargetDeptId) for index, detail := range details { iccid, msisdn, imsi := batchDetailSnapshotCards(detail) item := &model.BatchCardListModel{ RowNo: displayRowStart + index + 1, Iccid: iccid, Msisdn: msisdn, Imsi: imsi, DetailStatus: detail.DetailStatus, } if batch.BatchType == consts.BatchTypeAllocate { 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 } units = append(units, &item.Fields) } list = append(list, item) } if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), units); err != nil { return nil, 0, err } return } // listBatchDetails 获取批次卡清单明细。 func (s *sCommonBatchMain) listBatchDetails(ctx context.Context, batch *entity.BatchMain, query batchDetailQuery) (list []*entity.BatchDetail, totalCount int, err error) { columns := dao.BatchDetail.Columns() 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, batch.BatchType) if batch.BatchType == consts.BatchTypeAllocate && (batch.SourceBatchNo == "" || batch.SourceBatchNo == batch.BatchNo) { mainColumns := dao.BatchMain.Columns() childBatches := s.Model(ctx).Fields(mainColumns.BatchNo). Where(mainColumns.BatchType, consts.BatchTypeAllocate). Where(mainColumns.SourceBatchNo, batch.BatchNo). Where(mainColumns.SourceBatchNo + " <> " + mainColumns.BatchNo) mod = mod.Where("("+columns.BatchNo+" = ? OR "+columns.BatchNo+" IN (?))", batch.BatchNo, childBatches) } else { mod = mod.Where(columns.BatchNo, batch.BatchNo) } if query.DetailStatus != nil { mod = mod.Where(columns.DetailStatus, *query.DetailStatus) } if query.Iccid != "" { like := "%" + query.Iccid + "%" mod = mod.Where( "("+columns.ItemId+" LIKE ? OR JSON_UNQUOTE(JSON_EXTRACT("+columns.Snapshot+", '$.ICCID')) LIKE ?)", like, like, ) } if query.Msisdn != "" { mod = mod.Where( "JSON_UNQUOTE(JSON_EXTRACT("+columns.Snapshot+", '$.MSISDN')) LIKE ?", "%"+query.Msisdn+"%", ) } if query.CurrentStockOnly { mod = onlyCurrentStockDetails(mod) } mod = mod.Page(query.Page, query.PageSize).OrderAsc(columns.RowNo) if err = mod.ScanAndCount(&list, &totalCount, false); err != nil { return nil, 0, gerror.Wrap(err, "获取批次明细列表失败,请稍后重试") } return } func onlyCurrentStockDetails(mod *gdb.Model) *gdb.Model { detailTable := dao.BatchDetail.Table() stockTable := dao.TSimCardStock.Table() return mod.Where(fmt.Sprintf( "EXISTS (SELECT 1 FROM `%s` stock WHERE stock.`tenant_id` = `%s`.`tenant_id` AND stock.`batch_no` = `%s`.`batch_no` AND stock.`iccid` = COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`%s`.`snapshot`, '$.ICCID')), ''), `%s`.`item_id`))", stockTable, detailTable, detailTable, detailTable, detailTable, )) } func (s *sCommonBatchMain) getVisibleBatch(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.Model(ctx). Fields( columns.Id, columns.BatchNo, columns.BatchType, columns.TenantId, columns.DeptId, columns.BatchInfo, columns.TargetDeptId, columns.SourceBatchNo, columns.CreatedDept, columns.CreatedBy, ). Where(columns.BatchNo, batchNo) mod = visibleBatchTypeModel(mod, columns.BatchType, batchType) if err := mod.Scan(batch); err != nil { return nil, gerror.Wrap(err, "获取批次信息失败,请稍后重试") } if batch.Id == 0 { return nil, gerror.New("批次不存在") } if !visibleBatchTypeMatches(batch.BatchType, batchType) { return nil, gerror.New("批次类型不匹配") } return batch, nil } func batchDetailSnapshotCards(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 = detail.ItemId } return } // Export 导出批次主表 func (s *sCommonBatchMain) Export(ctx context.Context, in *model.BatchMainListParam) (err error) { list, totalCount, err := s.List(ctx, in) if err != nil { return } // 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...} tags, err := convert.GetEntityDescTags(model.BatchMainExportModel{}) if err != nil { return } var ( fileName = "导出批次主表-" + gctx.CtxId(ctx) sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list)) exports []model.BatchMainExportModel ) if err = gconv.Scan(list, &exports); err != nil { return } err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName) return } // Edit 修改/新增批次主表 func (s *sCommonBatchMain) Edit(ctx context.Context, in *model.BatchMainEditParam) (err error) { if in == nil { return gerror.New("批次参数不能为空") } if in.Id <= 0 { if in.ExecutionMode == nil { return gerror.New("新建批次必须指定单卡或批量操作规模") } if err = batchmetadata.ValidateCreation(batchmetadata.Console, *in.ExecutionMode); err != nil { return err } if *in.ExecutionMode == batchmetadata.Single && (in.TotalCount == nil || *in.TotalCount != 1) { return gerror.New("单卡批次总数量必须为1") } } data := do.BatchMain{} if err = gconv.Struct(in, &data); err != nil { return gerror.Wrap(err, "转换批次参数失败") } data.CreatedAt = nil data.UpdatedAt = nil // 验证'BatchNo'唯一 if err = xgorm.IsUnique(ctx, &dao.BatchMain, g.Map{dao.BatchMain.Columns().BatchNo: in.BatchNo}, "批次编号已存在", in.Id); err != nil { return } return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) { // 修改 if in.Id > 0 { data.UpdatedBy = contexts.GetUserId(ctx) data.SourceChannel = nil data.ExecutionMode = nil if _, err = s.Model(ctx).TX(tx). Fields(model.BatchMainUpdateFields{}). WherePri(in.Id).Data(data).Update(); err != nil { err = gerror.Wrap(err, "修改批次主表失败,请稍后重试!") } return } // 新增 data.Id = nil data.TenantId = contexts.GetTenantId(ctx) data.CreatedDept = contexts.GetDeptId(ctx) data.CreatedBy = contexts.GetUserId(ctx) data.SourceChannel = batchmetadata.Console data.ExecutionMode = *in.ExecutionMode if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).TX(tx). Fields(model.BatchMainInsertFields{}). Data(data).Insert(); err != nil { err = gerror.Wrap(err, "新增批次主表失败,请稍后重试!") } return }) } // Delete 删除批次主表 func (s *sCommonBatchMain) Delete(ctx context.Context, in *model.BatchMainDeleteParam) (err error) { if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil { err = gerror.Wrap(err, "删除批次主表失败,请稍后重试!") return } return } // View 获取批次主表指定信息 func (s *sCommonBatchMain) Requeue(ctx context.Context, in *model.BatchMainRequeueParam) (res *model.BatchMainRequeueModel, err error) { if in == nil { in = &model.BatchMainRequeueParam{} } batchNo := strings.TrimSpace(in.BatchNo) reason := strings.TrimSpace(in.Reason) if batchNo == "" { return nil, gerror.New("批次编号不能为空") } if reason == "" { return nil, gerror.New("重投原因不能为空") } batch, err := s.getRequeueBatch(ctx, batchNo) if err != nil { return nil, err } if err = validateRequeueBatch(batch); err != nil { return nil, err } message := batchExecuteMessage{BatchNo: batch.BatchNo, BatchType: batch.BatchType} if err = publishBatchExecuteMessage(ctx, consts.QueueBatchExecute, message); err != nil { g.Log().Errorf(ctx, "batch requeue publish failed, batchNo=%s, batchType=%d, tenantId=%s, userId=%d, reason=%s, err=%+v", batch.BatchNo, batch.BatchType, contexts.GetTenantId(ctx), contexts.GetUserId(ctx), reason, err) return nil, gerror.Wrap(err, "发布批次重投消息失败") } g.Log().Infof(ctx, "batch requeue published, batchNo=%s, batchType=%d, status=%d, runResult=%d, tenantId=%s, userId=%d, reason=%s", batch.BatchNo, batch.BatchType, batch.BatchAsyncStatus, batch.RunResult, contexts.GetTenantId(ctx), contexts.GetUserId(ctx), reason) return &model.BatchMainRequeueModel{ BatchNo: batch.BatchNo, BatchType: batch.BatchType, BatchAsyncStatus: batch.BatchAsyncStatus, RunResult: batch.RunResult, Requeued: true, }, nil } func (s *sCommonBatchMain) View(ctx context.Context, in *model.BatchMainViewParam) (res *model.BatchMainViewModel, err error) { mod := visibleBatchTypeModel( s.Model(ctx).WherePri(in.Id), dao.BatchMain.Columns().BatchType, nil, ) if err = mod.Hook(hook.MemberSummary).Scan(&res); err != nil { err = gerror.Wrap(err, "获取批次主表信息,请稍后重试!") return } if res == nil || res.Id == 0 { return nil, gerror.New("批次不存在") } if res.BatchType == consts.BatchTypeAllocate { fields := allocationunits.Resolve(res.BatchInfo, res.CreatedDept, res.TargetDeptId) if err = allocationunits.Fill(ctx, contexts.GetTenantId(ctx), []*allocationunits.Fields{&fields}); err != nil { return nil, err } res.SourceDeptId, res.SourceDeptName = fields.SourceDeptId, fields.SourceDeptName res.TargetDeptId, res.TargetDeptName, res.TargetDeptMode = fields.TargetDeptId, fields.TargetDeptName, fields.TargetDeptMode } return }