修复套餐快照集成测试与自动购包幂等
This commit is contained in:
@@ -8710,6 +8710,8 @@ components:
|
|||||||
description: 分配生效条件覆盖 (null:跟随套餐默认值, from_activation:实名激活时生效, from_purchase:购买即生效)
|
description: 分配生效条件覆盖 (null:跟随套餐默认值, from_activation:实名激活时生效, from_purchase:购买即生效)
|
||||||
nullable: true
|
nullable: true
|
||||||
type: string
|
type: string
|
||||||
|
required:
|
||||||
|
- expiry_base_override
|
||||||
type: object
|
type: object
|
||||||
DtoUpdateAssetPackageExpiresAtRequest:
|
DtoUpdateAssetPackageExpiresAtRequest:
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func NewShopPackageBatchAllocationHandler(service *batchAllocationService.Servic
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BatchAllocate 批量分配套餐
|
// BatchAllocate 批量分配套餐
|
||||||
|
// POST /api/admin/shop-package-allocations/batch
|
||||||
func (h *ShopPackageBatchAllocationHandler) BatchAllocate(c *fiber.Ctx) error {
|
func (h *ShopPackageBatchAllocationHandler) BatchAllocate(c *fiber.Ctx) error {
|
||||||
var req dto.BatchAllocatePackagesRequest
|
var req dto.BatchAllocatePackagesRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。
|
|
||||||
const testIDMask = 0x7fffffff
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
@@ -27,6 +24,9 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。
|
||||||
|
const testIDMask = 0x7fffffff
|
||||||
|
|
||||||
// TestBatchAllocatePackagesHTTPRequiresExplicitExpiryBase 验证批量分配显式选择并固化到全部记录。
|
// TestBatchAllocatePackagesHTTPRequiresExplicitExpiryBase 验证批量分配显式选择并固化到全部记录。
|
||||||
func TestBatchAllocatePackagesHTTPRequiresExplicitExpiryBase(t *testing.T) {
|
func TestBatchAllocatePackagesHTTPRequiresExplicitExpiryBase(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package order
|
package order
|
||||||
|
|
||||||
// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。
|
|
||||||
const testIDMask = testIDMask
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -17,6 +14,9 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。
|
||||||
|
const testIDMask = 0x7fffffff
|
||||||
|
|
||||||
// TestSynchronousPurchasePersistsImmutableTermsSnapshots 验证同步主套餐和加油包固化购买时计时条款。
|
// TestSynchronousPurchasePersistsImmutableTermsSnapshots 验证同步主套餐和加油包固化购买时计时条款。
|
||||||
func TestSynchronousPurchasePersistsImmutableTermsSnapshots(t *testing.T) {
|
func TestSynchronousPurchasePersistsImmutableTermsSnapshots(t *testing.T) {
|
||||||
tx := testutil.NewPostgresTransaction(t)
|
tx := testutil.NewPostgresTransaction(t)
|
||||||
@@ -99,13 +99,13 @@ func TestCEndPurchasePersistsSnapshotAndActivatesImmediately(t *testing.T) {
|
|||||||
service := &Service{shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
|
service := &Service{shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
order := &model.Order{
|
order := &model.Order{
|
||||||
Model: gorm.Model{ID: uint(time.Now().UnixNano() & testIDMask)},
|
Model: gorm.Model{ID: uint(time.Now().UnixNano() & testIDMask)},
|
||||||
OrderNo: "UR55-CEND-" + strconv.FormatInt(time.Now().UnixNano(), 10),
|
OrderNo: "UR55-CEND-" + strconv.FormatInt(time.Now().UnixNano(), 10),
|
||||||
OrderType: model.OrderTypeSingleCard,
|
OrderType: model.OrderTypeSingleCard,
|
||||||
BuyerType: model.BuyerTypePersonal,
|
BuyerType: model.BuyerTypePersonal,
|
||||||
IotCardID: &card.ID,
|
IotCardID: &card.ID,
|
||||||
TotalAmount: 100,
|
TotalAmount: 100,
|
||||||
Generation: 1,
|
Generation: 1,
|
||||||
}
|
}
|
||||||
if err := service.activateMainPackage(context.Background(), tx, order, pkg, constants.AssetWalletResourceTypeIotCard, card.ID, now); err != nil {
|
if err := service.activateMainPackage(context.Background(), tx, order, pkg, constants.AssetWalletResourceTypeIotCard, card.ID, now); err != nil {
|
||||||
t.Fatalf("C 端购买创建主套餐失败:%v", err)
|
t.Fatalf("C 端购买创建主套餐失败:%v", err)
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package packagepkg
|
package packagepkg
|
||||||
|
|
||||||
// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。
|
|
||||||
const testIDMask = testIDMask
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -16,6 +13,9 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。
|
||||||
|
const testIDMask = 0x7fffffff
|
||||||
|
|
||||||
// TestActivateByRealnameUsesPurchasedSnapshot 验证实名激活只使用购买快照计算起止时间。
|
// TestActivateByRealnameUsesPurchasedSnapshot 验证实名激活只使用购买快照计算起止时间。
|
||||||
func TestActivateByRealnameUsesPurchasedSnapshot(t *testing.T) {
|
func TestActivateByRealnameUsesPurchasedSnapshot(t *testing.T) {
|
||||||
tx := testutil.NewPostgresTransaction(t)
|
tx := testutil.NewPostgresTransaction(t)
|
||||||
@@ -134,6 +134,10 @@ func TestHistoricalFallbackFiresWarningAndIncrementsCounter(t *testing.T) {
|
|||||||
redisClient := testutil.NewRedisClient(t)
|
redisClient := testutil.NewRedisClient(t)
|
||||||
card := createActivationTermsCard(t, tx, constants.RealNameStatusVerified)
|
card := createActivationTermsCard(t, tx, constants.RealNameStatusVerified)
|
||||||
pkg := createActivationTermsPackage(t, tx, constants.PackageExpiryBaseFromActivation, 21)
|
pkg := createActivationTermsPackage(t, tx, constants.PackageExpiryBaseFromActivation, 21)
|
||||||
|
// 仅在会回滚的测试事务内禁用触发器,以构造 UR#55 上线前的历史空快照记录。
|
||||||
|
if err := tx.Exec("ALTER TABLE tb_package_usage DISABLE TRIGGER trg_validate_package_usage_terms_snapshot").Error; err != nil {
|
||||||
|
t.Fatalf("禁用快照校验触发器失败:%v", err)
|
||||||
|
}
|
||||||
// 历史记录:四个快照字段全为空值/零值
|
// 历史记录:四个快照字段全为空值/零值
|
||||||
unique := uint(time.Now().UnixNano() & testIDMask)
|
unique := uint(time.Now().UnixNano() & testIDMask)
|
||||||
usage := &model.PackageUsage{
|
usage := &model.PackageUsage{
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ type Service struct {
|
|||||||
packageAllocationStore *postgres.ShopPackageAllocationStore
|
packageAllocationStore *postgres.ShopPackageAllocationStore
|
||||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||||
shopStore *postgres.ShopStore
|
shopStore *postgres.ShopStore
|
||||||
auditService AuditService
|
auditService AuditLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuditService 敏感配置变更审计能力。
|
// AuditLogger 敏感配置变更审计能力。
|
||||||
type AuditService interface {
|
type AuditLogger interface {
|
||||||
LogOperation(ctx context.Context, log *model.AccountOperationLog)
|
LogOperation(ctx context.Context, log *model.AccountOperationLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ func New(
|
|||||||
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
packageAllocationStore *postgres.ShopPackageAllocationStore,
|
||||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||||
shopStore *postgres.ShopStore,
|
shopStore *postgres.ShopStore,
|
||||||
auditService AuditService,
|
auditService AuditLogger,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
db: db,
|
db: db,
|
||||||
|
|||||||
@@ -462,6 +462,10 @@ func (h *AutoPurchaseHandler) activatePackages(
|
|||||||
} else {
|
} else {
|
||||||
return errors.New("无效的订单载体")
|
return errors.New("无效的订单载体")
|
||||||
}
|
}
|
||||||
|
// 在查询既有记录前锁定载体,避免并发任务同时判定不存在而重复创建套餐使用记录。
|
||||||
|
if err := h.lockPackageCarrier(ctx, tx, carrierType, carrierID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
var existingUsage model.PackageUsage
|
var existingUsage model.PackageUsage
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package task
|
package task
|
||||||
|
|
||||||
// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。
|
|
||||||
const testIDMask = testIDMask
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -17,6 +14,9 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。
|
||||||
|
const testIDMask = 0x7fffffff
|
||||||
|
|
||||||
// TestAutoPurchasePersistsTermsSnapshotsAndRealnameDecision 验证自动购包复用快照并遵守实名起算规则。
|
// TestAutoPurchasePersistsTermsSnapshotsAndRealnameDecision 验证自动购包复用快照并遵守实名起算规则。
|
||||||
func TestAutoPurchasePersistsTermsSnapshotsAndRealnameDecision(t *testing.T) {
|
func TestAutoPurchasePersistsTermsSnapshotsAndRealnameDecision(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@@ -192,4 +192,3 @@ func newAutoPurchaseOrder(cardID uint, sellerShopID *uint) *model.Order {
|
|||||||
orderID := uint(time.Now().UnixNano() & testIDMask)
|
orderID := uint(time.Now().UnixNano() & testIDMask)
|
||||||
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}
|
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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user