修复 code review 问题:DTO 内部字段、错误消息、重复代码与测试覆盖

- 删除 ExpiryBaseOverrideSet 字段的 description 标签(json:"-" 内部字段不进文档)
- ValidateExpiryBaseOverride 两处 CodeInvalidParam 补充中文错误消息
- 提取 initPackageExpiryBaseFields 消除 toResponse/toResponseWithAllocation 中的重复初始化
- 删除 order/service.go 中"行业卡永远直接激活"过时注释
- 补充 T01 系列授权 HTTP 集成测试(Create + ManagePackages 两个入口)
- 补充 T03 C 端购买和平台代购(无分配)快照集成测试
- 补充 T04 from_purchase 默认值、卡未实名仍立即激活的自动购包测试
- 新增 testutil.NewRedisClient 供激活接续测试使用

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 10:56:46 +09:00
parent 9818537239
commit f7e0f07692
13 changed files with 931 additions and 40 deletions

View File

@@ -0,0 +1,169 @@
package task
import (
"context"
"strconv"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/internal/testutil"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"go.uber.org/zap"
"gorm.io/gorm"
)
// TestAutoPurchasePersistsTermsSnapshotsAndRealnameDecision 验证自动购包复用快照并遵守实名起算规则。
func TestAutoPurchasePersistsTermsSnapshotsAndRealnameDecision(t *testing.T) {
testCases := []struct {
name string
defaultBase string
override *string
realnameStatus int
expectedBase string
expectedStatus int
expectedPending bool
}{
{name: "跟随默认等待实名", defaultBase: constants.PackageExpiryBaseFromActivation, realnameStatus: constants.RealNameStatusNotVerified, expectedBase: constants.PackageExpiryBaseFromActivation, expectedStatus: constants.PackageUsageStatusPending, expectedPending: true},
{name: "覆盖购买即生效", defaultBase: constants.PackageExpiryBaseFromActivation, override: taskStringPointer(constants.PackageExpiryBaseFromPurchase), realnameStatus: constants.RealNameStatusNotVerified, expectedBase: constants.PackageExpiryBaseFromPurchase, expectedStatus: constants.PackageUsageStatusActive},
{name: "覆盖实名但已实名", defaultBase: constants.PackageExpiryBaseFromPurchase, override: taskStringPointer(constants.PackageExpiryBaseFromActivation), realnameStatus: constants.RealNameStatusVerified, expectedBase: constants.PackageExpiryBaseFromActivation, expectedStatus: constants.PackageUsageStatusActive},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
tx := testutil.NewPostgresTransaction(t)
card := createAutoPurchaseCard(t, tx, testCase.realnameStatus)
pkg := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, testCase.defaultBase, 31)
shopID := uint(time.Now().UnixNano() & 0x7fffffff)
if testCase.override != nil {
allocation := &model.ShopPackageAllocation{ShopID: shopID, PackageID: pkg.ID, CostPrice: 1, RetailPrice: 2, Status: constants.StatusEnabled, ShelfStatus: 1, ExpiryBaseOverride: testCase.override}
if err := tx.Create(allocation).Error; err != nil {
t.Fatalf("创建套餐覆盖配置失败:%v", err)
}
}
handler := &AutoPurchaseHandler{db: tx, shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
order := newAutoPurchaseOrder(card.ID, &shopID)
if err := handler.activateMainPackage(context.Background(), tx, order, pkg, constants.AssetWalletResourceTypeIotCard, card.ID, time.Now()); 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 != testCase.expectedBase || usage.CalendarTypeSnapshot != constants.PackageCalendarTypeByDay || usage.DurationDaysSnapshot != 31 || usage.Status != testCase.expectedStatus || usage.PendingRealnameActivation != testCase.expectedPending {
t.Fatalf("自动购包快照或实名决策错误:%+v", usage)
}
})
}
}
// TestAutoPurchaseDefaultFromPurchaseActivatesImmediatelyWithoutRealname 验证套餐默认为 from_purchase 时自动购包不需要实名即激活。
func TestAutoPurchaseDefaultFromPurchaseActivatesImmediatelyWithoutRealname(t *testing.T) {
tx := testutil.NewPostgresTransaction(t)
// 卡未实名,但套餐默认 from_purchase应立即激活
card := createAutoPurchaseCard(t, tx, constants.RealNameStatusNotVerified)
pkg := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, constants.PackageExpiryBaseFromPurchase, 14)
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("from_purchase 默认值自动购包失败:%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 != 14 {
t.Fatalf("from_purchase 默认值快照错误:%+v", usage)
}
if usage.Status != constants.PackageUsageStatusActive || usage.PendingRealnameActivation {
t.Fatalf("from_purchase 默认值未实名仍应立即激活status=%d pendingRealname=%v", usage.Status, usage.PendingRealnameActivation)
}
}
// TestAutoPurchaseMainAndAddonAreIdempotentByBusinessFact 验证主套餐、加油包和重复消费依赖数据库事实去重。
func TestAutoPurchaseMainAndAddonAreIdempotentByBusinessFact(t *testing.T) {
tx := testutil.NewPostgresTransaction(t)
card := createAutoPurchaseCard(t, tx, constants.RealNameStatusVerified)
formal := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, constants.PackageExpiryBaseFromPurchase, 30)
addon := createAutoPurchasePackage(t, tx, constants.PackageTypeAddon, constants.PackageExpiryBaseFromActivation, 7)
handler := &AutoPurchaseHandler{db: tx, shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
formalOrder := newAutoPurchaseOrder(card.ID, nil)
addonOrder := newAutoPurchaseOrder(card.ID, nil)
now := time.Now()
if err := handler.activatePackages(context.Background(), tx, formalOrder, []*model.Package{formal}, now); err != nil {
t.Fatalf("首次自动创建主套餐失败:%v", err)
}
if err := handler.activatePackages(context.Background(), tx, formalOrder, []*model.Package{formal}, now); err != nil {
t.Fatalf("重复消费应按业务事实幂等:%v", err)
}
if err := handler.activatePackages(context.Background(), tx, addonOrder, []*model.Package{addon}, now.Add(time.Minute)); err != nil {
t.Fatalf("自动创建加油包失败:%v", err)
}
var usages []model.PackageUsage
if err := tx.Where("order_id IN ?", []uint{formalOrder.ID, addonOrder.ID}).Order("order_id").Find(&usages).Error; err != nil {
t.Fatalf("查询自动购包记录失败:%v", err)
}
if len(usages) != 2 {
t.Fatalf("重复消费只应形成一组记录,实际 %d", len(usages))
}
for _, usage := range usages {
if usage.ExpiryBaseSnapshot == "" || usage.CalendarTypeSnapshot == "" {
t.Fatalf("自动购包不得产生空快照:%+v", usage)
}
}
if usages[1].MasterUsageID == nil {
t.Fatalf("加油包必须关联主套餐:%+v", usages[1])
}
}
// TestAutoPurchaseTransactionRollbackOnInvalidAddonTerms 验证同一事务中后续套餐失败时不留下部分使用记录。
func TestAutoPurchaseTransactionRollbackOnInvalidAddonTerms(t *testing.T) {
tx := testutil.NewPostgresTransaction(t)
card := createAutoPurchaseCard(t, tx, constants.RealNameStatusVerified)
formal := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, constants.PackageExpiryBaseFromPurchase, 30)
invalidAddon := createAutoPurchasePackage(t, tx, constants.PackageTypeAddon, "invalid", 7)
handler := &AutoPurchaseHandler{db: tx, shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
order := newAutoPurchaseOrder(card.ID, nil)
err := tx.Transaction(func(inner *gorm.DB) error {
return handler.activatePackages(context.Background(), inner, order, []*model.Package{formal, invalidAddon}, time.Now())
})
if err == nil {
t.Fatal("非法加油包计时条款必须使业务事务失败")
}
var count int64
if err := tx.Model(&model.PackageUsage{}).Where("order_id = ?", order.ID).Count(&count).Error; err != nil || count != 0 {
t.Fatalf("事务失败不应留下部分主套餐count=%d err=%v", count, err)
}
}
func createAutoPurchaseCard(t *testing.T, tx *gorm.DB, realnameStatus int) *model.IotCard {
t.Helper()
suffix := strconv.FormatInt(time.Now().UnixNano()%100000000000000000, 10)
iccid := "88" + suffix
if len(iccid) < 19 {
iccid += "0000000000000000000"[:19-len(iccid)]
}
iccid = iccid[:19]
card := &model.IotCard{ICCID: iccid, ICCID19: iccid, CarrierID: 1, RealNameStatus: realnameStatus, AssetStatus: constants.AssetStatusInStock, Generation: 1}
if err := tx.Create(card).Error; err != nil {
t.Fatalf("创建自动购包测试卡失败:%v", err)
}
return card
}
func createAutoPurchasePackage(t *testing.T, tx *gorm.DB, packageType, expiryBase string, durationDays int) *model.Package {
t.Helper()
suffix := strconv.FormatInt(time.Now().UnixNano(), 10)
pkg := &model.Package{PackageCode: "UR55-AUTO-" + suffix, PackageName: "UR55自动购包测试套餐", PackageType: packageType, DurationMonths: 1, DurationDays: durationDays, CalendarType: constants.PackageCalendarTypeByDay, ExpiryBase: expiryBase, Status: constants.StatusEnabled, ShelfStatus: 1, DataResetCycle: "monthly"}
if err := tx.Create(pkg).Error; err != nil {
t.Fatalf("创建自动购包测试套餐失败:%v", err)
}
return pkg
}
func newAutoPurchaseOrder(cardID uint, sellerShopID *uint) *model.Order {
orderID := uint(time.Now().UnixNano() & 0x7fffffff)
return &model.Order{Model: gorm.Model{ID: orderID}, OrderNo: "UR55-AUTO-" + strconv.FormatUint(uint64(orderID), 10), OrderType: model.OrderTypeSingleCard, IotCardID: &cardID, SellerShopID: sellerShopID, TotalAmount: 100, Generation: 1}
}
func taskStringPointer(value string) *string { return &value }