package outbound import ( "context" "fmt" "strings" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchimport" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "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" ) type Processor struct { repo *Repository opts PreviewOptions config batchimport.ProcessorConfig excelSubmitParam *model.TSimCardOutboundSubmitParam } func (p *Processor) ExecutePending(ctx context.Context, tx gdb.TX, batchNo string, in *model.TSimCardOutboundSubmitParam, rows []*Row, operator batchimport.OperatorContext, requiredSourceBatchNo string) (*CompleteOutput, error) { if strings.TrimSpace(batchNo) == "" { return nil, gerror.New("出库批次号不能为空") } if in == nil { return nil, gerror.New("出库参数不能为空") } if err := p.validateExecuteContext(ctx, tx, in, operator, rows, requiredSourceBatchNo); err != nil { return nil, err } return p.Complete(ctx, tx, &CompleteInput{ SubmitParam: in, BatchNo: batchNo, Rows: rows, Operator: operator, StrictStockIdentity: p.opts.StrictStockIdentity, RequiredSourceBatchNo: requiredSourceBatchNo, }) } const ( outboundCardPackageType = 101 outboundPoolPackageType = 102 ) func NewProcessor(opts PreviewOptions) *Processor { return &Processor{ repo: NewRepository(), opts: opts, config: batchimport.ProcessorConfig{ Code: "sim_card_outbound", BatchType: consts.BatchTypeOutbound, BatchNoPrefix: consts.BatchNoPrefixOutbound, TaskName: consts.ResourceBatchDownloadTaskSimCardOutbound, ExportScene: consts.ResourceExportSceneOutbound, ImportDir: consts.SimCardOutboundSubDirImportExcel, ResultDir: consts.SimCardOutboundSubDirResultExcel, MaxFileSize: consts.SimCardOutboundMaxFileSize, TemplateHeaders: excelHeaders(), RequiredHeaders: excelRequiredHeaders(), }, } } func (p *Processor) Config() batchimport.ProcessorConfig { return p.config } func (p *Processor) ParseRows(ctx context.Context, filePath string) ([]batchimport.Row, error) { parsed, err := ParseExcelFile(ctx, filePath) if err != nil { return nil, err } p.excelSubmitParam = nil if parsed != nil { p.excelSubmitParam = parsed.SubmitParam } result := make([]batchimport.Row, 0, len(parsed.Rows)) for _, row := range parsed.Rows { result = append(result, row) } return result, nil } func (p *Processor) ValidateRows(ctx context.Context, rows []batchimport.Row) error { outboundRows := make([]*Row, 0, len(rows)) for _, item := range rows { row, ok := item.(*Row) if !ok { item.AddErrorReason("导入行类型错误") continue } outboundRows = append(outboundRows, row) } opts := p.opts if opts.DeptId == 0 { opts.DeptId = contexts.GetDeptId(ctx) } return validateRows(ctx, outboundRows, opts, p.repo) } func (p *Processor) Execute(ctx context.Context, tx gdb.TX, req *batchimport.SubmitRequest, rows []batchimport.Row) error { if req == nil { return gerror.New("出库请求不能为空") } in := p.submitParams(req.BizParams) if in == nil { return gerror.New("出库参数类型错误") } outboundRows := make([]*Row, 0, len(rows)) for _, item := range rows { row, ok := item.(*Row) if !ok { return gerror.New("导入行类型错误") } outboundRows = append(outboundRows, row) } if err := p.validateExecuteContext(ctx, tx, in, req.Operator, outboundRows, requiredSourceBatchNo(req.BizParams)); err != nil { return err } output, err := p.Complete(ctx, tx, &CompleteInput{ SubmitParam: in, BatchNo: req.BatchNo, Rows: outboundRows, Operator: req.Operator, StrictStockIdentity: p.opts.StrictStockIdentity, RequiredSourceBatchNo: requiredSourceBatchNo(req.BizParams), }) if err != nil { return err } req.ExecutionResult = output return nil } func (p *Processor) BuildBatchInfo(ctx context.Context, req batchimport.SubmitRequest, result *batchimport.Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json { info := g.Map{} info["strictStockIdentity"] = p.opts.StrictStockIdentity if result != nil && result.ImportFileOssId > 0 { info["importFileOssId"] = result.ImportFileOssId } else if importOss != nil { info["importFileOssId"] = importOss.OssId } if resultOss != nil { info["resultFileOssId"] = resultOss.OssId } if downloadId > 0 { info["resultDownloadId"] = downloadId } if in := p.submitParams(req.BizParams); in != nil { normalizeSubmitParam(in) info["customerAgentId"] = in.CustomerAgentId info["packageCode"] = strings.TrimSpace(in.PackageCode) info["packageBeginTime"] = in.PackageBeginTime info["durationYears"] = in.DurationYears info["durationMonthPart"] = in.DurationMonthPart info["durationMonths"] = in.DurationMonths info["giftDurationYears"] = in.GiftDurationYears info["giftDurationMonthPart"] = in.GiftDurationMonthPart info["giftDurationMonths"] = in.GiftDurationMonths info["amount"] = strings.TrimSpace(in.Amount) info["offlineContractCompleted"] = in.OfflineContractCompleted info["remark"] = normalizedRemark(in) info["buyerNote"] = normalizedRemark(in) info["adminNote"] = normalizedRemark(in) } if in, ok := req.BizParams.(*model.TSimCardOutboundRangeSubmitParam); ok && in != nil { info["rangeCodeType"] = normalizeRangeCodeType(in.RangeCodeType) info["startIccid"] = strings.TrimSpace(in.StartIccid) info["endIccid"] = strings.TrimSpace(in.EndIccid) info["startMsisdn"] = strings.TrimSpace(in.StartMsisdn) info["endMsisdn"] = strings.TrimSpace(in.EndMsisdn) info["startImsi"] = strings.TrimSpace(in.StartImsi) info["endImsi"] = strings.TrimSpace(in.EndImsi) } if in, ok := req.BizParams.(*model.TSimCardOutboundBatchSubmitParam); ok && in != nil { info["inboundBatchNo"] = strings.TrimSpace(in.BatchNo) } if output, ok := req.ExecutionResult.(*CompleteOutput); ok && output != nil { info["orderSn"] = output.OrderSn info["quantity"] = output.Quantity info["sourceBatchNos"] = output.SourceBatchNos info["packageBeginTime"] = output.PackageBeginAt info["packageEndTime"] = output.PackageEndAt info["packageStatus"] = output.PackageStatus info["effectiveStatus"] = output.EffectiveStatus if output.Customer != nil { info["customerName"] = output.Customer.DeptName } if output.Package != nil { info["packageName"] = output.Package.PackageName info["packageType"] = output.Package.PackageType } } return gjson.New(info) } func (p *Processor) validateExecuteContext(ctx context.Context, tx gdb.TX, in *model.TSimCardOutboundSubmitParam, operator batchimport.OperatorContext, rows []*Row, requiredSourceBatchNo string) error { normalizeSubmitParam(in) if err := validateSubmitParam(in); err != nil { return err } if err := p.validateSelectablePackage(ctx, tx, in.PackageCode, operator.TenantId); err != nil { return err } iccids, err := validateOutboundIdentifiers(rows) if err != nil { return err } if len(iccids) == 0 { return errOutboundValidationRejected } stockByIccid, err := p.repo.LockStockCardsByIccids(ctx, tx, iccids) if err != nil { return gerror.Wrap(err, "校验出库库存卡失败") } formalByIccid, err := p.repo.GetExistingFormalIccids(ctx, iccids) if err != nil { return gerror.Wrap(err, "校验正式卡失败") } for _, row := range rows { if row == nil || row.Reason != "" { continue } stock := stockByIccid[row.Iccid] reason := validateOutboundRow(row, stock, formalByIccid[row.Iccid], operator, p.opts.StrictStockIdentity, requiredSourceBatchNo) if reason != "" { row.AddReason(reason) } } if hasInvalidRows(rows) { markRemainingRowsSkipped(rows) return errOutboundValidationRejected } return nil } func validateOutboundIdentifiers(rows []*Row) ([]string, error) { seen := make(map[string]*Row, len(rows)) for _, row := range rows { if row == nil { return nil, gerror.New("出库行不能为空") } row.Iccid = strings.TrimSpace(row.Iccid) if row.Iccid == "" { row.AddReason("ICCID不能为空") continue } if first, exists := seen[row.Iccid]; exists { row.AddReason(fmt.Sprintf("ICCID与第%d行重复", first.RowNo)) first.AddReason(fmt.Sprintf("ICCID与第%d行重复", row.RowNo)) continue } seen[row.Iccid] = row } iccids := make([]string, 0, len(seen)) for iccid, row := range seen { if row.Reason == "" { iccids = append(iccids, iccid) } } return iccids, nil } func (p *Processor) validateSelectablePackage(ctx context.Context, tx gdb.TX, packageCode, tenantId string) error { pkg, err := p.repo.LoadPackage(ctx, tx, tenantId, packageCode) if err != nil { return gerror.Wrap(err, "校验出库套餐失败") } if pkg == nil { return gerror.New("出库套餐不存在") } if !isOutboundSelectablePackageType(pkg.PackageType) { return gerror.New("出库套餐必须是卡套餐或池套餐") } if pkg.Enabled != 1 { return gerror.New("出库套餐未启用,不能出库") } return nil } func isOutboundSelectablePackageType(packageType int) bool { return packageType == outboundCardPackageType || packageType == outboundPoolPackageType } func normalizeSubmitParam(in *model.TSimCardOutboundSubmitParam) { if in == nil { return } if in.DurationMonths <= 0 && in.DurationYears > 0 { in.DurationMonths = in.DurationYears*12 + in.DurationMonthPart } if in.DurationMonths <= 0 && in.DurationMonthPart > 0 { in.DurationMonths = in.DurationMonthPart } if in.DurationYears <= 0 && in.DurationMonthPart <= 0 && in.DurationMonths > 0 { in.DurationYears = in.DurationMonths / 12 in.DurationMonthPart = in.DurationMonths % 12 } if in.GiftDurationMonths <= 0 && in.GiftDurationYears > 0 { in.GiftDurationMonths = in.GiftDurationYears*12 + in.GiftDurationMonthPart } if in.GiftDurationMonths <= 0 && in.GiftDurationMonthPart > 0 { in.GiftDurationMonths = in.GiftDurationMonthPart } if in.GiftDurationYears <= 0 && in.GiftDurationMonthPart <= 0 && in.GiftDurationMonths > 0 { in.GiftDurationYears = in.GiftDurationMonths / 12 in.GiftDurationMonthPart = in.GiftDurationMonths % 12 } disablePeriodOptions(in) if remark := normalizedRemark(in); remark != "" { in.Remark = remark in.BuyerNote = remark in.AdminNote = remark } } func disablePeriodOptions(in *model.TSimCardOutboundSubmitParam) { if in == nil { return } // 1 表示否。出库不再暴露测试期、沉默期配置,统一按无测试期/无沉默期保存。 in.HasTestPeriod = 1 in.TestPeriodDays = 0 in.TestPeriodLatestExpiryDate = nil in.HasSilentPeriod = 1 in.SilentPeriodDays = 0 in.SilentPeriodLatestExpiryDate = nil } func normalizedRemark(in *model.TSimCardOutboundSubmitParam) string { if in == nil { return "" } if value := strings.TrimSpace(in.Remark); value != "" { return value } if value := strings.TrimSpace(in.BuyerNote); value != "" { return value } return strings.TrimSpace(in.AdminNote) } func (p *Processor) submitParams(params any) *model.TSimCardOutboundSubmitParam { if in := outboundSubmitParams(params); in != nil { return in } return p.excelSubmitParam } func validateOutboundRow(row *Row, stock *entity.TSimCardStock, formalExists bool, operator batchimport.OperatorContext, strictStockIdentity bool, requiredSourceBatchNo string) string { var reasons []string if stock == nil { return "ICCID不存在库存记录,不能出库" } if stock.CreatedDept != operator.DeptId { reasons = append(reasons, "ICCID不属于当前机构库存,不能出库") } if requiredSourceBatchNo != "" && strings.TrimSpace(stock.BatchNo) != requiredSourceBatchNo { reasons = append(reasons, "ICCID不属于指定库存批次,不能出库") } if formalExists { reasons = append(reasons, "ICCID已存在正式卡记录,不能出库") } if strictStockIdentity { if strings.TrimSpace(row.Msisdn) != strings.TrimSpace(stock.Msisdn) { reasons = append(reasons, "MSISDN与库存记录不一致,不能出库") } if strings.TrimSpace(row.Imsi) != strings.TrimSpace(stock.Imsi) { reasons = append(reasons, "IMSI与库存记录不一致,不能出库") } } return strings.Join(reasons, ";") } func hasInvalidRows(rows []*Row) bool { for _, row := range rows { if row != nil && row.Reason != "" { return true } } return false } func markRemainingRowsSkipped(rows []*Row) { for _, row := range rows { if row != nil && row.Reason == "" { row.AddReason(outboundNotExecutedReason) } } } func outboundSubmitParams(params any) *model.TSimCardOutboundSubmitParam { switch in := params.(type) { case *model.TSimCardOutboundSubmitParam: return in case *model.TSimCardOutboundRangeSubmitParam: if in == nil { return nil } return &in.TSimCardOutboundSubmitParam case *model.TSimCardOutboundInputSubmitParam: if in == nil { return nil } return &in.TSimCardOutboundSubmitParam case *model.TSimCardOutboundBatchSubmitParam: if in == nil { return nil } return &in.TSimCardOutboundSubmitParam default: return nil } } func requiredSourceBatchNo(params any) string { if in, ok := params.(*model.TSimCardOutboundBatchSubmitParam); ok && in != nil { return strings.TrimSpace(in.BatchNo) } return "" }