实现套餐生效条件覆盖与购买快照

This commit is contained in:
2026-07-22 20:51:12 +09:00
parent c7f8b4c702
commit 9818537239
30 changed files with 1092 additions and 149 deletions

View File

@@ -12,11 +12,13 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
"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"
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
)
// AutoPurchasePayload 充值后自动购包任务载荷
@@ -496,7 +498,11 @@ func (h *AutoPurchaseHandler) activateMainPackage(
carrierID uint,
now time.Time,
) error {
_ = ctx
terms, err := h.resolvePackageTerms(ctx, pkg, order.SellerShopID)
if err != nil {
h.logger.Error("自动购包生成套餐计时快照失败", zap.Uint("package_id", pkg.ID), zap.Error(err))
return err
}
if err := h.lockPackageCarrier(ctx, tx, carrierType, carrierID); err != nil {
return err
}
@@ -512,6 +518,13 @@ func (h *AutoPurchaseHandler) activateMainPackage(
var expiresAt time.Time
var nextResetAt *time.Time
var pendingRealnameActivation bool
if terms.ExpiryBase == constants.PackageExpiryBaseFromActivation {
realnamed, realnameErr := h.isCarrierRealnamed(ctx, tx, carrierType, carrierID)
if realnameErr != nil {
return realnameErr
}
pendingRealnameActivation = !realnamed
}
if hasCurrentMain {
status = constants.PackageUsageStatusPending
@@ -522,16 +535,17 @@ func (h *AutoPurchaseHandler) activateMainPackage(
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)
if pendingRealnameActivation {
status = constants.PackageUsageStatusPending
} else {
status = constants.PackageUsageStatusActive
activatedAt = now
expiresAt = packagepkg.CalculateExpiryTime(terms.CalendarType, activatedAt, terms.DurationMonths, terms.DurationDays)
nextResetAt = packagepkg.CalculateNextResetTime(pkg.DataResetCycle, terms.CalendarType, now, activatedAt)
}
}
// REALNAME-02: 自动购包属于 C 端充值触发不需要等实名C 端已做前置实名检查)
// ExpiryBase 仅影响后台囤货路径,此处无需判断
virtualTotalMBSnapshot, displayGainRatioSnapshot, enableVirtualDataSnapshot := model.BuildPackageUsageSnapshotValues(pkg)
retailAmount := order.TotalAmount
usage := &model.PackageUsage{
@@ -558,6 +572,7 @@ func (h *AutoPurchaseHandler) activateMainPackage(
PackagePriceConfigStatus: pkg.PriceConfigStatus,
PackageIsGift: pkg.IsGift,
}
terms.Apply(usage)
if carrierType == constants.AssetWalletResourceTypeIotCard {
usage.IotCardID = carrierID
@@ -581,6 +596,30 @@ func (h *AutoPurchaseHandler) activateMainPackage(
}).Error
}
// isCarrierRealnamed 查询自动购包载体是否已满足实名激活条件。
func (h *AutoPurchaseHandler) isCarrierRealnamed(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (bool, error) {
switch carrierType {
case constants.AssetWalletResourceTypeIotCard, "card":
var card model.IotCard
if err := tx.WithContext(ctx).Select("real_name_status").First(&card, carrierID).Error; err != nil {
return false, err
}
return card.RealNameStatus == constants.RealNameStatusVerified, nil
case constants.AssetWalletResourceTypeDevice:
var count int64
subQuery := tx.WithContext(ctx).Model(&model.DeviceSimBinding{}).
Select("iot_card_id").Where("device_id = ? AND bind_status = ?", carrierID, constants.BindStatusBound)
if err := tx.WithContext(ctx).Model(&model.IotCard{}).
Where("id IN (?) AND real_name_status = ?", subQuery, constants.RealNameStatusVerified).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
default:
return false, pkgerrors.New(pkgerrors.CodeInvalidParam, "无效的套餐载体类型")
}
}
func (h *AutoPurchaseHandler) lockPackageCarrier(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) error {
switch carrierType {
case constants.AssetWalletResourceTypeIotCard, "card":
@@ -603,7 +642,11 @@ func (h *AutoPurchaseHandler) activateAddonPackage(
carrierID uint,
now time.Time,
) error {
_ = ctx
terms, err := h.resolvePackageTerms(ctx, pkg, order.SellerShopID)
if err != nil {
h.logger.Error("自动购包生成加油包计时快照失败", zap.Uint("package_id", pkg.ID), zap.Error(err))
return err
}
mainPackage, err := packagepkg.FindAttachableMainPackageForAddon(tx, carrierType, carrierID, now)
if err == gorm.ErrRecordNotFound {
return errors.New("必须有主套餐才能购买加油包")
@@ -649,6 +692,7 @@ func (h *AutoPurchaseHandler) activateAddonPackage(
PackagePriceConfigStatus: pkg.PriceConfigStatus,
PackageIsGift: pkg.IsGift,
}
terms.Apply(usage)
if carrierType == constants.AssetWalletResourceTypeIotCard {
usage.IotCardID = carrierID
@@ -659,6 +703,20 @@ func (h *AutoPurchaseHandler) activateAddonPackage(
return tx.Create(usage).Error
}
func (h *AutoPurchaseHandler) resolvePackageTerms(ctx context.Context, pkg *model.Package, sellerShopID *uint) (packagedomain.TermsSnapshot, error) {
var allocation *model.ShopPackageAllocation
if sellerShopID != nil && *sellerShopID > 0 {
found, err := h.shopPackageAllocationStore.GetByShopAndPackageForSystem(ctx, *sellerShopID, pkg.ID)
if err != nil && err != gorm.ErrRecordNotFound {
return packagedomain.TermsSnapshot{}, err
}
if err == nil {
allocation = found
}
}
return packagedomain.ResolveTermsSnapshot(pkg, allocation)
}
func parseLinkedPackageIDs(raw []byte) ([]uint, error) {
var packageIDs []uint
if len(raw) == 0 {