收敛转盘历史礼物补偿边界
This commit is contained in:
parent
fe468205e5
commit
93f44ecf52
@ -14,7 +14,7 @@ const (
|
|||||||
TypeEmojiPack = "emoji_pack"
|
TypeEmojiPack = "emoji_pack"
|
||||||
// TypeVIPTrialCard 是可在背包中独立计时、单选佩戴的 VIP 体验卡。
|
// TypeVIPTrialCard 是可在背包中独立计时、单选佩戴的 VIP 体验卡。
|
||||||
// VIP 等级和有效期事实仍由 user_vip_trial_cards 保存,资源目录只负责素材和通用背包展示。
|
// VIP 等级和有效期事实仍由 user_vip_trial_cards 保存,资源目录只负责素材和通用背包展示。
|
||||||
TypeVIPTrialCard = "vip_trial_card"
|
TypeVIPTrialCard = "vip_trial_card"
|
||||||
|
|
||||||
StatusActive = "active"
|
StatusActive = "active"
|
||||||
StatusDisabled = "disabled"
|
StatusDisabled = "disabled"
|
||||||
@ -35,10 +35,13 @@ const (
|
|||||||
GrantSourceAchievement = "achievement"
|
GrantSourceAchievement = "achievement"
|
||||||
GrantSourceResourceShop = "resource_shop"
|
GrantSourceResourceShop = "resource_shop"
|
||||||
GrantSourceGameRobotInit = "game_robot_init"
|
GrantSourceGameRobotInit = "game_robot_init"
|
||||||
GrantStatusDone = "succeeded"
|
// GrantSourceWheelReward 标识 activity-service 从转盘不可变结算快照发起的奖品发放。
|
||||||
GrantStatusRevoked = "revoked"
|
// 历史兼容还必须满足固定 reason/command namespace、本人操作和 disabled grantable gift 等条件;source 本身不是授权开关。
|
||||||
ResultWalletCredit = "wallet_credit"
|
GrantSourceWheelReward = "wheel_reward"
|
||||||
ResultEntitlement = "entitlement"
|
GrantStatusDone = "succeeded"
|
||||||
|
GrantStatusRevoked = "revoked"
|
||||||
|
ResultWalletCredit = "wallet_credit"
|
||||||
|
ResultEntitlement = "entitlement"
|
||||||
|
|
||||||
GroupItemTypeResource = "resource"
|
GroupItemTypeResource = "resource"
|
||||||
GroupItemTypeWalletAsset = "wallet_asset"
|
GroupItemTypeWalletAsset = "wallet_asset"
|
||||||
|
|||||||
@ -5803,6 +5803,296 @@ func TestGameRobotInitCanGrantActiveNonGrantableResource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWheelRewardCompensationCanGrantOnlyDisabledGrantableResource 锁定历史转盘补偿的最窄豁免边界:
|
||||||
|
// 协议标记匹配后仍只允许本人补偿 disabled grantable gift,且重放必须直接返回首次 grant,不能重复增加权益或 outbox。
|
||||||
|
func TestWheelRewardCompensationCanGrantOnlyDisabledGrantableResource(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
disabledGrantable, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||||
|
ResourceCode: "wheel_disabled_grantable_gift",
|
||||||
|
ResourceType: resourcedomain.TypeGift,
|
||||||
|
Name: "Disabled Historical Wheel Gift",
|
||||||
|
Status: resourcedomain.StatusDisabled,
|
||||||
|
Grantable: true,
|
||||||
|
GrantStrategy: resourcedomain.GrantStrategyIncreaseQuantity,
|
||||||
|
UsageScopes: []string{"wheel_reward"},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create disabled wheel resource failed: %v", err)
|
||||||
|
}
|
||||||
|
const baseCommandID = "wheel_resource:wheel_draw_1720000000000_0123456789abcdef"
|
||||||
|
command := resourcedomain.GrantResourceCommand{
|
||||||
|
CommandID: baseCommandID,
|
||||||
|
TargetUserID: 43101,
|
||||||
|
ResourceID: disabledGrantable.ResourceID,
|
||||||
|
Quantity: 2,
|
||||||
|
Reason: resourcedomain.GrantSourceWheelReward,
|
||||||
|
OperatorUserID: 43101,
|
||||||
|
GrantSource: resourcedomain.GrantSourceWheelReward,
|
||||||
|
}
|
||||||
|
first, err := svc.GrantResource(ctx, command)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("historical wheel compensation should grant disabled grantable resource: %v", err)
|
||||||
|
}
|
||||||
|
replayed, err := svc.GrantResource(ctx, command)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("historical wheel compensation replay failed: %v", err)
|
||||||
|
}
|
||||||
|
if first.GrantID == "" || replayed.GrantID != first.GrantID || len(first.Items) != 1 || len(replayed.Items) != 1 || replayed.Items[0].GrantItemID != first.Items[0].GrantItemID {
|
||||||
|
t.Fatalf("wheel compensation idempotency mismatch: first=%+v replayed=%+v", first, replayed)
|
||||||
|
}
|
||||||
|
_, quantity, remaining := repository.ActiveResourceEntitlement(command.TargetUserID, disabledGrantable.ResourceID)
|
||||||
|
if quantity != command.Quantity || remaining != command.Quantity {
|
||||||
|
t.Fatalf("wheel compensation replay must not double entitlement: quantity=%d remaining=%d command=%+v", quantity, remaining, command)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("resource_grants", "command_id = ?", command.CommandID); got != 1 {
|
||||||
|
t.Fatalf("wheel compensation replay must keep one grant, got %d", got)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("resource_grant_items", "grant_id = ?", first.GrantID); got != 1 {
|
||||||
|
t.Fatalf("wheel compensation replay must keep one grant item, got %d", got)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("wallet_outbox", "command_id = ? AND event_type = ?", command.CommandID, "ResourceGranted"); got != 1 {
|
||||||
|
t.Fatalf("wheel compensation replay must publish ResourceGranted once, got %d", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后台即使复用 wheel command/reason 也不能发 disabled 资源;admin source 必须继续沿用原有 active-only 语义。
|
||||||
|
adminCommand := command
|
||||||
|
adminCommand.CommandID = baseCommandID + ":2"
|
||||||
|
adminCommand.TargetUserID = 43102
|
||||||
|
adminCommand.GrantSource = resourcedomain.GrantSourceAdmin
|
||||||
|
if _, err := svc.GrantResource(ctx, adminCommand); !xerr.IsCode(err, xerr.Conflict) {
|
||||||
|
t.Fatalf("admin must not grant disabled wheel resource, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedResource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||||
|
ResourceCode: "wheel_deleted_grantable_gift",
|
||||||
|
ResourceType: resourcedomain.TypeGift,
|
||||||
|
Name: "Deleted Historical Wheel Gift",
|
||||||
|
Status: resourcedomain.StatusDisabled,
|
||||||
|
Grantable: true,
|
||||||
|
GrantStrategy: resourcedomain.GrantStrategyIncreaseQuantity,
|
||||||
|
UsageScopes: []string{"wheel_reward"},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create deletable wheel resource failed: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := svc.DeleteResource(ctx, resourcedomain.StatusCommand{ID: deletedResource.ResourceID, OperatorUserID: 90001}); err != nil {
|
||||||
|
t.Fatalf("delete wheel resource failed: %v", err)
|
||||||
|
}
|
||||||
|
deletedCommand := command
|
||||||
|
deletedCommand.CommandID = baseCommandID + ":3"
|
||||||
|
deletedCommand.TargetUserID = 43103
|
||||||
|
deletedCommand.OperatorUserID = deletedCommand.TargetUserID
|
||||||
|
deletedCommand.ResourceID = deletedResource.ResourceID
|
||||||
|
if _, err := svc.GrantResource(ctx, deletedCommand); !xerr.IsCode(err, xerr.Conflict) {
|
||||||
|
t.Fatalf("deleted resource must remain rejected for wheel compensation, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonGrantable, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||||
|
ResourceCode: "wheel_disabled_non_grantable_gift",
|
||||||
|
ResourceType: resourcedomain.TypeGift,
|
||||||
|
Name: "Non Grantable Historical Wheel Gift",
|
||||||
|
Status: resourcedomain.StatusDisabled,
|
||||||
|
Grantable: false,
|
||||||
|
GrantStrategy: resourcedomain.GrantStrategyIncreaseQuantity,
|
||||||
|
UsageScopes: []string{"wheel_reward"},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create non-grantable wheel resource failed: %v", err)
|
||||||
|
}
|
||||||
|
nonGrantableCommand := command
|
||||||
|
nonGrantableCommand.CommandID = baseCommandID + ":4"
|
||||||
|
nonGrantableCommand.TargetUserID = 43104
|
||||||
|
nonGrantableCommand.OperatorUserID = nonGrantableCommand.TargetUserID
|
||||||
|
nonGrantableCommand.ResourceID = nonGrantable.ResourceID
|
||||||
|
if _, err := svc.GrantResource(ctx, nonGrantableCommand); !xerr.IsCode(err, xerr.Conflict) {
|
||||||
|
t.Fatalf("non-grantable resource must remain rejected for wheel compensation, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只伪造 wheel_reward source 不足以进入兼容分支;reason 或命名空间任一不匹配都应在读取资源前作为畸形 Wheel 命令拒绝。
|
||||||
|
for _, forged := range []resourcedomain.GrantResourceCommand{
|
||||||
|
{
|
||||||
|
CommandID: baseCommandID + ":5", TargetUserID: 43105, ResourceID: disabledGrantable.ResourceID, Quantity: 1,
|
||||||
|
Reason: "manual repair", OperatorUserID: 43105, GrantSource: resourcedomain.GrantSourceWheelReward,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CommandID: "manual_resource:forged-command", TargetUserID: 43106, ResourceID: disabledGrantable.ResourceID, Quantity: 1,
|
||||||
|
Reason: resourcedomain.GrantSourceWheelReward, OperatorUserID: 43106, GrantSource: resourcedomain.GrantSourceWheelReward,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if _, err := svc.GrantResource(ctx, forged); !xerr.IsCode(err, xerr.InvalidArgument) {
|
||||||
|
t.Fatalf("forged wheel identity must not grant disabled resource: command=%+v err=%v", forged, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operatorMismatch := command
|
||||||
|
operatorMismatch.CommandID = baseCommandID + ":6"
|
||||||
|
operatorMismatch.TargetUserID = 43107
|
||||||
|
operatorMismatch.OperatorUserID = 90001
|
||||||
|
if _, err := svc.GrantResource(ctx, operatorMismatch); !xerr.IsCode(err, xerr.InvalidArgument) {
|
||||||
|
t.Fatalf("wheel compensation must not grant for a different operator, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledBadge, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||||
|
ResourceCode: "wheel_disabled_badge",
|
||||||
|
ResourceType: resourcedomain.TypeBadge,
|
||||||
|
Name: "Disabled Wheel Badge",
|
||||||
|
Status: resourcedomain.StatusDisabled,
|
||||||
|
Grantable: true,
|
||||||
|
GrantStrategy: resourcedomain.GrantStrategySetActiveFlag,
|
||||||
|
UsageScopes: []string{"profile"},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create disabled non-gift resource failed: %v", err)
|
||||||
|
}
|
||||||
|
nonGiftCommand := command
|
||||||
|
nonGiftCommand.CommandID = baseCommandID + ":7"
|
||||||
|
nonGiftCommand.TargetUserID = 43108
|
||||||
|
nonGiftCommand.OperatorUserID = nonGiftCommand.TargetUserID
|
||||||
|
nonGiftCommand.ResourceID = disabledBadge.ResourceID
|
||||||
|
if _, err := svc.GrantResource(ctx, nonGiftCommand); !xerr.IsCode(err, xerr.Conflict) {
|
||||||
|
t.Fatalf("disabled non-gift resource must remain rejected for wheel compensation, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledCoin, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||||
|
ResourceCode: "wheel_disabled_coin",
|
||||||
|
ResourceType: resourcedomain.TypeCoin,
|
||||||
|
Name: "Disabled Wheel Coin",
|
||||||
|
Status: resourcedomain.StatusDisabled,
|
||||||
|
Grantable: true,
|
||||||
|
GrantStrategy: resourcedomain.GrantStrategyWalletCredit,
|
||||||
|
WalletAssetType: ledger.AssetCoin,
|
||||||
|
WalletAssetAmount: 100,
|
||||||
|
UsageScopes: []string{"wallet"},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create disabled coin resource failed: %v", err)
|
||||||
|
}
|
||||||
|
coinCommand := command
|
||||||
|
coinCommand.CommandID = baseCommandID + ":8"
|
||||||
|
coinCommand.TargetUserID = 43109
|
||||||
|
coinCommand.OperatorUserID = coinCommand.TargetUserID
|
||||||
|
coinCommand.ResourceID = disabledCoin.ResourceID
|
||||||
|
if _, err := svc.GrantResource(ctx, coinCommand); !xerr.IsCode(err, xerr.Conflict) {
|
||||||
|
t.Fatalf("disabled wallet credit/coin resource must remain rejected for wheel compensation, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledWrongStrategy, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||||
|
ResourceCode: "wheel_disabled_wrong_strategy_gift",
|
||||||
|
ResourceType: resourcedomain.TypeGift,
|
||||||
|
Name: "Disabled Wheel Set Active Gift",
|
||||||
|
Status: resourcedomain.StatusDisabled,
|
||||||
|
Grantable: true,
|
||||||
|
GrantStrategy: resourcedomain.GrantStrategySetActiveFlag,
|
||||||
|
UsageScopes: []string{"wheel_reward"},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create disabled wrong-strategy gift failed: %v", err)
|
||||||
|
}
|
||||||
|
wrongStrategyCommand := command
|
||||||
|
wrongStrategyCommand.CommandID = baseCommandID + ":9"
|
||||||
|
wrongStrategyCommand.TargetUserID = 43110
|
||||||
|
wrongStrategyCommand.OperatorUserID = wrongStrategyCommand.TargetUserID
|
||||||
|
wrongStrategyCommand.ResourceID = disabledWrongStrategy.ResourceID
|
||||||
|
if _, err := svc.GrantResource(ctx, wrongStrategyCommand); !xerr.IsCode(err, xerr.Conflict) {
|
||||||
|
t.Fatalf("disabled gift with non-quantity strategy must remain rejected, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
durationCommand := command
|
||||||
|
durationCommand.CommandID = baseCommandID + ":10"
|
||||||
|
durationCommand.TargetUserID = 43111
|
||||||
|
durationCommand.OperatorUserID = durationCommand.TargetUserID
|
||||||
|
durationCommand.DurationMS = int64(time.Hour / time.Millisecond)
|
||||||
|
if _, err := svc.GrantResource(ctx, durationCommand); !xerr.IsCode(err, xerr.Conflict) {
|
||||||
|
t.Fatalf("disabled wheel gift with duration must remain rejected, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWheelRewardGrantRejectsMalformedCommandsForActiveResource 验证严格 Wheel 协议校验不依赖 disabled 分支;
|
||||||
|
// active 资源也不能接受被 trim 修复、非 canonical 数字、大小写漂移或多段 suffix 的 command_id。
|
||||||
|
func TestWheelRewardGrantRejectsMalformedCommandsForActiveResource(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
ctx := context.Background()
|
||||||
|
resource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||||
|
ResourceCode: "active_wheel_command_validation_gift",
|
||||||
|
ResourceType: resourcedomain.TypeGift,
|
||||||
|
Name: "Active Wheel Command Validation Gift",
|
||||||
|
Status: resourcedomain.StatusActive,
|
||||||
|
Grantable: true,
|
||||||
|
GrantStrategy: resourcedomain.GrantStrategyIncreaseQuantity,
|
||||||
|
UsageScopes: []string{"wheel_reward"},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create active wheel command validation resource failed: %v", err)
|
||||||
|
}
|
||||||
|
overlong := "wheel_resource:wheel_draw_1720000000000_" + strings.Repeat("a", 100)
|
||||||
|
if len(overlong) <= 128 {
|
||||||
|
t.Fatalf("test setup must exceed command_id limit: len=%d", len(overlong))
|
||||||
|
}
|
||||||
|
invalidCommandIDs := []string{
|
||||||
|
"",
|
||||||
|
"wheel_resource:wheel_draw__0123456789abcdef",
|
||||||
|
"wheel_resource:wheel_draw_0_0123456789abcdef",
|
||||||
|
"wheel_resource:wheel_draw_-1_0123456789abcdef",
|
||||||
|
"wheel_resource:wheel_draw_now_0123456789abcdef",
|
||||||
|
"wheel_resource:wheel_draw_01720000000000_0123456789abcdef",
|
||||||
|
"wheel_resource:wheel_draw_1720000000000_",
|
||||||
|
"wheel_resource:wheel_draw_1720000000000_abcdef",
|
||||||
|
"wheel_resource:wheel_draw_1720000000000_0123456789ABCDEF",
|
||||||
|
"wheel_resource:wheel_draw_1720000000000_0123456789abcdef:",
|
||||||
|
"wheel_resource:wheel_draw_1720000000000_0123456789abcdef:0",
|
||||||
|
"wheel_resource:wheel_draw_1720000000000_0123456789abcdef:-1",
|
||||||
|
"wheel_resource:wheel_draw_1720000000000_0123456789abcdef:item",
|
||||||
|
"wheel_resource:wheel_draw_1720000000000_0123456789abcdef:01",
|
||||||
|
"wheel_resource:wheel_draw_1720000000000_0123456789abcdef:1:2",
|
||||||
|
" wheel_resource:wheel_draw_1720000000000_0123456789abcdef",
|
||||||
|
"wheel_resource:wheel_draw_1720000000000_0123456789abcdef ",
|
||||||
|
overlong,
|
||||||
|
}
|
||||||
|
for index, commandID := range invalidCommandIDs {
|
||||||
|
_, err := svc.GrantResource(ctx, resourcedomain.GrantResourceCommand{
|
||||||
|
CommandID: commandID,
|
||||||
|
TargetUserID: 43200 + int64(index),
|
||||||
|
ResourceID: resource.ResourceID,
|
||||||
|
Quantity: 1,
|
||||||
|
Reason: resourcedomain.GrantSourceWheelReward,
|
||||||
|
OperatorUserID: 43200 + int64(index),
|
||||||
|
GrantSource: resourcedomain.GrantSourceWheelReward,
|
||||||
|
})
|
||||||
|
if !xerr.IsCode(err, xerr.InvalidArgument) {
|
||||||
|
t.Fatalf("active resource accepted malformed wheel command[%d]=%q: %v", index, commandID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validCommandID := "wheel_resource:wheel_draw_1720000000000_0123456789abcdef:1"
|
||||||
|
for _, malformedIdentity := range []resourcedomain.GrantResourceCommand{
|
||||||
|
{
|
||||||
|
CommandID: validCommandID, TargetUserID: 43250, ResourceID: resource.ResourceID, Quantity: 1,
|
||||||
|
Reason: "manual repair", OperatorUserID: 43250, GrantSource: resourcedomain.GrantSourceWheelReward,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CommandID: validCommandID, TargetUserID: 43251, ResourceID: resource.ResourceID, Quantity: 1,
|
||||||
|
Reason: resourcedomain.GrantSourceWheelReward, OperatorUserID: 90001, GrantSource: resourcedomain.GrantSourceWheelReward,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if _, err := svc.GrantResource(ctx, malformedIdentity); !xerr.IsCode(err, xerr.InvalidArgument) {
|
||||||
|
t.Fatalf("active resource accepted malformed wheel identity: command=%+v err=%v", malformedIdentity, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("resource_grants", "resource_id = ?", resource.ResourceID); got != 0 {
|
||||||
|
t.Fatalf("malformed active wheel commands must not persist grants, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateResourceRejectsDuplicateResourceCodeAsConflict(t *testing.T) {
|
func TestCreateResourceRejectsDuplicateResourceCodeAsConflict(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
svc := walletservice.New(repository)
|
svc := walletservice.New(repository)
|
||||||
|
|||||||
@ -48,7 +48,7 @@ func (r *Repository) GrantResource(ctx context.Context, command resourcedomain.G
|
|||||||
if resourcedomain.NormalizeResourceType(resource.ResourceType) == resourcedomain.TypeVIPTrialCard {
|
if resourcedomain.NormalizeResourceType(resource.ResourceType) == resourcedomain.TypeVIPTrialCard {
|
||||||
return resourcedomain.ResourceGrant{}, xerr.New(xerr.InvalidArgument, "vip_trial_card must use GrantVipTrialCard")
|
return resourcedomain.ResourceGrant{}, xerr.New(xerr.InvalidArgument, "vip_trial_card must use GrantVipTrialCard")
|
||||||
}
|
}
|
||||||
if err := validateGrantableResourceForSource(resource, command.GrantSource); err != nil {
|
if err := validateGrantableResourceForCommand(resource, command); err != nil {
|
||||||
return resourcedomain.ResourceGrant{}, err
|
return resourcedomain.ResourceGrant{}, err
|
||||||
}
|
}
|
||||||
grantID := resourceGrantID(command.AppCode, command.CommandID)
|
grantID := resourceGrantID(command.AppCode, command.CommandID)
|
||||||
@ -786,9 +786,12 @@ func normalizeResourceGrantsQuery(query resourcedomain.ListResourceGrantsQuery)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func normalizeGrantResourceCommand(command resourcedomain.GrantResourceCommand) resourcedomain.GrantResourceCommand {
|
func normalizeGrantResourceCommand(command resourcedomain.GrantResourceCommand) resourcedomain.GrantResourceCommand {
|
||||||
command.CommandID = strings.TrimSpace(command.CommandID)
|
|
||||||
command.Reason = strings.TrimSpace(command.Reason)
|
|
||||||
command.GrantSource = resourcedomain.NormalizeGrantSource(command.GrantSource)
|
command.GrantSource = resourcedomain.NormalizeGrantSource(command.GrantSource)
|
||||||
|
// 普通历史调用继续保留 trim 兼容;Wheel command 是结算事实标识,必须保留原始字节交给严格格式校验,不能把带空白的畸形输入静默修正成合法命令。
|
||||||
|
if command.GrantSource != resourcedomain.GrantSourceWheelReward {
|
||||||
|
command.CommandID = strings.TrimSpace(command.CommandID)
|
||||||
|
}
|
||||||
|
command.Reason = strings.TrimSpace(command.Reason)
|
||||||
if command.Quantity == 0 {
|
if command.Quantity == 0 {
|
||||||
command.Quantity = 1
|
command.Quantity = 1
|
||||||
}
|
}
|
||||||
@ -802,9 +805,53 @@ func validateGrantResourceCommand(command resourcedomain.GrantResourceCommand) e
|
|||||||
if command.DurationMS < 0 {
|
if command.DurationMS < 0 {
|
||||||
return xerr.New(xerr.InvalidArgument, "duration_ms is invalid")
|
return xerr.New(xerr.InvalidArgument, "duration_ms is invalid")
|
||||||
}
|
}
|
||||||
|
if command.GrantSource == resourcedomain.GrantSourceWheelReward {
|
||||||
|
// Wheel source 由 activity owner 使用:即便资源当前 active,也必须先固定命令形状和本人操作语义,不能把可伪造的协议字符串当成 disabled 例外的授权依据。
|
||||||
|
if command.Reason != resourcedomain.GrantSourceWheelReward || command.OperatorUserID != command.TargetUserID || !validWheelResourceGrantCommandID(command.CommandID) {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "wheel reward resource grant command is invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validWheelResourceGrantCommandID(commandID string) bool {
|
||||||
|
const (
|
||||||
|
commandPrefix = "wheel_resource"
|
||||||
|
drawPrefix = "wheel_draw_"
|
||||||
|
entropyLength = 16 // pkg/idgen.New 固定使用 8 bytes entropy,编码后必须是 16 位小写 hex。
|
||||||
|
maxLength = 128
|
||||||
|
)
|
||||||
|
if commandID == "" || len(commandID) > maxLength || strings.TrimSpace(commandID) != commandID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parts := strings.Split(commandID, ":")
|
||||||
|
if len(parts) != 2 && len(parts) != 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if parts[0] != commandPrefix || !strings.HasPrefix(parts[1], drawPrefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
drawParts := strings.Split(strings.TrimPrefix(parts[1], drawPrefix), "_")
|
||||||
|
if len(drawParts) != 2 || !validCanonicalPositiveDecimal(drawParts[0]) || len(drawParts[1]) != entropyLength || !isLowerHex(drawParts[1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(parts) == 2 || validCanonicalPositiveDecimal(parts[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func validCanonicalPositiveDecimal(value string) bool {
|
||||||
|
parsed, err := strconv.ParseUint(value, 10, 64)
|
||||||
|
return err == nil && parsed > 0 && strconv.FormatUint(parsed, 10) == value
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLowerHex(value string) bool {
|
||||||
|
for _, char := range value {
|
||||||
|
if (char < '0' || char > '9') && (char < 'a' || char > 'f') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value != ""
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeGrantResourceGroupCommand(command resourcedomain.GrantResourceGroupCommand) resourcedomain.GrantResourceGroupCommand {
|
func normalizeGrantResourceGroupCommand(command resourcedomain.GrantResourceGroupCommand) resourcedomain.GrantResourceGroupCommand {
|
||||||
command.CommandID = strings.TrimSpace(command.CommandID)
|
command.CommandID = strings.TrimSpace(command.CommandID)
|
||||||
command.Reason = strings.TrimSpace(command.Reason)
|
command.Reason = strings.TrimSpace(command.Reason)
|
||||||
@ -837,7 +884,6 @@ func validateRevokeResourceGrantCommand(command resourcedomain.RevokeResourceGra
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateGrantableResourceForSource(resource resourcedomain.Resource, grantSource string) error {
|
func validateGrantableResourceForSource(resource resourcedomain.Resource, grantSource string) error {
|
||||||
|
|
||||||
if grantSource == resourcedomain.GrantSourceGameRobotInit {
|
if grantSource == resourcedomain.GrantSourceGameRobotInit {
|
||||||
if resource.Status != resourcedomain.StatusActive {
|
if resource.Status != resourcedomain.StatusActive {
|
||||||
return xerr.New(xerr.Conflict, "resource is disabled")
|
return xerr.New(xerr.Conflict, "resource is disabled")
|
||||||
@ -856,6 +902,28 @@ func validateGrantableResourceForSource(resource resourcedomain.Resource, grantS
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateGrantableResourceForCommand(resource resourcedomain.Resource, command resourcedomain.GrantResourceCommand) error {
|
||||||
|
// 历史转盘结算可能在 outbox 堆积期间遇到奖品下架:disabled 仅表示停止新配置/展示,不应抹掉抽奖时已经固化的中奖事实。
|
||||||
|
// source/reason/command 前缀只用于识别协议,不能独立充当授权;本人操作和 gift-only 约束共同阻断后台借道、加币与自动装备副作用。
|
||||||
|
if resource.Status == resourcedomain.StatusDisabled && command.GrantSource == resourcedomain.GrantSourceWheelReward {
|
||||||
|
// deleted 表示资源事实已回收,且只会走下方原有校验;这里仍要求 grantable,确保运营明确关闭发放能力时补偿也会停住。
|
||||||
|
if !resource.Grantable {
|
||||||
|
return xerr.New(xerr.Conflict, "resource is not grantable")
|
||||||
|
}
|
||||||
|
if command.OperatorUserID != command.TargetUserID {
|
||||||
|
return xerr.New(xerr.PermissionDenied, "wheel reward compensation must be performed for the same user")
|
||||||
|
}
|
||||||
|
if resourcedomain.NormalizeResourceType(resource.ResourceType) != resourcedomain.TypeGift || resourcedomain.IsCoinResource(resource) || resourcedomain.IsWalletCredit(resource) {
|
||||||
|
return xerr.New(xerr.Conflict, "disabled wheel reward compensation only supports non-wallet gift resources")
|
||||||
|
}
|
||||||
|
if resourcedomain.NormalizeGrantStrategy(resource.GrantStrategy) != resourcedomain.GrantStrategyIncreaseQuantity || command.DurationMS != 0 {
|
||||||
|
return xerr.New(xerr.Conflict, "disabled wheel reward compensation requires quantity-only grant semantics")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return validateGrantableResourceForSource(resource, command.GrantSource)
|
||||||
|
}
|
||||||
|
|
||||||
func validateGrantableResource(resource resourcedomain.Resource) error {
|
func validateGrantableResource(resource resourcedomain.Resource) error {
|
||||||
if resource.Status != resourcedomain.StatusActive {
|
if resource.Status != resourcedomain.StatusActive {
|
||||||
return xerr.New(xerr.Conflict, "resource is disabled")
|
return xerr.New(xerr.Conflict, "resource is disabled")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user