package app import ( "context" "fmt" "sort" "strings" "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchmetadata" "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/batchimport" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do" "git.5iot.com/iotlcm-public/iotlcm-app/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/os/gtime" ) type appRenewBatchContext struct { Items []*appRenewPackageContext CurrentPackage *entity.TSimFlowPackageInfo Package *entity.TSimFlowPackageInfo Price *appRenewPriceSnapshot Party *appRenewOrderParty Durations []int OwnerType string RenewablePackageCodes []string RenewableTargets []*appRenewTargetContext } type appRenewPrepareBatchSnapshot struct { CustomerAgentId int64 CurrentPackageCode string CurrentPackageName string PackageCode string PackageName string PriceSource string AgentDefaultPriceId int64 TargetPriceId int64 RenewUnitPrice float64 RenewableDurationItems []int RenewablePackageCodes []string Targets map[string]*appRenewPrepareTargetSnapshot Quantity int } type appRenewPrepareTargetSnapshot struct { PackageCode string PackageName string RenewUnitPrice float64 PriceSource string AgentDefaultPriceId int64 TargetPriceId int64 Durations []int } // 批量续费约束做预校验: // 同一批次必须同当前套餐、同客户归属、同定价,才能生成统一续费档位。 func (s *sAppCard) RenewBatchPrepare(ctx context.Context, param *model.AppCardRenewBatchPrepareParam) (res *model.AppCardRenewPrepareModel, err error) { if param == nil { param = &model.AppCardRenewBatchPrepareParam{} } user, err := appCurrentUser(ctx) if err != nil { return nil, err } batch, err := s.buildRenewBatchContext(ctx, user, param.CardValues, param.PackageCode) if err != nil { return nil, err } ctx = appContextWithUser(ctx, user) processor := newAppRenewBatchProcessor(batch) rows := appRenewBatchRows(batch) req := batchimport.SubmitRequest{ BizParams: param, SourceType: consts.BatchSourceTypeBatchSelect, SourceChannel: batchmetadata.MiniProgram, ExecutionMode: batchmetadata.Bulk, BatchType: consts.BatchTypePrepare, FailureBatchType: consts.BatchTypeRenew, SkipExecute: true, TargetDeptId: batch.Party.CustomerAgentId, Operator: batchimport.OperatorContext{ TenantId: user.TenantId, DeptId: batch.Party.CreatedDept, UserId: user.UserId, }, } result, err := batchimport.RunRows(ctx, req, processor, rows, "miniapp-renew-prepare") if err != nil { return nil, gerror.Wrap(err, "写入续费预处理批次失败") } if result == nil || result.ImportStatus != consts.ImportStatusSuccess { failReason := "续费预处理批次校验失败" if result != nil && strings.TrimSpace(result.FailReason) != "" { failReason = strings.TrimSpace(result.FailReason) } return nil, gerror.New(failReason) } res = s.buildRenewBatchPrepareModel(ctx, user, batch) res.BatchNo = result.BatchNo res.BatchType = consts.BatchTypePrepare return res, nil } // RenewBatchSubmit 确认续费预处理批次,并在同一事务内完成余额扣款和批量续费落库。 // 批量续费必须先走预处理校验,避免卡片、套餐或价格在提交时发生变化。 func (s *sAppCard) RenewBatchSubmit(ctx context.Context, param *model.AppCardRenewBatchSubmitParam) (res *model.AppCardRenewSubmitModel, err error) { if param == nil { param = &model.AppCardRenewBatchSubmitParam{} } user, err := appCurrentUser(ctx) if err != nil { return nil, err } if len(param.Items) > 0 { return nil, gerror.New("批量续费请先校验生成预处理批次,再提交续费批次") } if strings.TrimSpace(param.BatchNo) != "" { return s.renewBatchSubmitPrepared(ctx, user, param) } batch, err := s.buildRenewBatchContext(ctx, user, param.CardValues, param.PackageCode) if err != nil { return nil, err } duration := appRenewDurationSelectionFromParam(param.DurationYears, param.DurationMonths, batch.Durations) if duration.Years <= 0 || duration.Months <= 0 { return nil, gerror.New("请选择续费年限") } if !appContainsInt(batch.Durations, duration.Years) { return nil, gerror.New("续费时长不在目标套餐可续费范围内") } quantity := len(batch.Items) singleCardAmount := appRenewTotalAmount(batch.Price.UnitPrice, duration.Years, 1) totalAmount := appRoundAmount(singleCardAmount * float64(quantity)) now := gtime.Now() var orderId int64 var orderSn string var batchNo string var minBeginAt *gtime.Time var maxEndAt *gtime.Time var cardPeriods []map[string]any err = dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { exec, err := s.prepareAppRenewExecution(ctx, tx, batch, duration.Months) if err != nil { return err } minBeginAt, maxEndAt = exec.MinBeginAt, exec.MaxEndAt cardPeriods = exec.CardPeriods orderSn = appOrderNo("SO", user.UserId, now) batchNo, err = appNextBatchNo(ctx, consts.BatchNoPrefixRenew) if err != nil { return err } if _, err = s.deductAppBalanceForDept(ctx, tx, user, orderSn, totalAmount, "批量套餐续费", batch.Party.CustomerAgentId, now); err != nil { return err } orderId, err = s.insertAppRenewOrder(ctx, tx, user, batch.Party, strings.TrimSpace(param.BuyerNote), orderSn, totalAmount, now) if err != nil { return err } if err = s.insertAppRenewSubscriptionBinds(ctx, tx, user, batch, exec, batchNo, orderSn, duration.Months, now); err != nil { return err } buyerNote := strings.TrimSpace(param.BuyerNote) if err = s.insertAppRenewBatchOrderItem(ctx, tx, user, batch, orderId, orderSn, batchNo, "", duration.Months, singleCardAmount, totalAmount, minBeginAt, maxEndAt, cardPeriods, now); err != nil { return err } return s.insertAppRenewSubmitBatch(ctx, tx, user, batch, batchNo, "", orderSn, duration.Months, singleCardAmount, totalAmount, minBeginAt, maxEndAt, cardPeriods, buyerNote, batchmetadata.Bulk, now) }) if err != nil { return nil, err } cardModels := s.renewBatchCardModels(ctx, user, batch) packageModel := s.renewPackageModel(batch.Items[0].Card, batch.Package, batch.OwnerType) option := appRenewOptionModel(duration.Years, batch.Price.UnitPrice, totalAmount, maxEndAt, appRenewRecommendIndex(batch.Durations) == appIndexOfInt(batch.Durations, duration.Years)) res = &model.AppCardRenewSubmitModel{ BatchNo: batchNo, BatchType: consts.BatchTypeRenew, OrderId: orderId, OrderSn: orderSn, TotalAmount: totalAmount, PriceText: option.PriceText, Quantity: quantity, OrderStatus: consts.OrderStatusCompleted, PayStatus: consts.PayStatusPaid, Status: "success", EffectiveRule: appRenewBatchEffectiveRule(), Card: cardModels[0], Cards: cardModels, CurrentPackage: s.renewPackageModel(batch.Items[0].Card, batch.CurrentPackage, batch.OwnerType), Package: packageModel, Option: option, } _ = s.publishAppBusinessAsyncEvent(ctx, appBusinessAsyncEvent{ EventType: appAsyncEventRenewCompleted, OrderSn: orderSn, BatchNo: batchNo, BatchType: consts.BatchTypeRenew, TenantId: user.TenantId, UserId: user.UserId, DeptId: user.DeptId, Trigger: "renew_batch_submit", }) return res, nil } func (s *sAppCard) renewBatchSubmitPrepared(ctx context.Context, user *entity.SysUser, param *model.AppCardRenewBatchSubmitParam) (*model.AppCardRenewSubmitModel, error) { now := gtime.Now() var orderId int64 var orderSn string var batchNo string var batch *appRenewBatchContext var snapshot *appRenewPrepareBatchSnapshot var duration appRenewDurationSelection var minBeginAt *gtime.Time var maxEndAt *gtime.Time var cardPeriods []map[string]any err := dao.BatchMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { prepareBatch, err := s.lockAppRenewPrepareBatch(ctx, tx, user, param.BatchNo) if err != nil { return err } snapshot, err = appRenewPrepareSnapshotFromBatch(prepareBatch) if err != nil { return err } selectedPackageCode := appRenewSubmitTargetPackageCode(snapshot, param.PackageCode) snapshotTarget := appRenewPrepareSnapshotTarget(snapshot, selectedPackageCode) if snapshotTarget == nil { return gerror.New("续费套餐不在当前套餐可续费范围内") } duration = appRenewDurationSelectionFromParam(param.DurationYears, param.DurationMonths, snapshotTarget.Durations) if duration.Years <= 0 || duration.Months <= 0 { return gerror.New("请选择续费年限") } if !appContainsInt(snapshotTarget.Durations, duration.Years) { return gerror.New("续费时长不在目标套餐可续费范围内") } cardValues, err := s.loadAppRenewPrepareBatchCards(ctx, tx, prepareBatch.BatchNo) if err != nil { return err } if len(cardValues) == 0 { return gerror.New("续费预处理批次没有可确认的卡") } if len(param.CardValues) > 0 && !appSameStringSet(appNormalizeRenewCardValues(param.CardValues), cardValues) { return gerror.New("提交卡片与预处理批次不一致,请重新校验") } batch, err = s.buildRenewBatchContext(ctx, user, cardValues, selectedPackageCode) if err != nil { return err } if !appSameRenewPrepareSnapshot(snapshot, batch) { return gerror.New("当前续费价格、客户或套餐已变化,请重新校验") } if !appContainsInt(batch.Durations, duration.Years) { return gerror.New("续费时长不在目标套餐可续费范围内") } quantity := len(batch.Items) singleCardAmount := appRenewTotalAmount(batch.Price.UnitPrice, duration.Years, 1) totalAmount := appRoundAmount(singleCardAmount * float64(quantity)) exec, err := s.prepareAppRenewExecution(ctx, tx, batch, duration.Months) if err != nil { return err } minBeginAt, maxEndAt = exec.MinBeginAt, exec.MaxEndAt cardPeriods = exec.CardPeriods orderSn = appOrderNo("SO", user.UserId, now) batchNo = prepareBatch.BatchNo sourceBatchNo := appRenewSourceBatchNoFromBatch(prepareBatch) if _, err = s.deductAppBalanceForDept(ctx, tx, user, orderSn, totalAmount, "批量套餐续费", batch.Party.CustomerAgentId, now); err != nil { return err } orderId, err = s.insertAppRenewOrder(ctx, tx, user, batch.Party, strings.TrimSpace(param.BuyerNote), orderSn, totalAmount, now) if err != nil { return err } if err = s.insertAppRenewSubscriptionBinds(ctx, tx, user, batch, exec, batchNo, orderSn, duration.Months, now); err != nil { return err } if err = s.insertAppRenewBatchOrderItem(ctx, tx, user, batch, orderId, orderSn, batchNo, sourceBatchNo, duration.Months, singleCardAmount, totalAmount, minBeginAt, maxEndAt, cardPeriods, now); err != nil { return err } return s.markAppRenewPrepareBatchConfirmed(ctx, tx, user, prepareBatch, batch, sourceBatchNo, orderSn, duration.Months, singleCardAmount, totalAmount, minBeginAt, maxEndAt, cardPeriods, strings.TrimSpace(param.BuyerNote), now) }) if err != nil { return nil, err } if batch == nil || len(batch.Items) == 0 { return nil, gerror.New("续费提交结果不完整") } cardModels := s.renewBatchCardModels(ctx, user, batch) packageModel := s.renewPackageModel(batch.Items[0].Card, batch.Package, batch.OwnerType) totalAmount := appRenewTotalAmount(batch.Price.UnitPrice, duration.Years, len(batch.Items)) option := appRenewOptionModel(duration.Years, batch.Price.UnitPrice, totalAmount, maxEndAt, appRenewRecommendIndex(batch.Durations) == appIndexOfInt(batch.Durations, duration.Years)) res := &model.AppCardRenewSubmitModel{ BatchNo: batchNo, BatchType: consts.BatchTypeRenew, OrderId: orderId, OrderSn: orderSn, TotalAmount: totalAmount, PriceText: option.PriceText, Quantity: len(batch.Items), OrderStatus: consts.OrderStatusCompleted, PayStatus: consts.PayStatusPaid, Status: "success", EffectiveRule: appRenewBatchEffectiveRule(), Card: cardModels[0], Cards: cardModels, CurrentPackage: s.renewPackageModel(batch.Items[0].Card, batch.CurrentPackage, batch.OwnerType), Package: packageModel, Option: option, } _ = s.publishAppBusinessAsyncEvent(ctx, appBusinessAsyncEvent{ EventType: appAsyncEventRenewCompleted, OrderSn: orderSn, BatchNo: batchNo, BatchType: consts.BatchTypeRenew, TenantId: user.TenantId, UserId: user.UserId, DeptId: user.DeptId, Trigger: "renew_prepared_submit", }) return res, nil } func (s *sAppCard) buildRenewBatchContext(ctx context.Context, user *entity.SysUser, cardValues []string, packageCode string) (*appRenewBatchContext, error) { values := appNormalizeRenewCardValues(cardValues) if len(values) == 0 { return nil, gerror.New("请输入需要续费的卡") } if len(values) > 100 { return nil, gerror.New("单次批量续费最多支持100张卡") } batch := &appRenewBatchContext{ Items: make([]*appRenewPackageContext, 0, len(values)), OwnerType: s.ownerType(ctx, user), } seenIccid := make(map[string]struct{}, len(values)) for index, value := range values { card, err := s.findVisibleCard(ctx, user, value) if err != nil { return nil, err } if card == nil { return nil, gerror.Newf("第%d张卡信息不存在或无权续费:%s", index+1, value) } if err = s.ensureOperateCard(ctx, user, card, "续费"); err != nil { return nil, err } iccid := strings.TrimSpace(card.Iccid) if _, exists := seenIccid[iccid]; exists { return nil, gerror.Newf("批量续费卡重复:%s", iccid) } seenIccid[iccid] = struct{}{} renewCtx, err := s.resolveRenewPackageContext(ctx, card, "") if err != nil { return nil, gerror.Newf("%s:%v", appFirstNotEmpty(card.Iccid, value), err) } if batch.CurrentPackage == nil { batch.CurrentPackage = renewCtx.Package } else if strings.TrimSpace(batch.CurrentPackage.PackageCode) != strings.TrimSpace(renewCtx.Package.PackageCode) { return nil, gerror.New("当前批次下存在多个当前套餐,不能统一续费") } party, err := s.renewOrderParty(ctx, user, card) if err != nil { return nil, err } if batch.Party == nil { batch.Party = party } else if !appSameRenewOrderParty(batch.Party, party) { return nil, gerror.New("当前批次下存在多个客户,不能统一续费") } batch.Items = append(batch.Items, renewCtx) } if len(batch.Items) == 0 || batch.CurrentPackage == nil || batch.Party == nil { return nil, gerror.New("续费批量校验结果不完整") } renewableCodes, renewableTargets, selectedTarget, err := s.resolveRenewTargetPackages(ctx, user, batch.Items[0], packageCode) if err != nil { return nil, err } batch.RenewablePackageCodes = renewableCodes batch.RenewableTargets = renewableTargets batch.Package = selectedTarget.Package batch.Price = selectedTarget.Price batch.Durations = selectedTarget.Durations for _, item := range batch.Items[1:] { price, err := s.resolveRenewUnitPrice(ctx, user, item.Card, batch.Package) if err != nil { return nil, err } if !appSameRenewPrice(batch.Price, price) { return nil, gerror.New("当前批次下存在不同定价,不能统一续费") } } if batch.Package == nil || batch.Price == nil || len(batch.Durations) == 0 { return nil, gerror.New("续费批量校验结果不完整") } return batch, nil } func (s *sAppCard) buildRenewBatchPrepareModel(ctx context.Context, user *entity.SysUser, batch *appRenewBatchContext) *model.AppCardRenewPrepareModel { cardModels := s.renewBatchCardModels(ctx, user, batch) quantity := len(cardModels) recommendIndex := appRenewRecommendIndex(batch.Durations) options := appRenewOptionModels(batch.Durations, batch.Price.UnitPrice, quantity, func(years int) *gtime.Time { return appRenewBatchMaxExpireAfter(batch.Items, appRenewActualDurationMonths(years)) }) return &model.AppCardRenewPrepareModel{ Card: cardModels[0], Cards: cardModels, Quantity: quantity, CurrentPackage: s.renewPackageModel(batch.Items[0].Card, batch.CurrentPackage, batch.OwnerType), Package: s.renewPackageModel(batch.Items[0].Card, batch.Package, batch.OwnerType), RenewablePackageCodes: batch.RenewablePackageCodes, RenewablePackages: s.renewTargetPackageOptionModels(batch.Items[0].Card, batch.RenewableTargets, batch.OwnerType, quantity, func(years int) *gtime.Time { return appRenewBatchMaxExpireAfter(batch.Items, appRenewActualDurationMonths(years)) }), Options: options, PriceSource: batch.Price.PriceSource, UnitPrice: batch.Price.UnitPrice, EffectiveRule: appRenewBatchEffectiveRule(), RecommendIndex: recommendIndex, } } func (s *sAppCard) renewBatchCardModels(ctx context.Context, user *entity.SysUser, batch *appRenewBatchContext) []*model.AppCardModel { cardModels := make([]*model.AppCardModel, 0, len(batch.Items)) for _, item := range batch.Items { cardModels = append(cardModels, s.cardRowToModel(ctx, item.Card, user, batch.OwnerType, true)) } return cardModels } func (s *sAppCard) insertAppRenewPrepareBatch(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appRenewBatchContext, batchNo string, now *gtime.Time) error { quantity := len(batch.Items) _, err := dao.BatchMain.Ctx(ctx).TX(tx).Data(do.BatchMain{ BatchNo: batchNo, BatchType: consts.BatchTypePrepare, BatchStatus: consts.BatchStatusSuccess, BatchAsyncStatus: consts.BatchAsyncStatusSuccess, SourceType: consts.BatchSourceTypeBatchSelect, SourceChannel: batchmetadata.MiniProgram, ExecutionMode: batchmetadata.Bulk, SourceBatchNo: batchNo, IsCrossDept: 0, TargetDeptId: batch.Party.CustomerAgentId, DeptId: batch.Party.CreatedDept, TotalCount: quantity, ValidCount: quantity, SuccessCount: quantity, FailCount: 0, RunResult: consts.BatchRunResultSuccess, BatchInfo: appRenewPrepareBatchInfo(batch, "", "", 0, 0), StartTime: now, EndTime: now, TenantId: user.TenantId, CreatedDept: batch.Party.CreatedDept, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }).Insert() if err != nil { return gerror.Wrap(err, "写入续费预处理主表失败") } details := make([]do.BatchDetail, 0, quantity) for index, item := range batch.Items { rowNo := appRenewBatchSourceRowNo(index) details = append(details, do.BatchDetail{ BatchNo: batchNo, BatchType: consts.BatchTypePrepare, DetailStatus: consts.BatchDetailStatusSuccess, BlankOutStatus: consts.BlankOutStatusNo, RowNo: rowNo, ItemId: item.Card.Iccid, Snapshot: appRenewPrepareDetailSnapshot(batch, item, rowNo), TenantId: user.TenantId, CreatedDept: batch.Party.CreatedDept, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }) } if len(details) == 0 { return nil } if _, err = dao.BatchDetail.Ctx(ctx).TX(tx).Data(details).Insert(); err != nil { return gerror.Wrap(err, "写入续费预处理明细失败") } return nil } func (s *sAppCard) insertAppRenewSubmitBatch(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appRenewBatchContext, batchNo string, sourceBatchNo string, orderSn string, durationMonths int, singleCardAmount float64, totalAmount float64, beginAt, endAt *gtime.Time, cardPeriods []map[string]any, buyerNote string, executionMode int, now *gtime.Time) error { sourceBatchNo = appRenewEffectiveSourceBatchNo(batchNo, sourceBatchNo) quantity := len(batch.Items) logFiles, err := s.ensureAppRenewBatchLogFiles(ctx, tx, user, batch, nil, batchNo, sourceBatchNo, orderSn, durationMonths, singleCardAmount, totalAmount, beginAt, endAt, cardPeriods, buyerNote, now) if err != nil { return err } batchInfo := appRenewBatchInfoWithLogFiles( appRenewSubmitBatchInfo(batch, batchNo, sourceBatchNo, orderSn, durationMonths, singleCardAmount, totalAmount, beginAt, endAt, cardPeriods, buyerNote, now), logFiles, ) _, err = dao.BatchMain.Ctx(ctx).TX(tx).Data(do.BatchMain{ BatchNo: batchNo, OrderSn: orderSn, BatchType: consts.BatchTypeRenew, BatchStatus: consts.BatchStatusSuccess, BatchAsyncStatus: consts.BatchAsyncStatusSuccess, SourceType: consts.BatchSourceTypeBatchSelect, SourceChannel: batchmetadata.MiniProgram, SourceBatchNo: strings.TrimSpace(sourceBatchNo), ExecutionMode: batchmetadata.NormalizeMode(executionMode), IsCrossDept: 0, TargetDeptId: batch.Party.CustomerAgentId, DeptId: batch.Party.CreatedDept, TotalCount: quantity, ValidCount: quantity, SuccessCount: quantity, FailCount: 0, RunResult: consts.BatchRunResultSuccess, BatchInfo: batchInfo, StartTime: now, EndTime: now, TenantId: user.TenantId, CreatedDept: batch.Party.CreatedDept, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }).Insert() if err != nil { return gerror.Wrap(err, "写入续费批次主表失败") } details := make([]do.BatchDetail, 0, quantity) for index, item := range batch.Items { rowNo := appRenewBatchSourceRowNo(index) details = append(details, do.BatchDetail{ BatchNo: batchNo, BatchType: consts.BatchTypeRenew, DetailStatus: consts.BatchDetailStatusSuccess, BlankOutStatus: consts.BlankOutStatusNo, RowNo: rowNo, ItemId: item.Card.Iccid, Snapshot: appRenewSubmitDetailSnapshot(batch, item, batchNo, sourceBatchNo, orderSn, durationMonths, singleCardAmount, cardPeriods, rowNo), TenantId: user.TenantId, CreatedDept: batch.Party.CreatedDept, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }) } if len(details) == 0 { return nil } if _, err = dao.BatchDetail.Ctx(ctx).TX(tx).Data(details).Insert(); err != nil { return gerror.Wrap(err, "写入续费批次明细失败") } return nil } func (s *sAppCard) lockAppRenewPrepareBatch(ctx context.Context, tx gdb.TX, user *entity.SysUser, batchNo string) (*entity.BatchMain, error) { batchNo = strings.TrimSpace(batchNo) if batchNo == "" { return nil, gerror.New("预处理批次号不能为空") } var batch *entity.BatchMain columns := dao.BatchMain.Columns() err := dao.BatchMain.Ctx(ctx).TX(tx). Where(columns.BatchNo, batchNo). Where(columns.BatchType, consts.BatchTypePrepare). Where(columns.RunResult, consts.BatchRunResultSuccess). Where(columns.TenantId, user.TenantId). Where(columns.CreatedBy, user.UserId). LockUpdate(). Limit(1). Scan(&batch) if err != nil { return nil, gerror.Wrap(err, "查询续费预处理批次失败") } if batch == nil || batch.Id == 0 { return nil, gerror.New("续费预处理批次不存在或无权确认") } if strings.TrimSpace(batch.OrderSn) != "" { return nil, gerror.New("续费预处理批次已生成订单,请勿重复提交") } if batch.BatchInfo == nil || batch.BatchInfo.Get("targetBatchType").Int() != consts.BatchTypeRenew { return nil, gerror.New("批次不是套餐续费预处理批次") } return batch, nil } func (s *sAppCard) loadAppRenewPrepareBatchCards(ctx context.Context, tx gdb.TX, batchNo string) ([]string, error) { var details []*entity.BatchDetail columns := dao.BatchDetail.Columns() err := dao.BatchDetail.Ctx(ctx).TX(tx). Fields(columns.RowNo, columns.ItemId). Where(columns.BatchNo, strings.TrimSpace(batchNo)). Where(columns.BatchType, consts.BatchTypePrepare). Where(columns.DetailStatus, consts.BatchDetailStatusSuccess). Where(columns.BlankOutStatus, consts.BlankOutStatusNo). OrderAsc(columns.RowNo). Scan(&details) if err != nil { return nil, gerror.Wrap(err, "查询续费预处理明细失败") } cardValues := make([]string, 0, len(details)) for _, detail := range details { if detail == nil || strings.TrimSpace(detail.ItemId) == "" { continue } cardValues = append(cardValues, strings.TrimSpace(detail.ItemId)) } return cardValues, nil } func appRenewSourceBatchNoFromBatch(batch *entity.BatchMain) string { if batch == nil { return "" } if batch.BatchInfo != nil { if sourceBatchNo := strings.TrimSpace(batch.BatchInfo.Get("sourceBatchNo").String()); sourceBatchNo != "" { return sourceBatchNo } } return strings.TrimSpace(batch.SourceBatchNo) } func (s *sAppCard) markAppRenewPrepareBatchConfirmed(ctx context.Context, tx gdb.TX, user *entity.SysUser, prepareBatch *entity.BatchMain, batch *appRenewBatchContext, sourceBatchNo string, orderSn string, durationMonths int, singleCardAmount float64, totalAmount float64, beginAt, endAt *gtime.Time, cardPeriods []map[string]any, buyerNote string, now *gtime.Time) error { if prepareBatch == nil { return gerror.New("续费预处理批次不存在") } batchNo := strings.TrimSpace(prepareBatch.BatchNo) sourceBatchNo = appRenewEffectiveSourceBatchNo(batchNo, sourceBatchNo) for index, item := range batch.Items { rowNo := appRenewBatchSourceRowNo(index) _, err := dao.BatchDetail.Ctx(ctx).TX(tx). Where(dao.BatchDetail.Columns().BatchNo, batchNo). Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare). Where(dao.BatchDetail.Columns().DetailStatus, consts.BatchDetailStatusSuccess). Where(dao.BatchDetail.Columns().ItemId, item.Card.Iccid). Data(do.BatchDetail{ RowNo: rowNo, Snapshot: appRenewSubmitDetailSnapshot(batch, item, batchNo, sourceBatchNo, orderSn, durationMonths, singleCardAmount, cardPeriods, rowNo), UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return gerror.Wrap(err, "更新续费批次明细快照失败") } } if _, err := dao.BatchDetail.Ctx(ctx).TX(tx). Where(dao.BatchDetail.Columns().BatchNo, batchNo). Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare). Data(do.BatchDetail{ BatchType: consts.BatchTypeRenew, UpdatedBy: user.UserId, UpdatedAt: now, }). Update(); err != nil { return gerror.Wrap(err, "更新续费批次明细类型失败") } logFiles, err := s.ensureAppRenewBatchLogFiles(ctx, tx, user, batch, prepareBatch.BatchInfo, batchNo, sourceBatchNo, orderSn, durationMonths, singleCardAmount, totalAmount, beginAt, endAt, cardPeriods, buyerNote, now) if err != nil { return err } batchInfo := appRenewBatchInfoWithLogFiles( appRenewSubmitBatchInfo(batch, batchNo, sourceBatchNo, orderSn, durationMonths, singleCardAmount, totalAmount, beginAt, endAt, cardPeriods, buyerNote, now), logFiles, ) _, err = dao.BatchMain.Ctx(ctx).TX(tx). Where(dao.BatchMain.Columns().Id, prepareBatch.Id). Where(dao.BatchMain.Columns().BatchType, consts.BatchTypePrepare). Data(do.BatchMain{ BatchType: consts.BatchTypeRenew, BatchStatus: consts.BatchStatusSuccess, BatchAsyncStatus: consts.BatchAsyncStatusSuccess, OrderSn: orderSn, TargetDeptId: batch.Party.CustomerAgentId, TotalCount: len(batch.Items), ValidCount: len(batch.Items), SuccessCount: len(batch.Items), FailCount: 0, RunResult: consts.BatchRunResultSuccess, BatchInfo: batchInfo, EndTime: now, UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return gerror.Wrap(err, "更新续费批次失败") } return nil } func appRenewEffectiveSourceBatchNo(batchNo string, sourceBatchNo string) string { sourceBatchNo = strings.TrimSpace(sourceBatchNo) if sourceBatchNo != "" { return sourceBatchNo } return strings.TrimSpace(batchNo) } func (s *sAppCard) insertAppRenewBatchOrderItem(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appRenewBatchContext, orderId int64, orderSn string, batchNo string, sourceBatchNo string, durationMonths int, singleCardAmount float64, totalAmount float64, beginAt, endAt *gtime.Time, cardPeriods []map[string]any, now *gtime.Time) error { sourceBatchNo = appRenewEffectiveSourceBatchNo(batchNo, sourceBatchNo) pkg := batch.Package durationYears := appRenewDurationYearsFromLegacy(durationMonths, batch.Durations) skuData := appRenewMarshalJSON([]map[string]any{ {"skuName": "套餐", "skuValue": pkg.PackageName, "packageCode": pkg.PackageCode, "packageType": pkg.PackageType}, {"skuName": "续费卡数", "skuValue": fmt.Sprintf("%d张", len(batch.Items)), "quantity": len(batch.Items)}, {"skuName": "续费时长", "skuValue": appRenewDurationYearLabel(durationYears), "durationYears": durationYears, "durationMonths": durationMonths}, {"skuName": "单卡金额", "skuValue": fmt.Sprintf("¥%.2f", singleCardAmount)}, }) extraSkuData := appRenewMarshalJSON(map[string]any{ "business": "miniapp_card_renew_batch", "batchNo": strings.TrimSpace(batchNo), "sourceBatchNo": strings.TrimSpace(sourceBatchNo), "packageCode": pkg.PackageCode, "durationYears": durationYears, "durationValue": durationYears, "durationMonths": durationMonths, "actualDurationMonths": durationMonths, "renewDurationUnit": appRenewDurationUnitYear, "renewDurationUnitText": appRenewDurationUnitYearText, "unitPrice": batch.Price.UnitPrice, "priceSource": batch.Price.PriceSource, "agentDefaultPriceId": batch.Price.AgentDefaultPriceId, "targetPriceId": batch.Price.TargetPriceId, "singleCardAmount": singleCardAmount, "totalAmount": totalAmount, "quantity": len(batch.Items), "cards": cardPeriods, }) _, err := dao.TOrderItem.Ctx(ctx).TX(tx).Data(do.TOrderItem{ OrderId: orderId, OrderSn: orderSn, CustomerType: batch.Party.CustomerType, CustomerAgentId: batch.Party.CustomerAgentId, EndUserId: batch.Party.EndUserId, Price: singleCardAmount, Quantity: len(batch.Items), ProductId: pkg.Id, ProductName: pkg.PackageName, ProductSn: pkg.PackageCode, PicThumb: "", SkuId: 0, SkuData: skuData, DeliveryQuantity: 0, ProductType: consts.ProductTypeCard, IsGift: 0, AgentId: batch.Party.AgentId, PrepayPrice: 0, Commission: "", OriginPrice: batch.Price.UnitPrice, PromotionData: "", ExtraSkuData: extraSkuData, PayMode: consts.PayModeYearMonth, ServiceBeginTime: beginAt, ServiceEndTime: endAt, TenantId: user.TenantId, CreatedDept: batch.Party.CreatedDept, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }).Insert() if err != nil { return gerror.Wrap(err, "创建批量续费订单项失败") } return nil } func appRenewPrepareBatchInfo(batch *appRenewBatchContext, renewBatchNo string, orderSn string, durationMonths int, totalAmount float64) *gjson.Json { durationYears := appRenewDurationYearsFromLegacy(durationMonths, batch.Durations) info := map[string]any{ "source": "app", "targetBatchType": consts.BatchTypeRenew, "targetBatchName": "套餐续费", "customerAgentId": batch.Party.CustomerAgentId, "currentPackageCode": batch.CurrentPackage.PackageCode, "currentPackageName": batch.CurrentPackage.PackageName, "currentPackageType": batch.CurrentPackage.PackageType, "packageCode": batch.Package.PackageCode, "packageName": batch.Package.PackageName, "packageType": batch.Package.PackageType, "quantity": len(batch.Items), "priceSource": batch.Price.PriceSource, "agentDefaultPriceId": batch.Price.AgentDefaultPriceId, "targetPriceId": batch.Price.TargetPriceId, "renewUnitPrice": batch.Price.UnitPrice, "renewDurationUnit": appRenewDurationUnitYear, "renewDurationUnitText": appRenewDurationUnitYearText, "renewableDurationOptions": batch.Durations, "renewablePackageCodes": batch.RenewablePackageCodes, "renewablePackages": appRenewTargetSnapshotList(batch), "cards": appRenewBatchCardSnapshotList(batch), } if strings.TrimSpace(renewBatchNo) != "" { info["renewBatchNo"] = strings.TrimSpace(renewBatchNo) } if strings.TrimSpace(orderSn) != "" { info["orderSn"] = strings.TrimSpace(orderSn) } if durationMonths > 0 { info["durationYears"] = durationYears info["durationValue"] = durationYears info["durationMonths"] = durationMonths info["actualDurationMonths"] = durationMonths info["amount"] = fmt.Sprintf("%.2f", totalAmount) } return gjson.New(info) } func appRenewSubmitBatchInfo(batch *appRenewBatchContext, batchNo string, sourceBatchNo string, orderSn string, durationMonths int, singleCardAmount float64, totalAmount float64, beginAt, endAt *gtime.Time, cardPeriods []map[string]any, buyerNote string, now *gtime.Time) *gjson.Json { durationYears := appRenewDurationYearsFromLegacy(durationMonths, batch.Durations) return gjson.New(map[string]any{ "source": "app", "batchNo": strings.TrimSpace(batchNo), "sourceBatchNo": strings.TrimSpace(sourceBatchNo), "orderSn": strings.TrimSpace(orderSn), "customerAgentId": batch.Party.CustomerAgentId, "currentPackageCode": batch.CurrentPackage.PackageCode, "currentPackageName": batch.CurrentPackage.PackageName, "currentPackageType": batch.CurrentPackage.PackageType, "packageCode": batch.Package.PackageCode, "packageName": batch.Package.PackageName, "packageType": batch.Package.PackageType, "durationYears": durationYears, "durationValue": durationYears, "durationMonths": durationMonths, "actualDurationMonths": durationMonths, "giftDurationMonths": 0, "quantity": len(batch.Items), "singleCardAmount": singleCardAmount, "amount": fmt.Sprintf("%.2f", totalAmount), "offlineContractCompleted": true, "buyerNote": strings.TrimSpace(buyerNote), "adminNote": "", "remark": "", "renewDate": appRenewBatchRenewDate(now), "renewTime": now, "priceSource": batch.Price.PriceSource, "agentDefaultPriceId": batch.Price.AgentDefaultPriceId, "targetPriceId": batch.Price.TargetPriceId, "renewUnitPrice": batch.Price.UnitPrice, "renewDurationUnit": appRenewDurationUnitYear, "renewDurationUnitText": appRenewDurationUnitYearText, "renewableDurationOptions": batch.Durations, "renewablePackageCodes": batch.RenewablePackageCodes, "renewablePackages": appRenewTargetSnapshotList(batch), "serviceBeginAt": appRenewFormatTime(beginAt), "serviceEndAt": appRenewFormatTime(endAt), "cards": cardPeriods, }) } func appRenewPrepareDetailSnapshot(batch *appRenewBatchContext, item *appRenewPackageContext, rowNo int) *gjson.Json { card := item.Card return gjson.New(map[string]any{ "ICCID": card.Iccid, "MSISDN": card.Msisdn, "IMSI": card.Imsi, "rowNo": rowNo, "cardId": card.Id, "customerAgentId": batch.Party.CustomerAgentId, "packageCode": batch.Package.PackageCode, "packageName": batch.Package.PackageName, "currentPackageCode": item.ExpectedCurrentPackageCode, "currentPackageName": batch.CurrentPackage.PackageName, "priceSource": batch.Price.PriceSource, "renewUnitPrice": batch.Price.UnitPrice, "agentDefaultPriceId": batch.Price.AgentDefaultPriceId, "targetPriceId": batch.Price.TargetPriceId, "expireTime": appRenewFormatTime(item.expireAt()), }) } func appRenewSubmitDetailSnapshot(batch *appRenewBatchContext, item *appRenewPackageContext, batchNo string, sourceBatchNo string, orderSn string, durationMonths int, singleCardAmount float64, cardPeriods []map[string]any, rowNo int) *gjson.Json { card := item.Card period := appRenewCardPeriodAt(cardPeriods, rowNo-2) durationYears := appRenewDurationYearsFromLegacy(durationMonths, batch.Durations) return gjson.New(map[string]any{ "ICCID": card.Iccid, "MSISDN": card.Msisdn, "IMSI": card.Imsi, "rowNo": rowNo, "cardId": card.Id, "renewBatchNo": strings.TrimSpace(batchNo), "sourceBatchNo": strings.TrimSpace(sourceBatchNo), "orderSn": strings.TrimSpace(orderSn), "customerAgentId": batch.Party.CustomerAgentId, "packageCode": batch.Package.PackageCode, "packageName": batch.Package.PackageName, "oldSubscriptionNo": period["oldSubscriptionNo"], "newSubscriptionNo": period["newSubscriptionNo"], "currentPackageCode": item.ExpectedCurrentPackageCode, "currentPackageName": batch.CurrentPackage.PackageName, "durationYears": durationYears, "durationValue": durationYears, "durationMonths": durationMonths, "actualDurationMonths": durationMonths, "singleCardAmount": singleCardAmount, "priceSource": batch.Price.PriceSource, "renewUnitPrice": batch.Price.UnitPrice, "renewDurationUnit": appRenewDurationUnitYear, "renewDurationUnitText": appRenewDurationUnitYearText, "agentDefaultPriceId": batch.Price.AgentDefaultPriceId, "targetPriceId": batch.Price.TargetPriceId, "serviceBeginAt": period["serviceBeginAt"], "serviceEndAt": period["serviceEndAt"], "renewEffectiveTime": period["renewEffectiveTime"], "renewExpectedExpireTime": period["renewExpectedExpireTime"], "cardFlowPkgEndAt": period["cardFlowPkgEndAt"], }) } func appRenewBatchSourceRowNo(index int) int { return index + 2 } func appRenewCardPeriodAt(cardPeriods []map[string]any, index int) map[string]any { if index >= 0 && index < len(cardPeriods) && cardPeriods[index] != nil { return cardPeriods[index] } return map[string]any{} } func appRenewBatchCardSnapshotList(batch *appRenewBatchContext) []map[string]any { cards := make([]map[string]any, 0, len(batch.Items)) for _, item := range batch.Items { cards = append(cards, map[string]any{ "cardId": item.Card.Id, "iccid": item.Card.Iccid, "msisdn": item.Card.Msisdn, "currentPackage": item.ExpectedCurrentPackageCode, "expireTime": appRenewFormatTime(item.expireAt()), }) } return cards } func appRenewTargetSnapshotList(batch *appRenewBatchContext) []map[string]any { if batch == nil { return nil } result := make([]map[string]any, 0, len(batch.RenewableTargets)) for _, target := range batch.RenewableTargets { if target == nil || target.Package == nil || target.Price == nil { continue } result = append(result, map[string]any{ "packageCode": target.Package.PackageCode, "packageName": target.Package.PackageName, "packageType": target.Package.PackageType, "flowMb": target.Package.FlowMb, "renewUnitPrice": target.Price.UnitPrice, "unitPrice": target.Price.UnitPrice, "priceSource": target.Price.PriceSource, "agentDefaultPriceId": target.Price.AgentDefaultPriceId, "targetPriceId": target.Price.TargetPriceId, "renewableDurationOptions": target.Durations, "durations": target.Durations, }) } return result } func appRenewPrepareSnapshotFromBatch(batch *entity.BatchMain) (*appRenewPrepareBatchSnapshot, error) { if batch == nil || batch.BatchInfo == nil { return nil, gerror.New("续费预处理批次缺少报价信息") } options := appNormalizeRenewYearOptions(batch.BatchInfo.Get("renewableDurationOptions").Ints()) sort.Ints(options) snapshot := &appRenewPrepareBatchSnapshot{ CustomerAgentId: batch.BatchInfo.Get("customerAgentId").Int64(), CurrentPackageCode: strings.TrimSpace(batch.BatchInfo.Get("currentPackageCode").String()), CurrentPackageName: strings.TrimSpace(batch.BatchInfo.Get("currentPackageName").String()), PackageCode: strings.TrimSpace(batch.BatchInfo.Get("packageCode").String()), PackageName: strings.TrimSpace(batch.BatchInfo.Get("packageName").String()), PriceSource: strings.TrimSpace(batch.BatchInfo.Get("priceSource").String()), AgentDefaultPriceId: batch.BatchInfo.Get("agentDefaultPriceId").Int64(), TargetPriceId: batch.BatchInfo.Get("targetPriceId").Int64(), RenewUnitPrice: appRoundAmount(batch.BatchInfo.Get("renewUnitPrice").Float64()), RenewableDurationItems: options, RenewablePackageCodes: appNormalizePackageCodes(batch.BatchInfo.Get("renewablePackageCodes").Strings()), Targets: appRenewPrepareTargetSnapshots(batch.BatchInfo), Quantity: batch.BatchInfo.Get("quantity").Int(), } if snapshot.CurrentPackageCode == "" { snapshot.CurrentPackageCode = snapshot.PackageCode } if len(snapshot.RenewablePackageCodes) == 0 { snapshot.RenewablePackageCodes = []string{snapshot.PackageCode} } if len(snapshot.Targets) == 0 && snapshot.PackageCode != "" { snapshot.Targets = map[string]*appRenewPrepareTargetSnapshot{ snapshot.PackageCode: { PackageCode: snapshot.PackageCode, PackageName: snapshot.PackageName, RenewUnitPrice: snapshot.RenewUnitPrice, PriceSource: snapshot.PriceSource, AgentDefaultPriceId: snapshot.AgentDefaultPriceId, TargetPriceId: snapshot.TargetPriceId, Durations: append([]int(nil), snapshot.RenewableDurationItems...), }, } } if snapshot.CustomerAgentId <= 0 || snapshot.PackageCode == "" || snapshot.RenewUnitPrice < 0 || len(snapshot.RenewableDurationItems) == 0 { return nil, gerror.New("续费预处理批次报价信息不完整") } return snapshot, nil } func appSameRenewPrepareSnapshot(snapshot *appRenewPrepareBatchSnapshot, batch *appRenewBatchContext) bool { if snapshot == nil || batch == nil || batch.CurrentPackage == nil || batch.Package == nil || batch.Price == nil || batch.Party == nil { return false } if snapshot.CustomerAgentId != batch.Party.CustomerAgentId || snapshot.CurrentPackageCode != strings.TrimSpace(batch.CurrentPackage.PackageCode) || snapshot.Quantity != len(batch.Items) { return false } target := appRenewPrepareSnapshotTarget(snapshot, batch.Package.PackageCode) return appSameRenewPrepareTargetSnapshot(target, batch) } func appRenewPrepareTargetSnapshots(info *gjson.Json) map[string]*appRenewPrepareTargetSnapshot { result := make(map[string]*appRenewPrepareTargetSnapshot) if info == nil { return result } for _, raw := range info.Get("renewablePackages").Interfaces() { item := gjson.New(raw) code := strings.TrimSpace(item.Get("packageCode").String()) if code == "" { continue } durations := appNormalizeRenewYearOptions(item.Get("renewableDurationOptions").Ints()) if len(durations) == 0 { durations = appNormalizeRenewYearOptions(item.Get("durations").Ints()) } sort.Ints(durations) result[code] = &appRenewPrepareTargetSnapshot{ PackageCode: code, PackageName: strings.TrimSpace(item.Get("packageName").String()), RenewUnitPrice: appRoundAmount(appFirstPositiveFloat64(item.Get("renewUnitPrice").Float64(), item.Get("unitPrice").Float64())), PriceSource: strings.TrimSpace(item.Get("priceSource").String()), AgentDefaultPriceId: item.Get("agentDefaultPriceId").Int64(), TargetPriceId: item.Get("targetPriceId").Int64(), Durations: durations, } } return result } func appRenewSubmitTargetPackageCode(snapshot *appRenewPrepareBatchSnapshot, requestedPackageCode string) string { requestedPackageCode = strings.TrimSpace(requestedPackageCode) if requestedPackageCode != "" { return requestedPackageCode } if snapshot == nil { return "" } return strings.TrimSpace(snapshot.PackageCode) } func appRenewPrepareSnapshotTarget(snapshot *appRenewPrepareBatchSnapshot, packageCode string) *appRenewPrepareTargetSnapshot { if snapshot == nil { return nil } packageCode = strings.TrimSpace(packageCode) if packageCode == "" { return nil } if target := snapshot.Targets[packageCode]; target != nil { return target } if snapshot.PackageCode == packageCode { return &appRenewPrepareTargetSnapshot{ PackageCode: snapshot.PackageCode, PackageName: snapshot.PackageName, RenewUnitPrice: snapshot.RenewUnitPrice, PriceSource: snapshot.PriceSource, AgentDefaultPriceId: snapshot.AgentDefaultPriceId, TargetPriceId: snapshot.TargetPriceId, Durations: append([]int(nil), snapshot.RenewableDurationItems...), } } return nil } func appSameRenewPrepareTargetSnapshot(snapshot *appRenewPrepareTargetSnapshot, batch *appRenewBatchContext) bool { if snapshot == nil || batch == nil || batch.Package == nil || batch.Price == nil { return false } durations := append([]int(nil), batch.Durations...) sort.Ints(durations) return snapshot.PackageCode == strings.TrimSpace(batch.Package.PackageCode) && appRoundAmount(snapshot.RenewUnitPrice) == appRoundAmount(batch.Price.UnitPrice) && snapshot.PriceSource == strings.TrimSpace(batch.Price.PriceSource) && snapshot.AgentDefaultPriceId == batch.Price.AgentDefaultPriceId && snapshot.TargetPriceId == batch.Price.TargetPriceId && appSameIntSlice(snapshot.Durations, durations) } func appFirstPositiveFloat64(values ...float64) float64 { for _, value := range values { if value > 0 { return value } } if len(values) > 0 { return values[0] } return 0 } func appNormalizeRenewCardValues(values []string) []string { seen := map[string]struct{}{} result := make([]string, 0, len(values)) for _, raw := range values { parts := strings.FieldsFunc(raw, func(r rune) bool { switch r { case ',', ',', ';', ';', '\n', '\r', '\t', ' ': return true default: return false } }) for _, part := range parts { value := strings.TrimSpace(part) if value == "" { continue } if _, exists := seen[value]; exists { continue } seen[value] = struct{}{} result = append(result, value) } } return result } func appSameRenewOrderParty(left, right *appRenewOrderParty) bool { if left == nil || right == nil { return false } return left.CustomerType == right.CustomerType && left.CustomerAgentId == right.CustomerAgentId && left.EndUserId == right.EndUserId && left.AgentId == right.AgentId && left.CreatedDept == right.CreatedDept } func appSameRenewPrice(left, right *appRenewPriceSnapshot) bool { if left == nil || right == nil { return false } return appRoundAmount(left.UnitPrice) == appRoundAmount(right.UnitPrice) && strings.TrimSpace(left.PriceSource) == strings.TrimSpace(right.PriceSource) && left.AgentDefaultPriceId == right.AgentDefaultPriceId && left.TargetPriceId == right.TargetPriceId } func appSameIntSlice(left, right []int) bool { if len(left) != len(right) { return false } for index := range left { if left[index] != right[index] { return false } } return true } func appSameStringSet(left, right []string) bool { if len(left) != len(right) { return false } counts := make(map[string]int, len(left)) for _, item := range left { counts[strings.TrimSpace(item)]++ } for _, item := range right { key := strings.TrimSpace(item) if counts[key] == 0 { return false } counts[key]-- } return true } func appRenewBatchMaxExpireAfter(items []*appRenewPackageContext, months int) *gtime.Time { var maxExpireAt *gtime.Time for _, item := range items { maxExpireAt = appLaterTime(maxExpireAt, appRenewExpireAfter(item.expireAt(), months)) } return maxExpireAt } func appEarlierTime(current, next *gtime.Time) *gtime.Time { if current == nil || current.IsZero() { return next } if next != nil && !next.IsZero() && next.Before(current) { return next } return current } func appLaterTime(current, next *gtime.Time) *gtime.Time { if current == nil || current.IsZero() { return next } if next != nil && !next.IsZero() && next.After(current) { return next } return current } func appIndexOfInt(values []int, target int) int { for index, value := range values { if value == target { return index } } return -1 } func appRenewBatchEffectiveRule() string { return "按各卡当前套餐到期时间分别顺延" }