Files
iotlcm/server/internal/logic/app/app_card_change.go
T
MiFaZhan 2ed5dfa6a1 chore: 初始开源发布
iotlcm-app:小程序端 + 用户端服务。

- app/:uni-app 小程序前端(基于 unibest,MIT)
  - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID
  - 内网后端地址 → admin.iotlcm.example.com
  - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明)
- server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0)
  - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件)
  - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化
  - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位)
  - 依赖号码尾部的断言同步更新
  - 补 server/LICENSE(Apache-2.0)
- 移除内部资产:CI 触发器
- 补根 README,LF 行尾 + .gitattributes

已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
2026-10-26 14:01:25 +08:00

1293 lines
48 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 "立即变更,原套餐终止后新套餐立即生效"
}