修复 code review 问题:提取重复函数、补全缺失测试用例
- 提取 ResolveTermsFromTx 到 service/package,消除 order 和 auto_purchase 中的重复实现 - 补全 domain 测试:新增 from_activation 覆盖用例,现覆盖两种覆盖值 - 补全激活集成测试:新增 from_purchase 立即激活场景、历史记录兼容回退计数器验证 - 补全自动购包集成测试:新增购买后修改套餐配置不影响已有快照的验证 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,15 +9,18 @@ import (
|
|||||||
|
|
||||||
// TestResolveTermsSnapshot 验证默认、覆盖及两种周期规则。
|
// TestResolveTermsSnapshot 验证默认、覆盖及两种周期规则。
|
||||||
func TestResolveTermsSnapshot(t *testing.T) {
|
func TestResolveTermsSnapshot(t *testing.T) {
|
||||||
override := constants.PackageExpiryBaseFromPurchase
|
overridePurchase := constants.PackageExpiryBaseFromPurchase
|
||||||
|
overrideActivation := constants.PackageExpiryBaseFromActivation
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
pkg model.Package
|
pkg model.Package
|
||||||
allocation *model.ShopPackageAllocation
|
allocation *model.ShopPackageAllocation
|
||||||
wantErr bool
|
wantErr bool
|
||||||
|
wantExpiryBase string
|
||||||
}{
|
}{
|
||||||
{name: "自然月默认", pkg: model.Package{ExpiryBase: constants.PackageExpiryBaseFromActivation, CalendarType: constants.PackageCalendarTypeNaturalMonth, DurationMonths: 12}},
|
{name: "自然月默认", pkg: model.Package{ExpiryBase: constants.PackageExpiryBaseFromActivation, CalendarType: constants.PackageCalendarTypeNaturalMonth, DurationMonths: 12}, wantExpiryBase: constants.PackageExpiryBaseFromActivation},
|
||||||
{name: "按天覆盖", pkg: model.Package{ExpiryBase: constants.PackageExpiryBaseFromActivation, CalendarType: constants.PackageCalendarTypeByDay, DurationDays: 30}, allocation: &model.ShopPackageAllocation{ExpiryBaseOverride: &override}},
|
{name: "from_purchase 覆盖", pkg: model.Package{ExpiryBase: constants.PackageExpiryBaseFromActivation, CalendarType: constants.PackageCalendarTypeByDay, DurationDays: 30}, allocation: &model.ShopPackageAllocation{ExpiryBaseOverride: &overridePurchase}, wantExpiryBase: constants.PackageExpiryBaseFromPurchase},
|
||||||
|
{name: "from_activation 覆盖", pkg: model.Package{ExpiryBase: constants.PackageExpiryBaseFromPurchase, CalendarType: constants.PackageCalendarTypeByDay, DurationDays: 14}, allocation: &model.ShopPackageAllocation{ExpiryBaseOverride: &overrideActivation}, wantExpiryBase: constants.PackageExpiryBaseFromActivation},
|
||||||
{name: "非法生效条件", pkg: model.Package{ExpiryBase: "invalid", CalendarType: constants.PackageCalendarTypeByDay, DurationDays: 30}, wantErr: true},
|
{name: "非法生效条件", pkg: model.Package{ExpiryBase: "invalid", CalendarType: constants.PackageCalendarTypeByDay, DurationDays: 30}, wantErr: true},
|
||||||
{name: "非法按天时长", pkg: model.Package{ExpiryBase: constants.PackageExpiryBaseFromPurchase, CalendarType: constants.PackageCalendarTypeByDay}, wantErr: true},
|
{name: "非法按天时长", pkg: model.Package{ExpiryBase: constants.PackageExpiryBaseFromPurchase, CalendarType: constants.PackageCalendarTypeByDay}, wantErr: true},
|
||||||
}
|
}
|
||||||
@@ -30,6 +33,9 @@ func TestResolveTermsSnapshot(t *testing.T) {
|
|||||||
if !test.wantErr && !got.IsValid() {
|
if !test.wantErr && !got.IsValid() {
|
||||||
t.Fatalf("快照应有效:%+v", got)
|
t.Fatalf("快照应有效:%+v", got)
|
||||||
}
|
}
|
||||||
|
if !test.wantErr && test.wantExpiryBase != "" && got.ExpiryBase != test.wantExpiryBase {
|
||||||
|
t.Fatalf("生效条件不符预期:want=%s got=%s", test.wantExpiryBase, got.ExpiryBase)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2446,21 +2446,8 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolvePackageTerms 在给定事务内查询分配覆盖并解析计时条款快照。
|
|
||||||
// 必须在事务内调用,确保分配读取与 PackageUsage 写入在同一连接,避免快照与提交值不一致。
|
|
||||||
func (s *Service) resolvePackageTerms(ctx context.Context, tx *gorm.DB, pkg *model.Package, sellerShopID *uint) (packagedomain.TermsSnapshot, error) {
|
func (s *Service) resolvePackageTerms(ctx context.Context, tx *gorm.DB, pkg *model.Package, sellerShopID *uint) (packagedomain.TermsSnapshot, error) {
|
||||||
var allocation *model.ShopPackageAllocation
|
return packagepkg.ResolveTermsFromTx(ctx, tx, pkg, sellerShopID)
|
||||||
if sellerShopID != nil && *sellerShopID > 0 {
|
|
||||||
store := postgres.NewShopPackageAllocationStore(tx)
|
|
||||||
found, err := store.GetByShopAndPackageForSystem(ctx, *sellerShopID, pkg.ID)
|
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
|
||||||
return packagedomain.TermsSnapshot{}, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐分配失败")
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
allocation = found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return packagedomain.ResolveTermsSnapshot(pkg, allocation)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) enqueueCommissionCalculation(ctx context.Context, orderID uint) {
|
func (s *Service) enqueueCommissionCalculation(ctx context.Context, orderID uint) {
|
||||||
|
|||||||
@@ -98,6 +98,69 @@ func TestRefundFollowUpActivatesNextPackageFromSnapshot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestFromPurchaseActivatesImmediatelyAtPurchaseTime 验证 from_purchase 快照在购买时直接激活,不等实名。
|
||||||
|
func TestFromPurchaseActivatesImmediatelyAtPurchaseTime(t *testing.T) {
|
||||||
|
tx := testutil.NewPostgresTransaction(t)
|
||||||
|
redisClient := testutil.NewRedisClient(t)
|
||||||
|
card := createActivationTermsCard(t, tx, constants.RealNameStatusNotVerified)
|
||||||
|
pkg := createActivationTermsPackage(t, tx, constants.PackageExpiryBaseFromPurchase, 7)
|
||||||
|
usage := createActivationTermsUsage(t, tx, pkg.ID, card.ID, 1, constants.PackageExpiryBaseFromPurchase, 7, false, time.Now())
|
||||||
|
service := NewActivationService(tx, redisClient, nil, nil, nil, zap.NewNop())
|
||||||
|
if err := service.ActivateSpecificPackage(context.Background(), usage.ID); err != nil {
|
||||||
|
t.Fatalf("from_purchase 直接激活失败:%v", err)
|
||||||
|
}
|
||||||
|
var refreshed model.PackageUsage
|
||||||
|
if err := tx.First(&refreshed, usage.ID).Error; err != nil {
|
||||||
|
t.Fatalf("查询激活结果失败:%v", err)
|
||||||
|
}
|
||||||
|
if refreshed.Status != constants.PackageUsageStatusActive || refreshed.ActivatedAt == nil || refreshed.ExpiresAt == nil {
|
||||||
|
t.Fatalf("from_purchase 套餐应已激活:status=%d activatedAt=%v expiresAt=%v", refreshed.Status, refreshed.ActivatedAt, refreshed.ExpiresAt)
|
||||||
|
}
|
||||||
|
if refreshed.PendingRealnameActivation {
|
||||||
|
t.Fatal("from_purchase 不应标记待实名")
|
||||||
|
}
|
||||||
|
expectedExpiry := CalculateExpiryTime(constants.PackageCalendarTypeByDay, *refreshed.ActivatedAt, 0, 7)
|
||||||
|
if !refreshed.ExpiresAt.Equal(expectedExpiry) {
|
||||||
|
t.Fatalf("to期时间不符:want=%v got=%v", expectedExpiry, refreshed.ExpiresAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHistoricalFallbackFiresWarningAndIncrementsCounter 验证历史记录缺少快照时回退套餐当前配置并递增计数器。
|
||||||
|
func TestHistoricalFallbackFiresWarningAndIncrementsCounter(t *testing.T) {
|
||||||
|
tx := testutil.NewPostgresTransaction(t)
|
||||||
|
redisClient := testutil.NewRedisClient(t)
|
||||||
|
card := createActivationTermsCard(t, tx, constants.RealNameStatusVerified)
|
||||||
|
pkg := createActivationTermsPackage(t, tx, constants.PackageExpiryBaseFromActivation, 21)
|
||||||
|
// 历史记录:四个快照字段全为空值/零值
|
||||||
|
unique := uint(time.Now().UnixNano() & 0x7fffffff)
|
||||||
|
usage := &model.PackageUsage{
|
||||||
|
OrderID: unique, OrderNo: "UR55-HIST-" + strconv.FormatUint(uint64(unique), 10),
|
||||||
|
PackageID: pkg.ID, PackageName: "UR55历史测试套餐",
|
||||||
|
UsageType: constants.AssetWalletResourceTypeIotCard, IotCardID: card.ID,
|
||||||
|
DataLimitMB: 1, Status: constants.PackageUsageStatusPending, Priority: 1, Generation: 1,
|
||||||
|
// 快照字段均留空,模拟 UR#55 上线前的旧数据
|
||||||
|
}
|
||||||
|
if err := tx.Omit("status").Create(usage).Error; err != nil {
|
||||||
|
t.Fatalf("创建历史测试使用记录失败:%v", err)
|
||||||
|
}
|
||||||
|
if err := tx.Model(usage).Update("status", constants.PackageUsageStatusPending).Error; err != nil {
|
||||||
|
t.Fatalf("设置历史测试状态失败:%v", err)
|
||||||
|
}
|
||||||
|
before := HistoricalTermsFallbackCount()
|
||||||
|
service := NewActivationService(tx, redisClient, nil, nil, nil, zap.NewNop())
|
||||||
|
if err := service.ActivateSpecificPackage(context.Background(), usage.ID); err != nil {
|
||||||
|
t.Fatalf("历史记录兼容激活失败:%v", err)
|
||||||
|
}
|
||||||
|
after := HistoricalTermsFallbackCount()
|
||||||
|
if after <= before {
|
||||||
|
t.Fatalf("历史回退计数器未递增:before=%d after=%d", before, after)
|
||||||
|
}
|
||||||
|
var refreshed model.PackageUsage
|
||||||
|
if err := tx.First(&refreshed, usage.ID).Error; err != nil || refreshed.Status != constants.PackageUsageStatusActive {
|
||||||
|
t.Fatalf("历史记录应兼容激活成功:status=%d err=%v", refreshed.Status, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestActivateSpecificPackageRejectsPartialSnapshot 验证新记录非法快照不会静默回退当前套餐配置。
|
// TestActivateSpecificPackageRejectsPartialSnapshot 验证新记录非法快照不会静默回退当前套餐配置。
|
||||||
func TestActivateSpecificPackageRejectsPartialSnapshot(t *testing.T) {
|
func TestActivateSpecificPackageRejectsPartialSnapshot(t *testing.T) {
|
||||||
tx := testutil.NewPostgresTransaction(t)
|
tx := testutil.NewPostgresTransaction(t)
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package packagepkg
|
package packagepkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
|
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var historicalTermsFallbackCount atomic.Uint64
|
var historicalTermsFallbackCount atomic.Uint64
|
||||||
@@ -41,6 +44,23 @@ func HistoricalTermsFallbackCount() uint64 {
|
|||||||
return historicalTermsFallbackCount.Load()
|
return historicalTermsFallbackCount.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveTermsFromTx 在给定事务内查询分配覆盖并解析计时条款快照。
|
||||||
|
// 必须在事务内调用,确保分配读取与 PackageUsage 写入在同一连接,避免快照与提交值不一致。
|
||||||
|
func ResolveTermsFromTx(ctx context.Context, tx *gorm.DB, pkg *model.Package, sellerShopID *uint) (packagedomain.TermsSnapshot, error) {
|
||||||
|
var allocation *model.ShopPackageAllocation
|
||||||
|
if sellerShopID != nil && *sellerShopID > 0 {
|
||||||
|
store := postgres.NewShopPackageAllocationStore(tx)
|
||||||
|
found, err := store.GetByShopAndPackageForSystem(ctx, *sellerShopID, pkg.ID)
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return packagedomain.TermsSnapshot{}, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐分配失败")
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
allocation = found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return packagedomain.ResolveTermsSnapshot(pkg, allocation)
|
||||||
|
}
|
||||||
|
|
||||||
func isEmptyHistoricalTerms(usage *model.PackageUsage) bool {
|
func isEmptyHistoricalTerms(usage *model.PackageUsage) bool {
|
||||||
return usage.ExpiryBaseSnapshot == "" && usage.CalendarTypeSnapshot == "" &&
|
return usage.ExpiryBaseSnapshot == "" && usage.CalendarTypeSnapshot == "" &&
|
||||||
usage.DurationMonthsSnapshot == 0 && usage.DurationDaysSnapshot == 0
|
usage.DurationMonthsSnapshot == 0 && usage.DurationDaysSnapshot == 0
|
||||||
|
|||||||
@@ -703,21 +703,8 @@ func (h *AutoPurchaseHandler) activateAddonPackage(
|
|||||||
return tx.Create(usage).Error
|
return tx.Create(usage).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolvePackageTerms 在给定事务内查询分配覆盖并解析计时条款快照。
|
|
||||||
// 必须在事务内调用,确保分配读取与 PackageUsage 写入在同一连接,避免快照与提交值不一致。
|
|
||||||
func (h *AutoPurchaseHandler) resolvePackageTerms(ctx context.Context, tx *gorm.DB, pkg *model.Package, sellerShopID *uint) (packagedomain.TermsSnapshot, error) {
|
func (h *AutoPurchaseHandler) resolvePackageTerms(ctx context.Context, tx *gorm.DB, pkg *model.Package, sellerShopID *uint) (packagedomain.TermsSnapshot, error) {
|
||||||
var allocation *model.ShopPackageAllocation
|
return packagepkg.ResolveTermsFromTx(ctx, tx, pkg, sellerShopID)
|
||||||
if sellerShopID != nil && *sellerShopID > 0 {
|
|
||||||
store := postgres.NewShopPackageAllocationStore(tx)
|
|
||||||
found, err := store.GetByShopAndPackageForSystem(ctx, *sellerShopID, pkg.ID)
|
|
||||||
if err != nil && err != gorm.ErrRecordNotFound {
|
|
||||||
return packagedomain.TermsSnapshot{}, pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "查询套餐分配失败")
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
allocation = found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return packagedomain.ResolveTermsSnapshot(pkg, allocation)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseLinkedPackageIDs(raw []byte) ([]uint, error) {
|
func parseLinkedPackageIDs(raw []byte) ([]uint, error) {
|
||||||
|
|||||||
@@ -136,6 +136,30 @@ func TestAutoPurchaseTransactionRollbackOnInvalidAddonTerms(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAutoPurchaseConfigChangeDoesNotModifySnapshot 验证自动购包后修改套餐配置不改变已有使用记录的计时快照。
|
||||||
|
func TestAutoPurchaseConfigChangeDoesNotModifySnapshot(t *testing.T) {
|
||||||
|
tx := testutil.NewPostgresTransaction(t)
|
||||||
|
card := createAutoPurchaseCard(t, tx, constants.RealNameStatusVerified)
|
||||||
|
pkg := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, constants.PackageExpiryBaseFromPurchase, 30)
|
||||||
|
handler := &AutoPurchaseHandler{db: tx, shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
|
||||||
|
order := newAutoPurchaseOrder(card.ID, nil)
|
||||||
|
if err := handler.activateMainPackage(context.Background(), tx, order, pkg, constants.AssetWalletResourceTypeIotCard, card.ID, time.Now()); err != nil {
|
||||||
|
t.Fatalf("自动购包创建主套餐失败:%v", err)
|
||||||
|
}
|
||||||
|
// 购买后修改套餐的生效条件和时长
|
||||||
|
if err := tx.Model(pkg).Updates(map[string]any{"expiry_base": constants.PackageExpiryBaseFromActivation, "duration_days": 99}).Error; err != nil {
|
||||||
|
t.Fatalf("修改套餐配置失败:%v", err)
|
||||||
|
}
|
||||||
|
var usage model.PackageUsage
|
||||||
|
if err := tx.Where("order_id = ? AND package_id = ?", order.ID, pkg.ID).First(&usage).Error; err != nil {
|
||||||
|
t.Fatalf("查询自动购包记录失败:%v", err)
|
||||||
|
}
|
||||||
|
// 快照应保留购买时的值,不随套餐修改而变化
|
||||||
|
if usage.ExpiryBaseSnapshot != constants.PackageExpiryBaseFromPurchase || usage.DurationDaysSnapshot != 30 {
|
||||||
|
t.Fatalf("套餐配置变更不应影响已购买快照:expiryBase=%s days=%d", usage.ExpiryBaseSnapshot, usage.DurationDaysSnapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createAutoPurchaseCard(t *testing.T, tx *gorm.DB, realnameStatus int) *model.IotCard {
|
func createAutoPurchaseCard(t *testing.T, tx *gorm.DB, realnameStatus int) *model.IotCard {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
suffix := strconv.FormatInt(time.Now().UnixNano()%100000000000000000, 10)
|
suffix := strconv.FormatInt(time.Now().UnixNano()%100000000000000000, 10)
|
||||||
|
|||||||
Reference in New Issue
Block a user