From 992757c0561e48d5712fcfa290e4f3da56933dee Mon Sep 17 00:00:00 2001 From: break Date: Thu, 23 Jul 2026 12:23:52 +0900 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A5=97=E9=A4=90=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95=E4=B8=8E=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=B4=AD=E5=8C=85=E5=B9=82=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/admin-openapi.yaml | 2 ++ .../admin/shop_package_batch_allocation.go | 1 + ...hop_package_expiry_base_integration_test.go | 6 +++--- .../order/package_terms_integration_test.go | 18 +++++++++--------- .../activation_terms_integration_test.go | 10 +++++++--- .../shop_package_batch_allocation/service.go | 8 ++++---- internal/task/auto_purchase.go | 4 ++++ .../auto_purchase_terms_integration_test.go | 7 +++---- 8 files changed, 33 insertions(+), 23 deletions(-) diff --git a/docs/admin-openapi.yaml b/docs/admin-openapi.yaml index 5ff362b..a95fc81 100644 --- a/docs/admin-openapi.yaml +++ b/docs/admin-openapi.yaml @@ -8710,6 +8710,8 @@ components: description: 分配生效条件覆盖 (null:跟随套餐默认值, from_activation:实名激活时生效, from_purchase:购买即生效) nullable: true type: string + required: + - expiry_base_override type: object DtoUpdateAssetPackageExpiresAtRequest: properties: diff --git a/internal/handler/admin/shop_package_batch_allocation.go b/internal/handler/admin/shop_package_batch_allocation.go index 524a416..341e616 100644 --- a/internal/handler/admin/shop_package_batch_allocation.go +++ b/internal/handler/admin/shop_package_batch_allocation.go @@ -21,6 +21,7 @@ func NewShopPackageBatchAllocationHandler(service *batchAllocationService.Servic } // BatchAllocate 批量分配套餐 +// POST /api/admin/shop-package-allocations/batch func (h *ShopPackageBatchAllocationHandler) BatchAllocate(c *fiber.Ctx) error { var req dto.BatchAllocatePackagesRequest if err := c.BodyParser(&req); err != nil { diff --git a/internal/handler/admin/shop_package_expiry_base_integration_test.go b/internal/handler/admin/shop_package_expiry_base_integration_test.go index c15149a..f762fe3 100644 --- a/internal/handler/admin/shop_package_expiry_base_integration_test.go +++ b/internal/handler/admin/shop_package_expiry_base_integration_test.go @@ -1,8 +1,5 @@ package admin -// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。 -const testIDMask = 0x7fffffff - import ( "bytes" "context" @@ -27,6 +24,9 @@ import ( "gorm.io/gorm" ) +// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。 +const testIDMask = 0x7fffffff + // TestBatchAllocatePackagesHTTPRequiresExplicitExpiryBase 验证批量分配显式选择并固化到全部记录。 func TestBatchAllocatePackagesHTTPRequiresExplicitExpiryBase(t *testing.T) { testCases := []struct { diff --git a/internal/service/order/package_terms_integration_test.go b/internal/service/order/package_terms_integration_test.go index 9230dd4..7925b6a 100644 --- a/internal/service/order/package_terms_integration_test.go +++ b/internal/service/order/package_terms_integration_test.go @@ -1,8 +1,5 @@ package order -// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。 -const testIDMask = testIDMask - import ( "context" "strconv" @@ -17,6 +14,9 @@ import ( "gorm.io/gorm" ) +// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。 +const testIDMask = 0x7fffffff + // TestSynchronousPurchasePersistsImmutableTermsSnapshots 验证同步主套餐和加油包固化购买时计时条款。 func TestSynchronousPurchasePersistsImmutableTermsSnapshots(t *testing.T) { tx := testutil.NewPostgresTransaction(t) @@ -99,13 +99,13 @@ func TestCEndPurchasePersistsSnapshotAndActivatesImmediately(t *testing.T) { service := &Service{shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()} now := time.Now() order := &model.Order{ - Model: gorm.Model{ID: uint(time.Now().UnixNano() & testIDMask)}, - OrderNo: "UR55-CEND-" + strconv.FormatInt(time.Now().UnixNano(), 10), - OrderType: model.OrderTypeSingleCard, - BuyerType: model.BuyerTypePersonal, - IotCardID: &card.ID, + Model: gorm.Model{ID: uint(time.Now().UnixNano() & testIDMask)}, + OrderNo: "UR55-CEND-" + strconv.FormatInt(time.Now().UnixNano(), 10), + OrderType: model.OrderTypeSingleCard, + BuyerType: model.BuyerTypePersonal, + IotCardID: &card.ID, TotalAmount: 100, - Generation: 1, + Generation: 1, } if err := service.activateMainPackage(context.Background(), tx, order, pkg, constants.AssetWalletResourceTypeIotCard, card.ID, now); err != nil { t.Fatalf("C 端购买创建主套餐失败:%v", err) diff --git a/internal/service/package/activation_terms_integration_test.go b/internal/service/package/activation_terms_integration_test.go index 6f5a339..e8f94d4 100644 --- a/internal/service/package/activation_terms_integration_test.go +++ b/internal/service/package/activation_terms_integration_test.go @@ -1,8 +1,5 @@ package packagepkg -// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。 -const testIDMask = testIDMask - import ( "context" "strconv" @@ -16,6 +13,9 @@ import ( "gorm.io/gorm" ) +// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。 +const testIDMask = 0x7fffffff + // TestActivateByRealnameUsesPurchasedSnapshot 验证实名激活只使用购买快照计算起止时间。 func TestActivateByRealnameUsesPurchasedSnapshot(t *testing.T) { tx := testutil.NewPostgresTransaction(t) @@ -134,6 +134,10 @@ func TestHistoricalFallbackFiresWarningAndIncrementsCounter(t *testing.T) { redisClient := testutil.NewRedisClient(t) card := createActivationTermsCard(t, tx, constants.RealNameStatusVerified) 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) usage := &model.PackageUsage{ diff --git a/internal/service/shop_package_batch_allocation/service.go b/internal/service/shop_package_batch_allocation/service.go index 8ebd5a5..d3e21a3 100644 --- a/internal/service/shop_package_batch_allocation/service.go +++ b/internal/service/shop_package_batch_allocation/service.go @@ -20,11 +20,11 @@ type Service struct { packageAllocationStore *postgres.ShopPackageAllocationStore seriesAllocationStore *postgres.ShopSeriesAllocationStore shopStore *postgres.ShopStore - auditService AuditService + auditService AuditLogger } -// AuditService 敏感配置变更审计能力。 -type AuditService interface { +// AuditLogger 敏感配置变更审计能力。 +type AuditLogger interface { LogOperation(ctx context.Context, log *model.AccountOperationLog) } @@ -34,7 +34,7 @@ func New( packageAllocationStore *postgres.ShopPackageAllocationStore, seriesAllocationStore *postgres.ShopSeriesAllocationStore, shopStore *postgres.ShopStore, - auditService AuditService, + auditService AuditLogger, ) *Service { return &Service{ db: db, diff --git a/internal/task/auto_purchase.go b/internal/task/auto_purchase.go index 6ab3c9a..d5a3bf5 100644 --- a/internal/task/auto_purchase.go +++ b/internal/task/auto_purchase.go @@ -462,6 +462,10 @@ func (h *AutoPurchaseHandler) activatePackages( } else { return errors.New("无效的订单载体") } + // 在查询既有记录前锁定载体,避免并发任务同时判定不存在而重复创建套餐使用记录。 + if err := h.lockPackageCarrier(ctx, tx, carrierType, carrierID); err != nil { + return err + } for _, pkg := range packages { var existingUsage model.PackageUsage diff --git a/internal/task/auto_purchase_terms_integration_test.go b/internal/task/auto_purchase_terms_integration_test.go index cba910d..d85dc64 100644 --- a/internal/task/auto_purchase_terms_integration_test.go +++ b/internal/task/auto_purchase_terms_integration_test.go @@ -1,8 +1,5 @@ package task -// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。 -const testIDMask = testIDMask - import ( "context" "strconv" @@ -17,6 +14,9 @@ import ( "gorm.io/gorm" ) +// testIDMask 用于将纳秒时间戳截断为合法的 uint 主键范围,仅限测试用。 +const testIDMask = 0x7fffffff + // TestAutoPurchasePersistsTermsSnapshotsAndRealnameDecision 验证自动购包复用快照并遵守实名起算规则。 func TestAutoPurchasePersistsTermsSnapshotsAndRealnameDecision(t *testing.T) { testCases := []struct { @@ -192,4 +192,3 @@ func newAutoPurchaseOrder(cardID uint, sellerShopID *uint) *model.Order { 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} } -