All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m57s
696 lines
22 KiB
Go
696 lines
22 KiB
Go
package task
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"time"
|
||
|
||
"github.com/bytedance/sonic"
|
||
"github.com/hibiken/asynq"
|
||
"github.com/redis/go-redis/v9"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
|
||
"github.com/break/junhong_cmp_fiber/internal/service/packageprice"
|
||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
)
|
||
|
||
// AutoPurchasePayload 充值后自动购包任务载荷
|
||
type AutoPurchasePayload struct {
|
||
RechargeOrderID uint `json:"recharge_order_id"`
|
||
}
|
||
|
||
// AutoPurchaseHandler 充值后自动购包任务处理器
|
||
type AutoPurchaseHandler struct {
|
||
db *gorm.DB
|
||
orderStore *postgres.OrderStore
|
||
rechargeOrderStore *postgres.RechargeOrderStore
|
||
paymentStore *postgres.PaymentStore
|
||
walletStore *postgres.AssetWalletStore
|
||
walletTransactionStore *postgres.AssetWalletTransactionStore
|
||
packageUsageStore *postgres.PackageUsageStore
|
||
shopPackageAllocationStore *postgres.ShopPackageAllocationStore // 用于查询卖家成本价
|
||
iotCardStore *postgres.IotCardStore // 用于获取卡的系列ID
|
||
deviceStore *postgres.DeviceStore // 用于获取设备的系列ID
|
||
redis *redis.Client
|
||
asynqClient *asynq.Client // 用于事务提交成功后触发佣金计算任务
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// NewAutoPurchaseHandler 创建充值后自动购包处理器
|
||
func NewAutoPurchaseHandler(
|
||
db *gorm.DB,
|
||
orderStore *postgres.OrderStore,
|
||
rechargeOrderStore *postgres.RechargeOrderStore,
|
||
paymentStore *postgres.PaymentStore,
|
||
walletStore *postgres.AssetWalletStore,
|
||
walletTransactionStore *postgres.AssetWalletTransactionStore,
|
||
packageUsageStore *postgres.PackageUsageStore,
|
||
redisClient *redis.Client,
|
||
asynqClient *asynq.Client,
|
||
logger *zap.Logger,
|
||
) *AutoPurchaseHandler {
|
||
if orderStore == nil {
|
||
orderStore = postgres.NewOrderStore(db, redisClient)
|
||
}
|
||
if rechargeOrderStore == nil {
|
||
rechargeOrderStore = postgres.NewRechargeOrderStore(db, redisClient)
|
||
}
|
||
if paymentStore == nil {
|
||
paymentStore = postgres.NewPaymentStore(db, redisClient)
|
||
}
|
||
if walletStore == nil {
|
||
walletStore = postgres.NewAssetWalletStore(db, redisClient)
|
||
}
|
||
if walletTransactionStore == nil {
|
||
walletTransactionStore = postgres.NewAssetWalletTransactionStore(db, redisClient)
|
||
}
|
||
if packageUsageStore == nil {
|
||
packageUsageStore = postgres.NewPackageUsageStore(db, redisClient)
|
||
}
|
||
|
||
return &AutoPurchaseHandler{
|
||
db: db,
|
||
orderStore: orderStore,
|
||
rechargeOrderStore: rechargeOrderStore,
|
||
paymentStore: paymentStore,
|
||
walletStore: walletStore,
|
||
walletTransactionStore: walletTransactionStore,
|
||
packageUsageStore: packageUsageStore,
|
||
shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(db),
|
||
iotCardStore: postgres.NewIotCardStore(db, redisClient),
|
||
deviceStore: postgres.NewDeviceStore(db, redisClient),
|
||
redis: redisClient,
|
||
asynqClient: asynqClient,
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
// ProcessTask 处理充值后自动购包任务
|
||
func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task) error {
|
||
var payload AutoPurchasePayload
|
||
if err := sonic.Unmarshal(task.Payload(), &payload); err != nil {
|
||
h.logger.Error("解析自动购包任务载荷失败", zap.Error(err))
|
||
return asynq.SkipRetry
|
||
}
|
||
if payload.RechargeOrderID == 0 {
|
||
h.logger.Error("自动购包任务载荷无效", zap.Uint("recharge_order_id", payload.RechargeOrderID))
|
||
return asynq.SkipRetry
|
||
}
|
||
|
||
rechargeOrder, err := h.rechargeOrderStore.GetByID(ctx, payload.RechargeOrderID)
|
||
if err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
h.logger.Warn("充值订单不存在,跳过自动购包", zap.Uint("recharge_order_id", payload.RechargeOrderID))
|
||
return asynq.SkipRetry
|
||
}
|
||
h.logger.Error("查询充值订单失败", zap.Uint("recharge_order_id", payload.RechargeOrderID), zap.Error(err))
|
||
return err
|
||
}
|
||
|
||
if rechargeOrder.AutoPurchaseStatus == constants.AutoPurchaseStatusSuccess {
|
||
return nil
|
||
}
|
||
if rechargeOrder.AutoPurchaseStatus == constants.AutoPurchaseStatusFailed {
|
||
return nil
|
||
}
|
||
|
||
packageIDs, err := parseLinkedPackageIDs(rechargeOrder.LinkedPackageIDs)
|
||
if err != nil {
|
||
h.logger.Error("解析关联套餐ID失败", zap.Uint("recharge_order_id", rechargeOrder.ID), zap.Error(err))
|
||
h.markAutoPurchaseFailedIfFinalRetry(ctx, rechargeOrder.ID)
|
||
return asynq.SkipRetry
|
||
}
|
||
if len(packageIDs) == 0 {
|
||
h.logger.Error("关联套餐ID为空,无法自动购包", zap.Uint("recharge_order_id", rechargeOrder.ID))
|
||
h.markAutoPurchaseFailedIfFinalRetry(ctx, rechargeOrder.ID)
|
||
return asynq.SkipRetry
|
||
}
|
||
|
||
packages, totalAmount, err := h.loadPackages(ctx, packageIDs)
|
||
if err != nil {
|
||
h.logger.Error("加载关联套餐失败", zap.Uint("recharge_order_id", rechargeOrder.ID), zap.Error(err))
|
||
h.markAutoPurchaseFailedIfFinalRetry(ctx, rechargeOrder.ID)
|
||
return err
|
||
}
|
||
|
||
// 获取资产系列ID,用于差价佣金和一次性佣金计算
|
||
var seriesID *uint
|
||
if rechargeOrder.LinkedCarrierID != nil && *rechargeOrder.LinkedCarrierID > 0 {
|
||
if rechargeOrder.LinkedCarrierType == "card" || rechargeOrder.LinkedCarrierType == constants.AssetWalletResourceTypeIotCard {
|
||
if card, cardErr := h.iotCardStore.GetByID(ctx, *rechargeOrder.LinkedCarrierID); cardErr == nil {
|
||
seriesID = card.SeriesID
|
||
} else {
|
||
h.logger.Warn("自动购包获取卡系列ID失败",
|
||
zap.Uint("card_id", *rechargeOrder.LinkedCarrierID),
|
||
zap.Error(cardErr))
|
||
}
|
||
} else if rechargeOrder.LinkedCarrierType == "device" || rechargeOrder.LinkedCarrierType == constants.AssetWalletResourceTypeDevice {
|
||
if device, deviceErr := h.deviceStore.GetByID(ctx, *rechargeOrder.LinkedCarrierID); deviceErr == nil {
|
||
seriesID = device.SeriesID
|
||
} else {
|
||
h.logger.Warn("自动购包获取设备系列ID失败",
|
||
zap.Uint("device_id", *rechargeOrder.LinkedCarrierID),
|
||
zap.Error(deviceErr))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取卖家成本价,用于差价佣金链式计算的起点
|
||
// 平台直销(ShopIDTag == 0)时成本价为 0,差价佣金计算会直接跳过
|
||
var sellerCostPrice int64
|
||
if rechargeOrder.ShopIDTag > 0 && len(packages) > 0 {
|
||
allocation, allocErr := h.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, rechargeOrder.ShopIDTag, packages[0].ID)
|
||
if allocErr == nil {
|
||
sellerCostPrice = allocation.CostPrice
|
||
} else {
|
||
h.logger.Warn("自动购包获取卖家成本价失败,差价佣金将计算为0",
|
||
zap.Uint("shop_id", rechargeOrder.ShopIDTag),
|
||
zap.Uint("package_id", packages[0].ID),
|
||
zap.Error(allocErr))
|
||
}
|
||
}
|
||
|
||
var createdOrderID uint
|
||
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
wallet, walletErr := h.walletStore.GetByID(ctx, rechargeOrder.AssetWalletID)
|
||
if walletErr != nil {
|
||
if walletErr == gorm.ErrRecordNotFound {
|
||
return errors.New("资产钱包不存在")
|
||
}
|
||
return walletErr
|
||
}
|
||
if wallet.GetAvailableBalance() < totalAmount {
|
||
return errors.New("钱包余额不足")
|
||
}
|
||
|
||
if err = h.walletStore.DeductBalanceWithTx(ctx, tx, wallet.ID, totalAmount, wallet.Version); err != nil {
|
||
return err
|
||
}
|
||
|
||
now := time.Now()
|
||
order, orderItems, buildErr := h.buildOrderAndItems(ctx, rechargeOrder, packages, totalAmount, seriesID, sellerCostPrice, now)
|
||
if buildErr != nil {
|
||
return buildErr
|
||
}
|
||
|
||
if err = tx.Create(order).Error; err != nil {
|
||
return err
|
||
}
|
||
createdOrderID = order.ID
|
||
|
||
for _, item := range orderItems {
|
||
item.OrderID = order.ID
|
||
}
|
||
if err = tx.CreateInBatches(orderItems, 100).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
payment := &model.Payment{
|
||
PaymentNo: order.OrderNo,
|
||
OrderID: order.ID,
|
||
OrderType: model.PaymentOrderTypePackage,
|
||
PaymentMethod: model.PaymentByWallet,
|
||
Amount: totalAmount,
|
||
Status: model.PaymentRecordStatusPaid,
|
||
}
|
||
if err = tx.Create(payment).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
refType := constants.ReferenceTypeOrder
|
||
walletTx := &model.AssetWalletTransaction{
|
||
AssetWalletID: wallet.ID,
|
||
ResourceType: wallet.ResourceType,
|
||
ResourceID: wallet.ResourceID,
|
||
UserID: rechargeOrder.UserID,
|
||
TransactionType: constants.AssetTransactionTypeDeduct,
|
||
Amount: -totalAmount,
|
||
BalanceBefore: wallet.Balance,
|
||
BalanceAfter: wallet.Balance - totalAmount,
|
||
Status: constants.TransactionStatusSuccess,
|
||
ReferenceType: &refType,
|
||
ReferenceNo: &order.OrderNo,
|
||
Creator: rechargeOrder.UserID,
|
||
ShopIDTag: wallet.ShopIDTag,
|
||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||
}
|
||
if err = h.walletTransactionStore.CreateWithTx(ctx, tx, walletTx); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err = h.activatePackages(ctx, tx, order, packages, now); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err = tx.Model(&model.RechargeOrder{}).
|
||
Where("id = ?", rechargeOrder.ID).
|
||
Update("auto_purchase_status", constants.AutoPurchaseStatusSuccess).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}); err != nil {
|
||
h.logger.Error("自动购包任务执行失败",
|
||
zap.Uint("recharge_record_id", rechargeOrder.ID),
|
||
zap.Error(err),
|
||
)
|
||
h.markAutoPurchaseFailedIfFinalRetry(ctx, rechargeOrder.ID)
|
||
return err
|
||
}
|
||
|
||
// 事务提交成功后触发佣金计算(不在事务内,防止任务提交后事务回滚的数据一致性问题)
|
||
if h.asynqClient != nil && createdOrderID > 0 {
|
||
payloadBytes, marshalErr := sonic.Marshal(map[string]any{"order_id": createdOrderID})
|
||
if marshalErr != nil {
|
||
h.logger.Warn("佣金任务载荷序列化失败",
|
||
zap.Uint("order_id", createdOrderID),
|
||
zap.Error(marshalErr))
|
||
} else {
|
||
commissionTask := asynq.NewTask(constants.TaskTypeCommission, payloadBytes,
|
||
asynq.Queue(constants.QueueLow),
|
||
asynq.MaxRetry(3),
|
||
)
|
||
if _, enqueueErr := h.asynqClient.EnqueueContext(ctx, commissionTask); enqueueErr != nil {
|
||
h.logger.Warn("自动购包后提交佣金任务失败",
|
||
zap.Uint("order_id", createdOrderID),
|
||
zap.Error(enqueueErr))
|
||
}
|
||
}
|
||
}
|
||
|
||
h.logger.Info("自动购包任务执行成功", zap.Uint("recharge_record_id", rechargeOrder.ID))
|
||
return nil
|
||
}
|
||
|
||
// NewAutoPurchaseTask 创建充值后自动购包任务
|
||
func NewAutoPurchaseTask(rechargeOrderID uint) (*asynq.Task, error) {
|
||
payloadBytes, err := sonic.Marshal(AutoPurchasePayload{RechargeOrderID: rechargeOrderID})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return asynq.NewTask(constants.TaskTypeAutoPurchaseAfterRecharge, payloadBytes,
|
||
asynq.MaxRetry(3),
|
||
asynq.Timeout(2*time.Minute),
|
||
asynq.Queue(constants.QueueDefault),
|
||
), nil
|
||
}
|
||
|
||
func (h *AutoPurchaseHandler) markAutoPurchaseFailedIfFinalRetry(ctx context.Context, rechargeOrderID uint) {
|
||
retryCount, ok := asynq.GetRetryCount(ctx)
|
||
if !ok {
|
||
return
|
||
}
|
||
maxRetry, ok := asynq.GetMaxRetry(ctx)
|
||
if !ok {
|
||
return
|
||
}
|
||
if retryCount < maxRetry-1 {
|
||
return
|
||
}
|
||
|
||
if err := h.db.WithContext(ctx).
|
||
Model(&model.RechargeOrder{}).
|
||
Where("id = ?", rechargeOrderID).
|
||
Update("auto_purchase_status", constants.AutoPurchaseStatusFailed).Error; err != nil {
|
||
h.logger.Error("更新自动购包失败状态失败",
|
||
zap.Uint("recharge_record_id", rechargeOrderID),
|
||
zap.Error(err),
|
||
)
|
||
return
|
||
}
|
||
|
||
h.logger.Warn("自动购包达到最大重试次数,已标记失败", zap.Uint("recharge_record_id", rechargeOrderID))
|
||
}
|
||
|
||
func (h *AutoPurchaseHandler) loadPackages(ctx context.Context, packageIDs []uint) ([]*model.Package, int64, error) {
|
||
packages := make([]*model.Package, 0, len(packageIDs))
|
||
if err := h.db.WithContext(ctx).Where("id IN ?", packageIDs).Find(&packages).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
if len(packages) != len(packageIDs) {
|
||
return nil, 0, gorm.ErrRecordNotFound
|
||
}
|
||
|
||
totalAmount := int64(0)
|
||
for _, pkg := range packages {
|
||
if pkg.IsGift {
|
||
return nil, 0, errors.New("赠送套餐不能进入自动购包链路")
|
||
}
|
||
totalAmount += packageprice.PackageEffectiveRetailPrice(pkg)
|
||
}
|
||
|
||
if err := validatePackageTypeMix(packages); err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
return packages, totalAmount, nil
|
||
}
|
||
|
||
func (h *AutoPurchaseHandler) buildOrderAndItems(
|
||
ctx context.Context,
|
||
rechargeOrder *model.RechargeOrder,
|
||
packages []*model.Package,
|
||
totalAmount int64,
|
||
seriesID *uint,
|
||
sellerCostPrice int64,
|
||
now time.Time,
|
||
) (*model.Order, []*model.OrderItem, error) {
|
||
orderType, iotCardID, deviceID, err := parseLinkedCarrier(rechargeOrder.LinkedOrderType, rechargeOrder.LinkedCarrierType, rechargeOrder.LinkedCarrierID)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
|
||
generation := rechargeOrder.Generation
|
||
if generation <= 0 {
|
||
generation = 1
|
||
}
|
||
|
||
var sellerShopID *uint
|
||
if rechargeOrder.ShopIDTag > 0 {
|
||
shopID := rechargeOrder.ShopIDTag
|
||
sellerShopID = &shopID
|
||
}
|
||
|
||
paidAmount := totalAmount
|
||
operatorAccountID, operatorAccountName := h.buildPersonalCustomerOperatorSnapshot(ctx, rechargeOrder.UserID)
|
||
order := &model.Order{
|
||
BaseModel: model.BaseModel{
|
||
Creator: rechargeOrder.UserID,
|
||
Updater: rechargeOrder.UserID,
|
||
},
|
||
OrderNo: h.orderStore.GenerateOrderNo(),
|
||
OrderType: orderType,
|
||
BuyerType: model.BuyerTypePersonal,
|
||
BuyerID: rechargeOrder.UserID,
|
||
IotCardID: iotCardID,
|
||
DeviceID: deviceID,
|
||
TotalAmount: totalAmount,
|
||
PaymentMethod: model.PaymentMethodWallet,
|
||
PaymentStatus: model.PaymentStatusPaid,
|
||
PaidAt: &now,
|
||
CommissionStatus: model.CommissionStatusPending,
|
||
CommissionConfigVersion: 0,
|
||
Source: constants.OrderSourceClient,
|
||
Generation: generation,
|
||
ActualPaidAmount: &paidAmount,
|
||
OperatorAccountID: operatorAccountID,
|
||
OperatorAccountType: model.OperatorAccountTypePersonalCustomer,
|
||
OperatorAccountName: operatorAccountName,
|
||
SellerShopID: sellerShopID,
|
||
SeriesID: seriesID,
|
||
SellerCostPrice: sellerCostPrice,
|
||
}
|
||
|
||
items := make([]*model.OrderItem, 0, len(packages))
|
||
for _, pkg := range packages {
|
||
unitPrice := packageprice.PackageEffectiveRetailPrice(pkg)
|
||
items = append(items, &model.OrderItem{
|
||
BaseModel: model.BaseModel{
|
||
Creator: rechargeOrder.UserID,
|
||
Updater: rechargeOrder.UserID,
|
||
},
|
||
PackageID: pkg.ID,
|
||
PackageName: pkg.PackageName,
|
||
Quantity: 1,
|
||
UnitPrice: unitPrice,
|
||
Amount: unitPrice,
|
||
PackagePriceConfigStatus: pkg.PriceConfigStatus,
|
||
PackageIsGift: pkg.IsGift,
|
||
})
|
||
}
|
||
|
||
return order, items, nil
|
||
}
|
||
|
||
func (h *AutoPurchaseHandler) buildPersonalCustomerOperatorSnapshot(ctx context.Context, customerID uint) (*uint, string) {
|
||
if customerID == 0 {
|
||
return nil, ""
|
||
}
|
||
|
||
var customer model.PersonalCustomer
|
||
if err := h.db.WithContext(ctx).Where("id = ?", customerID).First(&customer).Error; err != nil {
|
||
id := customerID
|
||
return &id, ""
|
||
}
|
||
|
||
id := customerID
|
||
return &id, customer.Nickname
|
||
}
|
||
|
||
func (h *AutoPurchaseHandler) activatePackages(
|
||
ctx context.Context,
|
||
tx *gorm.DB,
|
||
order *model.Order,
|
||
packages []*model.Package,
|
||
now time.Time,
|
||
) error {
|
||
carrierType := constants.AssetWalletResourceTypeIotCard
|
||
carrierID := uint(0)
|
||
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
|
||
carrierID = *order.IotCardID
|
||
} else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
|
||
carrierType = constants.AssetWalletResourceTypeDevice
|
||
carrierID = *order.DeviceID
|
||
} else {
|
||
return errors.New("无效的订单载体")
|
||
}
|
||
|
||
for _, pkg := range packages {
|
||
var existingUsage model.PackageUsage
|
||
err := tx.Where("order_id = ? AND package_id = ?", order.ID, pkg.ID).First(&existingUsage).Error
|
||
if err == nil {
|
||
continue
|
||
}
|
||
if err != gorm.ErrRecordNotFound {
|
||
return err
|
||
}
|
||
|
||
if pkg.PackageType == constants.PackageTypeFormal {
|
||
if err = h.activateMainPackage(ctx, tx, order, pkg, carrierType, carrierID, now); err != nil {
|
||
return err
|
||
}
|
||
continue
|
||
}
|
||
if pkg.PackageType == constants.PackageTypeAddon {
|
||
if err = h.activateAddonPackage(ctx, tx, order, pkg, carrierType, carrierID, now); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (h *AutoPurchaseHandler) activateMainPackage(
|
||
ctx context.Context,
|
||
tx *gorm.DB,
|
||
order *model.Order,
|
||
pkg *model.Package,
|
||
carrierType string,
|
||
carrierID uint,
|
||
now time.Time,
|
||
) error {
|
||
_ = ctx
|
||
var activeMainPackage model.PackageUsage
|
||
err := tx.Where("status = ?", constants.PackageUsageStatusActive).
|
||
Where("master_usage_id IS NULL").
|
||
Where(carrierType+"_id = ?", carrierID).
|
||
Order("priority ASC").
|
||
First(&activeMainPackage).Error
|
||
|
||
hasActiveMain := err == nil
|
||
|
||
var status int
|
||
var priority int
|
||
var activatedAt time.Time
|
||
var expiresAt time.Time
|
||
var nextResetAt *time.Time
|
||
var pendingRealnameActivation bool
|
||
|
||
if hasActiveMain {
|
||
status = constants.PackageUsageStatusPending
|
||
var maxPriority int
|
||
tx.Model(&model.PackageUsage{}).
|
||
Where(carrierType+"_id = ?", carrierID).
|
||
Select("COALESCE(MAX(priority), 0)").
|
||
Scan(&maxPriority)
|
||
priority = maxPriority + 1
|
||
} else {
|
||
status = constants.PackageUsageStatusActive
|
||
priority = 1
|
||
activatedAt = now
|
||
expiresAt = packagepkg.CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
|
||
nextResetAt = packagepkg.CalculateNextResetTime(pkg.DataResetCycle, pkg.CalendarType, now, activatedAt)
|
||
}
|
||
|
||
// REALNAME-02: 自动购包属于 C 端充值触发,不需要等实名(C 端已做前置实名检查)
|
||
// ExpiryBase 仅影响后台囤货路径,此处无需判断
|
||
|
||
virtualTotalMBSnapshot, displayGainRatioSnapshot, enableVirtualDataSnapshot := model.BuildPackageUsageSnapshotValues(pkg)
|
||
usage := &model.PackageUsage{
|
||
BaseModel: model.BaseModel{
|
||
Creator: order.Creator,
|
||
Updater: order.Creator,
|
||
},
|
||
OrderID: order.ID,
|
||
OrderNo: order.OrderNo,
|
||
PackageID: pkg.ID,
|
||
PackageName: pkg.PackageName,
|
||
UsageType: order.OrderType,
|
||
DataLimitMB: pkg.RealDataMB,
|
||
VirtualTotalMBSnapshot: virtualTotalMBSnapshot,
|
||
DisplayGainRatioSnapshot: displayGainRatioSnapshot,
|
||
EnableVirtualDataSnapshot: enableVirtualDataSnapshot,
|
||
Status: status,
|
||
Priority: priority,
|
||
DataResetCycle: pkg.DataResetCycle,
|
||
PendingRealnameActivation: pendingRealnameActivation,
|
||
Generation: order.Generation,
|
||
PaidAmount: order.ActualPaidAmount,
|
||
PackagePriceConfigStatus: pkg.PriceConfigStatus,
|
||
PackageIsGift: pkg.IsGift,
|
||
}
|
||
|
||
if carrierType == constants.AssetWalletResourceTypeIotCard {
|
||
usage.IotCardID = carrierID
|
||
} else {
|
||
usage.DeviceID = carrierID
|
||
}
|
||
|
||
if status == constants.PackageUsageStatusActive {
|
||
usage.ActivatedAt = &activatedAt
|
||
usage.ExpiresAt = &expiresAt
|
||
usage.NextResetAt = nextResetAt
|
||
}
|
||
|
||
if err = tx.Omit("status", "pending_realname_activation").Create(usage).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
return tx.Model(usage).Updates(map[string]any{
|
||
"status": usage.Status,
|
||
"pending_realname_activation": usage.PendingRealnameActivation,
|
||
}).Error
|
||
}
|
||
|
||
func (h *AutoPurchaseHandler) activateAddonPackage(
|
||
ctx context.Context,
|
||
tx *gorm.DB,
|
||
order *model.Order,
|
||
pkg *model.Package,
|
||
carrierType string,
|
||
carrierID uint,
|
||
now time.Time,
|
||
) error {
|
||
_ = ctx
|
||
var mainPackage model.PackageUsage
|
||
err := tx.Where("status IN ?", []int{constants.PackageUsageStatusPending, constants.PackageUsageStatusActive}).
|
||
Where("master_usage_id IS NULL").
|
||
Where(carrierType+"_id = ?", carrierID).
|
||
Order("priority ASC").
|
||
First(&mainPackage).Error
|
||
if err == gorm.ErrRecordNotFound {
|
||
return errors.New("必须有主套餐才能购买加油包")
|
||
}
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
var maxPriority int
|
||
tx.Model(&model.PackageUsage{}).
|
||
Where(carrierType+"_id = ?", carrierID).
|
||
Select("COALESCE(MAX(priority), 0)").
|
||
Scan(&maxPriority)
|
||
|
||
priority := maxPriority + 1
|
||
expiresAt := mainPackage.ExpiresAt
|
||
|
||
virtualTotalMBSnapshot, displayGainRatioSnapshot, enableVirtualDataSnapshot := model.BuildPackageUsageSnapshotValues(pkg)
|
||
usage := &model.PackageUsage{
|
||
BaseModel: model.BaseModel{
|
||
Creator: order.Creator,
|
||
Updater: order.Creator,
|
||
},
|
||
OrderID: order.ID,
|
||
OrderNo: order.OrderNo,
|
||
PackageID: pkg.ID,
|
||
PackageName: pkg.PackageName,
|
||
UsageType: order.OrderType,
|
||
DataLimitMB: pkg.RealDataMB,
|
||
VirtualTotalMBSnapshot: virtualTotalMBSnapshot,
|
||
DisplayGainRatioSnapshot: displayGainRatioSnapshot,
|
||
EnableVirtualDataSnapshot: enableVirtualDataSnapshot,
|
||
Status: constants.PackageUsageStatusActive,
|
||
Priority: priority,
|
||
MasterUsageID: &mainPackage.ID,
|
||
ActivatedAt: &now,
|
||
ExpiresAt: expiresAt,
|
||
DataResetCycle: pkg.DataResetCycle,
|
||
Generation: order.Generation,
|
||
PaidAmount: order.ActualPaidAmount,
|
||
PackagePriceConfigStatus: pkg.PriceConfigStatus,
|
||
PackageIsGift: pkg.IsGift,
|
||
}
|
||
|
||
if carrierType == constants.AssetWalletResourceTypeIotCard {
|
||
usage.IotCardID = carrierID
|
||
} else {
|
||
usage.DeviceID = carrierID
|
||
}
|
||
|
||
return tx.Create(usage).Error
|
||
}
|
||
|
||
func parseLinkedPackageIDs(raw []byte) ([]uint, error) {
|
||
var packageIDs []uint
|
||
if len(raw) == 0 {
|
||
return nil, nil
|
||
}
|
||
if err := sonic.Unmarshal(raw, &packageIDs); err != nil {
|
||
return nil, err
|
||
}
|
||
return packageIDs, nil
|
||
}
|
||
|
||
func parseLinkedCarrier(linkedOrderType string, linkedCarrierType string, linkedCarrierID *uint) (string, *uint, *uint, error) {
|
||
if linkedCarrierID == nil || *linkedCarrierID == 0 {
|
||
return "", nil, nil, errors.New("关联载体ID为空")
|
||
}
|
||
|
||
if linkedOrderType == model.OrderTypeSingleCard || linkedCarrierType == "card" || linkedCarrierType == constants.AssetWalletResourceTypeIotCard {
|
||
id := *linkedCarrierID
|
||
return model.OrderTypeSingleCard, &id, nil, nil
|
||
}
|
||
if linkedOrderType == model.OrderTypeDevice || linkedCarrierType == "device" || linkedCarrierType == constants.AssetWalletResourceTypeDevice {
|
||
id := *linkedCarrierID
|
||
return model.OrderTypeDevice, nil, &id, nil
|
||
}
|
||
|
||
return "", nil, nil, errors.New("关联载体类型无效")
|
||
}
|
||
|
||
func validatePackageTypeMix(packages []*model.Package) error {
|
||
hasFormal := false
|
||
hasAddon := false
|
||
|
||
for _, pkg := range packages {
|
||
switch pkg.PackageType {
|
||
case constants.PackageTypeFormal:
|
||
hasFormal = true
|
||
case constants.PackageTypeAddon:
|
||
hasAddon = true
|
||
}
|
||
|
||
if hasFormal && hasAddon {
|
||
return errors.New("不允许在同一订单中同时购买正式套餐和加油包")
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|