package iot import ( "context" "database/sql" "errors" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchmetadata" "strings" "time" "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/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/packageduration" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/simcardbatch/change" "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/iot_excelize" "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" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" "github.com/xuri/excelize/v2" ) const ( cardChangeEffectiveImmediate = "immediate" cardChangeEffectiveNextMonth = "next_month" ) type sIotTSimCardChange struct{} func NewIotTSimCardChange() *sIotTSimCardChange { return &sIotTSimCardChange{} } func init() { service.RegisterIotTSimCardChange(NewIotTSimCardChange()) } func (s *sIotTSimCardChange) DownloadExcelTemplate(ctx context.Context) error { f := excelize.NewFile() defer func() { _ = f.Close() }() headers := change.ExcelTemplateHeaders() if err := f.SetSheetRow("Sheet1", "A1", &headers); err != nil { return err } if err := iot_excelize.ApplyHeaderColumnsTextStyle(f); err != nil { return err } return iot_excelize.WriteHTTP(ctx, f, "sim_card_change_template") } // SubmitExcel Excel 仅作为 prepare source:构造 batch_type=4 的 Pending 变更预处理批次。 func (s *sIotTSimCardChange) SubmitExcel(ctx context.Context, in *model.TSimCardChangeExcelSubmitParam) (*model.TSimCardChangeExcelSubmitRes, error) { if in == nil { in = &model.TSimCardChangeExcelSubmitParam{} } file, err := s.getChangeUploadFile(ctx) if err != nil { return nil, err } excelProcessor := change.NewExcelProcessor() processor := change.NewPrepareProcessor() req := batchimport.SubmitRequest{ SourceChannel: batchmetadata.Console, ExecutionMode: batchmetadata.Bulk, File: file, SourceType: consts.BatchSourceTypeFile, Operator: currentOperator(ctx), BeforeSave: func(ctx context.Context, tx gdb.TX, request *batchimport.SubmitRequest, rows []batchimport.Row) error { opts := excelProcessor.ExcelSubmitOptions() if opts == nil { return gerror.New("excel change params are empty") } submitParam := opts.ToBatchSubmitParam() request.BizParams = submitParam changeRows, err := batchRowsToChangeRows(rows) if err != nil { return err } return s.prepareChangeRows(ctx, tx, request, processor, submitParam, changeRows) }, } result, err := batchimport.CreatePendingUpload(ctx, req, processor, batchimport.PendingOptions{ PrepareRows: func(ctx context.Context, rows []batchimport.Row) error { if len(rows) == 0 { return nil } return excelProcessor.ValidateRows(ctx, rows) }, }) if err != nil { return nil, err } res := changeExcelSubmitResult(result) if result.ImportStatus != consts.ImportStatusSuccess { return res, nil } if err = s.mergeChangeExcelBatchFileInfo(ctx, result.BatchNo, result.ImportFileOssId, result.ResultFileOssId, result.ResultDownloadId); err != nil { return nil, err } publishPendingExecute(ctx, result.BatchNo, consts.BatchTypePrepare, contexts.GetTenantId(ctx)) return res, nil } func (s *sIotTSimCardChange) PrepareBatch(ctx context.Context, in *model.TSimCardChangeBatchPrepareParam) (*model.TSimCardChangeBatchPrepareRes, error) { if in == nil { in = &model.TSimCardChangeBatchPrepareParam{} } in.SourceBatchNo = strings.TrimSpace(in.SourceBatchNo) if in.SourceBatchNo == "" { return nil, gerror.New("sourceBatchNo cannot be empty") } rows, err := s.buildChangeRows(ctx, in.SourceBatchNo) if err != nil { return nil, err } return s.prepareRows(ctx, &model.TSimCardChangeBatchSubmitParam{}, rows, consts.BatchSourceTypeBatchSelect, in.SourceBatchNo, in.SourceBatchNo, &model.TSimCardChangeBatchPrepareParam{ SourceBatchNo: in.SourceBatchNo, }) } func (s *sIotTSimCardChange) PrepareInput(ctx context.Context, in *model.TSimCardChangeInputPrepareParam) (*model.TSimCardChangeBatchPrepareRes, error) { if in == nil { in = &model.TSimCardChangeInputPrepareParam{} } rows, err := change.ParseInputRows(ctx, in.Input) if err != nil { return nil, err } changeRows, err := batchRowsToChangeRows(rows) if err != nil { return nil, err } return s.prepareRows(ctx, changeSubmitParamFromInput(in), changeRows, consts.BatchSourceTypeBatchInput, "", "input-prepare", nil) } func (s *sIotTSimCardChange) PrepareRange(ctx context.Context, in *model.TSimCardChangeRangePrepareParam) (*model.TSimCardChangeBatchPrepareRes, error) { if in == nil { in = &model.TSimCardChangeRangePrepareParam{} } rows, err := change.BuildRangeRows(ctx, in.StartIccid, in.EndIccid, in.StartMsisdn, in.EndMsisdn, in.StartImsi, in.EndImsi) if err != nil { return nil, err } changeRows, err := batchRowsToChangeRows(rows) if err != nil { return nil, err } return s.prepareRows(ctx, changeSubmitParamFromRange(in), changeRows, consts.BatchSourceTypeRange, "", "range-prepare", nil) } func (s *sIotTSimCardChange) InputOptions(ctx context.Context, in *model.TSimCardChangeInputOptionsParam) (*model.TSimCardChangeOptionsRes, error) { if in == nil { in = &model.TSimCardChangeInputOptionsParam{} } rows, err := change.ParseInputRows(ctx, in.Input) if err != nil { return nil, err } changeRows, err := batchRowsToChangeRows(rows) if err != nil { return nil, err } return s.changeOptions(ctx, changeRows, "") } func (s *sIotTSimCardChange) RangeOptions(ctx context.Context, in *model.TSimCardChangeRangeOptionsParam) (*model.TSimCardChangeOptionsRes, error) { if in == nil { in = &model.TSimCardChangeRangeOptionsParam{} } rows, err := change.BuildRangeRows(ctx, in.StartIccid, in.EndIccid, in.StartMsisdn, in.EndMsisdn, in.StartImsi, in.EndImsi) if err != nil { return nil, err } changeRows, err := batchRowsToChangeRows(rows) if err != nil { return nil, err } return s.changeOptions(ctx, changeRows, "") } func (s *sIotTSimCardChange) changeOptions(ctx context.Context, rows []*change.Row, sourceBatchNo string) (*model.TSimCardChangeOptionsRes, error) { if len(rows) == 0 { return nil, gerror.New("change cards cannot be empty") } processor := change.NewPrepareProcessor() operator := currentOperator(ctx) var prepared *change.PrepareContext err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { var err error prepared, err = processor.ValidatePrepareContext(ctx, tx, &model.TSimCardChangeBatchPrepareParam{ SourceBatchNo: strings.TrimSpace(sourceBatchNo), }, operator, rows) if err != nil { return err } return nil }) if err != nil { return nil, err } if prepared == nil || prepared.OldPackage == nil { return nil, gerror.New("change prepare result is incomplete") } packages, err := s.loadChangeablePackageOptions(ctx, prepared.CustomerAgentId, prepared.OldPackage) if err != nil { return nil, err } if len(packages) == 0 { return nil, gerror.New("current package has no available changeable packages") } return &model.TSimCardChangeOptionsRes{ CustomerAgentId: prepared.CustomerAgentId, CustomerName: prepared.CustomerName, OldPackageCode: prepared.OldPackage.PackageCode, OldPackageName: prepared.OldPackage.PackageName, OldPackageType: prepared.OldPackage.PackageType, Quantity: prepared.Quantity, Packages: packages, }, nil } func (s *sIotTSimCardChange) prepareRows(ctx context.Context, submitParam *model.TSimCardChangeBatchSubmitParam, rows []*change.Row, sourceType int, sourceBatchNo string, sourceName string, prepareParam *model.TSimCardChangeBatchPrepareParam) (*model.TSimCardChangeBatchPrepareRes, error) { if submitParam == nil { submitParam = &model.TSimCardChangeBatchSubmitParam{} } processor := change.NewPrepareProcessor() importRows := changeRowsToBatchRows(rows) req := batchimport.SubmitRequest{ SourceChannel: batchmetadata.Console, ExecutionMode: batchmetadata.Bulk, BizParams: prepareParamOrSubmitParam(prepareParam, submitParam), SourceType: sourceType, SourceBatchNo: strings.TrimSpace(sourceBatchNo), Operator: currentOperator(ctx), BeforeSave: func(ctx context.Context, tx gdb.TX, request *batchimport.SubmitRequest, batchRows []batchimport.Row) error { changeRows, err := batchRowsToChangeRows(batchRows) if err != nil { return err } return s.prepareChangeRows(ctx, tx, request, processor, submitParam, changeRows) }, } result, err := batchimport.CreatePendingRows(ctx, req, processor, importRows, batchimport.PendingOptions{ PrepareRows: func(ctx context.Context, rows []batchimport.Row) error { if len(rows) == 0 { return nil } return processor.ValidateRows(ctx, rows) }, }) if err != nil { return nil, err } res := &model.TSimCardChangeBatchPrepareRes{ BatchNo: result.BatchNo, BatchType: result.BatchType, TargetBatchType: consts.BatchTypeChange, Status: pendingStatus(result.ImportStatus), BatchAsyncStatus: consts.BatchAsyncStatusPending, RunResult: consts.BatchRunResultPending, Total: result.TotalRows, ValidCount: result.ValidRows, FailCount: result.ErrorRows, ResultDownloadId: result.ResultDownloadId, ResultFileOssId: result.ResultFileOssId, } if snapshot, ok := result.ExecutionResult.(*model.TSimCardChangePrepareSnapshot); ok && snapshot != nil { fillChangePrepareRes(res, snapshot) } publishPendingExecute(ctx, res.BatchNo, res.BatchType, contexts.GetTenantId(ctx)) return res, nil } func (s *sIotTSimCardChange) prepareChangeRows(ctx context.Context, tx gdb.TX, request *batchimport.SubmitRequest, processor *change.PrepareProcessor, submitParam *model.TSimCardChangeBatchSubmitParam, rows []*change.Row) error { prepared, err := processor.ValidatePrepareContext(ctx, tx, &model.TSimCardChangeBatchPrepareParam{ SourceBatchNo: request.SourceBatchNo, }, request.Operator, rows) if err != nil { return err } if prepared == nil || prepared.CustomerAgentId <= 0 || prepared.OldPackage == nil || strings.TrimSpace(prepared.OldPackage.PackageCode) == "" { return gerror.New("change prepare result is incomplete") } if submitParam != nil && strings.TrimSpace(submitParam.NewPackageCode) != "" { if strings.TrimSpace(submitParam.NewPackageCode) == strings.TrimSpace(prepared.OldPackage.PackageCode) { return gerror.New("newPackageCode cannot equal current package") } if !containsStringJSON(prepared.OldPackage.ChangeablePackageCodes, submitParam.NewPackageCode) { return gerror.New("newPackageCode is not in current package changeable range") } } request.TargetDeptId = prepared.CustomerAgentId request.ExecutionResult = &model.TSimCardChangePrepareSnapshot{ SourceBatchNo: strings.TrimSpace(request.SourceBatchNo), CustomerAgentId: prepared.CustomerAgentId, CustomerName: prepared.CustomerName, OldPackageCode: prepared.OldPackage.PackageCode, OldPackageName: prepared.OldPackage.PackageName, OldPackageType: prepared.OldPackage.PackageType, OldPackageFlowMb: prepared.OldPackage.FlowMb, Quantity: prepared.Quantity, } return nil } // SubmitBatch 作为 confirm 的语义:CAS 转换 batch_type=4+Success -> batch_type=102 Pending,发布异步任务。 func (s *sIotTSimCardChange) SubmitBatch(ctx context.Context, in *model.TSimCardChangeBatchSubmitParam) (*model.TSimCardChangeBatchSubmitRes, error) { if in == nil { in = &model.TSimCardChangeBatchSubmitParam{} } in.BatchNo = strings.TrimSpace(in.BatchNo) in.NewPackageCode = strings.TrimSpace(in.NewPackageCode) in.EffectiveMode = strings.TrimSpace(in.EffectiveMode) if in.BatchNo == "" { return nil, gerror.New("batchNo cannot be empty") } if in.NewPackageCode == "" { return nil, gerror.New("newPackageCode cannot be empty") } if in.DurationMonths <= 0 { return nil, gerror.New("durationMonths must be greater than 0") } if in.GiftDurationMonths < 0 { return nil, gerror.New("giftDurationMonths cannot be negative") } if !in.OfflineContractCompleted { return nil, gerror.New("please confirm offline contract is completed") } packageBeginTime, err := calcChangeBeginTime(in.EffectiveMode) if err != nil { return nil, err } operator := currentOperator(ctx) var prepareSnapshot *model.TSimCardChangePrepareSnapshot var priceSnapshot *model.TSimCardChangePriceSnapshot var converted *change.AsyncConfirmResult var sourceBatchNo string err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { batch, err := s.lockPrepareBatchForConfirm(ctx, tx, in.BatchNo) if err != nil { return err } prepareSnapshot, err = changePrepareSnapshotFromBatch(batch) if err != nil { return err } priceSnapshot, err = changePriceSnapshotFromBatch(batch) if err != nil { return err } priceSnapshot, err = s.refreshChangeConfirmPriceSnapshot(ctx, tx, prepareSnapshot, priceSnapshot) if err != nil { return err } sourceBatchNo = prepareSnapshot.SourceBatchNo if err = validateChangeConfirmContract( prepareSnapshot, priceSnapshot, jsonString(batch.BatchInfo, "effectiveMode", "changePrepareSnapshot.effectiveMode"), in, ); err != nil { return err } converted, err = change.ConvertPrepareToAsync(ctx, tx, &change.AsyncConfirmInput{ BatchNo: in.BatchNo, TargetBatchType: consts.BatchTypeChange, NewPackageCode: priceSnapshot.NewPackageCode, DurationMonths: in.DurationMonths, GiftDurationMonths: in.GiftDurationMonths, EffectiveMode: in.EffectiveMode, OfflineContract: in.OfflineContractCompleted, BuyerNote: in.BuyerNote, AdminNote: in.AdminNote, Remark: in.Remark, PriceSnapshot: priceSnapshot, SourceBatchNo: prepareSnapshot.SourceBatchNo, PackageBeginTimeStr: formatChangeTime(packageBeginTime), Operator: operator, }) return err }) if err != nil { return nil, err } if converted == nil || prepareSnapshot == nil || priceSnapshot == nil { return nil, gerror.New("change submit result is empty") } publishPendingExecute(ctx, converted.BatchNo, converted.BatchType, contexts.GetTenantId(ctx)) return &model.TSimCardChangeBatchSubmitRes{ BatchNo: converted.BatchNo, BatchType: converted.BatchType, BatchAsyncStatus: converted.BatchAsyncStatus, SourceBatchNo: sourceBatchNo, Status: "pending", NewPackageCode: priceSnapshot.NewPackageCode, NewPackageName: priceSnapshot.NewPackageName, OldPackageCode: prepareSnapshot.OldPackageCode, OldPackageName: prepareSnapshot.OldPackageName, DurationMonths: in.DurationMonths, GiftDurationMonths: in.GiftDurationMonths, EffectiveMode: in.EffectiveMode, PackageBeginTime: packageBeginTime, PackageEndTime: nil, }, nil } func validateChangeConfirmContract( prepareSnapshot *model.TSimCardChangePrepareSnapshot, priceSnapshot *model.TSimCardChangePriceSnapshot, preparedEffectiveMode string, in *model.TSimCardChangeBatchSubmitParam, ) error { if prepareSnapshot == nil || priceSnapshot == nil || in == nil { return gerror.New("change prepare contract is incomplete") } if strings.TrimSpace(in.NewPackageCode) == strings.TrimSpace(prepareSnapshot.OldPackageCode) { return gerror.New("newPackageCode cannot equal current package") } if strings.TrimSpace(in.NewPackageCode) != strings.TrimSpace(priceSnapshot.NewPackageCode) { return gerror.New("newPackageCode differs from prepare contract, please prepare again") } if !containsInt(priceSnapshot.RenewableDurationOptions, in.DurationMonths) { return gerror.New("durationMonths is not allowed for target package") } preparedEffectiveMode = strings.TrimSpace(preparedEffectiveMode) if preparedEffectiveMode != "" && preparedEffectiveMode != strings.TrimSpace(in.EffectiveMode) { return gerror.New("effectiveMode differs from prepare contract, please prepare again") } return nil } // PrepareResult 查询预处理批次当前异步状态。 func (s *sIotTSimCardChange) PrepareResult(ctx context.Context, batchNo string) (*model.TSimCardChangePrepareResultRes, error) { return loadPrepareResultChange(ctx, batchNo) } func (s *sIotTSimCardChange) refreshChangeConfirmPriceSnapshot(ctx context.Context, tx gdb.TX, prepareSnapshot *model.TSimCardChangePrepareSnapshot, priceSnapshot *model.TSimCardChangePriceSnapshot) (*model.TSimCardChangePriceSnapshot, error) { if prepareSnapshot == nil || priceSnapshot == nil { return nil, gerror.New("change prepare contract is incomplete") } oldPackageCode := strings.TrimSpace(prepareSnapshot.OldPackageCode) newPackageCode := strings.TrimSpace(priceSnapshot.NewPackageCode) if oldPackageCode == "" || newPackageCode == "" { return nil, gerror.New("change package contract is incomplete") } if oldPackageCode == newPackageCode { return nil, gerror.New("newPackageCode cannot equal current package") } oldPackage := &entity.TSimFlowPackageInfo{} columns := dao.TSimFlowPackageInfo.Columns() if err := tx.Ctx(ctx).Model(dao.TSimFlowPackageInfo.Table()). Where(columns.TenantId, contexts.GetTenantId(ctx)). Where(columns.PackageCode, oldPackageCode). Where(columns.Enabled, 1). LockUpdate(). Scan(oldPackage); err != nil { return nil, gerror.Wrap(err, "load current package failed") } if oldPackage.Id == 0 { return nil, gerror.New("current package not found or disabled") } if !containsStringJSON(oldPackage.ChangeablePackageCodes, newPackageCode) { return nil, gerror.Newf("target package %s is not in current package %s changeable range", newPackageCode, oldPackageCode) } refreshed, err := s.buildChangePriceSnapshot(ctx, tx, priceSnapshot.CustomerAgentId, newPackageCode, prepareSnapshot.OldPackageType) if err != nil { return nil, err } return refreshed, nil } func loadPrepareResultChange(ctx context.Context, batchNo string) (*model.TSimCardChangePrepareResultRes, error) { batchNo = strings.TrimSpace(batchNo) if batchNo == "" { return nil, gerror.New("批次号不能为空") } columns := dao.BatchMain.Columns() batch := &entity.BatchMain{} if err := dao.BatchMain.Ctx(ctx). Where(columns.BatchNo, batchNo). Where(columns.TenantId, contexts.GetTenantId(ctx)). Where(columns.CreatedDept, contexts.GetDeptId(ctx)). Scan(batch); err != nil { return nil, gerror.Wrap(err, "查询预处理批次失败") } if batch.Id == 0 { return nil, gerror.New("预处理批次不存在或不可见") } if batch.BatchType != consts.BatchTypePrepare && !(batch.BatchInfo != nil && batch.BatchInfo.Get("targetBatchType").Int() == consts.BatchTypeChange) { return nil, gerror.New("批次类型不匹配") } asyncStatus := consts.BatchAsyncStatusPending runResult := consts.BatchRunResultPending statusKey := "pending" switch { case batch.BatchAsyncStatus == consts.BatchAsyncStatusProcessing || batch.RunResult == consts.BatchRunResultRunning: asyncStatus = consts.BatchAsyncStatusProcessing runResult = consts.BatchRunResultRunning statusKey = "processing" case batch.BatchAsyncStatus == consts.BatchAsyncStatusSuccess && batch.RunResult == consts.BatchRunResultSuccess: asyncStatus = consts.BatchAsyncStatusSuccess runResult = consts.BatchRunResultSuccess statusKey = "success" case batch.BatchAsyncStatus == consts.BatchAsyncStatusFailed || batch.RunResult == consts.BatchRunResultFailed: asyncStatus = consts.BatchAsyncStatusFailed runResult = consts.BatchRunResultFailed statusKey = "failed" case batch.BatchAsyncStatus == consts.BatchAsyncStatusPartialSuccess || batch.RunResult == consts.BatchRunResultPartialSuccess: asyncStatus = consts.BatchAsyncStatusPartialSuccess runResult = consts.BatchRunResultPartialSuccess statusKey = "partial_success" } fields := buildChangePrepareFields(batch) res := &model.TSimCardChangePrepareResultRes{ BatchNo: batch.BatchNo, BatchType: batch.BatchType, TargetBatchType: consts.BatchTypeChange, Status: statusKey, BatchAsyncStatus: asyncStatus, RunResult: runResult, Total: batch.TotalCount, SuccessCount: batch.SuccessCount, FailCount: batch.FailCount, FailReason: strings.TrimSpace(batch.FailReason), StartTime: batch.StartTime, EndTime: batch.EndTime, Snapshot: gjson.New(fields), } if fields != nil { res.CustomerAgentId = gconv.Int64(fields["customerAgentId"]) res.CustomerName = strings.TrimSpace(gconv.String(fields["customerName"])) res.PackageCode = strings.TrimSpace(gconv.String(fields["newPackageCode"])) res.PackageName = strings.TrimSpace(gconv.String(fields["newPackageName"])) res.OldPackageCode = strings.TrimSpace(gconv.String(fields["oldPackageCode"])) res.OldPackageName = strings.TrimSpace(gconv.String(fields["oldPackageName"])) res.OldPackageType = gconv.Int(fields["oldPackageType"]) res.OldPackageFlowMb = gconv.Float64(fields["oldPackageFlowMb"]) res.Quantity = gconv.Int(fields["quantity"]) res.PriceSource = strings.TrimSpace(gconv.String(fields["priceSource"])) res.AgentDefaultPriceId = gconv.Int64(fields["agentDefaultPriceId"]) res.TargetPriceId = gconv.Int64(fields["targetPriceId"]) res.ChangeUnitPrice = gconv.Float64(fields["changeUnitPrice"]) res.RenewableDurationOptions = gconv.Ints(fields["renewableDurationOptions"]) } return res, nil } func (s *sIotTSimCardChange) buildChangeRows(ctx context.Context, sourceBatchNo string) ([]*change.Row, error) { rows, err := change.NewRepository().GetSourceBindRows(ctx, sourceBatchNo) if err != nil { return nil, gerror.Wrap(err, "load source order batch card details failed") } if len(rows) == 0 { return nil, gerror.New("source order batch has no changeable cards") } return rows, nil } func (s *sIotTSimCardChange) lockPrepareBatchForConfirm(ctx context.Context, tx gdb.TX, batchNo string) (*entity.BatchMain, error) { columns := dao.BatchMain.Columns() batch := &entity.BatchMain{} err := tx.Ctx(ctx).Model(dao.BatchMain.Table()). Where(columns.BatchNo, batchNo). Where(columns.BatchType, consts.BatchTypePrepare). Where(columns.BatchAsyncStatus, consts.BatchAsyncStatusSuccess). Where(columns.RunResult, consts.BatchRunResultSuccess). Where(columns.TenantId, contexts.GetTenantId(ctx)). Where(columns.CreatedDept, contexts.GetDeptId(ctx)). LockUpdate(). Scan(batch) if err != nil { return nil, gerror.Wrap(err, "load change prepare batch failed") } if batch.Id == 0 { return nil, gerror.New("change prepare batch not found or not yet completed") } if batch.BatchInfo == nil { return nil, gerror.New("change prepare batch info is missing") } if batch.BatchInfo.Get("targetBatchType").Int() != consts.BatchTypeChange { return nil, gerror.New("batch is not a sim card change prepare batch") } return batch, nil } func (s *sIotTSimCardChange) buildChangePriceSnapshot(ctx context.Context, tx gdb.TX, customerAgentId int64, packageCode string, oldPackageType int) (*model.TSimCardChangePriceSnapshot, error) { packageCode = strings.TrimSpace(packageCode) if customerAgentId <= 0 { return nil, gerror.New("customerAgentId cannot be empty") } if packageCode == "" { return nil, gerror.New("newPackageCode cannot be empty") } pkg := &entity.TSimFlowPackageInfo{} pkgColumns := dao.TSimFlowPackageInfo.Columns() if err := tx.Ctx(ctx).Model(dao.TSimFlowPackageInfo.Table()). Where(pkgColumns.TenantId, contexts.GetTenantId(ctx)). Where(pkgColumns.PackageCode, packageCode). Where(pkgColumns.Enabled, 1). Scan(pkg); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, gerror.New("target package not found or disabled") } return nil, gerror.Wrap(err, "load target package failed") } if pkg.Id == 0 { return nil, gerror.New("target package not found or disabled") } if oldPackageType > 0 && pkg.PackageType != oldPackageType { return nil, gerror.New("target package type does not match current package type") } options, err := packageduration.ChangeMonths(pkg) if err != nil { return nil, err } snapshot := &model.TSimCardChangePriceSnapshot{ CustomerAgentId: customerAgentId, NewPackageCode: pkg.PackageCode, NewPackageName: pkg.PackageName, NewPackageType: pkg.PackageType, NewPackageFlowMb: pkg.FlowMb, PriceSource: "system", ChangeUnitPrice: roundAmount(pkg.CostPrice*normalizePriceRatio(pkg.SysDefaultRatio) + pkg.SysDefaultProfit), RenewableDurationOptions: options, } priceOwnerDeptID, err := loadPackagePriceParentDeptID(ctx, tx, contexts.GetTenantId(ctx), customerAgentId) if err != nil { return nil, err } salePrice, err := resolvePackageEffectiveSalePrice(ctx, tx, contexts.GetTenantId(ctx), priceOwnerDeptID, customerAgentId, packagePriceSourceFromEntity(pkg)) if err != nil { return nil, err } snapshot.PriceSource = salePrice.Rule.PriceSource snapshot.AgentDefaultPriceId = salePrice.Rule.AgentDefaultPriceId snapshot.TargetPriceId = salePrice.Rule.TargetPriceId snapshot.ChangeUnitPrice = salePrice.AgentShipmentPrice return snapshot, nil } func (s *sIotTSimCardChange) loadChangeablePackageOptions(ctx context.Context, customerAgentId int64, oldPackage *entity.TSimFlowPackageInfo) ([]model.TSimCardChangePackageOption, error) { if oldPackage == nil { return nil, gerror.New("current package is empty") } codes := stringListFromJSON(oldPackage.ChangeablePackageCodes) if len(codes) == 0 { return nil, gerror.New("current package has no changeable packages configured") } options := make([]model.TSimCardChangePackageOption, 0, len(codes)) err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { for _, code := range codes { if code == strings.TrimSpace(oldPackage.PackageCode) { continue } snapshot, err := s.buildChangePriceSnapshot(ctx, tx, customerAgentId, code, oldPackage.PackageType) if err != nil { continue } options = append(options, model.TSimCardChangePackageOption{ PackageCode: snapshot.NewPackageCode, PackageName: snapshot.NewPackageName, PackageType: snapshot.NewPackageType, FlowMb: snapshot.NewPackageFlowMb, ChangeUnitPrice: snapshot.ChangeUnitPrice, RenewableDurationOptions: snapshot.RenewableDurationOptions, }) } return nil }) if err != nil { return nil, err } return options, nil } func (s *sIotTSimCardChange) loadChangePriceOverride(ctx context.Context, tx gdb.TX, scopeType int, targetAgentId int64, packageCode string) (*entity.TSimFlowPackagePrice, error) { columns := dao.TSimFlowPackagePrice.Columns() price := &entity.TSimFlowPackagePrice{} err := tx.Ctx(ctx).Model(dao.TSimFlowPackagePrice.Table()). Where(columns.TenantId, contexts.GetTenantId(ctx)). Where(columns.DeptId, contexts.GetDeptId(ctx)). Where(columns.AgentScopeType, scopeType). Where(columns.TargetAgentId, targetAgentId). Where(columns.PackageCode, packageCode). Scan(price) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil } return nil, gerror.Wrap(err, "load change package price failed") } if price.Id == 0 { return nil, nil } return price, nil } func batchRowsToChangeRows(rows []batchimport.Row) ([]*change.Row, error) { result := make([]*change.Row, 0, len(rows)) for _, item := range rows { row, ok := item.(*change.Row) if !ok || row == nil { return nil, gerror.New("change row type is invalid") } result = append(result, row) } return result, nil } func changeRowsToBatchRows(rows []*change.Row) []batchimport.Row { result := make([]batchimport.Row, 0, len(rows)) for _, row := range rows { result = append(result, row) } return result } func fillChangePrepareRes(res *model.TSimCardChangeBatchPrepareRes, snapshot *model.TSimCardChangePrepareSnapshot) { res.SourceBatchNo = snapshot.SourceBatchNo res.CustomerAgentId = snapshot.CustomerAgentId res.CustomerName = snapshot.CustomerName res.OldPackageCode = snapshot.OldPackageCode res.OldPackageName = snapshot.OldPackageName res.OldPackageType = snapshot.OldPackageType res.OldPackageFlowMb = snapshot.OldPackageFlowMb } func changePrepareSnapshotFromBatch(batch *entity.BatchMain) (*model.TSimCardChangePrepareSnapshot, error) { if batch == nil || batch.BatchInfo == nil { return nil, gerror.New("change prepare batch info is missing") } fields := buildChangePrepareFields(batch) snapshot := &model.TSimCardChangePrepareSnapshot{ SourceBatchNo: normalizedChangePrepareSourceBatchNo(batch, gconv.String(fields["sourceBatchNo"])), CustomerAgentId: gconv.Int64(fields["customerAgentId"]), CustomerName: strings.TrimSpace(gconv.String(fields["customerName"])), OldPackageCode: strings.TrimSpace(gconv.String(fields["oldPackageCode"])), OldPackageName: strings.TrimSpace(gconv.String(fields["oldPackageName"])), OldPackageType: gconv.Int(fields["oldPackageType"]), OldPackageFlowMb: gconv.Float64(fields["oldPackageFlowMb"]), Quantity: gconv.Int(fields["quantity"]), } if snapshot.OldPackageCode == "" { return nil, gerror.New("change prepare batch info is incomplete") } return snapshot, nil } func normalizedChangePrepareSourceBatchNo(batch *entity.BatchMain, sourceBatchNo string) string { if batch == nil || batch.SourceType != consts.BatchSourceTypeBatchSelect { return "" } batchNo := strings.TrimSpace(batch.BatchNo) sourceBatchNo = strings.TrimSpace(sourceBatchNo) if sourceBatchNo == "" || sourceBatchNo == batchNo { sourceBatchNo = strings.TrimSpace(batch.SourceBatchNo) } if sourceBatchNo == batchNo { return "" } return sourceBatchNo } func changePriceSnapshotFromBatch(batch *entity.BatchMain) (*model.TSimCardChangePriceSnapshot, error) { if batch == nil || batch.BatchInfo == nil { return nil, gerror.New("change prepare batch price info is missing") } fields := buildChangePrepareFields(batch) options := gconv.Ints(fields["renewableDurationOptions"]) snapshot := &model.TSimCardChangePriceSnapshot{ CustomerAgentId: gconv.Int64(fields["customerAgentId"]), NewPackageCode: strings.TrimSpace(gconv.String(fields["newPackageCode"])), NewPackageName: strings.TrimSpace(gconv.String(fields["newPackageName"])), NewPackageType: gconv.Int(fields["newPackageType"]), NewPackageFlowMb: gconv.Float64(fields["newPackageFlowMb"]), PriceSource: strings.TrimSpace(gconv.String(fields["priceSource"])), AgentDefaultPriceId: gconv.Int64(fields["agentDefaultPriceId"]), TargetPriceId: gconv.Int64(fields["targetPriceId"]), ChangeUnitPrice: roundAmount(gconv.Float64(fields["changeUnitPrice"])), RenewableDurationOptions: options, } if snapshot.CustomerAgentId <= 0 || snapshot.NewPackageCode == "" || snapshot.ChangeUnitPrice < 0 || len(snapshot.RenewableDurationOptions) == 0 { return nil, gerror.New("change prepare batch price info is incomplete") } return snapshot, nil } func buildChangePrepareFields(batch *entity.BatchMain) map[string]any { if batch == nil { return nil } fields := prepareInfoMap(batch.BatchInfo) if fields == nil { fields = map[string]any{} } if value := jsonField(batch.BatchInfo, "customerAgentId", "customer.deptId", "changePrepareSnapshot.customerAgentId", "changePriceSnapshot.customerAgentId"); value != nil { fields["customerAgentId"] = gconv.Int64(value) } if value := jsonField(batch.BatchInfo, "customerName", "customer.name", "changePrepareSnapshot.customerName"); value != nil { fields["customerName"] = strings.TrimSpace(value.String()) } if value := jsonField(batch.BatchInfo, "sourceBatchNo", "changePrepareSnapshot.sourceBatchNo"); value != nil { fields["sourceBatchNo"] = strings.TrimSpace(value.String()) } if value := jsonField(batch.BatchInfo, "oldPackageCode", "changePrepareSnapshot.oldPackageCode"); value != nil { fields["oldPackageCode"] = strings.TrimSpace(value.String()) } if value := jsonField(batch.BatchInfo, "oldPackageName", "changePrepareSnapshot.oldPackageName"); value != nil { fields["oldPackageName"] = strings.TrimSpace(value.String()) } if value := jsonField(batch.BatchInfo, "oldPackageType", "changePrepareSnapshot.oldPackageType"); value != nil { fields["oldPackageType"] = gconv.Int(value) } if value := jsonField(batch.BatchInfo, "oldPackageFlowMb", "changePrepareSnapshot.oldPackageFlowMb"); value != nil { fields["oldPackageFlowMb"] = gconv.Float64(value) } if value := jsonField(batch.BatchInfo, "newPackageCode", "changePriceSnapshot.newPackageCode"); value != nil { fields["newPackageCode"] = strings.TrimSpace(value.String()) fields["packageCode"] = strings.TrimSpace(value.String()) } if value := jsonField(batch.BatchInfo, "newPackageName", "changePriceSnapshot.newPackageName"); value != nil { fields["newPackageName"] = strings.TrimSpace(value.String()) fields["packageName"] = strings.TrimSpace(value.String()) } if value := jsonField(batch.BatchInfo, "newPackageType", "changePriceSnapshot.newPackageType"); value != nil { fields["newPackageType"] = gconv.Int(value) } if value := jsonField(batch.BatchInfo, "newPackageFlowMb", "changePriceSnapshot.newPackageFlowMb"); value != nil { fields["newPackageFlowMb"] = gconv.Float64(value) } if value := jsonField(batch.BatchInfo, "priceSource", "changePriceSnapshot.priceSource"); value != nil { fields["priceSource"] = strings.TrimSpace(value.String()) } if value := jsonField(batch.BatchInfo, "agentDefaultPriceId", "changePriceSnapshot.agentDefaultPriceId"); value != nil { fields["agentDefaultPriceId"] = gconv.Int64(value) } if value := jsonField(batch.BatchInfo, "targetPriceId", "changePriceSnapshot.targetPriceId"); value != nil { fields["targetPriceId"] = gconv.Int64(value) } if value := jsonField(batch.BatchInfo, "changeUnitPrice", "changePriceSnapshot.changeUnitPrice"); value != nil { fields["changeUnitPrice"] = roundAmount(gconv.Float64(value)) } if values := jsonInts(batch.BatchInfo, "renewableDurationOptions", "changePriceSnapshot.renewableDurationOptions"); len(values) > 0 { fields["renewableDurationOptions"] = values } if value := jsonField(batch.BatchInfo, "quantity", "changePrepareSnapshot.quantity"); value != nil { fields["quantity"] = gconv.Int(value) } else if batch.TotalCount > 0 { fields["quantity"] = batch.TotalCount } return fields } func containsStringJSON(jsonValue any, target string) bool { target = strings.TrimSpace(target) if target == "" || jsonValue == nil { return false } var values []any switch value := jsonValue.(type) { case interface{ Interfaces() []any }: values = value.Interfaces() default: values = gconv.SliceAny(value) } for _, item := range values { if strings.TrimSpace(gconv.String(item)) == target { return true } } return false } func stringListFromJSON(jsonValue any) []string { if jsonValue == nil { return nil } var values []any switch value := jsonValue.(type) { case interface{ Interfaces() []any }: values = value.Interfaces() default: values = gconv.SliceAny(value) } if len(values) == 0 { return nil } seen := make(map[string]struct{}, len(values)) result := make([]string, 0, len(values)) for _, item := range values { value := strings.TrimSpace(gconv.String(item)) if value == "" { continue } if _, ok := seen[value]; ok { continue } seen[value] = struct{}{} result = append(result, value) } return result } func calcChangeBeginTime(effectiveMode string) (*gtime.Time, error) { switch strings.TrimSpace(effectiveMode) { case cardChangeEffectiveImmediate: return gtime.Now(), nil case cardChangeEffectiveNextMonth: now := time.Now() nextMonth := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()) return gtime.NewFromTime(nextMonth), nil default: return nil, gerror.New("effectiveMode is unsupported") } } func changeSubmitParamFromInput(in *model.TSimCardChangeInputPrepareParam) *model.TSimCardChangeBatchSubmitParam { if in == nil { return &model.TSimCardChangeBatchSubmitParam{} } return &model.TSimCardChangeBatchSubmitParam{ NewPackageCode: strings.TrimSpace(in.NewPackageCode), DurationMonths: in.DurationMonths, GiftDurationMonths: in.GiftDurationMonths, EffectiveMode: strings.TrimSpace(in.EffectiveMode), OfflineContractCompleted: in.OfflineContractCompleted, } } func changeSubmitParamFromRange(in *model.TSimCardChangeRangePrepareParam) *model.TSimCardChangeBatchSubmitParam { if in == nil { return &model.TSimCardChangeBatchSubmitParam{} } return &model.TSimCardChangeBatchSubmitParam{ NewPackageCode: strings.TrimSpace(in.NewPackageCode), DurationMonths: in.DurationMonths, GiftDurationMonths: in.GiftDurationMonths, EffectiveMode: strings.TrimSpace(in.EffectiveMode), OfflineContractCompleted: in.OfflineContractCompleted, } } func prepareParamOrSubmitParam(prepareParam *model.TSimCardChangeBatchPrepareParam, submitParam *model.TSimCardChangeBatchSubmitParam) any { if prepareParam != nil { return prepareParam } return submitParam } func (s *sIotTSimCardChange) getChangeUploadFile(ctx context.Context) (*ghttp.UploadFile, error) { r := g.RequestFromCtx(ctx) if r == nil { return nil, gerror.New("current context is not an HTTP request") } file := r.GetUploadFile("file") if file == nil { return nil, gerror.New("please select upload file") } return file, nil } func changeExcelSubmitResult(result *batchimport.Result) *model.TSimCardChangeExcelSubmitRes { res := &model.TSimCardChangeExcelSubmitRes{ BatchType: consts.BatchTypePrepare, TargetBatchType: consts.BatchTypeChange, Status: pendingStatus(result.ImportStatus), BatchAsyncStatus: consts.BatchAsyncStatusPending, RunResult: consts.BatchRunResultPending, Total: result.TotalRows, } if result == nil { return res } res.BatchNo = result.BatchNo res.ResultDownloadId = result.ResultDownloadId res.ResultFileOssId = result.ResultFileOssId return res } func (s *sIotTSimCardChange) mergeChangeExcelBatchFileInfo(ctx context.Context, batchNo string, importFileOssId, resultFileOssId, resultDownloadId int64) error { operator := currentOperator(ctx) columns := dao.BatchMain.Columns() batch := &entity.BatchMain{} if err := dao.BatchMain.Ctx(ctx). Where(columns.BatchNo, strings.TrimSpace(batchNo)). Where(columns.TenantId, operator.TenantId). Where(columns.CreatedDept, operator.DeptId). Scan(batch); err != nil { return gerror.Wrap(err, "load change batch failed") } if batch.Id == 0 { return gerror.New("change batch not found") } info := changeExcelMergeBatchInfo(batch.BatchInfo, importFileOssId, resultFileOssId, resultDownloadId) if _, err := dao.BatchMain.Ctx(ctx). Where(columns.BatchNo, strings.TrimSpace(batchNo)). Data(do.BatchMain{ SourceOssId: batchFileOssIDValue(importFileOssId), ResultOssId: batchFileOssIDValue(resultFileOssId), BatchInfo: info, UpdatedBy: operator.UserId, UpdatedAt: gtime.Now(), }). Update(); err != nil { return gerror.Wrap(err, "update change batch file info failed") } return nil } func changeExcelMergeBatchInfo(info *gjson.Json, importFileOssId, resultFileOssId, resultDownloadId int64) *gjson.Json { data := g.Map{} if info != nil { for key, value := range info.Map() { data[key] = value } } if importFileOssId > 0 { data["importFileOssId"] = importFileOssId } if resultFileOssId > 0 { data["resultFileOssId"] = resultFileOssId } if resultDownloadId > 0 { data["resultDownloadId"] = resultDownloadId } return gjson.New(data) } func batchFileOssIDValue(ossID int64) any { if ossID <= 0 { return nil } return ossID } func trimChangeExcelFailReason(cause error) string { if cause == nil { return "change confirm failed" } reason := strings.TrimSpace(cause.Error()) if reason == "" { return "change confirm failed" } const maxLen = 500 runes := []rune(reason) if len(runes) <= maxLen { return reason } return string(runes[:maxLen]) } func formatChangeTime(t *gtime.Time) string { if t == nil { return "" } return t.Format("Y-m-d H:i:s") }