取消vip门槛
This commit is contained in:
parent
0a072894ec
commit
7a2d292c36
@ -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
|
|
||||||
@ -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"}
|
|
||||||
6
scripts/mysql/056_reset_vip_recharge_thresholds.sql
Normal file
6
scripts/mysql/056_reset_vip_recharge_thresholds.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
USE hyapp_wallet;
|
||||||
|
|
||||||
|
-- VIP 购买已改为所有等级直接按金币购买;保留字段只为兼容旧协议,线上已有累充门槛统一清零。
|
||||||
|
UPDATE vip_levels
|
||||||
|
SET required_recharge_coin_amount = 0
|
||||||
|
WHERE required_recharge_coin_amount <> 0;
|
||||||
@ -139,13 +139,14 @@ func (h *Handler) UpdateLevels(c *gin.Context) {
|
|||||||
input := make([]*walletv1.AdminVipLevelInput, 0, len(req.Levels))
|
input := make([]*walletv1.AdminVipLevelInput, 0, len(req.Levels))
|
||||||
for _, level := range req.Levels {
|
for _, level := range req.Levels {
|
||||||
input = append(input, &walletv1.AdminVipLevelInput{
|
input = append(input, &walletv1.AdminVipLevelInput{
|
||||||
Level: level.Level,
|
Level: level.Level,
|
||||||
Name: strings.TrimSpace(level.Name),
|
Name: strings.TrimSpace(level.Name),
|
||||||
Status: strings.TrimSpace(level.Status),
|
Status: strings.TrimSpace(level.Status),
|
||||||
PriceCoin: level.PriceCoin,
|
PriceCoin: level.PriceCoin,
|
||||||
DurationMs: level.DurationMS,
|
DurationMs: level.DurationMS,
|
||||||
RewardResourceGroupId: level.RewardResourceGroupID,
|
RewardResourceGroupId: level.RewardResourceGroupID,
|
||||||
RequiredRechargeCoinAmount: level.RequiredRechargeCoinAmount,
|
// VIP 累充门槛已下线;后台旧请求即使携带旧字段,也统一写 0,避免旧页面或缓存把门槛重新保存回来。
|
||||||
|
RequiredRechargeCoinAmount: 0,
|
||||||
SortOrder: level.SortOrder,
|
SortOrder: level.SortOrder,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -170,16 +171,17 @@ func vipLevelsFromProto(items []*walletv1.VipLevel) []vipLevelDTO {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
levels = append(levels, vipLevelDTO{
|
levels = append(levels, vipLevelDTO{
|
||||||
Level: item.GetLevel(),
|
Level: item.GetLevel(),
|
||||||
Name: item.GetName(),
|
Name: item.GetName(),
|
||||||
Status: item.GetStatus(),
|
Status: item.GetStatus(),
|
||||||
PriceCoin: item.GetPriceCoin(),
|
PriceCoin: item.GetPriceCoin(),
|
||||||
DurationMS: item.GetDurationMs(),
|
DurationMS: item.GetDurationMs(),
|
||||||
RewardResourceGroupID: item.GetRewardResourceGroupId(),
|
RewardResourceGroupID: item.GetRewardResourceGroupId(),
|
||||||
SortOrder: item.GetSortOrder(),
|
SortOrder: item.GetSortOrder(),
|
||||||
RechargeGateRequired: item.GetRechargeGateRequired(),
|
// 响应仍保留兼容字段,但后台配置页不再展示或依赖累充资格,统一返回无门槛。
|
||||||
RequiredRechargeCoinAmount: item.GetRequiredRechargeCoinAmount(),
|
RechargeGateRequired: false,
|
||||||
UserRechargeCoinAmount: item.GetUserRechargeCoinAmount(),
|
RequiredRechargeCoinAmount: 0,
|
||||||
|
UserRechargeCoinAmount: 0,
|
||||||
PurchaseLockedReason: item.GetPurchaseLockedReason(),
|
PurchaseLockedReason: item.GetPurchaseLockedReason(),
|
||||||
CreatedAtMS: item.GetCreatedAtMs(),
|
CreatedAtMS: item.GetCreatedAtMs(),
|
||||||
UpdatedAtMS: item.GetUpdatedAtMs(),
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
|
|||||||
@ -356,30 +356,30 @@ func TestGrantManagerResourceRejectsCrossCountryTarget(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGrantManagerVIPLimitsToVipOneToThree(t *testing.T) {
|
func TestGrantManagerVIPRejectsLevelAboveManagerLimit(t *testing.T) {
|
||||||
walletClient := &fakeWalletClient{}
|
walletClient := &fakeWalletClient{}
|
||||||
router := newManagerCenterTestRouter(walletClient, &fakeUserHostClient{}, &fakeUserProfileClient{})
|
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 := 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("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()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
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 {
|
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) {
|
func TestGrantManagerVIPUsesManagerCenterGrantSource(t *testing.T) {
|
||||||
walletClient := &fakeWalletClient{}
|
walletClient := &fakeWalletClient{}
|
||||||
router := newManagerCenterTestRouter(walletClient, &fakeUserHostClient{}, &fakeUserProfileClient{})
|
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 := 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("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()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
router.ServeHTTP(recorder, request)
|
router.ServeHTTP(recorder, request)
|
||||||
@ -387,7 +387,7 @@ func TestGrantManagerVIPUsesManagerCenterGrantSource(t *testing.T) {
|
|||||||
if recorder.Code != http.StatusOK {
|
if recorder.Code != http.StatusOK {
|
||||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
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)
|
t.Fatalf("wallet GrantVip request mismatch: %+v", req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ const managerBlockUserCapability = "manager_block_user"
|
|||||||
const managerTransferUserCountryCapability = "manager_transfer_user_country"
|
const managerTransferUserCountryCapability = "manager_transfer_user_country"
|
||||||
const managerGrantDayMS int64 = 24 * 60 * 60 * 1000
|
const managerGrantDayMS int64 = 24 * 60 * 60 * 1000
|
||||||
const managerGrantDefaultDurationMS int64 = 7 * managerGrantDayMS
|
const managerGrantDefaultDurationMS int64 = 7 * managerGrantDayMS
|
||||||
|
const managerGrantMaxVIPLevel int32 = 5
|
||||||
|
|
||||||
type managerGrantResourceData struct {
|
type managerGrantResourceData struct {
|
||||||
ResourceID string `json:"resource_id"`
|
ResourceID string `json:"resource_id"`
|
||||||
@ -397,7 +398,8 @@ func (h *Handler) grantManagerVIP(writer http.ResponseWriter, request *http.Requ
|
|||||||
commandID = strings.TrimSpace(body.CommandIDAlt)
|
commandID = strings.TrimSpace(body.CommandIDAlt)
|
||||||
}
|
}
|
||||||
targetUserID, targetOK := rawInt64(body.TargetUserID, body.TargetUserIDAlt)
|
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")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1425,7 +1425,7 @@ CREATE TABLE IF NOT EXISTS vip_levels (
|
|||||||
price_coin BIGINT NOT NULL COMMENT 'VIP 价格金币数',
|
price_coin BIGINT NOT NULL COMMENT 'VIP 价格金币数',
|
||||||
duration_ms BIGINT NOT NULL COMMENT '持续时长,毫秒',
|
duration_ms BIGINT NOT NULL COMMENT '持续时长,毫秒',
|
||||||
reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '奖励资源分组 ID',
|
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 '排序权重,数值越小越靠前',
|
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
updated_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(
|
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,
|
(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'
|
'SELECT 1'
|
||||||
);
|
);
|
||||||
PREPARE stmt FROM @ddl;
|
PREPARE stmt FROM @ddl;
|
||||||
|
|||||||
@ -5480,8 +5480,8 @@ func TestGrantVipUsesUnifiedActivationAndNoticeOutbox(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestVipLevelSixRequiresRechargeThreshold 验证 6-10 级 VIP 必须先达到累计充值门槛。
|
// TestVipLevelSixIgnoresLegacyRechargeThreshold 验证 VIP6 即使存在旧累充字段,也能按金币价格直接购买。
|
||||||
func TestVipLevelSixRequiresRechargeThreshold(t *testing.T) {
|
func TestVipLevelSixIgnoresLegacyRechargeThreshold(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
repository.SetBalance(51002, 50000)
|
repository.SetBalance(51002, 50000)
|
||||||
svc := walletservice.New(repository)
|
svc := walletservice.New(repository)
|
||||||
@ -5517,32 +5517,15 @@ func TestVipLevelSixRequiresRechargeThreshold(t *testing.T) {
|
|||||||
if _, err := svc.UpdateAdminVipLevels(context.Background(), commands, 9001); err != nil {
|
if _, err := svc.UpdateAdminVipLevels(context.Background(), commands, 9001); err != nil {
|
||||||
t.Fatalf("UpdateAdminVipLevels failed: %v", err)
|
t.Fatalf("UpdateAdminVipLevels failed: %v", err)
|
||||||
}
|
}
|
||||||
|
repository.SetVIPLevelRechargeThreshold(6, 5000)
|
||||||
|
|
||||||
_, packages, err := svc.ListVipPackages(context.Background(), 51002)
|
_, packages, err := svc.ListVipPackages(context.Background(), 51002)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ListVipPackages failed: %v", err)
|
t.Fatalf("ListVipPackages failed: %v", err)
|
||||||
}
|
}
|
||||||
level6 := vipLevelByNumber(packages, 6)
|
level6 := vipLevelByNumber(packages, 6)
|
||||||
if level6 == nil || level6.CanPurchase || level6.PurchaseLockedReason != "recharge_required" || level6.RequiredRechargeCoinAmount != 5000 {
|
if level6 == nil || !level6.CanPurchase || level6.PurchaseLockedReason != "" || level6.RechargeGateRequired || level6.RequiredRechargeCoinAmount != 0 || level6.UserRechargeCoinAmount != 0 {
|
||||||
t.Fatalf("vip level 6 should be locked by recharge gate: %+v", level6)
|
t.Fatalf("vip level 6 should ignore legacy 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)
|
|
||||||
}
|
}
|
||||||
receipt, err := svc.PurchaseVip(context.Background(), ledger.PurchaseVipCommand{
|
receipt, err := svc.PurchaseVip(context.Background(), ledger.PurchaseVipCommand{
|
||||||
CommandID: "cmd-vip6",
|
CommandID: "cmd-vip6",
|
||||||
|
|||||||
@ -89,9 +89,8 @@ func normalizeAdminVipLevelCommands(levels []ledger.AdminVipLevelCommand) ([]led
|
|||||||
if _, exists := byLevel[level.Level]; exists {
|
if _, exists := byLevel[level.Level]; exists {
|
||||||
return nil, xerr.New(xerr.InvalidArgument, "vip level is duplicated")
|
return nil, xerr.New(xerr.InvalidArgument, "vip level is duplicated")
|
||||||
}
|
}
|
||||||
if level.Level <= 5 {
|
// 累充门槛字段仅保留数据库和 proto 兼容性;后台保存任意等级时都归零,防止旧客户端把历史门槛重新写入生效数据。
|
||||||
level.RequiredRechargeCoinAmount = 0
|
level.RequiredRechargeCoinAmount = 0
|
||||||
}
|
|
||||||
if err := validateAdminVipLevelCommand(level); err != nil {
|
if err := validateAdminVipLevelCommand(level); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -129,9 +128,6 @@ func validateAdminVipLevelCommand(level ledger.AdminVipLevelCommand) error {
|
|||||||
if level.RewardResourceGroupID <= 0 {
|
if level.RewardResourceGroupID <= 0 {
|
||||||
return xerr.New(xerr.InvalidArgument, "active vip level reward_resource_group_id is required")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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")
|
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
|
newExpiresAt := baseExpiresAt + level.DurationMS
|
||||||
account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, false, nowMs)
|
account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, false, nowMs)
|
||||||
if err != nil {
|
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)
|
orderID := "vip_order_" + stableHash(command.AppCode+"|"+command.CommandID)
|
||||||
coinBalanceAfter := account.AvailableAmount - level.PriceCoin
|
coinBalanceAfter := account.AvailableAmount - level.PriceCoin
|
||||||
metadata := map[string]any{
|
metadata := map[string]any{
|
||||||
"app_code": command.AppCode,
|
"app_code": command.AppCode,
|
||||||
"order_id": orderID,
|
"order_id": orderID,
|
||||||
"user_id": command.UserID,
|
"user_id": command.UserID,
|
||||||
"target_level": command.Level,
|
"target_level": command.Level,
|
||||||
"previous_level": previousLevel,
|
"previous_level": previousLevel,
|
||||||
"price_coin": level.PriceCoin,
|
"price_coin": level.PriceCoin,
|
||||||
"expires_at_ms": newExpiresAt,
|
"expires_at_ms": newExpiresAt,
|
||||||
"balance_after": coinBalanceAfter,
|
"balance_after": coinBalanceAfter,
|
||||||
"reward_group_id": level.RewardResourceGroupID,
|
"reward_group_id": level.RewardResourceGroupID,
|
||||||
"duration_ms": level.DurationMS,
|
"duration_ms": level.DurationMS,
|
||||||
"required_recharge_coin_amount": level.RequiredRechargeCoinAmount,
|
// 兼容旧账务元数据字段;业务已不再按累计充值限制 VIP 购买,因此固定记录 0。
|
||||||
|
"required_recharge_coin_amount": int64(0),
|
||||||
}
|
}
|
||||||
if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeVIPPurchase, requestHash, orderID, metadata, nowMs); err != nil {
|
if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeVIPPurchase, requestHash, orderID, metadata, nowMs); err != nil {
|
||||||
return ledger.PurchaseVipReceipt{}, err
|
return ledger.PurchaseVipReceipt{}, err
|
||||||
|
|||||||
@ -33,11 +33,7 @@ func (r *Repository) ListVipPackages(ctx context.Context, userID int64) (ledger.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ledger.UserVip{}, nil, err
|
return ledger.UserVip{}, nil, err
|
||||||
}
|
}
|
||||||
totalRechargeCoin, err := r.userRechargeCoinAmount(ctx, r.db, userID)
|
levels, err := r.listVipLevels(ctx, nowMs, current.Level)
|
||||||
if err != nil {
|
|
||||||
return ledger.UserVip{}, nil, err
|
|
||||||
}
|
|
||||||
levels, err := r.listVipLevels(ctx, nowMs, current.Level, totalRechargeCoin)
|
|
||||||
return current, levels, err
|
return current, levels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +72,7 @@ func (r *Repository) ListAdminVipLevels(ctx context.Context) ([]ledger.VipLevel,
|
|||||||
return levels, rows.Err()
|
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, `
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
SELECT level, name, status, price_coin, duration_ms, reward_resource_group_id,
|
SELECT level, name, status, price_coin, duration_ms, reward_resource_group_id,
|
||||||
required_recharge_coin_amount, sort_order, created_at_ms, updated_at_ms
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
level.UserRechargeCoinAmount = totalRechargeCoin
|
// VIP 累充门槛已下线,旧字段只作为兼容输出保留;购买资格只看当前 VIP 等级和配置启停。
|
||||||
level.RechargeGateRequired = level.Level >= 6
|
level.UserRechargeCoinAmount = 0
|
||||||
|
level.RechargeGateRequired = false
|
||||||
|
level.RequiredRechargeCoinAmount = 0
|
||||||
level.CanPurchase = currentLevel == 0 || level.Level >= currentLevel
|
level.CanPurchase = currentLevel == 0 || level.Level >= currentLevel
|
||||||
if !level.CanPurchase {
|
if !level.CanPurchase {
|
||||||
level.PurchaseLockedReason = "current_vip_higher"
|
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)
|
level.RewardItems, err = r.vipRewardItems(ctx, level.RewardResourceGroupID, nowMs+level.DurationMS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -129,26 +123,12 @@ func (r *Repository) scanVipLevel(scanner scanTarget) (ledger.VipLevel, error) {
|
|||||||
&level.CreatedAtMS,
|
&level.CreatedAtMS,
|
||||||
&level.UpdatedAtMS,
|
&level.UpdatedAtMS,
|
||||||
)
|
)
|
||||||
level.RechargeGateRequired = level.Level >= 6
|
// 线上历史行可能还保存 VIP6+ 累充门槛;扫描后立即归零,保证后台、App 列表和购买事务读到同一个“无门槛”事实。
|
||||||
|
level.RechargeGateRequired = false
|
||||||
|
level.RequiredRechargeCoinAmount = 0
|
||||||
return level, err
|
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) {
|
func (r *Repository) ensureUserVipForUpdate(ctx context.Context, tx *sql.Tx, userID int64, nowMs int64) (ledger.UserVip, error) {
|
||||||
if _, err := tx.ExecContext(ctx, `
|
if _, err := tx.ExecContext(ctx, `
|
||||||
INSERT IGNORE INTO user_vip_memberships (
|
INSERT IGNORE INTO user_vip_memberships (
|
||||||
|
|||||||
@ -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) {
|
func (r *Repository) SetRechargeStats(userID int64, totalCoinAmount int64) {
|
||||||
r.t.Helper()
|
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.
|
// SetHostSalaryPolicy seeds the wallet runtime policy snapshot used by salary settlement tests.
|
||||||
func (r *Repository) SetHostSalaryPolicy(policy ledger.HostSalaryPolicy) {
|
func (r *Repository) SetHostSalaryPolicy(policy ledger.HostSalaryPolicy) {
|
||||||
r.t.Helper()
|
r.t.Helper()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user