From 7a2d292c36c6627e62234b5f50658a1fb7d84fe4 Mon Sep 17 00:00:00 2001 From: zhx Date: Sat, 4 Jul 2026 02:17:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=96=E6=B6=88vip=E9=97=A8=E6=A7=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/rocketmq/broker.local.conf | 12 --- run/im-c2c-broadcast-20260602-061847.jsonl | 92 ------------------- .../056_reset_vip_recharge_thresholds.sql | 6 ++ .../internal/modules/vipconfig/handler.go | 36 ++++---- .../http/manager_center_handler_test.go | 16 ++-- .../http/managerapi/manager_center_handler.go | 4 +- .../mysql/initdb/001_wallet_service.sql | 4 +- .../internal/service/wallet/service_test.go | 27 +----- .../storage/mysql/vip_admin_repository.go | 8 +- .../storage/mysql/vip_purchase_repository.go | 32 +++---- .../storage/mysql/vip_query_repository.go | 38 ++------ .../internal/testutil/mysqltest/mysqltest.go | 17 +++- 12 files changed, 82 insertions(+), 210 deletions(-) delete mode 100644 deploy/rocketmq/broker.local.conf delete mode 100644 run/im-c2c-broadcast-20260602-061847.jsonl create mode 100644 scripts/mysql/056_reset_vip_recharge_thresholds.sql diff --git a/deploy/rocketmq/broker.local.conf b/deploy/rocketmq/broker.local.conf deleted file mode 100644 index deb32b42..00000000 --- a/deploy/rocketmq/broker.local.conf +++ /dev/null @@ -1,12 +0,0 @@ -brokerClusterName = hyapp-local -brokerName = broker-a -brokerId = 0 -deleteWhen = 04 -fileReservedTime = 48 -brokerRole = ASYNC_MASTER -flushDiskType = ASYNC_FLUSH -autoCreateTopicEnable = true -# Local RocketMQ clients run in two places: Docker containers and host go run -# processes. Advertising the Docker host address keeps both paths reachable. -brokerIP1 = host.docker.internal -listenPort = 10911 diff --git a/run/im-c2c-broadcast-20260602-061847.jsonl b/run/im-c2c-broadcast-20260602-061847.jsonl deleted file mode 100644 index 32bcf75e..00000000 --- a/run/im-c2c-broadcast-20260602-061847.jsonl +++ /dev/null @@ -1,92 +0,0 @@ -{"at_ms":1780381127156,"mode":"dry-run","to_account":"424242","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"12345678","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"880585934","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"880585935","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"881586014","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"881586015","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"882586332","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"882586333","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990000001","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990000002","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990686001","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990686002","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990686003","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990686004","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990686005","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990686006","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990686007","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990686008","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990686009","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990686010","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990687001","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990687002","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990687003","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990687004","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990687005","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990687006","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990687007","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990687008","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990687009","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990687010","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990688001","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990688002","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990688003","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990688004","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990688005","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990688006","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990688007","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990688008","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990688009","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990688010","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"900000971589","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"900000971590","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"900000971617","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"900000971618","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"900000971718","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"900000971719","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"900503651195","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"900503651196","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"900503651197","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"900503651198","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"910004935471","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"910004935472","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"910004935473","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"910004935474","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"910004936181","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"910004936182","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"910004936183","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"910004936184","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"930000000001","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"930000000002","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"975824912277","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"975824912278","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"975825864464","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"975825864465","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"975827047904","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"975827047905","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"975828064123","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"975828064124","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990778061358","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"990778062287","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"991205120003","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"1778070991803","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"1778070991804","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"1778071196761","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"1778071196762","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"7049835865153","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"7049835865154","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"7049835865155","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"7049835865156","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"7049835865157","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"7049835865158","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"7049835865159","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"7049835865160","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"7049835865161","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"7049835865162","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"318705991371722752","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"318707093894860800","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"318707097803952128","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"318707100643495936","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"318707103256547328","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"318707105987039232","status":"dry_run"} -{"at_ms":1780381127156,"mode":"dry-run","to_account":"320078559995498496","status":"dry_run"} diff --git a/scripts/mysql/056_reset_vip_recharge_thresholds.sql b/scripts/mysql/056_reset_vip_recharge_thresholds.sql new file mode 100644 index 00000000..c5fb0da2 --- /dev/null +++ b/scripts/mysql/056_reset_vip_recharge_thresholds.sql @@ -0,0 +1,6 @@ +USE hyapp_wallet; + +-- VIP 购买已改为所有等级直接按金币购买;保留字段只为兼容旧协议,线上已有累充门槛统一清零。 +UPDATE vip_levels +SET required_recharge_coin_amount = 0 +WHERE required_recharge_coin_amount <> 0; diff --git a/server/admin/internal/modules/vipconfig/handler.go b/server/admin/internal/modules/vipconfig/handler.go index 501ff11a..e8e30488 100644 --- a/server/admin/internal/modules/vipconfig/handler.go +++ b/server/admin/internal/modules/vipconfig/handler.go @@ -139,13 +139,14 @@ func (h *Handler) UpdateLevels(c *gin.Context) { input := make([]*walletv1.AdminVipLevelInput, 0, len(req.Levels)) for _, level := range req.Levels { input = append(input, &walletv1.AdminVipLevelInput{ - Level: level.Level, - Name: strings.TrimSpace(level.Name), - Status: strings.TrimSpace(level.Status), - PriceCoin: level.PriceCoin, - DurationMs: level.DurationMS, - RewardResourceGroupId: level.RewardResourceGroupID, - RequiredRechargeCoinAmount: level.RequiredRechargeCoinAmount, + Level: level.Level, + Name: strings.TrimSpace(level.Name), + Status: strings.TrimSpace(level.Status), + PriceCoin: level.PriceCoin, + DurationMs: level.DurationMS, + RewardResourceGroupId: level.RewardResourceGroupID, + // VIP 累充门槛已下线;后台旧请求即使携带旧字段,也统一写 0,避免旧页面或缓存把门槛重新保存回来。 + RequiredRechargeCoinAmount: 0, SortOrder: level.SortOrder, }) } @@ -170,16 +171,17 @@ func vipLevelsFromProto(items []*walletv1.VipLevel) []vipLevelDTO { continue } levels = append(levels, vipLevelDTO{ - Level: item.GetLevel(), - Name: item.GetName(), - Status: item.GetStatus(), - PriceCoin: item.GetPriceCoin(), - DurationMS: item.GetDurationMs(), - RewardResourceGroupID: item.GetRewardResourceGroupId(), - SortOrder: item.GetSortOrder(), - RechargeGateRequired: item.GetRechargeGateRequired(), - RequiredRechargeCoinAmount: item.GetRequiredRechargeCoinAmount(), - UserRechargeCoinAmount: item.GetUserRechargeCoinAmount(), + Level: item.GetLevel(), + Name: item.GetName(), + Status: item.GetStatus(), + PriceCoin: item.GetPriceCoin(), + DurationMS: item.GetDurationMs(), + RewardResourceGroupID: item.GetRewardResourceGroupId(), + SortOrder: item.GetSortOrder(), + // 响应仍保留兼容字段,但后台配置页不再展示或依赖累充资格,统一返回无门槛。 + RechargeGateRequired: false, + RequiredRechargeCoinAmount: 0, + UserRechargeCoinAmount: 0, PurchaseLockedReason: item.GetPurchaseLockedReason(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), diff --git a/services/gateway-service/internal/transport/http/manager_center_handler_test.go b/services/gateway-service/internal/transport/http/manager_center_handler_test.go index 3e2a11a4..a13429ee 100644 --- a/services/gateway-service/internal/transport/http/manager_center_handler_test.go +++ b/services/gateway-service/internal/transport/http/manager_center_handler_test.go @@ -356,30 +356,30 @@ func TestGrantManagerResourceRejectsCrossCountryTarget(t *testing.T) { } } -func TestGrantManagerVIPLimitsToVipOneToThree(t *testing.T) { +func TestGrantManagerVIPRejectsLevelAboveManagerLimit(t *testing.T) { walletClient := &fakeWalletClient{} router := newManagerCenterTestRouter(walletClient, &fakeUserHostClient{}, &fakeUserProfileClient{}) - body := []byte(`{"command_id":"mgr-vip-4","target_user_id":"900001","level":4}`) + body := []byte(`{"command_id":"mgr-vip-6","target_user_id":"900001","level":6}`) request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/vip-grants", bytes.NewReader(body)) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) - request.Header.Set("X-Request-ID", "req-manager-vip4") + request.Header.Set("X-Request-ID", "req-manager-vip6") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) - assertEnvelope(t, recorder, http.StatusBadRequest, httpkit.CodeInvalidArgument, "req-manager-vip4") + assertEnvelope(t, recorder, http.StatusBadRequest, httpkit.CodeInvalidArgument, "req-manager-vip6") if walletClient.lastGrantVip != nil { - t.Fatalf("VIP4 must not reach wallet-service: %+v", walletClient.lastGrantVip) + t.Fatalf("VIP6 must not reach wallet-service: %+v", walletClient.lastGrantVip) } } func TestGrantManagerVIPUsesManagerCenterGrantSource(t *testing.T) { walletClient := &fakeWalletClient{} router := newManagerCenterTestRouter(walletClient, &fakeUserHostClient{}, &fakeUserProfileClient{}) - body := []byte(`{"command_id":"mgr-vip-3","target_user_id":"900001","level":3}`) + body := []byte(`{"command_id":"mgr-vip-5","target_user_id":"900001","level":5}`) request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/vip-grants", bytes.NewReader(body)) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) - request.Header.Set("X-Request-ID", "req-manager-vip3") + request.Header.Set("X-Request-ID", "req-manager-vip5") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) @@ -387,7 +387,7 @@ func TestGrantManagerVIPUsesManagerCenterGrantSource(t *testing.T) { if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } - if req := walletClient.lastGrantVip; req == nil || req.GetCommandId() != "mgr-vip-3" || req.GetTargetUserId() != 900001 || req.GetLevel() != 3 || req.GetGrantSource() != "manager_center" || req.GetOperatorUserId() != 42 { + if req := walletClient.lastGrantVip; req == nil || req.GetCommandId() != "mgr-vip-5" || req.GetTargetUserId() != 900001 || req.GetLevel() != 5 || req.GetGrantSource() != "manager_center" || req.GetOperatorUserId() != 42 { t.Fatalf("wallet GrantVip request mismatch: %+v", req) } } diff --git a/services/gateway-service/internal/transport/http/managerapi/manager_center_handler.go b/services/gateway-service/internal/transport/http/managerapi/manager_center_handler.go index 0a6b421a..463431ef 100644 --- a/services/gateway-service/internal/transport/http/managerapi/manager_center_handler.go +++ b/services/gateway-service/internal/transport/http/managerapi/manager_center_handler.go @@ -29,6 +29,7 @@ const managerBlockUserCapability = "manager_block_user" const managerTransferUserCountryCapability = "manager_transfer_user_country" const managerGrantDayMS int64 = 24 * 60 * 60 * 1000 const managerGrantDefaultDurationMS int64 = 7 * managerGrantDayMS +const managerGrantMaxVIPLevel int32 = 5 type managerGrantResourceData struct { ResourceID string `json:"resource_id"` @@ -397,7 +398,8 @@ func (h *Handler) grantManagerVIP(writer http.ResponseWriter, request *http.Requ commandID = strings.TrimSpace(body.CommandIDAlt) } targetUserID, targetOK := rawInt64(body.TargetUserID, body.TargetUserIDAlt) - if commandID == "" || !targetOK || targetUserID <= 0 || body.Level < 1 || body.Level > 3 { + // 经理中心只放开 VIP1-5 的运营赠送入口;钱包仍按 vip_levels.status 做最终配置校验,避免 H5 放开后绕过等级启停。 + if commandID == "" || !targetOK || targetUserID <= 0 || body.Level < 1 || body.Level > managerGrantMaxVIPLevel { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } diff --git a/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql b/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql index 6a74360d..99e28c72 100644 --- a/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql +++ b/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql @@ -1425,7 +1425,7 @@ CREATE TABLE IF NOT EXISTS vip_levels ( price_coin BIGINT NOT NULL COMMENT 'VIP 价格金币数', duration_ms BIGINT NOT NULL COMMENT '持续时长,毫秒', reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '奖励资源分组 ID', - required_recharge_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '购买所需累计充值金币,1-5 级固定为 0,6-10 级必须配置', + required_recharge_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '兼容旧 VIP 累充门槛字段,当前不参与购买资格', sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前', created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', @@ -1435,7 +1435,7 @@ CREATE TABLE IF NOT EXISTS vip_levels ( SET @ddl := IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vip_levels' AND COLUMN_NAME = 'required_recharge_coin_amount') = 0, - 'ALTER TABLE vip_levels ADD COLUMN required_recharge_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT ''购买所需累计充值金币,1-5 级固定为 0,6-10 级必须配置'' AFTER reward_resource_group_id', + 'ALTER TABLE vip_levels ADD COLUMN required_recharge_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT ''兼容旧 VIP 累充门槛字段,当前不参与购买资格'' AFTER reward_resource_group_id', 'SELECT 1' ); PREPARE stmt FROM @ddl; diff --git a/services/wallet-service/internal/service/wallet/service_test.go b/services/wallet-service/internal/service/wallet/service_test.go index 1df4dbd1..69eaa02c 100644 --- a/services/wallet-service/internal/service/wallet/service_test.go +++ b/services/wallet-service/internal/service/wallet/service_test.go @@ -5480,8 +5480,8 @@ func TestGrantVipUsesUnifiedActivationAndNoticeOutbox(t *testing.T) { } } -// TestVipLevelSixRequiresRechargeThreshold 验证 6-10 级 VIP 必须先达到累计充值门槛。 -func TestVipLevelSixRequiresRechargeThreshold(t *testing.T) { +// TestVipLevelSixIgnoresLegacyRechargeThreshold 验证 VIP6 即使存在旧累充字段,也能按金币价格直接购买。 +func TestVipLevelSixIgnoresLegacyRechargeThreshold(t *testing.T) { repository := mysqltest.NewRepository(t) repository.SetBalance(51002, 50000) svc := walletservice.New(repository) @@ -5517,32 +5517,15 @@ func TestVipLevelSixRequiresRechargeThreshold(t *testing.T) { if _, err := svc.UpdateAdminVipLevels(context.Background(), commands, 9001); err != nil { t.Fatalf("UpdateAdminVipLevels failed: %v", err) } + repository.SetVIPLevelRechargeThreshold(6, 5000) _, packages, err := svc.ListVipPackages(context.Background(), 51002) if err != nil { t.Fatalf("ListVipPackages failed: %v", err) } level6 := vipLevelByNumber(packages, 6) - if level6 == nil || level6.CanPurchase || level6.PurchaseLockedReason != "recharge_required" || level6.RequiredRechargeCoinAmount != 5000 { - t.Fatalf("vip level 6 should be locked by recharge gate: %+v", level6) - } - _, err = svc.PurchaseVip(context.Background(), ledger.PurchaseVipCommand{ - CommandID: "cmd-vip6-locked", - UserID: 51002, - Level: 6, - }) - if !xerr.IsCode(err, xerr.VIPRechargeRequired) { - t.Fatalf("expected VIP_RECHARGE_REQUIRED, got %v", err) - } - - repository.SetRechargeStats(51002, 5000) - _, packages, err = svc.ListVipPackages(context.Background(), 51002) - if err != nil { - t.Fatalf("ListVipPackages after recharge failed: %v", err) - } - level6 = vipLevelByNumber(packages, 6) - if level6 == nil || !level6.CanPurchase || level6.UserRechargeCoinAmount != 5000 { - t.Fatalf("vip level 6 should be purchasable after recharge: %+v", level6) + if level6 == nil || !level6.CanPurchase || level6.PurchaseLockedReason != "" || level6.RechargeGateRequired || level6.RequiredRechargeCoinAmount != 0 || level6.UserRechargeCoinAmount != 0 { + t.Fatalf("vip level 6 should ignore legacy recharge gate: %+v", level6) } receipt, err := svc.PurchaseVip(context.Background(), ledger.PurchaseVipCommand{ CommandID: "cmd-vip6", diff --git a/services/wallet-service/internal/storage/mysql/vip_admin_repository.go b/services/wallet-service/internal/storage/mysql/vip_admin_repository.go index 1d992dd1..623d5b06 100644 --- a/services/wallet-service/internal/storage/mysql/vip_admin_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_admin_repository.go @@ -89,9 +89,8 @@ func normalizeAdminVipLevelCommands(levels []ledger.AdminVipLevelCommand) ([]led if _, exists := byLevel[level.Level]; exists { return nil, xerr.New(xerr.InvalidArgument, "vip level is duplicated") } - if level.Level <= 5 { - level.RequiredRechargeCoinAmount = 0 - } + // 累充门槛字段仅保留数据库和 proto 兼容性;后台保存任意等级时都归零,防止旧客户端把历史门槛重新写入生效数据。 + level.RequiredRechargeCoinAmount = 0 if err := validateAdminVipLevelCommand(level); err != nil { return nil, err } @@ -129,9 +128,6 @@ func validateAdminVipLevelCommand(level ledger.AdminVipLevelCommand) error { if level.RewardResourceGroupID <= 0 { return xerr.New(xerr.InvalidArgument, "active vip level reward_resource_group_id is required") } - if level.Level >= 6 && level.RequiredRechargeCoinAmount <= 0 { - return xerr.New(xerr.InvalidArgument, "vip level 6-10 requires recharge threshold") - } return nil } diff --git a/services/wallet-service/internal/storage/mysql/vip_purchase_repository.go b/services/wallet-service/internal/storage/mysql/vip_purchase_repository.go index b158ca18..09bc246e 100644 --- a/services/wallet-service/internal/storage/mysql/vip_purchase_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_purchase_repository.go @@ -57,15 +57,6 @@ func (r *Repository) PurchaseVip(ctx context.Context, command ledger.PurchaseVip return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPDowngradeNotAllowed, "vip downgrade is not allowed") } } - if level.RechargeGateRequired { - totalRechargeCoin, err := r.userRechargeCoinAmount(ctx, tx, command.UserID) - if err != nil { - return ledger.PurchaseVipReceipt{}, err - } - if totalRechargeCoin < level.RequiredRechargeCoinAmount { - return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPRechargeRequired, "vip recharge requirement is not met") - } - } newExpiresAt := baseExpiresAt + level.DurationMS account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, false, nowMs) if err != nil { @@ -79,17 +70,18 @@ func (r *Repository) PurchaseVip(ctx context.Context, command ledger.PurchaseVip orderID := "vip_order_" + stableHash(command.AppCode+"|"+command.CommandID) coinBalanceAfter := account.AvailableAmount - level.PriceCoin metadata := map[string]any{ - "app_code": command.AppCode, - "order_id": orderID, - "user_id": command.UserID, - "target_level": command.Level, - "previous_level": previousLevel, - "price_coin": level.PriceCoin, - "expires_at_ms": newExpiresAt, - "balance_after": coinBalanceAfter, - "reward_group_id": level.RewardResourceGroupID, - "duration_ms": level.DurationMS, - "required_recharge_coin_amount": level.RequiredRechargeCoinAmount, + "app_code": command.AppCode, + "order_id": orderID, + "user_id": command.UserID, + "target_level": command.Level, + "previous_level": previousLevel, + "price_coin": level.PriceCoin, + "expires_at_ms": newExpiresAt, + "balance_after": coinBalanceAfter, + "reward_group_id": level.RewardResourceGroupID, + "duration_ms": level.DurationMS, + // 兼容旧账务元数据字段;业务已不再按累计充值限制 VIP 购买,因此固定记录 0。 + "required_recharge_coin_amount": int64(0), } if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeVIPPurchase, requestHash, orderID, metadata, nowMs); err != nil { return ledger.PurchaseVipReceipt{}, err diff --git a/services/wallet-service/internal/storage/mysql/vip_query_repository.go b/services/wallet-service/internal/storage/mysql/vip_query_repository.go index 2dc831ef..7f2d9ca7 100644 --- a/services/wallet-service/internal/storage/mysql/vip_query_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_query_repository.go @@ -33,11 +33,7 @@ func (r *Repository) ListVipPackages(ctx context.Context, userID int64) (ledger. if err != nil { return ledger.UserVip{}, nil, err } - totalRechargeCoin, err := r.userRechargeCoinAmount(ctx, r.db, userID) - if err != nil { - return ledger.UserVip{}, nil, err - } - levels, err := r.listVipLevels(ctx, nowMs, current.Level, totalRechargeCoin) + levels, err := r.listVipLevels(ctx, nowMs, current.Level) return current, levels, err } @@ -76,7 +72,7 @@ func (r *Repository) ListAdminVipLevels(ctx context.Context) ([]ledger.VipLevel, return levels, rows.Err() } -func (r *Repository) listVipLevels(ctx context.Context, nowMs int64, currentLevel int32, totalRechargeCoin int64) ([]ledger.VipLevel, error) { +func (r *Repository) listVipLevels(ctx context.Context, nowMs int64, currentLevel int32) ([]ledger.VipLevel, error) { rows, err := r.db.QueryContext(ctx, ` SELECT level, name, status, price_coin, duration_ms, reward_resource_group_id, required_recharge_coin_amount, sort_order, created_at_ms, updated_at_ms @@ -96,16 +92,14 @@ func (r *Repository) listVipLevels(ctx context.Context, nowMs int64, currentLeve if err != nil { return nil, err } - level.UserRechargeCoinAmount = totalRechargeCoin - level.RechargeGateRequired = level.Level >= 6 + // VIP 累充门槛已下线,旧字段只作为兼容输出保留;购买资格只看当前 VIP 等级和配置启停。 + level.UserRechargeCoinAmount = 0 + level.RechargeGateRequired = false + level.RequiredRechargeCoinAmount = 0 level.CanPurchase = currentLevel == 0 || level.Level >= currentLevel if !level.CanPurchase { level.PurchaseLockedReason = "current_vip_higher" } - if level.CanPurchase && level.RechargeGateRequired && totalRechargeCoin < level.RequiredRechargeCoinAmount { - level.CanPurchase = false - level.PurchaseLockedReason = "recharge_required" - } level.RewardItems, err = r.vipRewardItems(ctx, level.RewardResourceGroupID, nowMs+level.DurationMS) if err != nil { return nil, err @@ -129,26 +123,12 @@ func (r *Repository) scanVipLevel(scanner scanTarget) (ledger.VipLevel, error) { &level.CreatedAtMS, &level.UpdatedAtMS, ) - level.RechargeGateRequired = level.Level >= 6 + // 线上历史行可能还保存 VIP6+ 累充门槛;扫描后立即归零,保证后台、App 列表和购买事务读到同一个“无门槛”事实。 + level.RechargeGateRequired = false + level.RequiredRechargeCoinAmount = 0 return level, err } -func (r *Repository) userRechargeCoinAmount(ctx context.Context, querier interface { - QueryRowContext(context.Context, string, ...any) *sql.Row -}, userID int64) (int64, error) { - var total int64 - err := querier.QueryRowContext(ctx, ` - SELECT total_coin_amount - FROM wallet_user_recharge_stats - WHERE app_code = ? AND user_id = ?`, - appcode.FromContext(ctx), userID, - ).Scan(&total) - if errors.Is(err, sql.ErrNoRows) { - return 0, nil - } - return total, err -} - func (r *Repository) ensureUserVipForUpdate(ctx context.Context, tx *sql.Tx, userID int64, nowMs int64) (ledger.UserVip, error) { if _, err := tx.ExecContext(ctx, ` INSERT IGNORE INTO user_vip_memberships ( diff --git a/services/wallet-service/internal/testutil/mysqltest/mysqltest.go b/services/wallet-service/internal/testutil/mysqltest/mysqltest.go index 56c6c728..c462cd11 100644 --- a/services/wallet-service/internal/testutil/mysqltest/mysqltest.go +++ b/services/wallet-service/internal/testutil/mysqltest/mysqltest.go @@ -599,7 +599,7 @@ func (r *Repository) SetRechargePolicy(regionID int64, policyVersion string, coi } } -// SetRechargeStats prepares cumulative recharge stats for VIP gate tests. +// SetRechargeStats prepares cumulative recharge stats for recharge-related wallet tests. func (r *Repository) SetRechargeStats(userID int64, totalCoinAmount int64) { r.t.Helper() @@ -618,6 +618,21 @@ func (r *Repository) SetRechargeStats(userID int64, totalCoinAmount int64) { } } +// SetVIPLevelRechargeThreshold writes a legacy VIP threshold directly, so tests can verify old rows no longer affect purchase eligibility. +func (r *Repository) SetVIPLevelRechargeThreshold(level int32, totalCoinAmount int64) { + r.t.Helper() + + nowMs := time.Now().UnixMilli() + if _, err := r.schema.DB.ExecContext(context.Background(), ` + UPDATE vip_levels + SET required_recharge_coin_amount = ?, updated_at_ms = ? + WHERE level = ?`, + totalCoinAmount, nowMs, level, + ); err != nil { + r.t.Fatalf("set vip level recharge threshold failed: %v", err) + } +} + // SetHostSalaryPolicy seeds the wallet runtime policy snapshot used by salary settlement tests. func (r *Repository) SetHostSalaryPolicy(policy ledger.HostSalaryPolicy) { r.t.Helper()