Files
iotlcm/server/internal/logic/app/app_card_renew_batch.go
T

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