package inbound import ( "context" "io" "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/batchimport" "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/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do" "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/net/ghttp" ) const inputInlineMaxRowsConfigKey = "batch.inbound.inputInlineMaxRows" type BatchExecuteMessage struct { BatchNo string `json:"batchNo"` BatchType int `json:"batchType"` } type PendingResult struct { BatchNo string BatchType int } func CreatePendingExcel(ctx context.Context, file *ghttp.UploadFile, in *model.TSimCardInboundExcelSubmitParam) (*PendingResult, error) { if in == nil { in = &model.TSimCardInboundExcelSubmitParam{} } if file == nil { return nil, gerror.New("请选择上传文件") } if err := service.SysOss().CheckType(ctx, consts.SysUploadFileTypeFile, file); err != nil { return nil, err } if file.Size > consts.SimCardInboundMaxFileSize { return nil, gerror.Newf("上传文件大小超过限制,最大为%dM", consts.SimCardInboundMaxFileSize>>20) } batchNo, err := batchimport.NextBatchNoContext(ctx, consts.BatchNoPrefixInbound) if err != nil { return nil, err } content, err := readUploadContent(file) if err != nil { return nil, err } importOss, err := service.SysOss().SaveContent(ctx, &model.SysOssSaveContentParam{ Content: content, FileName: batchNo + "-import.xlsx", NewFileType: consts.SysUploadNewFileTypeUserFile, SubDirName: consts.SimCardInboundSubDirImportExcel, }) if err != nil { return nil, gerror.Wrap(err, "保存入库原始Excel失败") } info := newPendingBatchInfo() info["source"] = g.Map{"importFileOssId": importOss.OssId} info["inboundOptions"] = inboundOptions(in.InboundCommon()) return createPendingAndPublish(ctx, pendingInput{ BatchNo: batchNo, SourceType: consts.BatchSourceTypeFile, SourceOssId: importOss.OssId, BatchInfo: gjson.New(info), }) } func CreatePendingRange(ctx context.Context, in *model.TSimCardInboundRangeSubmitParam) (*PendingResult, error) { if in == nil { in = &model.TSimCardInboundRangeSubmitParam{} } if err := validateInboundCommon(in.InboundCommon()); err != nil { return nil, err } rangeInfo, err := pendingRangeInfo(in) if err != nil { return nil, err } batchNo, err := batchimport.NextBatchNoContext(ctx, consts.BatchNoPrefixInbound) if err != nil { return nil, err } info := newPendingBatchInfo() info["source"] = g.Map{"range": rangeInfo} info["inboundOptions"] = inboundOptions(in.InboundCommon()) return createPendingAndPublish(ctx, pendingInput{ BatchNo: batchNo, SourceType: consts.BatchSourceTypeRange, BatchInfo: gjson.New(info), }) } func CreatePendingInput(ctx context.Context, in *model.TSimCardInboundInputSubmitParam) (*PendingResult, error) { if in == nil { in = &model.TSimCardInboundInputSubmitParam{} } if err := validateInboundCommon(in.InboundCommon()); err != nil { return nil, err } inputText := in.Input if strings.TrimSpace(inputText) == "" { return nil, gerror.New("批量输入入库数据为空") } batchNo, err := batchimport.NextBatchNoContext(ctx, consts.BatchNoPrefixInbound) if err != nil { return nil, err } var sourceOssId int64 source := g.Map{} if countNonEmptyLines(inputText) <= inputInlineMaxRows(ctx) { source["inputText"] = inputText } else { inputOss, saveErr := service.SysOss().SaveContent(ctx, &model.SysOssSaveContentParam{ Content: []byte(inputText), FileName: batchNo + "-input.txt", NewFileType: consts.SysUploadNewFileTypeUserFile, SubDirName: consts.SimCardInboundSubDirImportInput, }) if saveErr != nil { return nil, gerror.Wrap(saveErr, "保存入库批量输入原文失败") } source["inputFileOssId"] = inputOss.OssId sourceOssId = inputOss.OssId } info := newPendingBatchInfo() info["source"] = source info["inboundOptions"] = inboundOptions(in.InboundCommon()) return createPendingAndPublish(ctx, pendingInput{ BatchNo: batchNo, SourceType: consts.BatchSourceTypeBatchInput, SourceOssId: sourceOssId, BatchInfo: gjson.New(info), }) } type pendingInput struct { BatchNo string SourceType int SourceOssId int64 BatchInfo *gjson.Json } func createPendingAndPublish(ctx context.Context, input pendingInput) (*PendingResult, error) { if strings.TrimSpace(input.BatchNo) == "" { return nil, gerror.New("批次号为空") } operator := batchimport.OperatorContext{ TenantId: contexts.GetTenantId(ctx), DeptId: contexts.GetDeptId(ctx), UserId: contexts.GetUserId(ctx), } err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { if _, insertErr := tx.Ctx(ctx).Model(dao.BatchMain.Table()).Data(do.BatchMain{ BatchNo: input.BatchNo, OrderSn: "", BatchType: consts.BatchTypeInbound, BatchStatus: consts.BatchStatusNormal, BatchAsyncStatus: consts.BatchAsyncStatusPending, SourceType: input.SourceType, SourceChannel: batchmetadata.Console, ExecutionMode: batchmetadata.Bulk, SourceBatchNo: input.BatchNo, SourceOssId: inboundOssIDValue(input.SourceOssId), IsCrossDept: 0, DeptId: operator.DeptId, TotalCount: 0, ValidCount: 0, SuccessCount: 0, FailCount: 0, RunResult: consts.BatchRunResultPending, FailReason: "", BatchInfo: input.BatchInfo, TenantId: operator.TenantId, CreatedDept: operator.DeptId, CreatedBy: operator.UserId, UpdatedBy: operator.UserId, }).Insert(); insertErr != nil { return gerror.Wrap(insertErr, "写入待处理入库批次失败") } return nil }) if err != nil { return nil, err } publishBatch(ctx, input.BatchNo) return &PendingResult{BatchNo: input.BatchNo, BatchType: consts.BatchTypeInbound}, nil } func publishBatch(ctx context.Context, batchNo string) { if err := rabbitmq.Publish(ctx, consts.QueueBatchExecute, BatchExecuteMessage{ BatchNo: batchNo, BatchType: consts.BatchTypeInbound, }); err != nil { g.Log().Errorf(ctx, "inbound batch async publish failed, batchNo=%s, batchType=%d, tenantId=%s, error=%+v", batchNo, consts.BatchTypeInbound, contexts.GetTenantId(ctx), err) } } func inboundOssIDValue(ossID int64) any { if ossID <= 0 { return nil } return ossID } func newPendingBatchInfo() g.Map { return g.Map{ "resultFile": g.Map{ "status": "pending", }, } } func inboundOptions(common model.TSimCardInboundCommonFields) g.Map { return g.Map{ "chanKey": strings.TrimSpace(common.ChanKey), "validateUpstreamBasicInfo": common.ValidateUpstreamBasicInfo, "cardType": strings.TrimSpace(common.CardType), "upPkgCode": strings.TrimSpace(common.UpPkgCode), "suppliersCode": strings.TrimSpace(common.SuppliersCode), "upstreamActivationTime": common.UpstreamActivationTime, "insertToPlugEnabled": common.InsertToPlugEnabled, "carrierTrafficCutoffEnabled": common.CarrierTrafficCutoffEnabled, "agreementExpiryDate": common.AgreementExpiryDate, "hasTestPeriod": common.HasTestPeriod, "testPeriodDays": common.TestPeriodDays, "testPeriodLatestExpiryDate": common.TestPeriodLatestExpiryDate, "hasSilentPeriod": common.HasSilentPeriod, "silentPeriodDays": common.SilentPeriodDays, "silentPeriodLatestExpiryDate": common.SilentPeriodLatestExpiryDate, } } func validateInboundCommon(common model.TSimCardInboundCommonFields) error { if strings.TrimSpace(common.ChanKey) == "" { return gerror.New("通道编号不能为空") } return nil } func pendingRangeInfo(in *model.TSimCardInboundRangeSubmitParam) (g.Map, error) { rangeType := strings.ToLower(strings.TrimSpace(in.RangeType)) if rangeType == "" { rangeType = "iccid" } if rangeType != "iccid" && rangeType != "msisdn" && rangeType != "imsi" { return nil, gerror.New("连号类型必须是 iccid、msisdn 或 imsi") } values := g.Map{ "rangeType": rangeType, "startIccid": strings.TrimSpace(in.StartIccid), "endIccid": strings.TrimSpace(in.EndIccid), "startMsisdn": strings.TrimSpace(in.StartMsisdn), "endMsisdn": strings.TrimSpace(in.EndMsisdn), "startImsi": strings.TrimSpace(in.StartImsi), "endImsi": strings.TrimSpace(in.EndImsi), } fieldNames := map[string][2]string{ "iccid": {"startIccid", "endIccid"}, "msisdn": {"startMsisdn", "endMsisdn"}, "imsi": {"startImsi", "endImsi"}, } for _, key := range fieldNames[rangeType] { if strings.TrimSpace(values[key].(string)) == "" { return nil, gerror.Newf("%s不能为空", key) } } return values, nil } func countNonEmptyLines(input string) int { count := 0 for _, line := range strings.Split(input, "\n") { if strings.TrimSpace(line) != "" { count++ } } return count } func inputInlineMaxRows(ctx context.Context) int { value := g.Cfg().MustGet(ctx, inputInlineMaxRowsConfigKey, consts.SimCardInboundDefaultInputInlineMaxRows).Int() if value <= 0 { return consts.SimCardInboundDefaultInputInlineMaxRows } return value } func readUploadContent(file *ghttp.UploadFile) ([]byte, error) { reader, err := file.Open() if err != nil { return nil, gerror.Wrap(err, "打开上传文件失败") } defer reader.Close() content, err := io.ReadAll(reader) if err != nil { return nil, gerror.Wrap(err, "读取上传文件失败") } return content, nil }