package app import ( "context" "encoding/json" "fmt" "reflect" "sort" "strings" "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/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" "github.com/gogf/gf/v2/util/gconv" ) type appCardChangeBatchContext struct { Items []*appCardChangeItemContext CurrentPackage *entity.TSimFlowPackageInfo TargetOptions []*appCardChangeTargetContext Party *appRenewOrderParty OwnerType string } type appCardChangeItemContext struct { Card *appCardQueryRow LatestBind *entity.TSimCardBindPkg ExpectedCurrentPackageCode string RowNo int } type appCardChangeTargetContext struct { Package *entity.TSimFlowPackageInfo Price *appRenewPriceSnapshot Durations []int } type appCardChangePrepareSnapshot struct { CustomerAgentId int64 OldPackageCode string Quantity int Targets map[string]*appCardChangeTargetSnapshot } type appCardChangeTargetSnapshot struct { PackageCode string PackageName string UnitPrice float64 PriceSource string AgentDefaultPriceId int64 TargetPriceId int64 Durations []int } type appCardChangeExecutionContext struct { OldBindsByIccid map[string]*entity.TSimCardBindPkg NewBindsByIccid map[string]*entity.TSimCardBindPkg ServiceBeginAt *gtime.Time ServiceEndAt *gtime.Time CardPeriods []map[string]any } // ChangePrepare 校验企业用户套餐变更条件,并写入预处理批次。 func (s *sAppCard) ChangePrepare(ctx context.Context, param *model.AppCardChangePrepareParam) (res *model.AppCardChangePrepareModel, err error) { if param == nil { param = &model.AppCardChangePrepareParam{} } user, err := appCurrentUser(ctx) if err != nil { return nil, err } batch, err := s.buildChangeBatchContext(ctx, user, param.CardValues, appChangeRequestedPackageCode(param.NewPackageCode, param.TargetPackageCode)) if err != nil { return nil, err } now := gtime.Now() batchNo, err := appNextBatchNo(ctx, consts.BatchNoPrefixChange) if err != nil { return nil, err } if err = dao.BatchMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { return s.insertAppChangePrepareBatch(ctx, tx, user, batch, batchNo, now) }); err != nil { return nil, gerror.Wrap(err, "写入套餐变更预处理批次失败") } res = s.buildChangePrepareModel(ctx, user, batch) res.BatchNo = batchNo res.BatchType = consts.BatchTypePrepare return res, nil } // ChangeSubmit 确认套餐变更预处理批次,并在同一事务内完成余额扣款和本地套餐变更。 func (s *sAppCard) ChangeSubmit(ctx context.Context, param *model.AppCardChangeSubmitParam) (res *model.AppCardChangeSubmitModel, err error) { if param == nil { param = &model.AppCardChangeSubmitParam{} } if param.DurationMonths <= 0 { return nil, gerror.New("请选择套餐时长") } user, err := appCurrentUser(ctx) if err != nil { return nil, err } if s.isTerminalUser(ctx, user) { return nil, gerror.New("终端用户暂不能使用套餐变更功能") } now := gtime.Now() var batch *appCardChangeBatchContext var snapshot *appCardChangePrepareSnapshot var target *appCardChangeTargetContext var orderId int64 var orderSn string var serviceBeginAt *gtime.Time var serviceEndAt *gtime.Time var cardPeriods []map[string]any err = dao.BatchMain.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { prepareBatch, err := s.lockAppChangePrepareBatch(ctx, tx, user, param.BatchNo) if err != nil { return err } snapshot, err = appChangePrepareSnapshotFromBatch(prepareBatch) if err != nil { return err } cardValues, err := s.loadAppChangePrepareBatchCards(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.buildChangeBatchContext(ctx, user, cardValues, strings.TrimSpace(param.NewPackageCode)) if err != nil { return err } if !appSameChangePrepareSnapshot(snapshot, batch) { return gerror.New("当前套餐变更客户、套餐或价格已变化,请重新校验") } target = batch.findTarget(param.NewPackageCode) if target == nil { return gerror.New("目标套餐不在当前可变更套餐范围内") } if !appContainsInt(target.Durations, param.DurationMonths) { return gerror.New("套餐时长不在目标套餐可办理范围内") } unitAmount := appRoundAmount(target.Price.UnitPrice * float64(param.DurationMonths)) totalAmount := appRoundAmount(unitAmount * float64(len(batch.Items))) exec, err := s.prepareAppChangeExecution(ctx, tx, batch, target, param.DurationMonths, now) if err != nil { return err } serviceBeginAt, serviceEndAt = exec.ServiceBeginAt, exec.ServiceEndAt cardPeriods = exec.CardPeriods orderSn = appOrderNo("SO", user.UserId, now) if _, err = s.deductAppBalance(ctx, tx, user, orderSn, totalAmount, "套餐变更", now); err != nil { return err } orderId, err = s.insertAppChangeOrder(ctx, tx, user, batch.Party, strings.TrimSpace(param.BuyerNote), orderSn, totalAmount, now) if err != nil { return err } if err = s.completeAppChangeSubscriptions(ctx, tx, user, batch, target, exec, prepareBatch.BatchNo, orderSn, param.DurationMonths, now); err != nil { return err } if err = s.insertAppChangeOrderItem(ctx, tx, user, batch, target, orderId, orderSn, prepareBatch.BatchNo, param.DurationMonths, unitAmount, totalAmount, serviceBeginAt, serviceEndAt, cardPeriods, now); err != nil { return err } return s.markAppChangePrepareBatchConfirmed(ctx, tx, user, prepareBatch, batch, target, exec, orderSn, param.DurationMonths, unitAmount, totalAmount, strings.TrimSpace(param.BuyerNote), strings.TrimSpace(param.Remark), now) }) if err != nil { return nil, err } if batch == nil || target == nil { return nil, gerror.New("套餐变更提交结果不完整") } cardModels := s.changeBatchCardModels(ctx, user, batch) currentPackage := s.renewPackageModel(batch.Items[0].Card, batch.CurrentPackage, batch.OwnerType) newPackage := s.renewPackageModel(batch.Items[0].Card, target.Package, batch.OwnerType) totalAmount := appRoundAmount(target.Price.UnitPrice * float64(param.DurationMonths) * float64(len(batch.Items))) option := &model.AppRenewOptionModel{ DurationMonths: param.DurationMonths, Label: appRenewDurationLabel(param.DurationMonths), UnitPrice: target.Price.UnitPrice, TotalAmount: totalAmount, PriceText: fmt.Sprintf("¥%.2f", totalAmount), ExpireAfter: serviceEndAt, Recommended: appRenewRecommendIndex(target.Durations) == appIndexOfInt(target.Durations, param.DurationMonths), } res = &model.AppCardChangeSubmitModel{ BatchNo: strings.TrimSpace(param.BatchNo), BatchType: consts.BatchTypeChange, OrderId: orderId, OrderSn: orderSn, TotalAmount: totalAmount, PriceText: option.PriceText, Quantity: len(cardModels), OrderStatus: consts.OrderStatusCompleted, PayStatus: consts.PayStatusPaid, Status: "success", EffectiveRule: appChangeEffectiveRule(), Cards: cardModels, CurrentPackage: currentPackage, NewPackage: newPackage, Option: option, } _ = s.publishAppBusinessAsyncEvent(ctx, appBusinessAsyncEvent{ EventType: appAsyncEventChangeCompleted, OrderSn: orderSn, BatchNo: strings.TrimSpace(param.BatchNo), BatchType: consts.BatchTypeChange, TenantId: user.TenantId, UserId: user.UserId, DeptId: user.DeptId, Trigger: "change_submit", }) return res, nil } func (s *sAppCard) buildChangeBatchContext(ctx context.Context, user *entity.SysUser, cardValues []string, targetPackageCode string) (*appCardChangeBatchContext, error) { if s.isTerminalUser(ctx, user) { return nil, gerror.New("终端用户暂不能使用套餐变更功能") } if user.DeptId <= 0 { return nil, gerror.New("当前账号未绑定企业,不能使用套餐变更功能") } values := appNormalizeRenewCardValues(cardValues) if len(values) == 0 { return nil, gerror.New("请选择需要变更套餐的卡") } if len(values) > 100 { return nil, gerror.New("单次批量套餐变更最多支持100张卡") } batch := &appCardChangeBatchContext{ Items: make([]*appCardChangeItemContext, 0, len(values)), OwnerType: appCardOwnerAgent, } seenIccid := make(map[string]struct{}, len(values)) customerAgentId := int64(0) currentPackageCode := "" 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{}{} latestBind, err := s.loadLatestRenewBind(ctx, card) if err != nil { return nil, err } if latestBind == nil || latestBind.Id == 0 { return nil, gerror.Newf("ICCID没有订购记录,不能变更套餐:%s", iccid) } cardPackageCode := strings.TrimSpace(appCardCurrentPackageCode(card)) if cardPackageCode == "" || card.PackageStatus == consts.PackageStatusNone { return nil, gerror.Newf("ICCID没有当前套餐,不能变更套餐:%s", iccid) } if strings.TrimSpace(latestBind.PackageCode) != cardPackageCode { return nil, gerror.Newf("ICCID当前套餐与最新订购记录不一致,不能变更套餐:%s", iccid) } if currentPackageCode == "" { currentPackageCode = cardPackageCode } else if currentPackageCode != cardPackageCode { return nil, gerror.New("当前批次下存在多个当前套餐,不能统一变更") } if customerAgentId == 0 { customerAgentId = card.DeptId } else if customerAgentId != card.DeptId { return nil, gerror.New("当前批次下存在多个客户,不能统一变更套餐") } batch.Items = append(batch.Items, &appCardChangeItemContext{ Card: card, LatestBind: latestBind, ExpectedCurrentPackageCode: cardPackageCode, RowNo: index + 2, }) } currentPackage, err := s.findRenewPackageOrNil(ctx, user.TenantId, currentPackageCode) if err != nil { return nil, err } if currentPackage == nil { return nil, gerror.Newf("当前套餐编码 %s 未配置套餐信息", currentPackageCode) } batch.CurrentPackage = currentPackage party, err := s.renewOrderParty(ctx, user, batch.Items[0].Card) if err != nil { return nil, err } batch.Party = party targets, err := s.resolveChangeTargetPackages(ctx, user, batch, strings.TrimSpace(targetPackageCode)) if err != nil { return nil, err } if len(targets) == 0 { return nil, gerror.New("当前套餐没有可变更的目标套餐") } batch.TargetOptions = targets return batch, nil } func (s *sAppCard) resolveChangeTargetPackages(ctx context.Context, user *entity.SysUser, batch *appCardChangeBatchContext, targetPackageCode string) ([]*appCardChangeTargetContext, error) { codes := appChangePackageCodes(batch.CurrentPackage.ChangeablePackageCodes) if len(codes) == 0 { return nil, gerror.New("当前套餐未配置可变更套餐") } currentCode := strings.TrimSpace(batch.CurrentPackage.PackageCode) filteredCodes := make([]string, 0, len(codes)) for _, code := range codes { code = strings.TrimSpace(code) if code == "" || code == currentCode { continue } if targetPackageCode != "" && code != targetPackageCode { continue } filteredCodes = append(filteredCodes, code) } if targetPackageCode != "" && len(filteredCodes) == 0 { return nil, gerror.New("目标套餐不在当前套餐可变更范围内") } packages, err := s.loadChangePackagesByCodes(ctx, user.TenantId, filteredCodes) if err != nil { return nil, err } result := make([]*appCardChangeTargetContext, 0, len(filteredCodes)) for _, code := range filteredCodes { pkg := packages[code] if pkg == nil { continue } durations, err := appChangeDurationOptions(pkg, batch.CurrentPackage) if err != nil { return nil, gerror.Newf("目标套餐%s:%v", pkg.PackageName, err) } price, err := s.resolveAgentRenewUnitPrice(ctx, user, batch.Items[0].Card, pkg) if err != nil { return nil, err } if price.UnitPrice < 0 { return nil, gerror.Newf("目标套餐%s价格配置不正确", pkg.PackageName) } result = append(result, &appCardChangeTargetContext{ Package: pkg, Price: price, Durations: durations, }) } return result, nil } func (s *sAppCard) loadChangePackagesByCodes(ctx context.Context, tenantId string, codes []string) (map[string]*entity.TSimFlowPackageInfo, error) { result := make(map[string]*entity.TSimFlowPackageInfo, len(codes)) if len(codes) == 0 { return result, nil } var rows []*entity.TSimFlowPackageInfo err := dao.TSimFlowPackageInfo.Ctx(ctx). Where(dao.TSimFlowPackageInfo.Columns().TenantId, tenantId). WhereIn(dao.TSimFlowPackageInfo.Columns().PackageCode, codes). Scan(&rows) if err != nil { return nil, gerror.Wrap(err, "查询可变更套餐失败") } for _, row := range rows { if row != nil && strings.TrimSpace(row.PackageCode) != "" && row.Enabled != 0 { result[strings.TrimSpace(row.PackageCode)] = row } } return result, nil } func (batch *appCardChangeBatchContext) findTarget(packageCode string) *appCardChangeTargetContext { packageCode = strings.TrimSpace(packageCode) for _, target := range batch.TargetOptions { if target != nil && target.Package != nil && strings.TrimSpace(target.Package.PackageCode) == packageCode { return target } } return nil } func (s *sAppCard) insertAppChangePrepareBatch(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appCardChangeBatchContext, 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, SourceBatchNo: batchNo, IsCrossDept: 0, TargetDeptId: batch.Party.CustomerAgentId, DeptId: batch.Party.CreatedDept, TotalCount: quantity, ValidCount: quantity, SuccessCount: quantity, FailCount: 0, RunResult: consts.BatchRunResultSuccess, BatchInfo: appChangePrepareBatchInfo(batch, batchNo), 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 _, item := range batch.Items { details = append(details, do.BatchDetail{ BatchNo: batchNo, BatchType: consts.BatchTypePrepare, DetailStatus: consts.BatchDetailStatusSuccess, BlankOutStatus: consts.BlankOutStatusNo, RowNo: item.RowNo, ItemId: item.Card.Iccid, Snapshot: appChangePrepareDetailSnapshot(batch, item), TenantId: user.TenantId, CreatedDept: batch.Party.CreatedDept, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }) } if _, err = dao.BatchDetail.Ctx(ctx).TX(tx).Data(details).Insert(); err != nil { return gerror.Wrap(err, "写入套餐变更预处理明细失败") } return nil } func (s *sAppCard) lockAppChangePrepareBatch(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.BatchTypeChange { return nil, gerror.New("批次不是套餐变更预处理批次") } return batch, nil } func (s *sAppCard) loadAppChangePrepareBatchCards(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) != "" { cardValues = append(cardValues, strings.TrimSpace(detail.ItemId)) } } return cardValues, nil } func (s *sAppCard) prepareAppChangeExecution(ctx context.Context, tx gdb.TX, batch *appCardChangeBatchContext, target *appCardChangeTargetContext, durationMonths int, now *gtime.Time) (*appCardChangeExecutionContext, error) { exec := &appCardChangeExecutionContext{ OldBindsByIccid: make(map[string]*entity.TSimCardBindPkg, len(batch.Items)), NewBindsByIccid: make(map[string]*entity.TSimCardBindPkg, len(batch.Items)), ServiceBeginAt: now, ServiceEndAt: now.AddDate(0, durationMonths, 0), CardPeriods: make([]map[string]any, 0, len(batch.Items)), } for _, item := range batch.Items { lockedCard, err := s.lockRenewCardForSubmit(ctx, tx, item.Card, item.ExpectedCurrentPackageCode) if err != nil { return nil, err } latestBind, err := s.lockLatestRenewBindForSubmit(ctx, tx, lockedCard, &appRenewPackageContext{ Card: item.Card, Package: batch.CurrentPackage, ExpectedCurrentPackageCode: item.ExpectedCurrentPackageCode, }) if err != nil { return nil, err } exec.OldBindsByIccid[item.Card.Iccid] = latestBind item.LatestBind = latestBind exec.CardPeriods = append(exec.CardPeriods, map[string]any{ "rowNo": item.RowNo, "cardId": item.Card.Id, "iccid": item.Card.Iccid, "msisdn": item.Card.Msisdn, "oldSubscriptionNo": latestBind.SubscriptionNo, "oldPackageCode": batch.CurrentPackage.PackageCode, "newPackageCode": target.Package.PackageCode, "oldExpectedExpireTime": appRenewFormatTime(latestBind.ExpectedExpireTime), "newEffectiveTime": appRenewFormatTime(exec.ServiceBeginAt), "newExpectedExpireTime": appRenewFormatTime(exec.ServiceEndAt), }) } return exec, nil } func (s *sAppCard) completeAppChangeSubscriptions(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appCardChangeBatchContext, target *appCardChangeTargetContext, exec *appCardChangeExecutionContext, batchNo string, orderSn string, durationMonths int, now *gtime.Time) error { if err := s.archiveAppChangeOldBinds(ctx, tx, user, batch, exec, batchNo, now); err != nil { return err } rows := make([]do.TSimCardBindPkg, 0, len(batch.Items)) for _, item := range batch.Items { subscriptionNo := appOrderNo("SUB", user.UserId, now) row := do.TSimCardBindPkg{ SubscriptionNo: subscriptionNo, BatchNo: batchNo, Iccid: item.Card.Iccid, Msisdn: strings.TrimSpace(item.Card.Msisdn), DeptId: fmt.Sprintf("%d", batch.Party.CustomerAgentId), PackageType: target.Package.PackageType, PackageCode: target.Package.PackageCode, PreviousPackageCode: batch.CurrentPackage.PackageCode, BusinessType: consts.PackageBusinessTypeChange, EffectiveStatus: consts.EffectiveStatusEffective, HandleTime: now, EffectiveTime: exec.ServiceBeginAt, ExpectedExpireTime: exec.ServiceEndAt, DurationMonths: durationMonths, GiftDurationMonths: 0, OrderCode: orderSn, Remark: batchNo, TenantId: user.TenantId, CreatedDept: batch.Party.CreatedDept, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, } rows = append(rows, row) exec.NewBindsByIccid[item.Card.Iccid] = &entity.TSimCardBindPkg{ SubscriptionNo: subscriptionNo, BatchNo: batchNo, Iccid: item.Card.Iccid, Msisdn: strings.TrimSpace(item.Card.Msisdn), DeptId: fmt.Sprintf("%d", batch.Party.CustomerAgentId), PackageType: target.Package.PackageType, PackageCode: target.Package.PackageCode, PreviousPackageCode: batch.CurrentPackage.PackageCode, BusinessType: consts.PackageBusinessTypeChange, EffectiveStatus: consts.EffectiveStatusEffective, HandleTime: now, EffectiveTime: exec.ServiceBeginAt, ExpectedExpireTime: exec.ServiceEndAt, DurationMonths: durationMonths, OrderCode: orderSn, Remark: batchNo, TenantId: user.TenantId, CreatedDept: batch.Party.CreatedDept, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, } } if _, err := dao.TSimCardBindPkg.Ctx(ctx).TX(tx).Data(rows).Insert(); err != nil { return gerror.Wrap(err, "写入套餐变更新订购记录失败") } return s.updateCardsAfterAppChange(ctx, tx, user, batch, target, exec, now) } func (s *sAppCard) archiveAppChangeOldBinds(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appCardChangeBatchContext, exec *appCardChangeExecutionContext, batchNo string, now *gtime.Time) error { logRows := make([]do.TSimBindPackageLogs, 0, len(batch.Items)) subscriptionNos := make([]string, 0, len(batch.Items)) for _, item := range batch.Items { bind := exec.OldBindsByIccid[item.Card.Iccid] if bind == nil || strings.TrimSpace(bind.SubscriptionNo) == "" { return gerror.Newf("ICCID当前订购记录不完整:%s", item.Card.Iccid) } subscriptionNos = append(subscriptionNos, bind.SubscriptionNo) logRows = append(logRows, do.TSimBindPackageLogs{ SubscriptionNo: bind.SubscriptionNo, BatchNo: bind.BatchNo, Iccid: bind.Iccid, Msisdn: bind.Msisdn, DeptId: bind.DeptId, PackageType: bind.PackageType, PackageCode: bind.PackageCode, PreviousPackageCode: bind.PreviousPackageCode, BusinessType: bind.BusinessType, EffectiveStatus: consts.EffectiveStatusTerminated, HandleTime: bind.HandleTime, EffectiveTime: bind.EffectiveTime, ExpectedExpireTime: bind.ExpectedExpireTime, ActualExpireTime: now, DurationMonths: bind.DurationMonths, GiftDurationMonths: bind.GiftDurationMonths, OrderCode: bind.OrderCode, Remark: appAppendRemark(bind.Remark, fmt.Sprintf("changeBatchNo=%s", batchNo)), TenantId: bind.TenantId, CreatedDept: bind.CreatedDept, CreatedBy: bind.CreatedBy, CreatedAt: bind.CreatedAt, UpdatedBy: user.UserId, UpdatedAt: now, }) } if len(logRows) > 0 { if _, err := dao.TSimBindPackageLogs.Ctx(ctx).TX(tx).Data(logRows).OnConflict(dao.TSimBindPackageLogs.Columns().SubscriptionNo).Save(); err != nil { return gerror.Wrap(err, "归档原套餐订购记录失败") } } result, err := dao.TSimCardBindPkg.Ctx(ctx).TX(tx). WhereIn(dao.TSimCardBindPkg.Columns().SubscriptionNo, subscriptionNos). Delete() if err != nil { return gerror.Wrap(err, "删除原套餐订购记录失败") } affected, err := result.RowsAffected() if err != nil { return gerror.Wrap(err, "读取原套餐订购记录删除数量失败") } if int(affected) != len(subscriptionNos) { return gerror.New("部分当前订购记录已变化,请重新校验") } return nil } func (s *sAppCard) updateCardsAfterAppChange(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appCardChangeBatchContext, target *appCardChangeTargetContext, exec *appCardChangeExecutionContext, now *gtime.Time) error { for _, item := range batch.Items { result, err := dao.TSimCardInfo.Ctx(ctx).TX(tx). Where(dao.TSimCardInfo.Columns().TenantId, item.Card.TenantId). Where(dao.TSimCardInfo.Columns().Iccid, item.Card.Iccid). Where(dao.TSimCardInfo.Columns().CurrentPackageCode, item.ExpectedCurrentPackageCode). Data(do.TSimCardInfo{ CurrentPackageCode: target.Package.PackageCode, PackageStatus: consts.PackageStatusEffective, FlowPkgBeginAt: exec.ServiceBeginAt, FlowPkgPresentAt: nil, FlowPkgRealPresentAt: nil, FlowPkgEndAt: exec.ServiceEndAt, DataServiceStatus: 1, PackageTotalFlowMb: target.Package.FlowMb, UsedFlowMb: 0, TrafficCutoffStatus: 0, TrafficCutoffThreshold: 0, SmsServiceStatus: 0, PackageTotalSms: 0, UsedSms: 0, VoiceServiceStatus: 0, PackageTotalVoice: 0, UsedVoice: 0, RoamServiceStatus: 0, PackageTotalRoamFlowMb: 0, UsedRoamFlowMb: 0, UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return gerror.Wrapf(err, "更新卡%s套餐失败", item.Card.Iccid) } affected, err := result.RowsAffected() if err != nil { return gerror.Wrap(err, "读取套餐变更卡更新数量失败") } if affected != 1 { return gerror.Newf("卡%s套餐已变化,请刷新后重试", item.Card.Iccid) } item.Card.CurrentPackageCode = target.Package.PackageCode item.Card.PackageStatus = consts.PackageStatusEffective item.Card.FlowPkgBeginAt = exec.ServiceBeginAt item.Card.FlowPkgEndAt = exec.ServiceEndAt item.Card.PackageTotalFlowMb = target.Package.FlowMb item.Card.UsedFlowMb = 0 } return nil } func (s *sAppCard) insertAppChangeOrder(ctx context.Context, tx gdb.TX, user *entity.SysUser, party *appRenewOrderParty, buyerNote string, orderSn string, totalAmount float64, now *gtime.Time) (int64, error) { orderId, err := dao.TOrder.Ctx(ctx).TX(tx).Data(do.TOrder{ OrderSn: orderSn, ParentOrderId: 0, ParentOrderSn: "", CustomerType: party.CustomerType, CustomerAgentId: party.CustomerAgentId, EndUserId: party.EndUserId, AgentId: party.AgentId, OrderType: consts.OrderTypeCardSubscribe, OrderStatus: consts.OrderStatusCompleted, ShippingStatus: 0, ReturnStatus: 0, Consignee: "", Mobile: "", Email: "", Address: "", AreaCode: "", BuyerNote: strings.TrimSpace(buyerNote), AdminNote: "", Remark: "小程序套餐变更余额扣款订单", ShippingMethod: consts.ShippingMethodNone, LogisticsId: 0, LogisticsName: "", ShippingTypeId: 0, ShippingTypeName: "", TrackingNo: "", PayType: consts.PayTypeBalance, PayStatus: consts.PayStatusPaid, PayOrderSn: "", PayTime: now, TotalAmount: totalAmount, ProductAmount: totalAmount, ShippingFee: 0, ServiceFee: 0, InvoiceFee: 0, CouponAmount: 0, DiscountAmount: 0, Balance: totalAmount, PaidAmount: totalAmount, OnlinePaidAmount: 0, OfflinePaidAmount: 0, UnpaidAmount: 0, UnrefundAmount: 0, IsNeedCommission: 0, DistributionStatus: 0, TenantId: user.TenantId, CreatedDept: party.CreatedDept, CreatedBy: user.UserId, CreatedAt: now, UpdatedBy: user.UserId, UpdatedAt: now, }).InsertAndGetId() if err != nil { return 0, gerror.Wrap(err, "创建套餐变更订单失败") } return orderId, nil } func (s *sAppCard) insertAppChangeOrderItem(ctx context.Context, tx gdb.TX, user *entity.SysUser, batch *appCardChangeBatchContext, target *appCardChangeTargetContext, orderId int64, orderSn string, batchNo string, durationMonths int, unitAmount float64, totalAmount float64, beginAt, endAt *gtime.Time, cardPeriods []map[string]any, now *gtime.Time) error { skuData := appRenewMarshalJSON([]map[string]any{ {"skuName": "原套餐", "skuValue": batch.CurrentPackage.PackageName, "packageCode": batch.CurrentPackage.PackageCode}, {"skuName": "目标套餐", "skuValue": target.Package.PackageName, "packageCode": target.Package.PackageCode}, {"skuName": "变更卡数", "skuValue": fmt.Sprintf("%d张", len(batch.Items)), "quantity": len(batch.Items)}, {"skuName": "套餐时长", "skuValue": appRenewDurationLabel(durationMonths), "durationMonths": durationMonths}, }) extraSkuData := appRenewMarshalJSON(map[string]any{ "business": "miniapp_card_change_batch", "batchNo": strings.TrimSpace(batchNo), "oldPackageCode": batch.CurrentPackage.PackageCode, "newPackageCode": target.Package.PackageCode, "durationMonths": durationMonths, "unitPrice": target.Price.UnitPrice, "unitAmount": unitAmount, "totalAmount": totalAmount, "quantity": len(batch.Items), "priceSource": target.Price.PriceSource, "agentDefaultPriceId": target.Price.AgentDefaultPriceId, "targetPriceId": target.Price.TargetPriceId, "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: unitAmount, Quantity: len(batch.Items), ProductId: target.Package.Id, ProductName: target.Package.PackageName, ProductSn: target.Package.PackageCode, PicThumb: "", SkuId: 0, SkuData: skuData, DeliveryQuantity: 0, ProductType: consts.ProductTypeCard, IsGift: 0, AgentId: batch.Party.AgentId, PrepayPrice: 0, Commission: "", OriginPrice: target.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 (s *sAppCard) markAppChangePrepareBatchConfirmed(ctx context.Context, tx gdb.TX, user *entity.SysUser, prepareBatch *entity.BatchMain, batch *appCardChangeBatchContext, target *appCardChangeTargetContext, exec *appCardChangeExecutionContext, orderSn string, durationMonths int, unitAmount float64, totalAmount float64, buyerNote string, remark string, now *gtime.Time) error { batchNo := strings.TrimSpace(prepareBatch.BatchNo) for _, item := range batch.Items { _, err := dao.BatchDetail.Ctx(ctx).TX(tx). Where(dao.BatchDetail.Columns().BatchNo, batchNo). Where(dao.BatchDetail.Columns().ItemId, item.Card.Iccid). Data(do.BatchDetail{ BatchType: consts.BatchTypeChange, Snapshot: appChangeSubmitDetailSnapshot(batch, target, exec, item, batchNo, orderSn, durationMonths, unitAmount), UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return gerror.Wrap(err, "更新套餐变更批次明细失败") } } _, err := dao.BatchMain.Ctx(ctx).TX(tx). Where(dao.BatchMain.Columns().Id, prepareBatch.Id). Data(do.BatchMain{ BatchType: consts.BatchTypeChange, 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: appChangeSubmitBatchInfo(batch, target, exec, batchNo, orderSn, durationMonths, unitAmount, totalAmount, buyerNote, remark, now), EndTime: now, UpdatedBy: user.UserId, UpdatedAt: now, }). Update() if err != nil { return gerror.Wrap(err, "更新套餐变更批次主表失败") } return nil } func (s *sAppCard) buildChangePrepareModel(ctx context.Context, user *entity.SysUser, batch *appCardChangeBatchContext) *model.AppCardChangePrepareModel { targets := make([]*model.AppCardChangePackageOptionModel, 0, len(batch.TargetOptions)) quantity := len(batch.Items) for _, target := range batch.TargetOptions { options := make([]*model.AppRenewOptionModel, 0, len(target.Durations)) recommendIndex := appRenewRecommendIndex(target.Durations) for index, month := range target.Durations { totalAmount := appRoundAmount(target.Price.UnitPrice * float64(month) * float64(quantity)) options = append(options, &model.AppRenewOptionModel{ DurationMonths: month, Label: appRenewDurationLabel(month), UnitPrice: target.Price.UnitPrice, TotalAmount: totalAmount, PriceText: fmt.Sprintf("¥%.2f", totalAmount), ExpireAfter: gtime.Now().AddDate(0, month, 0), Recommended: index == recommendIndex, }) } targets = append(targets, &model.AppCardChangePackageOptionModel{ Package: s.renewPackageModel(batch.Items[0].Card, target.Package, batch.OwnerType), Options: options, UnitPrice: target.Price.UnitPrice, PriceText: fmt.Sprintf("¥%.2f/月", target.Price.UnitPrice), }) } return &model.AppCardChangePrepareModel{ Quantity: quantity, Cards: s.changeBatchCardModels(ctx, user, batch), CurrentPackage: s.renewPackageModel(batch.Items[0].Card, batch.CurrentPackage, batch.OwnerType), TargetPackages: targets, EffectiveRule: appChangeEffectiveRule(), } } func (s *sAppCard) changeBatchCardModels(ctx context.Context, user *entity.SysUser, batch *appCardChangeBatchContext) []*model.AppCardModel { cards := make([]*model.AppCardModel, 0, len(batch.Items)) for _, item := range batch.Items { cards = append(cards, s.cardRowToModel(ctx, item.Card, user, batch.OwnerType, true)) } return cards } func appChangePrepareBatchInfo(batch *appCardChangeBatchContext, batchNo string) *gjson.Json { return gjson.New(map[string]any{ "source": "app", "batchNo": strings.TrimSpace(batchNo), "sourceBatchNo": strings.TrimSpace(batchNo), "targetBatchType": consts.BatchTypeChange, "targetBatchName": "套餐变更", "customerAgentId": batch.Party.CustomerAgentId, "oldPackageCode": batch.CurrentPackage.PackageCode, "oldPackageName": batch.CurrentPackage.PackageName, "oldPackageType": batch.CurrentPackage.PackageType, "quantity": len(batch.Items), "targetPackages": appChangeTargetSnapshotList(batch), "cards": appChangeCardSnapshotList(batch), }) } func appChangeSubmitBatchInfo(batch *appCardChangeBatchContext, target *appCardChangeTargetContext, exec *appCardChangeExecutionContext, batchNo string, orderSn string, durationMonths int, unitAmount float64, totalAmount float64, buyerNote string, remark string, now *gtime.Time) *gjson.Json { return gjson.New(map[string]any{ "source": "app", "batchNo": strings.TrimSpace(batchNo), "sourceBatchNo": strings.TrimSpace(batchNo), "orderSn": strings.TrimSpace(orderSn), "customerAgentId": batch.Party.CustomerAgentId, "oldPackageCode": batch.CurrentPackage.PackageCode, "oldPackageName": batch.CurrentPackage.PackageName, "newPackageCode": target.Package.PackageCode, "newPackageName": target.Package.PackageName, "durationMonths": durationMonths, "giftDurationMonths": 0, "effectiveMode": "immediate", "offlineContractCompleted": true, "changeUnitPrice": target.Price.UnitPrice, "singleCardAmount": unitAmount, "amount": fmt.Sprintf("%.2f", totalAmount), "quantity": len(batch.Items), "buyerNote": strings.TrimSpace(buyerNote), "remark": strings.TrimSpace(remark), "priceSource": target.Price.PriceSource, "agentDefaultPriceId": target.Price.AgentDefaultPriceId, "targetPriceId": target.Price.TargetPriceId, "packageBeginTime": appRenewFormatTime(exec.ServiceBeginAt), "packageEndTime": appRenewFormatTime(exec.ServiceEndAt), "changeTime": now, "cards": exec.CardPeriods, }) } func appChangePrepareDetailSnapshot(batch *appCardChangeBatchContext, item *appCardChangeItemContext) *gjson.Json { return gjson.New(map[string]any{ "ICCID": item.Card.Iccid, "MSISDN": item.Card.Msisdn, "IMSI": item.Card.Imsi, "rowNo": item.RowNo, "cardId": item.Card.Id, "customerAgentId": batch.Party.CustomerAgentId, "oldPackageCode": batch.CurrentPackage.PackageCode, "oldPackageName": batch.CurrentPackage.PackageName, "oldSubscriptionNo": item.LatestBind.SubscriptionNo, "oldExpectedExpire": appRenewFormatTime(item.LatestBind.ExpectedExpireTime), "targetPackageCodes": appChangeTargetPackageCodes(batch), }) } func appChangeSubmitDetailSnapshot(batch *appCardChangeBatchContext, target *appCardChangeTargetContext, exec *appCardChangeExecutionContext, item *appCardChangeItemContext, batchNo string, orderSn string, durationMonths int, unitAmount float64) *gjson.Json { oldBind := exec.OldBindsByIccid[item.Card.Iccid] newBind := exec.NewBindsByIccid[item.Card.Iccid] return gjson.New(map[string]any{ "ICCID": item.Card.Iccid, "MSISDN": item.Card.Msisdn, "IMSI": item.Card.Imsi, "rowNo": item.RowNo, "cardId": item.Card.Id, "changeBatchNo": strings.TrimSpace(batchNo), "orderSn": strings.TrimSpace(orderSn), "customerAgentId": batch.Party.CustomerAgentId, "oldPackageCode": batch.CurrentPackage.PackageCode, "oldPackageName": batch.CurrentPackage.PackageName, "newPackageCode": target.Package.PackageCode, "newPackageName": target.Package.PackageName, "oldSubscriptionNo": appBindSubscriptionNo(oldBind), "newSubscriptionNo": appBindSubscriptionNo(newBind), "durationMonths": durationMonths, "changeUnitPrice": target.Price.UnitPrice, "singleCardAmount": unitAmount, "newEffectiveTime": appRenewFormatTime(exec.ServiceBeginAt), "newExpectedExpireTime": appRenewFormatTime(exec.ServiceEndAt), }) } func appChangePrepareSnapshotFromBatch(batch *entity.BatchMain) (*appCardChangePrepareSnapshot, error) { if batch == nil || batch.BatchInfo == nil { return nil, gerror.New("套餐变更预处理批次缺少快照") } snapshot := &appCardChangePrepareSnapshot{ CustomerAgentId: batch.BatchInfo.Get("customerAgentId").Int64(), OldPackageCode: strings.TrimSpace(batch.BatchInfo.Get("oldPackageCode").String()), Quantity: batch.BatchInfo.Get("quantity").Int(), Targets: make(map[string]*appCardChangeTargetSnapshot), } for _, raw := range batch.BatchInfo.Get("targetPackages").Interfaces() { item := gjson.New(raw) code := strings.TrimSpace(item.Get("packageCode").String()) if code == "" { continue } durations := item.Get("durations").Ints() sort.Ints(durations) snapshot.Targets[code] = &appCardChangeTargetSnapshot{ PackageCode: code, PackageName: strings.TrimSpace(item.Get("packageName").String()), UnitPrice: appRoundAmount(item.Get("unitPrice").Float64()), PriceSource: strings.TrimSpace(item.Get("priceSource").String()), AgentDefaultPriceId: item.Get("agentDefaultPriceId").Int64(), TargetPriceId: item.Get("targetPriceId").Int64(), Durations: durations, } } if snapshot.CustomerAgentId <= 0 || snapshot.OldPackageCode == "" || snapshot.Quantity <= 0 || len(snapshot.Targets) == 0 { return nil, gerror.New("套餐变更预处理批次快照不完整") } return snapshot, nil } func appSameChangePrepareSnapshot(snapshot *appCardChangePrepareSnapshot, batch *appCardChangeBatchContext) bool { if snapshot == nil || batch == nil || batch.CurrentPackage == nil || batch.Party == nil { return false } if snapshot.CustomerAgentId != batch.Party.CustomerAgentId || snapshot.OldPackageCode != strings.TrimSpace(batch.CurrentPackage.PackageCode) || snapshot.Quantity != len(batch.Items) { return false } for _, target := range batch.TargetOptions { if target == nil || target.Package == nil || target.Price == nil { return false } saved := snapshot.Targets[strings.TrimSpace(target.Package.PackageCode)] if saved == nil || !appSameChangeTargetSnapshot(saved, target) { return false } } return true } func appSameChangeTargetSnapshot(snapshot *appCardChangeTargetSnapshot, target *appCardChangeTargetContext) bool { durations := append([]int(nil), target.Durations...) sort.Ints(durations) return snapshot.PackageCode == strings.TrimSpace(target.Package.PackageCode) && appRoundAmount(snapshot.UnitPrice) == appRoundAmount(target.Price.UnitPrice) && snapshot.PriceSource == strings.TrimSpace(target.Price.PriceSource) && snapshot.AgentDefaultPriceId == target.Price.AgentDefaultPriceId && snapshot.TargetPriceId == target.Price.TargetPriceId && appSameIntSlice(snapshot.Durations, durations) } func appChangeTargetSnapshotList(batch *appCardChangeBatchContext) []map[string]any { result := make([]map[string]any, 0, len(batch.TargetOptions)) for _, target := range batch.TargetOptions { result = append(result, map[string]any{ "packageCode": target.Package.PackageCode, "packageName": target.Package.PackageName, "packageType": target.Package.PackageType, "flowMb": target.Package.FlowMb, "unitPrice": target.Price.UnitPrice, "priceSource": target.Price.PriceSource, "agentDefaultPriceId": target.Price.AgentDefaultPriceId, "targetPriceId": target.Price.TargetPriceId, "durations": target.Durations, }) } return result } func appChangeCardSnapshotList(batch *appCardChangeBatchContext) []map[string]any { result := make([]map[string]any, 0, len(batch.Items)) for _, item := range batch.Items { result = append(result, map[string]any{ "cardId": item.Card.Id, "iccid": item.Card.Iccid, "msisdn": item.Card.Msisdn, "oldPackageCode": batch.CurrentPackage.PackageCode, "oldSubscriptionNo": item.LatestBind.SubscriptionNo, }) } return result } func appChangeTargetPackageCodes(batch *appCardChangeBatchContext) []string { result := make([]string, 0, len(batch.TargetOptions)) for _, target := range batch.TargetOptions { if target != nil && target.Package != nil { result = append(result, strings.TrimSpace(target.Package.PackageCode)) } } return result } func appChangePackageCodes(value *gjson.Json) []string { if value == nil || value.IsNil() { return nil } return appNormalizePackageCodes(appChangePackageCodesFromValue(value.Interface())) } func appChangePackageCodesFromValue(value any) []string { switch typed := value.(type) { case nil: return nil case []any: result := make([]string, 0, len(typed)) for _, item := range typed { result = append(result, appChangePackageCodesFromValue(item)...) } return result case map[string]any: keys := []string{"packageCode", "code", "value", "package_code"} for _, key := range keys { if value, ok := typed[key]; ok { return appChangePackageCodesFromValue(value) } } listKeys := []string{"list", "items", "options", "values", "data", "changeablePackageCodes"} result := make([]string, 0) for _, key := range listKeys { if value, ok := typed[key]; ok { result = append(result, appChangePackageCodesFromValue(value)...) } } return result case string: return appChangePackageCodesFromString(typed) default: if result := appChangePackageCodesFromReflectValue(value); len(result) > 0 { return result } if code := strings.TrimSpace(gconv.String(value)); code != "" { return []string{code} } return nil } } func appChangePackageCodesFromString(value string) []string { value = strings.TrimSpace(value) if value == "" { return nil } if strings.HasPrefix(value, "[") || strings.HasPrefix(value, "{") { var decoded any if err := json.Unmarshal([]byte(value), &decoded); err == nil { return appChangePackageCodesFromValue(decoded) } } parts := strings.FieldsFunc(value, func(r rune) bool { switch r { case ',', ',', ';', ';', '|', '/', '、', '\n', '\r', '\t', ' ': return true default: return false } }) if len(parts) == 0 { parts = []string{value} } return parts } func appChangePackageCodesFromReflectValue(value any) []string { rv := reflect.ValueOf(value) if !rv.IsValid() { return nil } switch rv.Kind() { case reflect.Slice, reflect.Array: result := make([]string, 0, rv.Len()) for i := 0; i < rv.Len(); i++ { result = append(result, appChangePackageCodesFromValue(rv.Index(i).Interface())...) } return result case reflect.Map: values := make(map[string]any, rv.Len()) iter := rv.MapRange() for iter.Next() { values[gconv.String(iter.Key().Interface())] = iter.Value().Interface() } return appChangePackageCodesFromValue(values) default: return nil } } func appNormalizePackageCodes(values []string) []string { seen := make(map[string]struct{}, len(values)) result := make([]string, 0, len(values)) for _, raw := range values { value := strings.TrimSpace(raw) if value == "" { continue } if _, exists := seen[value]; exists { continue } seen[value] = struct{}{} result = append(result, value) } return result } func appChangeDurationOptions(target *entity.TSimFlowPackageInfo, current *entity.TSimFlowPackageInfo) ([]int, error) { durations, err := appRenewableDurationOptions(target) if err == nil { return durations, nil } if current != nil { if fallback, fallbackErr := appRenewableDurationOptions(current); fallbackErr == nil { return fallback, nil } } return nil, err } func appChangeRequestedPackageCode(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func appBindSubscriptionNo(bind *entity.TSimCardBindPkg) string { if bind == nil { return "" } return strings.TrimSpace(bind.SubscriptionNo) } func appAppendRemark(old string, addition string) string { old = strings.TrimSpace(old) addition = strings.TrimSpace(addition) if old == "" { return addition } if addition == "" { return old } return old + "; " + addition } func appChangeEffectiveRule() string { return "立即变更,原套餐终止后新套餐立即生效" }