Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d52cffc44 |
@ -6175,6 +6175,9 @@ type UserResourceEntitlement struct {
|
|||||||
// pinned_gift_configs 仅在 source_snapshot_id 非空且资源类型为 gift 时返回。内容已由 wallet
|
// pinned_gift_configs 仅在 source_snapshot_id 非空且资源类型为 gift 时返回。内容已由 wallet
|
||||||
// 校验 hash/resource/app 绑定,App 背包必须优先使用它,legacy entitlement 才回退当前目录。
|
// 校验 hash/resource/app 绑定,App 背包必须优先使用它,legacy entitlement 才回退当前目录。
|
||||||
PinnedGiftConfigs []*GiftConfig `protobuf:"bytes,17,rep,name=pinned_gift_configs,json=pinnedGiftConfigs,proto3" json:"pinned_gift_configs,omitempty"`
|
PinnedGiftConfigs []*GiftConfig `protobuf:"bytes,17,rep,name=pinned_gift_configs,json=pinnedGiftConfigs,proto3" json:"pinned_gift_configs,omitempty"`
|
||||||
|
// vip_benefit_codes 是该资源在当前用户有效 VIP 等级下命中的真实权益编码。
|
||||||
|
// 展示层用它区分同为 badge 的 VIP 身份标识、勋章和普通徽章,禁止按 URL 或名称猜测。
|
||||||
|
VipBenefitCodes []string `protobuf:"bytes,18,rep,name=vip_benefit_codes,json=vipBenefitCodes,proto3" json:"vip_benefit_codes,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -6328,6 +6331,13 @@ func (x *UserResourceEntitlement) GetPinnedGiftConfigs() []*GiftConfig {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *UserResourceEntitlement) GetVipBenefitCodes() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.VipBenefitCodes
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type ResourceGrantItem struct {
|
type ResourceGrantItem struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
GrantItemId int64 `protobuf:"varint,1,opt,name=grant_item_id,json=grantItemId,proto3" json:"grant_item_id,omitempty"`
|
GrantItemId int64 `protobuf:"varint,1,opt,name=grant_item_id,json=grantItemId,proto3" json:"grant_item_id,omitempty"`
|
||||||
@ -27854,7 +27864,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
"\x12created_by_user_id\x18\f \x01(\x03R\x0fcreatedByUserId\x12+\n" +
|
"\x12created_by_user_id\x18\f \x01(\x03R\x0fcreatedByUserId\x12+\n" +
|
||||||
"\x12updated_by_user_id\x18\r \x01(\x03R\x0fupdatedByUserId\x12\"\n" +
|
"\x12updated_by_user_id\x18\r \x01(\x03R\x0fupdatedByUserId\x12\"\n" +
|
||||||
"\rcreated_at_ms\x18\x0e \x01(\x03R\vcreatedAtMs\x12\"\n" +
|
"\rcreated_at_ms\x18\x0e \x01(\x03R\vcreatedAtMs\x12\"\n" +
|
||||||
"\rupdated_at_ms\x18\x0f \x01(\x03R\vupdatedAtMs\"\xb8\x05\n" +
|
"\rupdated_at_ms\x18\x0f \x01(\x03R\vupdatedAtMs\"\xe4\x05\n" +
|
||||||
"\x17UserResourceEntitlement\x12\x19\n" +
|
"\x17UserResourceEntitlement\x12\x19\n" +
|
||||||
"\bapp_code\x18\x01 \x01(\tR\aappCode\x12%\n" +
|
"\bapp_code\x18\x01 \x01(\tR\aappCode\x12%\n" +
|
||||||
"\x0eentitlement_id\x18\x02 \x01(\tR\rentitlementId\x12\x17\n" +
|
"\x0eentitlement_id\x18\x02 \x01(\tR\rentitlementId\x12\x17\n" +
|
||||||
@ -27874,7 +27884,8 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
"\bequipped\x18\x0e \x01(\bR\bequipped\x12,\n" +
|
"\bequipped\x18\x0e \x01(\bR\bequipped\x12,\n" +
|
||||||
"\x12source_snapshot_id\x18\x0f \x01(\tR\x10sourceSnapshotId\x124\n" +
|
"\x12source_snapshot_id\x18\x0f \x01(\tR\x10sourceSnapshotId\x124\n" +
|
||||||
"\x16resource_snapshot_hash\x18\x10 \x01(\tR\x14resourceSnapshotHash\x12K\n" +
|
"\x16resource_snapshot_hash\x18\x10 \x01(\tR\x14resourceSnapshotHash\x12K\n" +
|
||||||
"\x13pinned_gift_configs\x18\x11 \x03(\v2\x1b.hyapp.wallet.v1.GiftConfigR\x11pinnedGiftConfigs\"\x86\x03\n" +
|
"\x13pinned_gift_configs\x18\x11 \x03(\v2\x1b.hyapp.wallet.v1.GiftConfigR\x11pinnedGiftConfigs\x12*\n" +
|
||||||
|
"\x11vip_benefit_codes\x18\x12 \x03(\tR\x0fvipBenefitCodes\"\x86\x03\n" +
|
||||||
"\x11ResourceGrantItem\x12\"\n" +
|
"\x11ResourceGrantItem\x12\"\n" +
|
||||||
"\rgrant_item_id\x18\x01 \x01(\x03R\vgrantItemId\x12\x19\n" +
|
"\rgrant_item_id\x18\x01 \x01(\x03R\vgrantItemId\x12\x19\n" +
|
||||||
"\bgrant_id\x18\x02 \x01(\tR\agrantId\x12\x1f\n" +
|
"\bgrant_id\x18\x02 \x01(\tR\agrantId\x12\x1f\n" +
|
||||||
|
|||||||
@ -779,6 +779,9 @@ message UserResourceEntitlement {
|
|||||||
// pinned_gift_configs 仅在 source_snapshot_id 非空且资源类型为 gift 时返回。内容已由 wallet
|
// pinned_gift_configs 仅在 source_snapshot_id 非空且资源类型为 gift 时返回。内容已由 wallet
|
||||||
// 校验 hash/resource/app 绑定,App 背包必须优先使用它,legacy entitlement 才回退当前目录。
|
// 校验 hash/resource/app 绑定,App 背包必须优先使用它,legacy entitlement 才回退当前目录。
|
||||||
repeated GiftConfig pinned_gift_configs = 17;
|
repeated GiftConfig pinned_gift_configs = 17;
|
||||||
|
// vip_benefit_codes 是该资源在当前用户有效 VIP 等级下命中的真实权益编码。
|
||||||
|
// 展示层用它区分同为 badge 的 VIP 身份标识、勋章和普通徽章,禁止按 URL 或名称猜测。
|
||||||
|
repeated string vip_benefit_codes = 18;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ResourceGrantItem {
|
message ResourceGrantItem {
|
||||||
|
|||||||
@ -3689,6 +3689,7 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
|||||||
EntitlementId: "ent-badge-tile",
|
EntitlementId: "ent-badge-tile",
|
||||||
ResourceId: 8105,
|
ResourceId: 8105,
|
||||||
Resource: shortBadgeResource,
|
Resource: shortBadgeResource,
|
||||||
|
VipBenefitCodes: []string{"vip_badge_identity"},
|
||||||
Equipped: true,
|
Equipped: true,
|
||||||
ExpiresAtMs: 1999999999999,
|
ExpiresAtMs: 1999999999999,
|
||||||
},
|
},
|
||||||
@ -3816,7 +3817,9 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
|||||||
}
|
}
|
||||||
badge := badges[0].(map[string]any)
|
badge := badges[0].(map[string]any)
|
||||||
badgeResource := badge["resource"].(map[string]any)
|
badgeResource := badge["resource"].(map[string]any)
|
||||||
if badge["badge_form"] != "tile" || badgeResource["preview_url"] != "https://cdn.example/badge-tile.png" {
|
vipBenefitCodes, vipBenefitCodesOK := badgeResource["vip_benefit_codes"].([]any)
|
||||||
|
if badge["badge_form"] != "tile" || badgeResource["preview_url"] != "https://cdn.example/badge-tile.png" ||
|
||||||
|
!vipBenefitCodesOK || len(vipBenefitCodes) != 1 || vipBenefitCodes[0] != "vip_badge_identity" {
|
||||||
t.Fatalf("join profile must include equipped tile badge resource: %+v", badge)
|
t.Fatalf("join profile must include equipped tile badge resource: %+v", badge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2989,7 +2989,13 @@ func roomAppearanceResourceFromEntitlement(item *walletv1.UserResourceEntitlemen
|
|||||||
if item == nil {
|
if item == nil {
|
||||||
return map[string]any{}
|
return map[string]any{}
|
||||||
}
|
}
|
||||||
return roomAppearanceResourceFromProto(item.GetResource(), item.GetEntitlementId(), item.GetExpiresAtMs())
|
resource := roomAppearanceResourceFromProto(item.GetResource(), item.GetEntitlementId(), item.GetExpiresAtMs())
|
||||||
|
if len(resource) > 0 && len(item.GetVipBenefitCodes()) > 0 {
|
||||||
|
// VIP 身份标识、VIP 勋章和普通徽章共用 badge 类型;必须透传 wallet 已校验的
|
||||||
|
// 资源级 benefit code,Flutter 才能在不猜素材名称的前提下选择正确展示层。
|
||||||
|
resource["vip_benefit_codes"] = append([]string(nil), item.GetVipBenefitCodes()...)
|
||||||
|
}
|
||||||
|
return resource
|
||||||
}
|
}
|
||||||
|
|
||||||
func roomAppearanceResourceFromProto(item *walletv1.Resource, entitlementID string, expiresAtMS int64) map[string]any {
|
func roomAppearanceResourceFromProto(item *walletv1.Resource, entitlementID string, expiresAtMS int64) map[string]any {
|
||||||
|
|||||||
@ -371,7 +371,12 @@ func appearanceResourceFromEntitlement(item *walletv1.UserResourceEntitlement) m
|
|||||||
return map[string]any{}
|
return map[string]any{}
|
||||||
}
|
}
|
||||||
// entitlement_id/expires_at_ms 是佩戴态字段,resource 本身不带这两个用户维度。
|
// entitlement_id/expires_at_ms 是佩戴态字段,resource 本身不带这两个用户维度。
|
||||||
return appearanceResourceFromProto(item.GetResource(), item.GetEntitlementId(), item.GetExpiresAtMs())
|
resource := appearanceResourceFromProto(item.GetResource(), item.GetEntitlementId(), item.GetExpiresAtMs())
|
||||||
|
if len(resource) > 0 && len(item.GetVipBenefitCodes()) > 0 {
|
||||||
|
// 与房间资料保持同一资源级分类契约,完整个人页不得再次按 badge 图片或名称猜 VIP 身份。
|
||||||
|
resource["vip_benefit_codes"] = append([]string(nil), item.GetVipBenefitCodes()...)
|
||||||
|
}
|
||||||
|
return resource
|
||||||
}
|
}
|
||||||
|
|
||||||
func appearanceResourceFromProto(item *walletv1.Resource, entitlementID string, expiresAtMS int64) map[string]any {
|
func appearanceResourceFromProto(item *walletv1.Resource, entitlementID string, expiresAtMS int64) map[string]any {
|
||||||
|
|||||||
@ -18,6 +18,8 @@ type UserResourceEntitlement struct {
|
|||||||
GiftSnapshotHash string
|
GiftSnapshotHash string
|
||||||
GiftSnapshotJSON string
|
GiftSnapshotJSON string
|
||||||
PinnedGiftConfigs []GiftConfig
|
PinnedGiftConfigs []GiftConfig
|
||||||
|
// VIPBenefitCodes 由当前 VIP program、有效等级和 trial 规则共同计算,只描述本资源命中的权益。
|
||||||
|
VIPBenefitCodes []string
|
||||||
CreatedAtMS int64
|
CreatedAtMS int64
|
||||||
UpdatedAtMS int64
|
UpdatedAtMS int64
|
||||||
Equipped bool
|
Equipped bool
|
||||||
|
|||||||
@ -174,6 +174,54 @@ func TestVipUserSettingsDefaultPartialUpdateAndAppScope(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestVipEquippedResourceProjectsBenefitCode 锁定同为 badge 类型的资源仍携带精确 VIP 权益编码,
|
||||||
|
// gateway/Flutter 才能把 vip_badge_identity 放进身份行而不是普通长徽章行。
|
||||||
|
func TestVipEquippedResourceProjectsBenefitCode(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
service := walletservice.New(repository)
|
||||||
|
ctx := appcode.WithContext(context.Background(), "fami")
|
||||||
|
const userID int64 = 940101
|
||||||
|
|
||||||
|
badge, err := service.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||||
|
ResourceCode: "vip1_identity_projection",
|
||||||
|
ResourceType: resourcedomain.TypeBadge,
|
||||||
|
Name: "VIP1 Identity Projection",
|
||||||
|
Status: resourcedomain.StatusActive,
|
||||||
|
Grantable: true,
|
||||||
|
GrantStrategy: resourcedomain.GrantStrategySetActiveFlag,
|
||||||
|
UsageScopes: []string{"profile"},
|
||||||
|
AssetURL: "https://cdn.example/vip1-identity.png",
|
||||||
|
MetadataJSON: `{"badge_form":"strip","badge_kind":"normal"}`,
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create VIP identity badge failed: %v", err)
|
||||||
|
}
|
||||||
|
repository.BindVIPBenefitResourceForApp(
|
||||||
|
"fami", 1, ledger.VipBenefitCodeBadgeIdentity,
|
||||||
|
badge.ResourceID, resourcedomain.TypeBadge, true,
|
||||||
|
)
|
||||||
|
repository.SetVIPLevelStatusForApp("fami", 1, ledger.VipStatusActive, 100)
|
||||||
|
repository.SetBalanceForApp("fami", userID, 1000)
|
||||||
|
if _, err := service.PurchaseVip(ctx, ledger.PurchaseVipCommand{
|
||||||
|
CommandID: "vip-identity-projection", UserID: userID, Level: 1,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("purchase VIP1 failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := service.BatchGetUserEquippedResources(ctx, resourcedomain.BatchGetUserEquippedResourcesQuery{
|
||||||
|
AppCode: "fami", UserIDs: []int64{userID}, ResourceTypes: []string{resourcedomain.TypeBadge},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("batch get VIP identity equipment failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(items) != 1 || len(items[0].Resources) != 1 ||
|
||||||
|
len(items[0].Resources[0].VIPBenefitCodes) != 1 ||
|
||||||
|
items[0].Resources[0].VIPBenefitCodes[0] != ledger.VipBenefitCodeBadgeIdentity {
|
||||||
|
t.Fatalf("VIP identity resource benefit projection mismatch: %+v", items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func containsVipBenefit(items []ledger.VipBenefit, code string) bool {
|
func containsVipBenefit(items []ledger.VipBenefit, code string) bool {
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if item.BenefitCode == code {
|
if item.BenefitCode == code {
|
||||||
|
|||||||
@ -622,8 +622,8 @@ type vipResourceBindingKey struct {
|
|||||||
|
|
||||||
type currentVipResourceBinding struct {
|
type currentVipResourceBinding struct {
|
||||||
ResourceType string
|
ResourceType string
|
||||||
PaidAllowed bool
|
PaidBenefitCodes []string
|
||||||
TrialAllowed bool
|
TrialBenefitCodes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type vipBatchEffectiveLevels struct {
|
type vipBatchEffectiveLevels struct {
|
||||||
@ -667,13 +667,13 @@ func (r *Repository) filterCurrentVipEquipmentBatch(ctx context.Context, userIDs
|
|||||||
ledger.VipProgramTypeTieredPrivilegeV1, appcode.FromContext(ctx))
|
ledger.VipProgramTypeTieredPrivilegeV1, appcode.FromContext(ctx))
|
||||||
mappingArgs = append(mappingArgs, int64AnyArgs(resourceIDs)...)
|
mappingArgs = append(mappingArgs, int64AnyArgs(resourceIDs)...)
|
||||||
rows, err := tx.QueryContext(ctx, `
|
rows, err := tx.QueryContext(ctx, `
|
||||||
SELECT history.resource_id, 0 AS level, '' AS resource_type,
|
SELECT history.resource_id, 0 AS level, '' AS resource_type, '' AS benefit_code,
|
||||||
FALSE AS trial_enabled, FALSE AS current_allowed
|
FALSE AS trial_enabled, FALSE AS current_allowed
|
||||||
FROM vip_benefit_resource_binding_history AS history
|
FROM vip_benefit_resource_binding_history AS history
|
||||||
WHERE history.app_code = ? AND history.resource_id IN (`+placeholders(len(resourceIDs))+`)
|
WHERE history.app_code = ? AND history.resource_id IN (`+placeholders(len(resourceIDs))+`)
|
||||||
AND history.program_type = ?
|
AND history.program_type = ?
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT benefit.resource_id, benefit.level, benefit.resource_type,
|
SELECT benefit.resource_id, benefit.level, benefit.resource_type, benefit.benefit_code,
|
||||||
(benefit.trial_enabled AND benefit.benefit_code <> 'daily_coin_rebate') AS trial_enabled,
|
(benefit.trial_enabled AND benefit.benefit_code <> 'daily_coin_rebate') AS trial_enabled,
|
||||||
(benefit.status = 'active' AND resource.resource_id IS NOT NULL) AS current_allowed
|
(benefit.status = 'active' AND resource.resource_id IS NOT NULL) AS current_allowed
|
||||||
FROM vip_level_benefits AS benefit
|
FROM vip_level_benefits AS benefit
|
||||||
@ -693,9 +693,10 @@ func (r *Repository) filterCurrentVipEquipmentBatch(ctx context.Context, userIDs
|
|||||||
var resourceID int64
|
var resourceID int64
|
||||||
var level int32
|
var level int32
|
||||||
var resourceType string
|
var resourceType string
|
||||||
|
var benefitCode string
|
||||||
var trialEnabled bool
|
var trialEnabled bool
|
||||||
var currentAllowed bool
|
var currentAllowed bool
|
||||||
if err := rows.Scan(&resourceID, &level, &resourceType, &trialEnabled, ¤tAllowed); err != nil {
|
if err := rows.Scan(&resourceID, &level, &resourceType, &benefitCode, &trialEnabled, ¤tAllowed); err != nil {
|
||||||
_ = rows.Close()
|
_ = rows.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -706,8 +707,10 @@ func (r *Repository) filterCurrentVipEquipmentBatch(ctx context.Context, userIDs
|
|||||||
key := vipResourceBindingKey{Level: level, ResourceID: resourceID}
|
key := vipResourceBindingKey{Level: level, ResourceID: resourceID}
|
||||||
binding := bindings[key]
|
binding := bindings[key]
|
||||||
binding.ResourceType = resourcedomain.NormalizeResourceType(resourceType)
|
binding.ResourceType = resourcedomain.NormalizeResourceType(resourceType)
|
||||||
binding.PaidAllowed = true
|
binding.PaidBenefitCodes = appendUniqueNormalizedVIPCode(binding.PaidBenefitCodes, benefitCode)
|
||||||
binding.TrialAllowed = binding.TrialAllowed || trialEnabled
|
if trialEnabled {
|
||||||
|
binding.TrialBenefitCodes = appendUniqueNormalizedVIPCode(binding.TrialBenefitCodes, benefitCode)
|
||||||
|
}
|
||||||
bindings[key] = binding
|
bindings[key] = binding
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
@ -748,31 +751,50 @@ func filterVipEquipmentByAccess(grouped map[int64][]resourcedomain.UserResourceE
|
|||||||
filtered = append(filtered, entitlement)
|
filtered = append(filtered, entitlement)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !hasCurrentVipResourceBindingAtOrBelow(bindings, effectiveLevel, effectiveSource,
|
benefitCodes := currentVipBenefitCodesAtOrBelow(bindings, effectiveLevel, effectiveSource,
|
||||||
entitlement.ResourceID, entitlement.Resource.ResourceType) {
|
entitlement.ResourceID, entitlement.Resource.ResourceType)
|
||||||
|
if len(benefitCodes) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 同为 badge 的 VIP 身份标识、勋章和普通徽章不能靠素材名称区分;将 wallet
|
||||||
|
// 已完成等级、trial 和改绑校验后的资源级权益事实随 entitlement 一并投影。
|
||||||
|
entitlement.VIPBenefitCodes = benefitCodes
|
||||||
filtered = append(filtered, entitlement)
|
filtered = append(filtered, entitlement)
|
||||||
}
|
}
|
||||||
grouped[userID] = filtered
|
grouped[userID] = filtered
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasCurrentVipResourceBindingAtOrBelow 只在后台配置的有限 VIP 等级内倒序查找,因此不会引入
|
// currentVipBenefitCodesAtOrBelow 只在后台配置的有限 VIP 等级内倒序查找,因此不会引入
|
||||||
// 数据库请求或按用户增长的复杂度;倒序保证同一资源重复绑定时优先采用最高可用等级的试用策略。
|
// 数据库请求或按用户增长的复杂度;同一资源跨等级复用时合并所有当前可用权益编码,供展示层精确分流。
|
||||||
func hasCurrentVipResourceBindingAtOrBelow(bindings map[vipResourceBindingKey]currentVipResourceBinding, effectiveLevel int32, effectiveSource string, resourceID int64, resourceType string) bool {
|
func currentVipBenefitCodesAtOrBelow(bindings map[vipResourceBindingKey]currentVipResourceBinding, effectiveLevel int32, effectiveSource string, resourceID int64, resourceType string) []string {
|
||||||
resourceType = resourcedomain.NormalizeResourceType(resourceType)
|
resourceType = resourcedomain.NormalizeResourceType(resourceType)
|
||||||
|
result := make([]string, 0, 2)
|
||||||
for level := effectiveLevel; level > 0; level-- {
|
for level := effectiveLevel; level > 0; level-- {
|
||||||
binding, exists := bindings[vipResourceBindingKey{Level: level, ResourceID: resourceID}]
|
binding, exists := bindings[vipResourceBindingKey{Level: level, ResourceID: resourceID}]
|
||||||
if !exists || binding.ResourceType != resourceType {
|
if !exists || binding.ResourceType != resourceType {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if effectiveSource == ledger.VipEffectiveSourceTrial && binding.TrialAllowed {
|
codes := binding.PaidBenefitCodes
|
||||||
return true
|
if effectiveSource == ledger.VipEffectiveSourceTrial {
|
||||||
|
codes = binding.TrialBenefitCodes
|
||||||
}
|
}
|
||||||
if effectiveSource == ledger.VipEffectiveSourcePaid && binding.PaidAllowed {
|
for _, code := range codes {
|
||||||
return true
|
result = appendUniqueNormalizedVIPCode(result, code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendUniqueNormalizedVIPCode(values []string, value string) []string {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||||
|
if normalized == "" {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
for _, existing := range values {
|
||||||
|
if existing == normalized {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(values, normalized)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -850,6 +850,21 @@ func (r *Repository) SetVIPLevelStatusForApp(appCode string, level int32, status
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BindVIPBenefitResourceForApp 将既有等级权益绑定到测试资源,覆盖购买、自动佩戴和展示投影的真实链路。
|
||||||
|
func (r *Repository) BindVIPBenefitResourceForApp(appCode string, level int32, benefitCode string, resourceID int64, resourceType string, trialEnabled bool) {
|
||||||
|
r.t.Helper()
|
||||||
|
if _, err := r.schema.DB.ExecContext(context.Background(), `
|
||||||
|
UPDATE vip_level_benefits
|
||||||
|
SET resource_id = ?, resource_type = ?, trial_enabled = ?, auto_equip = TRUE,
|
||||||
|
status = 'active', updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND level = ? AND benefit_code = ?`,
|
||||||
|
resourceID, resourceType, trialEnabled, time.Now().UTC().UnixMilli(),
|
||||||
|
appCode, level, benefitCode,
|
||||||
|
); err != nil {
|
||||||
|
r.t.Fatalf("bind vip benefit resource failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetVIPDailyCoinRebateAmount 写入测试专用等级金额并递增 program 版本,模拟后台完整配置发布。
|
// SetVIPDailyCoinRebateAmount 写入测试专用等级金额并递增 program 版本,模拟后台完整配置发布。
|
||||||
// amount<=0 时仍写入,供服务端校验/运行防线测试构造异常历史数据。
|
// amount<=0 时仍写入,供服务端校验/运行防线测试构造异常历史数据。
|
||||||
func (r *Repository) SetVIPDailyCoinRebateAmount(appCode string, level int32, amount int64, status string) {
|
func (r *Repository) SetVIPDailyCoinRebateAmount(appCode string, level int32, amount int64, status string) {
|
||||||
|
|||||||
@ -200,6 +200,7 @@ func userResourceToProto(item resourcedomain.UserResourceEntitlement) *walletv1.
|
|||||||
Equipped: item.Equipped,
|
Equipped: item.Equipped,
|
||||||
SourceSnapshotId: item.SourceSnapshotID,
|
SourceSnapshotId: item.SourceSnapshotID,
|
||||||
ResourceSnapshotHash: item.ResourceSnapshotHash,
|
ResourceSnapshotHash: item.ResourceSnapshotHash,
|
||||||
|
VipBenefitCodes: append([]string(nil), item.VIPBenefitCodes...),
|
||||||
}
|
}
|
||||||
for _, gift := range item.PinnedGiftConfigs {
|
for _, gift := range item.PinnedGiftConfigs {
|
||||||
out.PinnedGiftConfigs = append(out.PinnedGiftConfigs, giftConfigToProto(gift))
|
out.PinnedGiftConfigs = append(out.PinnedGiftConfigs, giftConfigToProto(gift))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user