506 lines
19 KiB
Go
506 lines
19 KiB
Go
package renewchange
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gogf/gf/v2/database/gdb"
|
|
"github.com/gogf/gf/v2/encoding/gjson"
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
"github.com/gogf/gf/v2/os/gtime"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
|
|
rc "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/renewchange"
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/packagecustomer"
|
|
)
|
|
|
|
// ChangeExecutor executes a confirmed batch_type=102 change as one business
|
|
// transaction. The transaction includes old subscription cleanup, the new
|
|
// order/subscriptions, card reset, detail success, and batch success.
|
|
type ChangeExecutor struct {
|
|
repo *Repository
|
|
nowFunc NowFunc
|
|
}
|
|
|
|
func NewChangeExecutor(repo *Repository) *ChangeExecutor {
|
|
if repo == nil {
|
|
repo = NewRepository()
|
|
}
|
|
return &ChangeExecutor{repo: repo, nowFunc: timeNow}
|
|
}
|
|
|
|
func (c *ChangeExecutor) SetNowFunc(now NowFunc) { c.nowFunc = now }
|
|
|
|
func (c *ChangeExecutor) Execute(ctx context.Context, batchNo string) error {
|
|
batchNo = strings.TrimSpace(batchNo)
|
|
if batchNo == "" {
|
|
return gerror.New("change batch number is empty")
|
|
}
|
|
main, err := c.repo.LoadProcessingBatch(ctx, batchNo, consts.BatchTypeChange)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
details, err := c.repo.LoadRunningDetails(ctx, batchNo, consts.BatchTypeChange)
|
|
if err != nil {
|
|
return gerror.Wrap(err, "load change details failed")
|
|
}
|
|
if len(details) == 0 {
|
|
return c.finalizeFailure(ctx, main, "change batch has no runnable details")
|
|
}
|
|
contract, err := ParseExecutionContract(main)
|
|
if err != nil {
|
|
return c.finalizeFailure(ctx, main, err.Error())
|
|
}
|
|
if contract.BatchType != consts.BatchTypeChange {
|
|
return c.finalizeFailure(ctx, main, "change execution contract has invalid batch type")
|
|
}
|
|
|
|
now := pnow(c.nowFunc)
|
|
orderSN, skipped, err := c.apply(ctx, main, contract, details, now)
|
|
if err != nil {
|
|
return c.finalizeFailure(ctx, main, err.Error())
|
|
}
|
|
if skipped {
|
|
return nil
|
|
}
|
|
packagecustomer.PublishSyncForDeptsSafe(ctx, main.TenantId, main.CreatedBy, []int64{contract.CustomerAgentID}, "change", main.BatchNo)
|
|
|
|
rows, snapshotErr := c.repo.ResolveResultSnapshot(ctx, main.BatchNo, main.BatchType)
|
|
if snapshotErr == nil {
|
|
_ = c.repo.WriteResultFileSafe(ctx, main.BatchNo, main.BatchType, main.TenantId, main.CreatedDept, main.CreatedBy, rows)
|
|
}
|
|
g.Log().Infof(ctx, "change batch success, batchNo:%s, orderSn:%s, tenant:%s", main.BatchNo, orderSN, main.TenantId)
|
|
return nil
|
|
}
|
|
|
|
// apply is deliberately the only success transaction. A duplicate delivery
|
|
// that reaches this method after a completed transaction is safely ignored.
|
|
func (c *ChangeExecutor) apply(ctx context.Context, main *rc.BatchMainRow, contract *ExecutionContract, details []*rc.BatchDetailRow, now time.Time) (orderSN string, skipped bool, err error) {
|
|
iccids := uniqueIccidsFromDetails(details)
|
|
if len(iccids) != len(details) {
|
|
return "", false, gerror.New("change batch contains duplicate or empty ICCID")
|
|
}
|
|
if contract.Quantity != len(iccids) {
|
|
return "", false, gerror.Newf("change contract quantity mismatch: contract=%d actual=%d", contract.Quantity, len(iccids))
|
|
}
|
|
|
|
err = c.repo.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
lockedMain, lockErr := c.repo.Tables().LockChangeBatch(ctx, tx, main.BatchNo)
|
|
if lockErr != nil {
|
|
return lockErr
|
|
}
|
|
if lockedMain.BatchAsyncStatus != consts.BatchAsyncStatusProcessing {
|
|
skipped = true
|
|
return nil
|
|
}
|
|
lockedDetails, lockErr := c.repo.Tables().LockChangeDetails(ctx, tx, main.BatchNo)
|
|
if lockErr != nil {
|
|
return lockErr
|
|
}
|
|
if err = validateRunningChangeDetails(lockedDetails, details); err != nil {
|
|
return err
|
|
}
|
|
|
|
cards, lockErr := c.repo.Tables().LockCardsByIccids(ctx, tx, iccids)
|
|
if lockErr != nil {
|
|
return lockErr
|
|
}
|
|
if len(cards) != len(iccids) {
|
|
return gerror.New("some change cards no longer exist")
|
|
}
|
|
bindRows, lockErr := c.repo.Tables().LockChangeBinds(ctx, tx, iccids)
|
|
if lockErr != nil {
|
|
return lockErr
|
|
}
|
|
currentBinds, futureBinds, lockErr := resolveCurrentSubscriptions(bindRows, iccids)
|
|
if lockErr != nil {
|
|
return lockErr
|
|
}
|
|
|
|
oldPackageCode := readChangeOldPackageCode(main.BatchInfo)
|
|
if oldPackageCode == "" {
|
|
return gerror.New("change contract old package is missing")
|
|
}
|
|
newPackage, loadErr := c.repo.Tables().LoadChangePackage(ctx, tx, contract.TenantID, contract.PackageCode)
|
|
if loadErr != nil {
|
|
return loadErr
|
|
}
|
|
oldPackage, loadErr := c.repo.Tables().LoadChangePackage(ctx, tx, contract.TenantID, oldPackageCode)
|
|
if loadErr != nil {
|
|
return loadErr
|
|
}
|
|
if newPackage.Enabled != 1 || oldPackage.Enabled != 1 {
|
|
return gerror.New("change package is disabled")
|
|
}
|
|
if newPackage.PackageType != oldPackage.PackageType {
|
|
return gerror.New("new package type differs from current package")
|
|
}
|
|
if !containsPackageCode(oldPackage.ChangeablePackageCodes, contract.PackageCode) {
|
|
return gerror.New("new package is not in current package changeable range")
|
|
}
|
|
durationOptions, durationErr := changeDurationOptions(newPackage.RenewableDurationConfig)
|
|
if durationErr != nil {
|
|
return gerror.Wrap(durationErr, "load target package change duration options failed")
|
|
}
|
|
if !rc.ContainsInt(durationOptions, contract.DurationMonths) {
|
|
return gerror.Newf("change duration %d months is not allowed for target package", contract.DurationMonths)
|
|
}
|
|
customer, loadErr := c.repo.Tables().LoadChangeDept(ctx, tx, contract.TenantID, contract.CustomerAgentID)
|
|
if loadErr != nil {
|
|
return loadErr
|
|
}
|
|
if customer.DeptId != contract.SellerAgentID && customer.ParentId != contract.SellerAgentID {
|
|
return gerror.New("change customer is outside the operator scope")
|
|
}
|
|
|
|
sourceBatchNo := contract.SourceBatchNo
|
|
for _, iccid := range iccids {
|
|
card := cards[iccid]
|
|
current := currentBinds[iccid]
|
|
var expectedMSISDN, expectedIMSI string
|
|
if detail := findDetailByICCID(lockedDetails, iccid); detail != nil && detail.Snapshot != nil {
|
|
expectedMSISDN = strings.TrimSpace(detail.Snapshot.Get("msisdn").String())
|
|
expectedIMSI = strings.TrimSpace(detail.Snapshot.Get("imsi").String())
|
|
}
|
|
if err = validateChangeCard(card, current, contract, iccid, oldPackageCode, sourceBatchNo, expectedMSISDN, expectedIMSI); err != nil {
|
|
return gerror.Wrapf(err, "validate change card failed, iccid:%s", iccid)
|
|
}
|
|
for _, future := range futureBinds[iccid] {
|
|
if future == nil || future.TenantId != contract.TenantID || future.DeptId != contract.CustomerAgentID {
|
|
return gerror.Newf("future change subscription is inconsistent: %s", iccid)
|
|
}
|
|
}
|
|
}
|
|
|
|
currentList := make([]*rc.SimCardBindPkg, 0, len(iccids))
|
|
currentOrderCodes := make([]string, 0, len(iccids))
|
|
currentOrderQuantities := make(map[string]int)
|
|
for _, iccid := range iccids {
|
|
currentList = append(currentList, currentBinds[iccid])
|
|
if orderCode := strings.TrimSpace(currentBinds[iccid].OrderCode); orderCode != "" {
|
|
currentOrderCodes = append(currentOrderCodes, orderCode)
|
|
currentOrderQuantities[orderCode]++
|
|
}
|
|
}
|
|
if err = c.repo.Tables().ArchiveChangeBinds(ctx, tx, currentList, consts.EffectiveStatusTerminated, gtime.NewFromTime(now), main.BatchNo, contract.OperatorUserID); err != nil {
|
|
return err
|
|
}
|
|
currentSubscriptionNos := subscriptionNos(currentList)
|
|
if affected, deleteErr := c.repo.Tables().DeleteChangeBinds(ctx, tx, currentSubscriptionNos); deleteErr != nil {
|
|
return deleteErr
|
|
} else if int(affected) != len(currentSubscriptionNos) {
|
|
return gerror.New("current change subscriptions changed concurrently")
|
|
}
|
|
if err = c.repo.Tables().UpdateChangeOrdersAfterRemoval(ctx, tx, currentOrderCodes, main.BatchNo, "lastUnsubscribe", currentOrderQuantities, gtime.NewFromTime(now), contract.OperatorUserID); err != nil {
|
|
return err
|
|
}
|
|
|
|
futureList := flattenFutureBinds(futureBinds, iccids)
|
|
futureOrderCodes := make([]string, 0, len(futureList))
|
|
futureOrderQuantities := make(map[string]int)
|
|
for _, bind := range futureList {
|
|
if orderCode := strings.TrimSpace(bind.OrderCode); orderCode != "" {
|
|
futureOrderCodes = append(futureOrderCodes, orderCode)
|
|
futureOrderQuantities[orderCode]++
|
|
}
|
|
}
|
|
if err = c.repo.Tables().ArchiveChangeBinds(ctx, tx, futureList, consts.EffectiveStatusVoided, gtime.NewFromTime(now), main.BatchNo, contract.OperatorUserID); err != nil {
|
|
return err
|
|
}
|
|
futureSubscriptionNos := subscriptionNos(futureList)
|
|
if affected, deleteErr := c.repo.Tables().DeleteChangeBinds(ctx, tx, futureSubscriptionNos); deleteErr != nil {
|
|
return deleteErr
|
|
} else if int(affected) != len(futureSubscriptionNos) {
|
|
return gerror.New("future change subscriptions changed concurrently")
|
|
}
|
|
if err = c.repo.Tables().UpdateChangeOrdersAfterRemoval(ctx, tx, futureOrderCodes, main.BatchNo, "lastChange", futureOrderQuantities, gtime.NewFromTime(now), contract.OperatorUserID); err != nil {
|
|
return err
|
|
}
|
|
|
|
beginAt, resolveErr := resolveChangeBeginAt(contract, now)
|
|
if resolveErr != nil {
|
|
return resolveErr
|
|
}
|
|
endAt := contract.ServiceEnd(beginAt)
|
|
packageStatus := calculateChangePackageStatus(now, beginAt, endAt)
|
|
effectiveStatus := calculateChangeEffectiveStatus(now, beginAt, endAt)
|
|
orderSN, err = rc.NextOrderSN(now)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
order, buildErr := BuildOrderDO(contract, orderSN, now)
|
|
if buildErr != nil {
|
|
return buildErr
|
|
}
|
|
orderID, insertErr := c.repo.Tables().InsertContractOrder(ctx, tx, order)
|
|
if insertErr != nil {
|
|
return gerror.Wrap(insertErr, "insert change order failed")
|
|
}
|
|
item, buildErr := BuildOrderItemDO(contract, OrderProductContract{ID: newPackage.Id, Code: newPackage.PackageCode, Name: newPackage.PackageName, Type: newPackage.PackageType}, orderID, orderSN, beginAt, endAt, now)
|
|
if buildErr != nil {
|
|
return buildErr
|
|
}
|
|
if err = c.repo.Tables().InsertContractOrderItem(ctx, tx, item); err != nil {
|
|
return gerror.Wrap(err, "insert change order item failed")
|
|
}
|
|
|
|
snapshots := make(map[int]*gjson.Json, len(lockedDetails))
|
|
for _, detail := range lockedDetails {
|
|
iccid := strings.TrimSpace(detail.ItemId)
|
|
card := cards[iccid]
|
|
newSubscriptionNo := rc.GenerateSubscriptionNo("SUB")
|
|
newBind := buildChangeBind(contract, newPackage, card, main.BatchNo, oldPackageCode, orderSN, newSubscriptionNo, effectiveStatus, beginAt, endAt, now)
|
|
if err = c.repo.Tables().InsertBind(ctx, tx, newBind); err != nil {
|
|
return gerror.Wrap(err, "insert change subscription failed")
|
|
}
|
|
if err = c.repo.Tables().UpdateChangeCard(ctx, tx, iccid, newPackage.PackageCode, packageStatus, newPackage.FlowMb, gtime.NewFromTime(beginAt), gtime.NewFromTime(endAt), contract.OperatorUserID); err != nil {
|
|
return err
|
|
}
|
|
snapshots[detail.RowNo] = buildChangeDetailSnapshot(detail, card, currentBinds[iccid], newSubscriptionNo, futureBinds[iccid], contract, oldPackageCode, orderSN, beginAt, endAt)
|
|
}
|
|
if err = c.repo.Tables().MarkChangeDetailsSuccess(ctx, tx, main.BatchNo, lockedDetails, snapshots, contract.OperatorUserID); err != nil {
|
|
return err
|
|
}
|
|
info := mergeBatchInfo(main.BatchInfo, map[string]any{
|
|
"orderSn": orderSN,
|
|
"oldPackageCode": oldPackageCode,
|
|
"newPackageCode": newPackage.PackageCode,
|
|
"newPackageName": newPackage.PackageName,
|
|
"durationMonths": contract.DurationMonths,
|
|
"giftDurationMonths": contract.GiftDurationMonths,
|
|
"effectiveMode": contract.EffectiveMode,
|
|
"packageBeginTime": formatTime(gtime.NewFromTime(beginAt)),
|
|
"packageEndTime": formatTime(gtime.NewFromTime(endAt)),
|
|
"amount": contract.TotalAmount(),
|
|
"changeTime": formatTime(gtime.NewFromTime(now)),
|
|
})
|
|
return c.repo.UpdateBatchMainInfo(ctx, tx, main.BatchNo, main.BatchType, buildChangeSuccessBatchMain(main, contract, gjson.New(info), orderSN, lockedMain.StartTime, now, len(lockedDetails)))
|
|
})
|
|
return orderSN, skipped, err
|
|
}
|
|
|
|
func buildChangeBind(contract *ExecutionContract, pkg *rc.FlowPackageInfo, card *rc.SimCardInfo, batchNo, oldPackageCode, orderSN, subscriptionNo string, effectiveStatus int, beginAt, endAt, now time.Time) *rc.SimCardBindPkg {
|
|
return &rc.SimCardBindPkg{
|
|
BatchNo: batchNo,
|
|
Iccid: card.Iccid,
|
|
Msisdn: card.Msisdn,
|
|
DeptId: contract.CustomerAgentID,
|
|
PackageType: pkg.PackageType,
|
|
PackageCode: pkg.PackageCode,
|
|
PreviousPackageCode: oldPackageCode,
|
|
BusinessType: consts.PackageBusinessTypeChange,
|
|
EffectiveStatus: effectiveStatus,
|
|
HandleTime: gtime.NewFromTime(now),
|
|
EffectiveTime: gtime.NewFromTime(beginAt),
|
|
ExpectedExpireTime: gtime.NewFromTime(endAt),
|
|
DurationMonths: contract.DurationMonths,
|
|
GiftDurationMonths: contract.GiftDurationMonths,
|
|
OrderCode: orderSN,
|
|
SubscriptionNo: subscriptionNo,
|
|
Remark: "卡变更套餐自动生成",
|
|
TenantId: contract.TenantID,
|
|
CreatedDept: contract.CreatedDeptID,
|
|
CreatedBy: contract.OperatorUserID,
|
|
CreatedAt: gtime.NewFromTime(now),
|
|
UpdatedBy: contract.OperatorUserID,
|
|
UpdatedAt: gtime.NewFromTime(now),
|
|
}
|
|
}
|
|
|
|
func buildChangeSuccessBatchMain(main *rc.BatchMainRow, contract *ExecutionContract, info *gjson.Json, orderSN string, startTime *gtime.Time, now time.Time, validCount int) *rc.BatchMainRow {
|
|
return &rc.BatchMainRow{
|
|
BatchType: main.BatchType,
|
|
BatchAsyncStatus: consts.BatchAsyncStatusSuccess,
|
|
RunResult: consts.BatchRunResultSuccess,
|
|
TotalCount: main.TotalCount,
|
|
ValidCount: validCount,
|
|
SuccessCount: validCount,
|
|
FailCount: 0,
|
|
BatchInfo: info,
|
|
OrderSn: orderSN,
|
|
StartTime: startTime,
|
|
EndTime: gtime.NewFromTime(now),
|
|
TargetDeptId: contract.CustomerAgentID,
|
|
UpdatedBy: contract.OperatorUserID,
|
|
}
|
|
}
|
|
|
|
func (c *ChangeExecutor) finalizeFailure(ctx context.Context, main *rc.BatchMainRow, reason string) error {
|
|
now := pnow(c.nowFunc)
|
|
err := c.repo.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
if err := c.repo.MarkAllDetailsFailed(ctx, tx, main.BatchNo, main.BatchType, reason, main.CreatedBy); err != nil {
|
|
return err
|
|
}
|
|
return c.repo.FinalizeBatchMain(ctx, tx, main.BatchNo, main.BatchType, &rc.BatchMainRow{
|
|
BatchAsyncStatus: consts.BatchAsyncStatusFailed,
|
|
RunResult: consts.BatchRunResultFailed,
|
|
FailReason: reason,
|
|
TotalCount: main.TotalCount,
|
|
FailCount: main.TotalCount,
|
|
EndTime: gtime.NewFromTime(now),
|
|
UpdatedBy: main.CreatedBy,
|
|
})
|
|
})
|
|
if err != nil {
|
|
return gerror.Wrap(err, "finalize change failure failed")
|
|
}
|
|
g.Log().Warningf(ctx, "change batch failed, batchNo:%s, reason:%s, tenant:%s", main.BatchNo, reason, main.TenantId)
|
|
return nil
|
|
}
|
|
|
|
func validateRunningChangeDetails(locked, loaded []*rc.BatchDetailRow) error {
|
|
if len(locked) != len(loaded) {
|
|
return gerror.New("change detail count changed during execution")
|
|
}
|
|
for _, detail := range locked {
|
|
if detail == nil || detail.DetailStatus != consts.BatchDetailStatusRunning {
|
|
return gerror.New("change detail is no longer running")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateChangeCard(card *rc.SimCardInfo, bind *rc.SimCardBindPkg, contract *ExecutionContract, iccid, oldPackageCode, sourceBatchNo, expectedMSISDN, expectedIMSI string) error {
|
|
if contract == nil {
|
|
return gerror.New("change execution contract is missing")
|
|
}
|
|
return validateCurrentSubscription(card, bind, subscriptionValidation{
|
|
ICCID: iccid,
|
|
MSISDN: expectedMSISDN,
|
|
IMSI: expectedIMSI,
|
|
TenantID: contract.TenantID,
|
|
CustomerDeptID: contract.CustomerAgentID,
|
|
PackageCode: oldPackageCode,
|
|
SourceBatchNo: sourceBatchNo,
|
|
})
|
|
}
|
|
|
|
func findDetailByICCID(details []*rc.BatchDetailRow, iccid string) *rc.BatchDetailRow {
|
|
for _, detail := range details {
|
|
if detail != nil && strings.TrimSpace(detail.ItemId) == strings.TrimSpace(iccid) {
|
|
return detail
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readChangeOldPackageCode(info *gjson.Json) string {
|
|
return firstString(info, "oldPackageCode", "changePrepareSnapshot.oldPackageCode")
|
|
}
|
|
|
|
func containsPackageCode(values *gjson.Json, target string) bool {
|
|
if values == nil || values.IsNil() {
|
|
return false
|
|
}
|
|
for _, value := range values.Interfaces() {
|
|
if strings.TrimSpace(gconvString(value)) == strings.TrimSpace(target) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func resolveChangeBeginAt(contract *ExecutionContract, now time.Time) (time.Time, error) {
|
|
if contract == nil || contract.PackageBeginTime == nil {
|
|
return time.Time{}, gerror.New("change package begin time is missing")
|
|
}
|
|
switch strings.TrimSpace(contract.EffectiveMode) {
|
|
case "immediate":
|
|
return now, nil
|
|
case "next_month":
|
|
begin := contract.PackageBeginTime.Time
|
|
if begin.Day() != 1 || begin.Hour() != 0 || begin.Minute() != 0 || begin.Second() != 0 {
|
|
return time.Time{}, gerror.New("change next month begin time is invalid")
|
|
}
|
|
return begin, nil
|
|
default:
|
|
return time.Time{}, gerror.New("change effective mode is unsupported")
|
|
}
|
|
}
|
|
|
|
func calculateChangePackageStatus(now, begin, end time.Time) int {
|
|
if begin.After(now) {
|
|
return consts.PackageStatusPending
|
|
}
|
|
if !end.After(now) {
|
|
return consts.PackageStatusExpired
|
|
}
|
|
return consts.PackageStatusEffective
|
|
}
|
|
|
|
func calculateChangeEffectiveStatus(now, begin, end time.Time) int {
|
|
if begin.After(now) {
|
|
return consts.EffectiveStatusPending
|
|
}
|
|
if !end.After(now) {
|
|
return consts.EffectiveStatusExpired
|
|
}
|
|
return consts.EffectiveStatusEffective
|
|
}
|
|
|
|
func subscriptionNos(rows []*rc.SimCardBindPkg) []string {
|
|
result := make([]string, 0, len(rows))
|
|
for _, row := range rows {
|
|
if row != nil {
|
|
result = append(result, strings.TrimSpace(row.SubscriptionNo))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func flattenFutureBinds(future map[string][]*rc.SimCardBindPkg, iccids []string) []*rc.SimCardBindPkg {
|
|
result := make([]*rc.SimCardBindPkg, 0)
|
|
for _, iccid := range iccids {
|
|
result = append(result, future[iccid]...)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func buildChangeDetailSnapshot(detail *rc.BatchDetailRow, card *rc.SimCardInfo, oldBind *rc.SimCardBindPkg, newSubscriptionNo string, voided []*rc.SimCardBindPkg, contract *ExecutionContract, oldPackageCode, orderSN string, beginAt, endAt time.Time) *gjson.Json {
|
|
data := map[string]any{}
|
|
if detail != nil && detail.Snapshot != nil {
|
|
for key, value := range detail.Snapshot.Map() {
|
|
data[key] = value
|
|
}
|
|
}
|
|
data["ICCID"] = card.Iccid
|
|
data["MSISDN"] = card.Msisdn
|
|
data["IMSI"] = card.Imsi
|
|
data["iccid"] = card.Iccid
|
|
data["msisdn"] = card.Msisdn
|
|
data["imsi"] = card.Imsi
|
|
data["oldPackageCode"] = oldPackageCode
|
|
data["newPackageCode"] = contract.PackageCode
|
|
data["newPackageName"] = contract.PackageName
|
|
data["oldSubscriptionNo"] = oldBind.SubscriptionNo
|
|
data["oldOrderSn"] = oldBind.OrderCode
|
|
data["newSubscriptionNo"] = newSubscriptionNo
|
|
data["newOrderSn"] = orderSN
|
|
data["durationMonths"] = contract.DurationMonths
|
|
data["giftDurationMonths"] = contract.GiftDurationMonths
|
|
data["effectiveMode"] = contract.EffectiveMode
|
|
data["changeUnitPrice"] = contract.UnitPrice
|
|
data["amount"] = roundAmount(contract.UnitPrice * float64(contract.DurationMonths))
|
|
data["newEffectiveTime"] = formatTime(gtime.NewFromTime(beginAt))
|
|
data["newExpectedExpireTime"] = formatTime(gtime.NewFromTime(endAt))
|
|
data["oldActualExpireTime"] = formatTime(gtime.Now())
|
|
voidedNos := make([]string, 0, len(voided))
|
|
for _, row := range voided {
|
|
if row != nil {
|
|
voidedNos = append(voidedNos, row.SubscriptionNo)
|
|
}
|
|
}
|
|
data["voidedSubscriptionNos"] = voidedNos
|
|
return gjson.New(data)
|
|
}
|
|
|
|
func gconvString(value any) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(fmt.Sprint(value))
|
|
}
|