From fdcd29126d42de03b64d7f2b083caa7fb6bae472 Mon Sep 17 00:00:00 2001 From: zhx Date: Wed, 15 Jul 2026 19:34:11 +0800 Subject: [PATCH] feat(wallet): expand VIP and resource management flows --- api/proto/wallet/v1/wallet.pb.go | 1249 ++++++++++++----- api/proto/wallet/v1/wallet.proto | 60 +- api/proto/wallet/v1/wallet_grpc.pb.go | 38 + docs/Fami_VIP_Flutter对接方案.md | 226 ++- docs/VIP系统架构.md | 24 +- docs/新版VIP权限策略_Flutter客户端对接文档.md | 374 ++++- pkg/xerr/catalog.go | 8 + pkg/xerr/errors.go | 16 + pkg/xerr/grpc_test.go | 8 + scripts/apply-local-mysql-initdb.sh | 12 +- scripts/mysql/062_vip_user_settings.sql | 3 + .../mysql/067_vip_user_privacy_settings.sql | 33 + .../integration/walletclient/client.go | 5 + server/admin/internal/model/models.go | 4 +- .../internal/modules/resource/handler.go | 42 + .../internal/modules/resource/handler_test.go | 58 +- .../internal/modules/resource/request.go | 6 + .../admin/internal/modules/resource/routes.go | 1 + .../internal/modules/vipconfig/handler.go | 134 +- .../modules/vipconfig/handler_test.go | 14 +- .../transport/http/vip_handler_test.go | 74 +- .../http/walletapi/app_wallet_handler.go | 66 +- .../transport/http/walletapi/vip_handler.go | 49 +- .../http/walletapi/vip_settings_handler.go | 16 +- .../room-service/internal/room/service/vip.go | 11 +- .../internal/room/service/vip_test.go | 8 +- .../mysql/initdb/001_wallet_service.sql | 6 +- .../002_resource_shop_price_tiers.sql | 48 + .../internal/domain/ledger/vip.go | 67 +- .../domain/ledger/vip_presentation.go | 145 ++ .../domain/ledger/vip_presentation_test.go | 46 + .../internal/domain/resource/entitlement.go | 9 + .../internal/domain/resource/shop.go | 10 +- .../service/wallet/ports/repository.go | 1 + .../internal/service/wallet/resource.go | 9 + .../internal/service/wallet/service_test.go | 248 +++- .../internal/service/wallet/vip.go | 6 +- .../vip_configurable_integration_test.go | 20 +- .../vip_user_settings_integration_test.go | 19 +- .../mysql/resource_entitlement_repository.go | 104 ++ .../storage/mysql/resource_shop_repository.go | 144 +- .../storage/mysql/vip_admin_repository.go | 10 + .../storage/mysql/vip_program_repository.go | 7 +- .../storage/mysql/vip_purchase_repository.go | 54 +- .../mysql/vip_trial_card_repository.go | 22 +- .../mysql/vip_user_settings_repository.go | 39 +- .../internal/transport/grpc/resource.go | 15 + .../transport/grpc/resource_command_mapper.go | 1 + .../internal/transport/grpc/vip.go | 79 +- 49 files changed, 3047 insertions(+), 601 deletions(-) create mode 100644 scripts/mysql/067_vip_user_privacy_settings.sql create mode 100644 services/wallet-service/deploy/mysql/migrations/002_resource_shop_price_tiers.sql create mode 100644 services/wallet-service/internal/domain/ledger/vip_presentation.go create mode 100644 services/wallet-service/internal/domain/ledger/vip_presentation_test.go diff --git a/api/proto/wallet/v1/wallet.pb.go b/api/proto/wallet/v1/wallet.pb.go index 67d1ffa2..d492df7c 100644 --- a/api/proto/wallet/v1/wallet.pb.go +++ b/api/proto/wallet/v1/wallet.pb.go @@ -10178,8 +10178,10 @@ type ResourceShopItemInput struct { EffectiveFromMs int64 `protobuf:"varint,5,opt,name=effective_from_ms,json=effectiveFromMs,proto3" json:"effective_from_ms,omitempty"` EffectiveToMs int64 `protobuf:"varint,6,opt,name=effective_to_ms,json=effectiveToMs,proto3" json:"effective_to_ms,omitempty"` SortOrder int32 `protobuf:"varint,7,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // coin_price 是当前售卖规格自己的金币价;新建传 0 使用资源列表价,更新已有规格传 0 保留当前售价。 + CoinPrice int64 `protobuf:"varint,8,opt,name=coin_price,json=coinPrice,proto3" json:"coin_price,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ResourceShopItemInput) Reset() { @@ -10261,6 +10263,13 @@ func (x *ResourceShopItemInput) GetSortOrder() int32 { return 0 } +func (x *ResourceShopItemInput) GetCoinPrice() int64 { + if x != nil { + return x.CoinPrice + } + return 0 +} + type ListResourceShopItemsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` @@ -17408,12 +17417,13 @@ type VipBenefit struct { ResourceId int64 `protobuf:"varint,7,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` ResourceType string `protobuf:"bytes,8,opt,name=resource_type,json=resourceType,proto3" json:"resource_type,omitempty"` // execution_scope 标识 wallet/room/user/notice 等实际权限执行方,不代表服务间直连。 - ExecutionScope string `protobuf:"bytes,9,opt,name=execution_scope,json=executionScope,proto3" json:"execution_scope,omitempty"` - AutoEquip bool `protobuf:"varint,10,opt,name=auto_equip,json=autoEquip,proto3" json:"auto_equip,omitempty"` - SortOrder int32 `protobuf:"varint,11,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` - MetadataJson string `protobuf:"bytes,12,opt,name=metadata_json,json=metadataJson,proto3" json:"metadata_json,omitempty"` - CreatedAtMs int64 `protobuf:"varint,13,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` - UpdatedAtMs int64 `protobuf:"varint,14,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` + ExecutionScope string `protobuf:"bytes,9,opt,name=execution_scope,json=executionScope,proto3" json:"execution_scope,omitempty"` + AutoEquip bool `protobuf:"varint,10,opt,name=auto_equip,json=autoEquip,proto3" json:"auto_equip,omitempty"` + SortOrder int32 `protobuf:"varint,11,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + MetadataJson string `protobuf:"bytes,12,opt,name=metadata_json,json=metadataJson,proto3" json:"metadata_json,omitempty"` + CreatedAtMs int64 `protobuf:"varint,13,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` + UpdatedAtMs int64 `protobuf:"varint,14,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` + Presentation *VipBenefitPresentation `protobuf:"bytes,15,opt,name=presentation,proto3" json:"presentation,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -17546,6 +17556,13 @@ func (x *VipBenefit) GetUpdatedAtMs() int64 { return 0 } +func (x *VipBenefit) GetPresentation() *VipBenefitPresentation { + if x != nil { + return x.Presentation + } + return nil +} + type VipLevel struct { state protoimpl.MessageState `protogen:"open.v1"` Level int32 `protobuf:"varint,1,opt,name=level,proto3" json:"level,omitempty"` @@ -17994,16 +18011,19 @@ func (x *VipTrialCard) GetUpdatedAtMs() int64 { } // VipUserSettings 是用户按 App 隔离的可关闭 VIP 功能偏好。没有持久记录时 wallet -// 仍返回两个默认开启值,updated_at_ms=0;设置本身不授予任何 VIP 权益。 +// 仍返回五个默认开启值,updated_at_ms=0;设置本身不授予任何 VIP 权益。 type VipUserSettings struct { - state protoimpl.MessageState `protogen:"open.v1"` - AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"` - UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - RoomEntryNoticeEnabled bool `protobuf:"varint,3,opt,name=room_entry_notice_enabled,json=roomEntryNoticeEnabled,proto3" json:"room_entry_notice_enabled,omitempty"` - OnlineGlobalNoticeEnabled bool `protobuf:"varint,4,opt,name=online_global_notice_enabled,json=onlineGlobalNoticeEnabled,proto3" json:"online_global_notice_enabled,omitempty"` - UpdatedAtMs int64 `protobuf:"varint,5,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"` + UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + RoomEntryNoticeEnabled bool `protobuf:"varint,3,opt,name=room_entry_notice_enabled,json=roomEntryNoticeEnabled,proto3" json:"room_entry_notice_enabled,omitempty"` + OnlineGlobalNoticeEnabled bool `protobuf:"varint,4,opt,name=online_global_notice_enabled,json=onlineGlobalNoticeEnabled,proto3" json:"online_global_notice_enabled,omitempty"` + UpdatedAtMs int64 `protobuf:"varint,5,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` + HideProfileDataEnabled bool `protobuf:"varint,6,opt,name=hide_profile_data_enabled,json=hideProfileDataEnabled,proto3" json:"hide_profile_data_enabled,omitempty"` + AnonymousProfileVisitEnabled bool `protobuf:"varint,7,opt,name=anonymous_profile_visit_enabled,json=anonymousProfileVisitEnabled,proto3" json:"anonymous_profile_visit_enabled,omitempty"` + LeaderboardInvisibleEnabled bool `protobuf:"varint,8,opt,name=leaderboard_invisible_enabled,json=leaderboardInvisibleEnabled,proto3" json:"leaderboard_invisible_enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *VipUserSettings) Reset() { @@ -18071,6 +18091,27 @@ func (x *VipUserSettings) GetUpdatedAtMs() int64 { return 0 } +func (x *VipUserSettings) GetHideProfileDataEnabled() bool { + if x != nil { + return x.HideProfileDataEnabled + } + return false +} + +func (x *VipUserSettings) GetAnonymousProfileVisitEnabled() bool { + if x != nil { + return x.AnonymousProfileVisitEnabled + } + return false +} + +func (x *VipUserSettings) GetLeaderboardInvisibleEnabled() bool { + if x != nil { + return x.LeaderboardInvisibleEnabled + } + return false +} + // VipState 同时返回付费会员、当前佩戴体验卡和最终生效会员,避免调用方自行合并两层状态。 // effective_benefits 是钱包按 paid/trial 来源、该等级显式矩阵和 trial_enabled 过滤后的资格事实; // 对可关闭通知,执行方还必须结合 user_settings 或调用 CheckVipBenefit。 @@ -18492,8 +18533,10 @@ type PurchaseVipResponse struct { CoinBalanceAfter int64 `protobuf:"varint,5,opt,name=coin_balance_after,json=coinBalanceAfter,proto3" json:"coin_balance_after,omitempty"` RewardItems []*VipRewardItem `protobuf:"bytes,6,rep,name=reward_items,json=rewardItems,proto3" json:"reward_items,omitempty"` State *VipState `protobuf:"bytes,7,opt,name=state,proto3" json:"state,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // coin_balance 是购买事务提交后的带版本余额快照;coin_balance_after 仅保留旧客户端兼容。 + CoinBalance *AssetBalance `protobuf:"bytes,8,opt,name=coin_balance,json=coinBalance,proto3" json:"coin_balance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PurchaseVipResponse) Reset() { @@ -18575,6 +18618,13 @@ func (x *PurchaseVipResponse) GetState() *VipState { return nil } +func (x *PurchaseVipResponse) GetCoinBalance() *AssetBalance { + if x != nil { + return x.CoinBalance + } + return nil +} + type DebitCPBreakupFeeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` CommandId string `protobuf:"bytes,1,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"` @@ -19283,14 +19333,17 @@ func (x *UpdateAdminVipProgramConfigResponse) GetServerTimeMs() int64 { // UpdateMyVipSettingsRequest 使用 optional 保留 PATCH 语义;显式 false 不能被当成未提交。 type UpdateMyVipSettingsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - AppCode string `protobuf:"bytes,2,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"` - UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` - RoomEntryNoticeEnabled *bool `protobuf:"varint,4,opt,name=room_entry_notice_enabled,json=roomEntryNoticeEnabled,proto3,oneof" json:"room_entry_notice_enabled,omitempty"` - OnlineGlobalNoticeEnabled *bool `protobuf:"varint,5,opt,name=online_global_notice_enabled,json=onlineGlobalNoticeEnabled,proto3,oneof" json:"online_global_notice_enabled,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + AppCode string `protobuf:"bytes,2,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"` + UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + RoomEntryNoticeEnabled *bool `protobuf:"varint,4,opt,name=room_entry_notice_enabled,json=roomEntryNoticeEnabled,proto3,oneof" json:"room_entry_notice_enabled,omitempty"` + OnlineGlobalNoticeEnabled *bool `protobuf:"varint,5,opt,name=online_global_notice_enabled,json=onlineGlobalNoticeEnabled,proto3,oneof" json:"online_global_notice_enabled,omitempty"` + HideProfileDataEnabled *bool `protobuf:"varint,6,opt,name=hide_profile_data_enabled,json=hideProfileDataEnabled,proto3,oneof" json:"hide_profile_data_enabled,omitempty"` + AnonymousProfileVisitEnabled *bool `protobuf:"varint,7,opt,name=anonymous_profile_visit_enabled,json=anonymousProfileVisitEnabled,proto3,oneof" json:"anonymous_profile_visit_enabled,omitempty"` + LeaderboardInvisibleEnabled *bool `protobuf:"varint,8,opt,name=leaderboard_invisible_enabled,json=leaderboardInvisibleEnabled,proto3,oneof" json:"leaderboard_invisible_enabled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpdateMyVipSettingsRequest) Reset() { @@ -19358,6 +19411,27 @@ func (x *UpdateMyVipSettingsRequest) GetOnlineGlobalNoticeEnabled() bool { return false } +func (x *UpdateMyVipSettingsRequest) GetHideProfileDataEnabled() bool { + if x != nil && x.HideProfileDataEnabled != nil { + return *x.HideProfileDataEnabled + } + return false +} + +func (x *UpdateMyVipSettingsRequest) GetAnonymousProfileVisitEnabled() bool { + if x != nil && x.AnonymousProfileVisitEnabled != nil { + return *x.AnonymousProfileVisitEnabled + } + return false +} + +func (x *UpdateMyVipSettingsRequest) GetLeaderboardInvisibleEnabled() bool { + if x != nil && x.LeaderboardInvisibleEnabled != nil { + return *x.LeaderboardInvisibleEnabled + } + return false +} + type UpdateMyVipSettingsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Settings *VipUserSettings `protobuf:"bytes,1,opt,name=settings,proto3" json:"settings,omitempty"` @@ -25782,6 +25856,371 @@ func (x *GetPointWithdrawalConfigResponse) GetPolicyInstanceCode() string { return "" } +// VipBenefitPreviewItem 是同一权益下的一项预览内容;独立稳定键允许 Flutter 在多图、动态资源间切换, +// 且不会把数组下标误当成资源身份。 +type VipBenefitPreviewItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + PreviewId string `protobuf:"bytes,1,opt,name=preview_id,json=previewId,proto3" json:"preview_id,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // media_type: image/animation;animation 使用 animation_url,静态兜底使用 preview_url/asset_url。 + MediaType string `protobuf:"bytes,3,opt,name=media_type,json=mediaType,proto3" json:"media_type,omitempty"` + AssetUrl string `protobuf:"bytes,4,opt,name=asset_url,json=assetUrl,proto3" json:"asset_url,omitempty"` + PreviewUrl string `protobuf:"bytes,5,opt,name=preview_url,json=previewUrl,proto3" json:"preview_url,omitempty"` + AnimationUrl string `protobuf:"bytes,6,opt,name=animation_url,json=animationUrl,proto3" json:"animation_url,omitempty"` + // composition_type: none/user_avatar_center/user_avatar_waveform,客户端据此把当前用户头像合成到预览中。 + CompositionType string `protobuf:"bytes,7,opt,name=composition_type,json=compositionType,proto3" json:"composition_type,omitempty"` + SortOrder int32 `protobuf:"varint,8,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VipBenefitPreviewItem) Reset() { + *x = VipBenefitPreviewItem{} + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[295] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VipBenefitPreviewItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VipBenefitPreviewItem) ProtoMessage() {} + +func (x *VipBenefitPreviewItem) ProtoReflect() protoreflect.Message { + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[295] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VipBenefitPreviewItem.ProtoReflect.Descriptor instead. +func (*VipBenefitPreviewItem) Descriptor() ([]byte, []int) { + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{295} +} + +func (x *VipBenefitPreviewItem) GetPreviewId() string { + if x != nil { + return x.PreviewId + } + return "" +} + +func (x *VipBenefitPreviewItem) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *VipBenefitPreviewItem) GetMediaType() string { + if x != nil { + return x.MediaType + } + return "" +} + +func (x *VipBenefitPreviewItem) GetAssetUrl() string { + if x != nil { + return x.AssetUrl + } + return "" +} + +func (x *VipBenefitPreviewItem) GetPreviewUrl() string { + if x != nil { + return x.PreviewUrl + } + return "" +} + +func (x *VipBenefitPreviewItem) GetAnimationUrl() string { + if x != nil { + return x.AnimationUrl + } + return "" +} + +func (x *VipBenefitPreviewItem) GetCompositionType() string { + if x != nil { + return x.CompositionType + } + return "" +} + +func (x *VipBenefitPreviewItem) GetSortOrder() int32 { + if x != nil { + return x.SortOrder + } + return 0 +} + +type VipBenefitNumericReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + Label string `protobuf:"bytes,1,opt,name=label,proto3" json:"label,omitempty"` + Value int64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"` + Unit string `protobuf:"bytes,3,opt,name=unit,proto3" json:"unit,omitempty"` + // period: once/daily/monthly,由服务端配置返回,客户端只展示不据此结算。 + Period string `protobuf:"bytes,4,opt,name=period,proto3" json:"period,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VipBenefitNumericReward) Reset() { + *x = VipBenefitNumericReward{} + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[296] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VipBenefitNumericReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VipBenefitNumericReward) ProtoMessage() {} + +func (x *VipBenefitNumericReward) ProtoReflect() protoreflect.Message { + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[296] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VipBenefitNumericReward.ProtoReflect.Descriptor instead. +func (*VipBenefitNumericReward) Descriptor() ([]byte, []int) { + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{296} +} + +func (x *VipBenefitNumericReward) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *VipBenefitNumericReward) GetValue() int64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *VipBenefitNumericReward) GetUnit() string { + if x != nil { + return x.Unit + } + return "" +} + +func (x *VipBenefitNumericReward) GetPeriod() string { + if x != nil { + return x.Period + } + return "" +} + +// VipBenefitPresentation 是 Flutter 可直接渲染的结构化特权说明。metadata_json 继续保留后台扩展和 +// 历史兼容,但客户端不得按 benefit_code 自行解析 metadata_json 生成预览。 +type VipBenefitPresentation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Description string `protobuf:"bytes,1,opt,name=description,proto3" json:"description,omitempty"` + PreviewItems []*VipBenefitPreviewItem `protobuf:"bytes,2,rep,name=preview_items,json=previewItems,proto3" json:"preview_items,omitempty"` + NumericReward *VipBenefitNumericReward `protobuf:"bytes,3,opt,name=numeric_reward,json=numericReward,proto3" json:"numeric_reward,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VipBenefitPresentation) Reset() { + *x = VipBenefitPresentation{} + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[297] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VipBenefitPresentation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VipBenefitPresentation) ProtoMessage() {} + +func (x *VipBenefitPresentation) ProtoReflect() protoreflect.Message { + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[297] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VipBenefitPresentation.ProtoReflect.Descriptor instead. +func (*VipBenefitPresentation) Descriptor() ([]byte, []int) { + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{297} +} + +func (x *VipBenefitPresentation) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *VipBenefitPresentation) GetPreviewItems() []*VipBenefitPreviewItem { + if x != nil { + return x.PreviewItems + } + return nil +} + +func (x *VipBenefitPresentation) GetNumericReward() *VipBenefitNumericReward { + if x != nil { + return x.NumericReward + } + return nil +} + +// RevokeUserResourceRequest targets one entitlement instead of its source grant. This keeps +// resource-group siblings intact when an operator removes a single badge/frame from a user. +type RevokeUserResourceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + AppCode string `protobuf:"bytes,2,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"` + UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + EntitlementId string `protobuf:"bytes,4,opt,name=entitlement_id,json=entitlementId,proto3" json:"entitlement_id,omitempty"` + Reason string `protobuf:"bytes,5,opt,name=reason,proto3" json:"reason,omitempty"` + OperatorUserId int64 `protobuf:"varint,6,opt,name=operator_user_id,json=operatorUserId,proto3" json:"operator_user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeUserResourceRequest) Reset() { + *x = RevokeUserResourceRequest{} + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[298] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeUserResourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeUserResourceRequest) ProtoMessage() {} + +func (x *RevokeUserResourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[298] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeUserResourceRequest.ProtoReflect.Descriptor instead. +func (*RevokeUserResourceRequest) Descriptor() ([]byte, []int) { + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{298} +} + +func (x *RevokeUserResourceRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *RevokeUserResourceRequest) GetAppCode() string { + if x != nil { + return x.AppCode + } + return "" +} + +func (x *RevokeUserResourceRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *RevokeUserResourceRequest) GetEntitlementId() string { + if x != nil { + return x.EntitlementId + } + return "" +} + +func (x *RevokeUserResourceRequest) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *RevokeUserResourceRequest) GetOperatorUserId() int64 { + if x != nil { + return x.OperatorUserId + } + return 0 +} + +type RevokeUserResourceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Resource *UserResourceEntitlement `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeUserResourceResponse) Reset() { + *x = RevokeUserResourceResponse{} + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[299] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeUserResourceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeUserResourceResponse) ProtoMessage() {} + +func (x *RevokeUserResourceResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_wallet_v1_wallet_proto_msgTypes[299] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeUserResourceResponse.ProtoReflect.Descriptor instead. +func (*RevokeUserResourceResponse) Descriptor() ([]byte, []int) { + return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{299} +} + +func (x *RevokeUserResourceResponse) GetResource() *UserResourceEntitlement { + if x != nil { + return x.Resource + } + return nil +} + var File_proto_wallet_v1_wallet_proto protoreflect.FileDescriptor const file_proto_wallet_v1_wallet_proto_rawDesc = "" + @@ -26843,7 +27282,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\tpage_size\x18\x06 \x01(\x05R\bpageSize\"j\n" + "\x1aListResourceGrantsResponse\x126\n" + "\x06grants\x18\x01 \x03(\v2\x1e.hyapp.wallet.v1.ResourceGrantR\x06grants\x12\x14\n" + - "\x05total\x18\x02 \x01(\x03R\x05total\"\x8a\x02\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\"\xa9\x02\n" + "\x15ResourceShopItemInput\x12 \n" + "\fshop_item_id\x18\x01 \x01(\x03R\n" + "shopItemId\x12\x1f\n" + @@ -26854,7 +27293,9 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\x11effective_from_ms\x18\x05 \x01(\x03R\x0feffectiveFromMs\x12&\n" + "\x0feffective_to_ms\x18\x06 \x01(\x03R\reffectiveToMs\x12\x1d\n" + "\n" + - "sort_order\x18\a \x01(\x05R\tsortOrder\"\x81\x02\n" + + "sort_order\x18\a \x01(\x05R\tsortOrder\x12\x1d\n" + + "\n" + + "coin_price\x18\b \x01(\x03R\tcoinPrice\"\x81\x02\n" + "\x1cListResourceShopItemsRequest\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" + @@ -27572,7 +28013,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\x0econfig_version\x18\v \x01(\x03R\rconfigVersion\x12-\n" + "\x13updated_by_admin_id\x18\f \x01(\x03R\x10updatedByAdminId\x12\"\n" + "\rcreated_at_ms\x18\r \x01(\x03R\vcreatedAtMs\x12\"\n" + - "\rupdated_at_ms\x18\x0e \x01(\x03R\vupdatedAtMs\"\xe0\x03\n" + + "\rupdated_at_ms\x18\x0e \x01(\x03R\vupdatedAtMs\"\xad\x04\n" + "\n" + "VipBenefit\x12!\n" + "\fbenefit_code\x18\x01 \x01(\tR\vbenefitCode\x12\x12\n" + @@ -27592,7 +28033,8 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "sort_order\x18\v \x01(\x05R\tsortOrder\x12#\n" + "\rmetadata_json\x18\f \x01(\tR\fmetadataJson\x12\"\n" + "\rcreated_at_ms\x18\r \x01(\x03R\vcreatedAtMs\x12\"\n" + - "\rupdated_at_ms\x18\x0e \x01(\x03R\vupdatedAtMs\"\xdc\x05\n" + + "\rupdated_at_ms\x18\x0e \x01(\x03R\vupdatedAtMs\x12K\n" + + "\fpresentation\x18\x0f \x01(\v2'.hyapp.wallet.v1.VipBenefitPresentationR\fpresentation\"\xdc\x05\n" + "\bVipLevel\x12\x14\n" + "\x05level\x18\x01 \x01(\x05R\x05level\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" + @@ -27644,13 +28086,16 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\fgrant_source\x18\r \x01(\tR\vgrantSource\x12&\n" + "\x0fsource_grant_id\x18\x0e \x01(\tR\rsourceGrantId\x12\"\n" + "\rcreated_at_ms\x18\x0f \x01(\x03R\vcreatedAtMs\x12\"\n" + - "\rupdated_at_ms\x18\x10 \x01(\x03R\vupdatedAtMs\"\xe5\x01\n" + + "\rupdated_at_ms\x18\x10 \x01(\x03R\vupdatedAtMs\"\xab\x03\n" + "\x0fVipUserSettings\x12\x19\n" + "\bapp_code\x18\x01 \x01(\tR\aappCode\x12\x17\n" + "\auser_id\x18\x02 \x01(\x03R\x06userId\x129\n" + "\x19room_entry_notice_enabled\x18\x03 \x01(\bR\x16roomEntryNoticeEnabled\x12?\n" + "\x1conline_global_notice_enabled\x18\x04 \x01(\bR\x19onlineGlobalNoticeEnabled\x12\"\n" + - "\rupdated_at_ms\x18\x05 \x01(\x03R\vupdatedAtMs\"\xfd\x03\n" + + "\rupdated_at_ms\x18\x05 \x01(\x03R\vupdatedAtMs\x129\n" + + "\x19hide_profile_data_enabled\x18\x06 \x01(\bR\x16hideProfileDataEnabled\x12E\n" + + "\x1fanonymous_profile_visit_enabled\x18\a \x01(\bR\x1canonymousProfileVisitEnabled\x12B\n" + + "\x1dleaderboard_invisible_enabled\x18\b \x01(\bR\x1bleaderboardInvisibleEnabled\"\xfd\x03\n" + "\bVipState\x123\n" + "\bpaid_vip\x18\x01 \x01(\v2\x18.hyapp.wallet.v1.UserVipR\apaidVip\x12M\n" + "\x13equipped_trial_card\x18\x02 \x01(\v2\x1d.hyapp.wallet.v1.VipTrialCardR\x11equippedTrialCard\x12=\n" + @@ -27684,7 +28129,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "command_id\x18\x01 \x01(\tR\tcommandId\x12\x19\n" + "\bapp_code\x18\x02 \x01(\tR\aappCode\x12\x17\n" + "\auser_id\x18\x03 \x01(\x03R\x06userId\x12\x14\n" + - "\x05level\x18\x04 \x01(\x05R\x05level\"\xc4\x02\n" + + "\x05level\x18\x04 \x01(\x05R\x05level\"\x86\x03\n" + "\x13PurchaseVipResponse\x12\x19\n" + "\border_id\x18\x01 \x01(\tR\aorderId\x12%\n" + "\x0etransaction_id\x18\x02 \x01(\tR\rtransactionId\x12*\n" + @@ -27693,7 +28138,8 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "coin_spent\x18\x04 \x01(\x03R\tcoinSpent\x12,\n" + "\x12coin_balance_after\x18\x05 \x01(\x03R\x10coinBalanceAfter\x12A\n" + "\freward_items\x18\x06 \x03(\v2\x1e.hyapp.wallet.v1.VipRewardItemR\vrewardItems\x12/\n" + - "\x05state\x18\a \x01(\v2\x19.hyapp.wallet.v1.VipStateR\x05state\"\xd3\x01\n" + + "\x05state\x18\a \x01(\v2\x19.hyapp.wallet.v1.VipStateR\x05state\x12@\n" + + "\fcoin_balance\x18\b \x01(\v2\x1d.hyapp.wallet.v1.AssetBalanceR\vcoinBalance\"\xd3\x01\n" + "\x18DebitCPBreakupFeeRequest\x12\x1d\n" + "\n" + "command_id\x18\x01 \x01(\tR\tcommandId\x12\x19\n" + @@ -27754,16 +28200,22 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\x10operator_user_id\x18\x04 \x01(\x03R\x0eoperatorUserId\"\x86\x01\n" + "#UpdateAdminVipProgramConfigResponse\x129\n" + "\x06config\x18\x01 \x01(\v2!.hyapp.wallet.v1.VipProgramConfigR\x06config\x12$\n" + - "\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\xb4\x02\n" + + "\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\xed\x04\n" + "\x1aUpdateMyVipSettingsRequest\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" + "\bapp_code\x18\x02 \x01(\tR\aappCode\x12\x17\n" + "\auser_id\x18\x03 \x01(\x03R\x06userId\x12>\n" + "\x19room_entry_notice_enabled\x18\x04 \x01(\bH\x00R\x16roomEntryNoticeEnabled\x88\x01\x01\x12D\n" + - "\x1conline_global_notice_enabled\x18\x05 \x01(\bH\x01R\x19onlineGlobalNoticeEnabled\x88\x01\x01B\x1c\n" + + "\x1conline_global_notice_enabled\x18\x05 \x01(\bH\x01R\x19onlineGlobalNoticeEnabled\x88\x01\x01\x12>\n" + + "\x19hide_profile_data_enabled\x18\x06 \x01(\bH\x02R\x16hideProfileDataEnabled\x88\x01\x01\x12J\n" + + "\x1fanonymous_profile_visit_enabled\x18\a \x01(\bH\x03R\x1canonymousProfileVisitEnabled\x88\x01\x01\x12G\n" + + "\x1dleaderboard_invisible_enabled\x18\b \x01(\bH\x04R\x1bleaderboardInvisibleEnabled\x88\x01\x01B\x1c\n" + "\x1a_room_entry_notice_enabledB\x1f\n" + - "\x1d_online_global_notice_enabled\"\x81\x01\n" + + "\x1d_online_global_notice_enabledB\x1c\n" + + "\x1a_hide_profile_data_enabledB\"\n" + + " _anonymous_profile_visit_enabledB \n" + + "\x1e_leaderboard_invisible_enabled\"\x81\x01\n" + "\x1bUpdateMyVipSettingsResponse\x12<\n" + "\bsettings\x18\x01 \x01(\v2 .hyapp.wallet.v1.VipUserSettingsR\bsettings\x12$\n" + "\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\xd2\x03\n" + @@ -28370,12 +28822,44 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\x0epoints_per_usd\x18\x02 \x01(\x03R\fpointsPerUsd\x12\x17\n" + "\afee_bps\x18\x03 \x01(\x05R\x06feeBps\x12%\n" + "\x0eminimum_points\x18\x04 \x01(\x03R\rminimumPoints\x120\n" + - "\x14policy_instance_code\x18\x05 \x01(\tR\x12policyInstanceCode2\xf4\x03\n" + + "\x14policy_instance_code\x18\x05 \x01(\tR\x12policyInstanceCode\"\x98\x02\n" + + "\x15VipBenefitPreviewItem\x12\x1d\n" + + "\n" + + "preview_id\x18\x01 \x01(\tR\tpreviewId\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12\x1d\n" + + "\n" + + "media_type\x18\x03 \x01(\tR\tmediaType\x12\x1b\n" + + "\tasset_url\x18\x04 \x01(\tR\bassetUrl\x12\x1f\n" + + "\vpreview_url\x18\x05 \x01(\tR\n" + + "previewUrl\x12#\n" + + "\ranimation_url\x18\x06 \x01(\tR\fanimationUrl\x12)\n" + + "\x10composition_type\x18\a \x01(\tR\x0fcompositionType\x12\x1d\n" + + "\n" + + "sort_order\x18\b \x01(\x05R\tsortOrder\"q\n" + + "\x17VipBenefitNumericReward\x12\x14\n" + + "\x05label\x18\x01 \x01(\tR\x05label\x12\x14\n" + + "\x05value\x18\x02 \x01(\x03R\x05value\x12\x12\n" + + "\x04unit\x18\x03 \x01(\tR\x04unit\x12\x16\n" + + "\x06period\x18\x04 \x01(\tR\x06period\"\xd8\x01\n" + + "\x16VipBenefitPresentation\x12 \n" + + "\vdescription\x18\x01 \x01(\tR\vdescription\x12K\n" + + "\rpreview_items\x18\x02 \x03(\v2&.hyapp.wallet.v1.VipBenefitPreviewItemR\fpreviewItems\x12O\n" + + "\x0enumeric_reward\x18\x03 \x01(\v2(.hyapp.wallet.v1.VipBenefitNumericRewardR\rnumericReward\"\xd7\x01\n" + + "\x19RevokeUserResourceRequest\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" + + "\bapp_code\x18\x02 \x01(\tR\aappCode\x12\x17\n" + + "\auser_id\x18\x03 \x01(\x03R\x06userId\x12%\n" + + "\x0eentitlement_id\x18\x04 \x01(\tR\rentitlementId\x12\x16\n" + + "\x06reason\x18\x05 \x01(\tR\x06reason\x12(\n" + + "\x10operator_user_id\x18\x06 \x01(\x03R\x0eoperatorUserId\"b\n" + + "\x1aRevokeUserResourceResponse\x12D\n" + + "\bresource\x18\x01 \x01(\v2(.hyapp.wallet.v1.UserResourceEntitlementR\bresource2\xf4\x03\n" + "\x11WalletCronService\x12n\n" + "%ProcessHostSalaryDailySettlementBatch\x12!.hyapp.wallet.v1.CronBatchRequest\x1a\".hyapp.wallet.v1.CronBatchResponse\x12r\n" + ")ProcessHostSalaryHalfMonthSettlementBatch\x12!.hyapp.wallet.v1.CronBatchRequest\x1a\".hyapp.wallet.v1.CronBatchResponse\x12g\n" + "\x1eProcessHostSalaryMonthEndBatch\x12!.hyapp.wallet.v1.CronBatchRequest\x1a\".hyapp.wallet.v1.CronBatchResponse\x12\x91\x01\n" + - "\x1eProcessVipDailyCoinRebateBatch\x126.hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchRequest\x1a7.hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse2\xe0r\n" + + "\x1eProcessVipDailyCoinRebateBatch\x126.hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchRequest\x1a7.hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse2\xcfs\n" + "\rWalletService\x12R\n" + "\tDebitGift\x12!.hyapp.wallet.v1.DebitGiftRequest\x1a\".hyapp.wallet.v1.DebitGiftResponse\x12^\n" + "\x0fDebitDirectGift\x12'.hyapp.wallet.v1.DebitDirectGiftRequest\x1a\".hyapp.wallet.v1.DebitGiftResponse\x12a\n" + @@ -28429,7 +28913,8 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\rGrantResource\x12%.hyapp.wallet.v1.GrantResourceRequest\x1a&.hyapp.wallet.v1.ResourceGrantResponse\x12h\n" + "\x12GrantResourceGroup\x12*.hyapp.wallet.v1.GrantResourceGroupRequest\x1a&.hyapp.wallet.v1.ResourceGrantResponse\x12t\n" + "\x18GrantPinnedResourceGroup\x120.hyapp.wallet.v1.GrantPinnedResourceGroupRequest\x1a&.hyapp.wallet.v1.ResourceGrantResponse\x12j\n" + - "\x13RevokeResourceGrant\x12+.hyapp.wallet.v1.RevokeResourceGrantRequest\x1a&.hyapp.wallet.v1.ResourceGrantResponse\x12j\n" + + "\x13RevokeResourceGrant\x12+.hyapp.wallet.v1.RevokeResourceGrantRequest\x1a&.hyapp.wallet.v1.ResourceGrantResponse\x12m\n" + + "\x12RevokeUserResource\x12*.hyapp.wallet.v1.RevokeUserResourceRequest\x1a+.hyapp.wallet.v1.RevokeUserResourceResponse\x12j\n" + "\x11ListUserResources\x12).hyapp.wallet.v1.ListUserResourcesRequest\x1a*.hyapp.wallet.v1.ListUserResourcesResponse\x12j\n" + "\x11EquipUserResource\x12).hyapp.wallet.v1.EquipUserResourceRequest\x1a*.hyapp.wallet.v1.EquipUserResourceResponse\x12p\n" + "\x13UnequipUserResource\x12+.hyapp.wallet.v1.UnequipUserResourceRequest\x1a,.hyapp.wallet.v1.UnequipUserResourceResponse\x12\x8e\x01\n" + @@ -28516,7 +29001,7 @@ func file_proto_wallet_v1_wallet_proto_rawDescGZIP() []byte { return file_proto_wallet_v1_wallet_proto_rawDescData } -var file_proto_wallet_v1_wallet_proto_msgTypes = make([]protoimpl.MessageInfo, 296) +var file_proto_wallet_v1_wallet_proto_msgTypes = make([]protoimpl.MessageInfo, 301) var file_proto_wallet_v1_wallet_proto_goTypes = []any{ (*DebitGiftRequest)(nil), // 0: hyapp.wallet.v1.DebitGiftRequest (*DebitDirectGiftRequest)(nil), // 1: hyapp.wallet.v1.DebitDirectGiftRequest @@ -28813,7 +29298,12 @@ var file_proto_wallet_v1_wallet_proto_goTypes = []any{ (*TransferPointToCoinSellerResponse)(nil), // 292: hyapp.wallet.v1.TransferPointToCoinSellerResponse (*GetPointWithdrawalConfigRequest)(nil), // 293: hyapp.wallet.v1.GetPointWithdrawalConfigRequest (*GetPointWithdrawalConfigResponse)(nil), // 294: hyapp.wallet.v1.GetPointWithdrawalConfigResponse - nil, // 295: hyapp.wallet.v1.HandleV5PayNotifyRequest.FieldsEntry + (*VipBenefitPreviewItem)(nil), // 295: hyapp.wallet.v1.VipBenefitPreviewItem + (*VipBenefitNumericReward)(nil), // 296: hyapp.wallet.v1.VipBenefitNumericReward + (*VipBenefitPresentation)(nil), // 297: hyapp.wallet.v1.VipBenefitPresentation + (*RevokeUserResourceRequest)(nil), // 298: hyapp.wallet.v1.RevokeUserResourceRequest + (*RevokeUserResourceResponse)(nil), // 299: hyapp.wallet.v1.RevokeUserResourceResponse + nil, // 300: hyapp.wallet.v1.HandleV5PayNotifyRequest.FieldsEntry } var file_proto_wallet_v1_wallet_proto_depIdxs = []int32{ 3, // 0: hyapp.wallet.v1.BatchDebitGiftRequest.targets:type_name -> hyapp.wallet.v1.DebitGiftTarget @@ -28898,340 +29388,347 @@ var file_proto_wallet_v1_wallet_proto_depIdxs = []int32{ 157, // 79: hyapp.wallet.v1.H5RechargeOptionsResponse.payment_methods:type_name -> hyapp.wallet.v1.ThirdPartyPaymentMethod 169, // 80: hyapp.wallet.v1.ListTemporaryRechargeOrdersResponse.orders:type_name -> hyapp.wallet.v1.ExternalRechargeOrder 169, // 81: hyapp.wallet.v1.H5RechargeOrderResponse.order:type_name -> hyapp.wallet.v1.ExternalRechargeOrder - 295, // 82: hyapp.wallet.v1.HandleV5PayNotifyRequest.fields:type_name -> hyapp.wallet.v1.HandleV5PayNotifyRequest.FieldsEntry + 300, // 82: hyapp.wallet.v1.HandleV5PayNotifyRequest.fields:type_name -> hyapp.wallet.v1.HandleV5PayNotifyRequest.FieldsEntry 184, // 83: hyapp.wallet.v1.GetDiamondExchangeConfigResponse.rules:type_name -> hyapp.wallet.v1.DiamondExchangeRule 187, // 84: hyapp.wallet.v1.ListWalletTransactionsResponse.transactions:type_name -> hyapp.wallet.v1.WalletTransaction - 190, // 85: hyapp.wallet.v1.VipLevel.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem - 192, // 86: hyapp.wallet.v1.VipLevel.benefits:type_name -> hyapp.wallet.v1.VipBenefit - 194, // 87: hyapp.wallet.v1.VipState.paid_vip:type_name -> hyapp.wallet.v1.UserVip - 195, // 88: hyapp.wallet.v1.VipState.equipped_trial_card:type_name -> hyapp.wallet.v1.VipTrialCard - 194, // 89: hyapp.wallet.v1.VipState.effective_vip:type_name -> hyapp.wallet.v1.UserVip - 192, // 90: hyapp.wallet.v1.VipState.effective_benefits:type_name -> hyapp.wallet.v1.VipBenefit - 191, // 91: hyapp.wallet.v1.VipState.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig - 196, // 92: hyapp.wallet.v1.VipState.user_settings:type_name -> hyapp.wallet.v1.VipUserSettings - 194, // 93: hyapp.wallet.v1.ListVipPackagesResponse.current_vip:type_name -> hyapp.wallet.v1.UserVip - 193, // 94: hyapp.wallet.v1.ListVipPackagesResponse.packages:type_name -> hyapp.wallet.v1.VipLevel - 197, // 95: hyapp.wallet.v1.ListVipPackagesResponse.state:type_name -> hyapp.wallet.v1.VipState - 191, // 96: hyapp.wallet.v1.ListVipPackagesResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig - 194, // 97: hyapp.wallet.v1.GetMyVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip - 197, // 98: hyapp.wallet.v1.GetMyVipResponse.state:type_name -> hyapp.wallet.v1.VipState - 194, // 99: hyapp.wallet.v1.PurchaseVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip - 190, // 100: hyapp.wallet.v1.PurchaseVipResponse.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem - 197, // 101: hyapp.wallet.v1.PurchaseVipResponse.state:type_name -> hyapp.wallet.v1.VipState - 8, // 102: hyapp.wallet.v1.DebitCPBreakupFeeResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 103: hyapp.wallet.v1.DebitWheelDrawResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 194, // 104: hyapp.wallet.v1.GrantVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip - 190, // 105: hyapp.wallet.v1.GrantVipResponse.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem - 197, // 106: hyapp.wallet.v1.GrantVipResponse.state:type_name -> hyapp.wallet.v1.VipState - 191, // 107: hyapp.wallet.v1.GetVipProgramConfigResponse.config:type_name -> hyapp.wallet.v1.VipProgramConfig - 192, // 108: hyapp.wallet.v1.GetVipProgramConfigResponse.benefits:type_name -> hyapp.wallet.v1.VipBenefit - 191, // 109: hyapp.wallet.v1.UpdateAdminVipProgramConfigRequest.config:type_name -> hyapp.wallet.v1.VipProgramConfig - 191, // 110: hyapp.wallet.v1.UpdateAdminVipProgramConfigResponse.config:type_name -> hyapp.wallet.v1.VipProgramConfig - 196, // 111: hyapp.wallet.v1.UpdateMyVipSettingsResponse.settings:type_name -> hyapp.wallet.v1.VipUserSettings - 216, // 112: hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse.run:type_name -> hyapp.wallet.v1.VipDailyCoinRebateRun - 217, // 113: hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateResponse.rebate:type_name -> hyapp.wallet.v1.VipDailyCoinRebate - 217, // 114: hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesResponse.rebates:type_name -> hyapp.wallet.v1.VipDailyCoinRebate - 217, // 115: hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse.rebate:type_name -> hyapp.wallet.v1.VipDailyCoinRebate - 8, // 116: hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse.coin_balance:type_name -> hyapp.wallet.v1.AssetBalance - 195, // 117: hyapp.wallet.v1.GrantVipTrialCardResponse.trial_card:type_name -> hyapp.wallet.v1.VipTrialCard - 197, // 118: hyapp.wallet.v1.GrantVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState - 195, // 119: hyapp.wallet.v1.EquipVipTrialCardResponse.trial_card:type_name -> hyapp.wallet.v1.VipTrialCard - 197, // 120: hyapp.wallet.v1.EquipVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState - 197, // 121: hyapp.wallet.v1.UnequipVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState - 192, // 122: hyapp.wallet.v1.CheckVipBenefitResponse.benefit:type_name -> hyapp.wallet.v1.VipBenefit - 197, // 123: hyapp.wallet.v1.CheckVipBenefitResponse.state:type_name -> hyapp.wallet.v1.VipState - 192, // 124: hyapp.wallet.v1.AdminVipLevelInput.benefits:type_name -> hyapp.wallet.v1.VipBenefit - 193, // 125: hyapp.wallet.v1.ListAdminVipLevelsResponse.levels:type_name -> hyapp.wallet.v1.VipLevel - 191, // 126: hyapp.wallet.v1.ListAdminVipLevelsResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig - 234, // 127: hyapp.wallet.v1.UpdateAdminVipLevelsRequest.levels:type_name -> hyapp.wallet.v1.AdminVipLevelInput - 193, // 128: hyapp.wallet.v1.UpdateAdminVipLevelsResponse.levels:type_name -> hyapp.wallet.v1.VipLevel - 191, // 129: hyapp.wallet.v1.UpdateAdminVipLevelsResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig - 8, // 130: hyapp.wallet.v1.CreditTaskRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 131: hyapp.wallet.v1.CreditLuckyGiftRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 132: hyapp.wallet.v1.CreditWheelRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 133: hyapp.wallet.v1.CreditRoomTurnoverRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 134: hyapp.wallet.v1.CreditInviteActivityRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 135: hyapp.wallet.v1.CreditAgencyOpeningRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 254, // 136: hyapp.wallet.v1.RedPacket.claims:type_name -> hyapp.wallet.v1.RedPacketClaim - 253, // 137: hyapp.wallet.v1.GetRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig - 253, // 138: hyapp.wallet.v1.UpdateRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig - 255, // 139: hyapp.wallet.v1.CreateRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket - 254, // 140: hyapp.wallet.v1.ClaimRedPacketResponse.claim:type_name -> hyapp.wallet.v1.RedPacketClaim - 255, // 141: hyapp.wallet.v1.ClaimRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket - 255, // 142: hyapp.wallet.v1.ListRedPacketsResponse.packets:type_name -> hyapp.wallet.v1.RedPacket - 255, // 143: hyapp.wallet.v1.GetRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket - 255, // 144: hyapp.wallet.v1.RetryRedPacketRefundResponse.packet:type_name -> hyapp.wallet.v1.RedPacket - 48, // 145: hyapp.wallet.v1.ResourceShopPurchaseOrder.resource:type_name -> hyapp.wallet.v1.Resource - 274, // 146: hyapp.wallet.v1.ListResourceShopPurchaseOrdersResponse.orders:type_name -> hyapp.wallet.v1.ResourceShopPurchaseOrder - 279, // 147: hyapp.wallet.v1.GetHostRevenueStatsResponse.stats:type_name -> hyapp.wallet.v1.HostRevenueStats - 282, // 148: hyapp.wallet.v1.GetAgencyPointShareStatsResponse.stats:type_name -> hyapp.wallet.v1.AgencyPointShareStats - 285, // 149: hyapp.wallet.v1.GetAgencyHostGiftStatsResponse.stats:type_name -> hyapp.wallet.v1.AgencyHostGiftStats - 288, // 150: hyapp.wallet.v1.ListPointWithdrawalCoinSellersResponse.sellers:type_name -> hyapp.wallet.v1.PointWithdrawalCoinSellerConfig - 272, // 151: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryDailySettlementBatch:input_type -> hyapp.wallet.v1.CronBatchRequest - 272, // 152: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryHalfMonthSettlementBatch:input_type -> hyapp.wallet.v1.CronBatchRequest - 272, // 153: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryMonthEndBatch:input_type -> hyapp.wallet.v1.CronBatchRequest - 218, // 154: hyapp.wallet.v1.WalletCronService.ProcessVipDailyCoinRebateBatch:input_type -> hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchRequest - 0, // 155: hyapp.wallet.v1.WalletService.DebitGift:input_type -> hyapp.wallet.v1.DebitGiftRequest - 1, // 156: hyapp.wallet.v1.WalletService.DebitDirectGift:input_type -> hyapp.wallet.v1.DebitDirectGiftRequest - 4, // 157: hyapp.wallet.v1.WalletService.BatchDebitGift:input_type -> hyapp.wallet.v1.BatchDebitGiftRequest - 7, // 158: hyapp.wallet.v1.WalletService.DebitRobotGift:input_type -> hyapp.wallet.v1.DebitRobotGiftRequest - 9, // 159: hyapp.wallet.v1.WalletService.GetBalances:input_type -> hyapp.wallet.v1.GetBalancesRequest - 13, // 160: hyapp.wallet.v1.WalletService.GetActiveHostSalaryPolicy:input_type -> hyapp.wallet.v1.GetActiveHostSalaryPolicyRequest - 16, // 161: hyapp.wallet.v1.WalletService.GetHostSalaryProgress:input_type -> hyapp.wallet.v1.GetHostSalaryProgressRequest - 280, // 162: hyapp.wallet.v1.WalletService.GetHostRevenueStats:input_type -> hyapp.wallet.v1.GetHostRevenueStatsRequest - 283, // 163: hyapp.wallet.v1.WalletService.GetAgencyPointShareStats:input_type -> hyapp.wallet.v1.GetAgencyPointShareStatsRequest - 286, // 164: hyapp.wallet.v1.WalletService.GetAgencyHostGiftStats:input_type -> hyapp.wallet.v1.GetAgencyHostGiftStatsRequest - 19, // 165: hyapp.wallet.v1.WalletService.GetTeamHostSalaryStats:input_type -> hyapp.wallet.v1.GetTeamHostSalaryStatsRequest - 21, // 166: hyapp.wallet.v1.WalletService.AdminCreditAsset:input_type -> hyapp.wallet.v1.AdminCreditAssetRequest - 23, // 167: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:input_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockRequest - 25, // 168: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:input_type -> hyapp.wallet.v1.TransferCoinFromSellerRequest - 27, // 169: hyapp.wallet.v1.WalletService.TransferCoinSellerStockToChild:input_type -> hyapp.wallet.v1.TransferCoinSellerStockToChildRequest - 30, // 170: hyapp.wallet.v1.WalletService.ListCoinSellerSalaryExchangeRateTiers:input_type -> hyapp.wallet.v1.ListCoinSellerSalaryExchangeRateTiersRequest - 32, // 171: hyapp.wallet.v1.WalletService.ExchangeSalaryToCoin:input_type -> hyapp.wallet.v1.ExchangeSalaryToCoinRequest - 34, // 172: hyapp.wallet.v1.WalletService.TransferSalaryToCoinSeller:input_type -> hyapp.wallet.v1.TransferSalaryToCoinSellerRequest - 289, // 173: hyapp.wallet.v1.WalletService.ListPointWithdrawalCoinSellers:input_type -> hyapp.wallet.v1.ListPointWithdrawalCoinSellersRequest - 291, // 174: hyapp.wallet.v1.WalletService.TransferPointToCoinSeller:input_type -> hyapp.wallet.v1.TransferPointToCoinSellerRequest - 293, // 175: hyapp.wallet.v1.WalletService.GetPointWithdrawalConfig:input_type -> hyapp.wallet.v1.GetPointWithdrawalConfigRequest - 36, // 176: hyapp.wallet.v1.WalletService.FreezeSalaryWithdrawal:input_type -> hyapp.wallet.v1.FreezeSalaryWithdrawalRequest - 38, // 177: hyapp.wallet.v1.WalletService.SettleSalaryWithdrawal:input_type -> hyapp.wallet.v1.SettleSalaryWithdrawalRequest - 40, // 178: hyapp.wallet.v1.WalletService.ReleaseSalaryWithdrawal:input_type -> hyapp.wallet.v1.ReleaseSalaryWithdrawalRequest - 42, // 179: hyapp.wallet.v1.WalletService.FreezePointWithdrawal:input_type -> hyapp.wallet.v1.FreezePointWithdrawalRequest - 44, // 180: hyapp.wallet.v1.WalletService.SettlePointWithdrawal:input_type -> hyapp.wallet.v1.SettlePointWithdrawalRequest - 46, // 181: hyapp.wallet.v1.WalletService.ReleasePointWithdrawal:input_type -> hyapp.wallet.v1.ReleasePointWithdrawalRequest - 61, // 182: hyapp.wallet.v1.WalletService.ListResources:input_type -> hyapp.wallet.v1.ListResourcesRequest - 63, // 183: hyapp.wallet.v1.WalletService.GetResource:input_type -> hyapp.wallet.v1.GetResourceRequest - 65, // 184: hyapp.wallet.v1.WalletService.CreateResource:input_type -> hyapp.wallet.v1.CreateResourceRequest - 66, // 185: hyapp.wallet.v1.WalletService.UpdateResource:input_type -> hyapp.wallet.v1.UpdateResourceRequest - 67, // 186: hyapp.wallet.v1.WalletService.SetResourceStatus:input_type -> hyapp.wallet.v1.SetResourceStatusRequest - 68, // 187: hyapp.wallet.v1.WalletService.DeleteResource:input_type -> hyapp.wallet.v1.DeleteResourceRequest - 69, // 188: hyapp.wallet.v1.WalletService.BatchDeleteResources:input_type -> hyapp.wallet.v1.BatchDeleteResourcesRequest - 72, // 189: hyapp.wallet.v1.WalletService.ListResourceGroups:input_type -> hyapp.wallet.v1.ListResourceGroupsRequest - 74, // 190: hyapp.wallet.v1.WalletService.GetResourceGroup:input_type -> hyapp.wallet.v1.GetResourceGroupRequest - 76, // 191: hyapp.wallet.v1.WalletService.CreateResourceGroup:input_type -> hyapp.wallet.v1.CreateResourceGroupRequest - 77, // 192: hyapp.wallet.v1.WalletService.UpdateResourceGroup:input_type -> hyapp.wallet.v1.UpdateResourceGroupRequest - 78, // 193: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:input_type -> hyapp.wallet.v1.SetResourceGroupStatusRequest - 52, // 194: hyapp.wallet.v1.WalletService.PinResourceGroupSnapshot:input_type -> hyapp.wallet.v1.PinResourceGroupSnapshotRequest - 80, // 195: hyapp.wallet.v1.WalletService.ListGiftConfigs:input_type -> hyapp.wallet.v1.ListGiftConfigsRequest - 82, // 196: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:input_type -> hyapp.wallet.v1.ListGiftTypeConfigsRequest - 277, // 197: hyapp.wallet.v1.WalletService.GetGiftCatalogVersion:input_type -> hyapp.wallet.v1.GetGiftCatalogVersionRequest - 86, // 198: hyapp.wallet.v1.WalletService.CreateGiftConfig:input_type -> hyapp.wallet.v1.CreateGiftConfigRequest - 87, // 199: hyapp.wallet.v1.WalletService.BatchCreateGiftConfigs:input_type -> hyapp.wallet.v1.BatchCreateGiftConfigsRequest - 89, // 200: hyapp.wallet.v1.WalletService.UpdateGiftConfig:input_type -> hyapp.wallet.v1.UpdateGiftConfigRequest - 90, // 201: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:input_type -> hyapp.wallet.v1.SetGiftConfigStatusRequest - 91, // 202: hyapp.wallet.v1.WalletService.DeleteGiftConfig:input_type -> hyapp.wallet.v1.DeleteGiftConfigRequest - 84, // 203: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:input_type -> hyapp.wallet.v1.UpsertGiftTypeConfigRequest - 93, // 204: hyapp.wallet.v1.WalletService.GrantResource:input_type -> hyapp.wallet.v1.GrantResourceRequest - 94, // 205: hyapp.wallet.v1.WalletService.GrantResourceGroup:input_type -> hyapp.wallet.v1.GrantResourceGroupRequest - 95, // 206: hyapp.wallet.v1.WalletService.GrantPinnedResourceGroup:input_type -> hyapp.wallet.v1.GrantPinnedResourceGroupRequest - 96, // 207: hyapp.wallet.v1.WalletService.RevokeResourceGrant:input_type -> hyapp.wallet.v1.RevokeResourceGrantRequest - 98, // 208: hyapp.wallet.v1.WalletService.ListUserResources:input_type -> hyapp.wallet.v1.ListUserResourcesRequest - 100, // 209: hyapp.wallet.v1.WalletService.EquipUserResource:input_type -> hyapp.wallet.v1.EquipUserResourceRequest - 102, // 210: hyapp.wallet.v1.WalletService.UnequipUserResource:input_type -> hyapp.wallet.v1.UnequipUserResourceRequest - 104, // 211: hyapp.wallet.v1.WalletService.BatchGetUserEquippedResources:input_type -> hyapp.wallet.v1.BatchGetUserEquippedResourcesRequest - 107, // 212: hyapp.wallet.v1.WalletService.ListResourceGrants:input_type -> hyapp.wallet.v1.ListResourceGrantsRequest - 110, // 213: hyapp.wallet.v1.WalletService.ListResourceShopItems:input_type -> hyapp.wallet.v1.ListResourceShopItemsRequest - 112, // 214: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:input_type -> hyapp.wallet.v1.UpsertResourceShopItemsRequest - 114, // 215: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:input_type -> hyapp.wallet.v1.SetResourceShopItemStatusRequest - 275, // 216: hyapp.wallet.v1.WalletService.ListResourceShopPurchaseOrders:input_type -> hyapp.wallet.v1.ListResourceShopPurchaseOrdersRequest - 116, // 217: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:input_type -> hyapp.wallet.v1.PurchaseResourceShopItemRequest - 119, // 218: hyapp.wallet.v1.WalletService.ListRechargeBills:input_type -> hyapp.wallet.v1.ListRechargeBillsRequest - 121, // 219: hyapp.wallet.v1.WalletService.GetRechargeBillSummary:input_type -> hyapp.wallet.v1.GetRechargeBillSummaryRequest - 125, // 220: hyapp.wallet.v1.WalletService.BatchGetUserRechargeStats:input_type -> hyapp.wallet.v1.BatchGetUserRechargeStatsRequest - 127, // 221: hyapp.wallet.v1.WalletService.GetRechargeBillOverview:input_type -> hyapp.wallet.v1.GetRechargeBillOverviewRequest - 133, // 222: hyapp.wallet.v1.WalletService.RefreshGooglePaymentPrices:input_type -> hyapp.wallet.v1.RefreshGooglePaymentPricesRequest - 137, // 223: hyapp.wallet.v1.WalletService.GetWalletOverview:input_type -> hyapp.wallet.v1.GetWalletOverviewRequest - 140, // 224: hyapp.wallet.v1.WalletService.GetWalletValueSummary:input_type -> hyapp.wallet.v1.GetWalletValueSummaryRequest - 143, // 225: hyapp.wallet.v1.WalletService.GetUserGiftWall:input_type -> hyapp.wallet.v1.GetUserGiftWallRequest - 146, // 226: hyapp.wallet.v1.WalletService.ListRechargeProducts:input_type -> hyapp.wallet.v1.ListRechargeProductsRequest - 148, // 227: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:input_type -> hyapp.wallet.v1.ConfirmGooglePaymentRequest - 150, // 228: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:input_type -> hyapp.wallet.v1.ListAdminRechargeProductsRequest - 152, // 229: hyapp.wallet.v1.WalletService.CreateRechargeProduct:input_type -> hyapp.wallet.v1.CreateRechargeProductRequest - 153, // 230: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:input_type -> hyapp.wallet.v1.UpdateRechargeProductRequest - 154, // 231: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:input_type -> hyapp.wallet.v1.DeleteRechargeProductRequest - 159, // 232: hyapp.wallet.v1.WalletService.ListThirdPartyPaymentChannels:input_type -> hyapp.wallet.v1.ListThirdPartyPaymentChannelsRequest - 161, // 233: hyapp.wallet.v1.WalletService.SetThirdPartyPaymentMethodStatus:input_type -> hyapp.wallet.v1.SetThirdPartyPaymentMethodStatusRequest - 163, // 234: hyapp.wallet.v1.WalletService.UpdateThirdPartyPaymentRate:input_type -> hyapp.wallet.v1.UpdateThirdPartyPaymentRateRequest - 165, // 235: hyapp.wallet.v1.WalletService.SyncThirdPartyPaymentMethods:input_type -> hyapp.wallet.v1.SyncThirdPartyPaymentMethodsRequest - 167, // 236: hyapp.wallet.v1.WalletService.ListH5RechargeOptions:input_type -> hyapp.wallet.v1.H5RechargeOptionsRequest - 170, // 237: hyapp.wallet.v1.WalletService.CreateH5RechargeOrder:input_type -> hyapp.wallet.v1.CreateH5RechargeOrderRequest - 171, // 238: hyapp.wallet.v1.WalletService.CreateTemporaryRechargeOrder:input_type -> hyapp.wallet.v1.CreateTemporaryRechargeOrderRequest - 172, // 239: hyapp.wallet.v1.WalletService.GetTemporaryRechargeOrder:input_type -> hyapp.wallet.v1.GetTemporaryRechargeOrderRequest - 173, // 240: hyapp.wallet.v1.WalletService.ListTemporaryRechargeOrders:input_type -> hyapp.wallet.v1.ListTemporaryRechargeOrdersRequest - 175, // 241: hyapp.wallet.v1.WalletService.SubmitH5RechargeTx:input_type -> hyapp.wallet.v1.SubmitH5RechargeTxRequest - 176, // 242: hyapp.wallet.v1.WalletService.GetH5RechargeOrder:input_type -> hyapp.wallet.v1.GetH5RechargeOrderRequest - 178, // 243: hyapp.wallet.v1.WalletService.VerifyCoinSellerRechargeReceipt:input_type -> hyapp.wallet.v1.VerifyCoinSellerRechargeReceiptRequest - 180, // 244: hyapp.wallet.v1.WalletService.HandleMifapayNotify:input_type -> hyapp.wallet.v1.HandleMifapayNotifyRequest - 182, // 245: hyapp.wallet.v1.WalletService.HandleV5PayNotify:input_type -> hyapp.wallet.v1.HandleV5PayNotifyRequest - 185, // 246: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:input_type -> hyapp.wallet.v1.GetDiamondExchangeConfigRequest - 188, // 247: hyapp.wallet.v1.WalletService.ListWalletTransactions:input_type -> hyapp.wallet.v1.ListWalletTransactionsRequest - 198, // 248: hyapp.wallet.v1.WalletService.ListVipPackages:input_type -> hyapp.wallet.v1.ListVipPackagesRequest - 210, // 249: hyapp.wallet.v1.WalletService.GetVipProgramConfig:input_type -> hyapp.wallet.v1.GetVipProgramConfigRequest - 200, // 250: hyapp.wallet.v1.WalletService.GetMyVip:input_type -> hyapp.wallet.v1.GetMyVipRequest - 232, // 251: hyapp.wallet.v1.WalletService.CheckVipBenefit:input_type -> hyapp.wallet.v1.CheckVipBenefitRequest - 202, // 252: hyapp.wallet.v1.WalletService.PurchaseVip:input_type -> hyapp.wallet.v1.PurchaseVipRequest - 204, // 253: hyapp.wallet.v1.WalletService.DebitCPBreakupFee:input_type -> hyapp.wallet.v1.DebitCPBreakupFeeRequest - 206, // 254: hyapp.wallet.v1.WalletService.DebitWheelDraw:input_type -> hyapp.wallet.v1.DebitWheelDrawRequest - 208, // 255: hyapp.wallet.v1.WalletService.GrantVip:input_type -> hyapp.wallet.v1.GrantVipRequest - 226, // 256: hyapp.wallet.v1.WalletService.GrantVipTrialCard:input_type -> hyapp.wallet.v1.GrantVipTrialCardRequest - 228, // 257: hyapp.wallet.v1.WalletService.EquipVipTrialCard:input_type -> hyapp.wallet.v1.EquipVipTrialCardRequest - 230, // 258: hyapp.wallet.v1.WalletService.UnequipVipTrialCard:input_type -> hyapp.wallet.v1.UnequipVipTrialCardRequest - 235, // 259: hyapp.wallet.v1.WalletService.ListAdminVipLevels:input_type -> hyapp.wallet.v1.ListAdminVipLevelsRequest - 237, // 260: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:input_type -> hyapp.wallet.v1.UpdateAdminVipLevelsRequest - 212, // 261: hyapp.wallet.v1.WalletService.UpdateAdminVipProgramConfig:input_type -> hyapp.wallet.v1.UpdateAdminVipProgramConfigRequest - 214, // 262: hyapp.wallet.v1.WalletService.UpdateMyVipSettings:input_type -> hyapp.wallet.v1.UpdateMyVipSettingsRequest - 220, // 263: hyapp.wallet.v1.WalletService.GetMyCurrentVipDailyCoinRebate:input_type -> hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateRequest - 222, // 264: hyapp.wallet.v1.WalletService.ListMyVipDailyCoinRebateStatuses:input_type -> hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesRequest - 224, // 265: hyapp.wallet.v1.WalletService.ClaimVipDailyCoinRebate:input_type -> hyapp.wallet.v1.ClaimVipDailyCoinRebateRequest - 239, // 266: hyapp.wallet.v1.WalletService.CreditTaskReward:input_type -> hyapp.wallet.v1.CreditTaskRewardRequest - 241, // 267: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:input_type -> hyapp.wallet.v1.CreditLuckyGiftRewardRequest - 243, // 268: hyapp.wallet.v1.WalletService.CreditWheelReward:input_type -> hyapp.wallet.v1.CreditWheelRewardRequest - 245, // 269: hyapp.wallet.v1.WalletService.CreditRoomTurnoverReward:input_type -> hyapp.wallet.v1.CreditRoomTurnoverRewardRequest - 247, // 270: hyapp.wallet.v1.WalletService.CreditInviteActivityReward:input_type -> hyapp.wallet.v1.CreditInviteActivityRewardRequest - 249, // 271: hyapp.wallet.v1.WalletService.CreditAgencyOpeningReward:input_type -> hyapp.wallet.v1.CreditAgencyOpeningRewardRequest - 251, // 272: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:input_type -> hyapp.wallet.v1.ApplyGameCoinChangeRequest - 256, // 273: hyapp.wallet.v1.WalletService.GetRedPacketConfig:input_type -> hyapp.wallet.v1.GetRedPacketConfigRequest - 258, // 274: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:input_type -> hyapp.wallet.v1.UpdateRedPacketConfigRequest - 260, // 275: hyapp.wallet.v1.WalletService.CreateRedPacket:input_type -> hyapp.wallet.v1.CreateRedPacketRequest - 262, // 276: hyapp.wallet.v1.WalletService.ClaimRedPacket:input_type -> hyapp.wallet.v1.ClaimRedPacketRequest - 264, // 277: hyapp.wallet.v1.WalletService.ListRedPackets:input_type -> hyapp.wallet.v1.ListRedPacketsRequest - 266, // 278: hyapp.wallet.v1.WalletService.GetRedPacket:input_type -> hyapp.wallet.v1.GetRedPacketRequest - 268, // 279: hyapp.wallet.v1.WalletService.ExpireRedPackets:input_type -> hyapp.wallet.v1.ExpireRedPacketsRequest - 270, // 280: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:input_type -> hyapp.wallet.v1.RetryRedPacketRefundRequest - 273, // 281: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryDailySettlementBatch:output_type -> hyapp.wallet.v1.CronBatchResponse - 273, // 282: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryHalfMonthSettlementBatch:output_type -> hyapp.wallet.v1.CronBatchResponse - 273, // 283: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryMonthEndBatch:output_type -> hyapp.wallet.v1.CronBatchResponse - 219, // 284: hyapp.wallet.v1.WalletCronService.ProcessVipDailyCoinRebateBatch:output_type -> hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse - 2, // 285: hyapp.wallet.v1.WalletService.DebitGift:output_type -> hyapp.wallet.v1.DebitGiftResponse - 2, // 286: hyapp.wallet.v1.WalletService.DebitDirectGift:output_type -> hyapp.wallet.v1.DebitGiftResponse - 6, // 287: hyapp.wallet.v1.WalletService.BatchDebitGift:output_type -> hyapp.wallet.v1.BatchDebitGiftResponse - 2, // 288: hyapp.wallet.v1.WalletService.DebitRobotGift:output_type -> hyapp.wallet.v1.DebitGiftResponse - 10, // 289: hyapp.wallet.v1.WalletService.GetBalances:output_type -> hyapp.wallet.v1.GetBalancesResponse - 14, // 290: hyapp.wallet.v1.WalletService.GetActiveHostSalaryPolicy:output_type -> hyapp.wallet.v1.GetActiveHostSalaryPolicyResponse - 17, // 291: hyapp.wallet.v1.WalletService.GetHostSalaryProgress:output_type -> hyapp.wallet.v1.GetHostSalaryProgressResponse - 281, // 292: hyapp.wallet.v1.WalletService.GetHostRevenueStats:output_type -> hyapp.wallet.v1.GetHostRevenueStatsResponse - 284, // 293: hyapp.wallet.v1.WalletService.GetAgencyPointShareStats:output_type -> hyapp.wallet.v1.GetAgencyPointShareStatsResponse - 287, // 294: hyapp.wallet.v1.WalletService.GetAgencyHostGiftStats:output_type -> hyapp.wallet.v1.GetAgencyHostGiftStatsResponse - 20, // 295: hyapp.wallet.v1.WalletService.GetTeamHostSalaryStats:output_type -> hyapp.wallet.v1.GetTeamHostSalaryStatsResponse - 22, // 296: hyapp.wallet.v1.WalletService.AdminCreditAsset:output_type -> hyapp.wallet.v1.AdminCreditAssetResponse - 24, // 297: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:output_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockResponse - 26, // 298: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:output_type -> hyapp.wallet.v1.TransferCoinFromSellerResponse - 28, // 299: hyapp.wallet.v1.WalletService.TransferCoinSellerStockToChild:output_type -> hyapp.wallet.v1.TransferCoinSellerStockToChildResponse - 31, // 300: hyapp.wallet.v1.WalletService.ListCoinSellerSalaryExchangeRateTiers:output_type -> hyapp.wallet.v1.ListCoinSellerSalaryExchangeRateTiersResponse - 33, // 301: hyapp.wallet.v1.WalletService.ExchangeSalaryToCoin:output_type -> hyapp.wallet.v1.ExchangeSalaryToCoinResponse - 35, // 302: hyapp.wallet.v1.WalletService.TransferSalaryToCoinSeller:output_type -> hyapp.wallet.v1.TransferSalaryToCoinSellerResponse - 290, // 303: hyapp.wallet.v1.WalletService.ListPointWithdrawalCoinSellers:output_type -> hyapp.wallet.v1.ListPointWithdrawalCoinSellersResponse - 292, // 304: hyapp.wallet.v1.WalletService.TransferPointToCoinSeller:output_type -> hyapp.wallet.v1.TransferPointToCoinSellerResponse - 294, // 305: hyapp.wallet.v1.WalletService.GetPointWithdrawalConfig:output_type -> hyapp.wallet.v1.GetPointWithdrawalConfigResponse - 37, // 306: hyapp.wallet.v1.WalletService.FreezeSalaryWithdrawal:output_type -> hyapp.wallet.v1.FreezeSalaryWithdrawalResponse - 39, // 307: hyapp.wallet.v1.WalletService.SettleSalaryWithdrawal:output_type -> hyapp.wallet.v1.SettleSalaryWithdrawalResponse - 41, // 308: hyapp.wallet.v1.WalletService.ReleaseSalaryWithdrawal:output_type -> hyapp.wallet.v1.ReleaseSalaryWithdrawalResponse - 43, // 309: hyapp.wallet.v1.WalletService.FreezePointWithdrawal:output_type -> hyapp.wallet.v1.FreezePointWithdrawalResponse - 45, // 310: hyapp.wallet.v1.WalletService.SettlePointWithdrawal:output_type -> hyapp.wallet.v1.SettlePointWithdrawalResponse - 47, // 311: hyapp.wallet.v1.WalletService.ReleasePointWithdrawal:output_type -> hyapp.wallet.v1.ReleasePointWithdrawalResponse - 62, // 312: hyapp.wallet.v1.WalletService.ListResources:output_type -> hyapp.wallet.v1.ListResourcesResponse - 64, // 313: hyapp.wallet.v1.WalletService.GetResource:output_type -> hyapp.wallet.v1.GetResourceResponse - 71, // 314: hyapp.wallet.v1.WalletService.CreateResource:output_type -> hyapp.wallet.v1.ResourceResponse - 71, // 315: hyapp.wallet.v1.WalletService.UpdateResource:output_type -> hyapp.wallet.v1.ResourceResponse - 71, // 316: hyapp.wallet.v1.WalletService.SetResourceStatus:output_type -> hyapp.wallet.v1.ResourceResponse - 71, // 317: hyapp.wallet.v1.WalletService.DeleteResource:output_type -> hyapp.wallet.v1.ResourceResponse - 70, // 318: hyapp.wallet.v1.WalletService.BatchDeleteResources:output_type -> hyapp.wallet.v1.BatchDeleteResourcesResponse - 73, // 319: hyapp.wallet.v1.WalletService.ListResourceGroups:output_type -> hyapp.wallet.v1.ListResourceGroupsResponse - 75, // 320: hyapp.wallet.v1.WalletService.GetResourceGroup:output_type -> hyapp.wallet.v1.GetResourceGroupResponse - 79, // 321: hyapp.wallet.v1.WalletService.CreateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse - 79, // 322: hyapp.wallet.v1.WalletService.UpdateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse - 79, // 323: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:output_type -> hyapp.wallet.v1.ResourceGroupResponse - 53, // 324: hyapp.wallet.v1.WalletService.PinResourceGroupSnapshot:output_type -> hyapp.wallet.v1.PinResourceGroupSnapshotResponse - 81, // 325: hyapp.wallet.v1.WalletService.ListGiftConfigs:output_type -> hyapp.wallet.v1.ListGiftConfigsResponse - 83, // 326: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:output_type -> hyapp.wallet.v1.ListGiftTypeConfigsResponse - 278, // 327: hyapp.wallet.v1.WalletService.GetGiftCatalogVersion:output_type -> hyapp.wallet.v1.GetGiftCatalogVersionResponse - 92, // 328: hyapp.wallet.v1.WalletService.CreateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse - 88, // 329: hyapp.wallet.v1.WalletService.BatchCreateGiftConfigs:output_type -> hyapp.wallet.v1.BatchCreateGiftConfigsResponse - 92, // 330: hyapp.wallet.v1.WalletService.UpdateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse - 92, // 331: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:output_type -> hyapp.wallet.v1.GiftConfigResponse - 92, // 332: hyapp.wallet.v1.WalletService.DeleteGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse - 85, // 333: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:output_type -> hyapp.wallet.v1.GiftTypeConfigResponse - 97, // 334: hyapp.wallet.v1.WalletService.GrantResource:output_type -> hyapp.wallet.v1.ResourceGrantResponse - 97, // 335: hyapp.wallet.v1.WalletService.GrantResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse - 97, // 336: hyapp.wallet.v1.WalletService.GrantPinnedResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse - 97, // 337: hyapp.wallet.v1.WalletService.RevokeResourceGrant:output_type -> hyapp.wallet.v1.ResourceGrantResponse - 99, // 338: hyapp.wallet.v1.WalletService.ListUserResources:output_type -> hyapp.wallet.v1.ListUserResourcesResponse - 101, // 339: hyapp.wallet.v1.WalletService.EquipUserResource:output_type -> hyapp.wallet.v1.EquipUserResourceResponse - 103, // 340: hyapp.wallet.v1.WalletService.UnequipUserResource:output_type -> hyapp.wallet.v1.UnequipUserResourceResponse - 106, // 341: hyapp.wallet.v1.WalletService.BatchGetUserEquippedResources:output_type -> hyapp.wallet.v1.BatchGetUserEquippedResourcesResponse - 108, // 342: hyapp.wallet.v1.WalletService.ListResourceGrants:output_type -> hyapp.wallet.v1.ListResourceGrantsResponse - 111, // 343: hyapp.wallet.v1.WalletService.ListResourceShopItems:output_type -> hyapp.wallet.v1.ListResourceShopItemsResponse - 113, // 344: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:output_type -> hyapp.wallet.v1.UpsertResourceShopItemsResponse - 115, // 345: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:output_type -> hyapp.wallet.v1.ResourceShopItemResponse - 276, // 346: hyapp.wallet.v1.WalletService.ListResourceShopPurchaseOrders:output_type -> hyapp.wallet.v1.ListResourceShopPurchaseOrdersResponse - 117, // 347: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:output_type -> hyapp.wallet.v1.PurchaseResourceShopItemResponse - 120, // 348: hyapp.wallet.v1.WalletService.ListRechargeBills:output_type -> hyapp.wallet.v1.ListRechargeBillsResponse - 123, // 349: hyapp.wallet.v1.WalletService.GetRechargeBillSummary:output_type -> hyapp.wallet.v1.GetRechargeBillSummaryResponse - 126, // 350: hyapp.wallet.v1.WalletService.BatchGetUserRechargeStats:output_type -> hyapp.wallet.v1.BatchGetUserRechargeStatsResponse - 132, // 351: hyapp.wallet.v1.WalletService.GetRechargeBillOverview:output_type -> hyapp.wallet.v1.GetRechargeBillOverviewResponse - 135, // 352: hyapp.wallet.v1.WalletService.RefreshGooglePaymentPrices:output_type -> hyapp.wallet.v1.RefreshGooglePaymentPricesResponse - 138, // 353: hyapp.wallet.v1.WalletService.GetWalletOverview:output_type -> hyapp.wallet.v1.GetWalletOverviewResponse - 141, // 354: hyapp.wallet.v1.WalletService.GetWalletValueSummary:output_type -> hyapp.wallet.v1.GetWalletValueSummaryResponse - 144, // 355: hyapp.wallet.v1.WalletService.GetUserGiftWall:output_type -> hyapp.wallet.v1.GetUserGiftWallResponse - 147, // 356: hyapp.wallet.v1.WalletService.ListRechargeProducts:output_type -> hyapp.wallet.v1.ListRechargeProductsResponse - 149, // 357: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:output_type -> hyapp.wallet.v1.ConfirmGooglePaymentResponse - 151, // 358: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:output_type -> hyapp.wallet.v1.ListAdminRechargeProductsResponse - 155, // 359: hyapp.wallet.v1.WalletService.CreateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse - 155, // 360: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse - 156, // 361: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:output_type -> hyapp.wallet.v1.DeleteRechargeProductResponse - 160, // 362: hyapp.wallet.v1.WalletService.ListThirdPartyPaymentChannels:output_type -> hyapp.wallet.v1.ListThirdPartyPaymentChannelsResponse - 162, // 363: hyapp.wallet.v1.WalletService.SetThirdPartyPaymentMethodStatus:output_type -> hyapp.wallet.v1.ThirdPartyPaymentMethodResponse - 162, // 364: hyapp.wallet.v1.WalletService.UpdateThirdPartyPaymentRate:output_type -> hyapp.wallet.v1.ThirdPartyPaymentMethodResponse - 166, // 365: hyapp.wallet.v1.WalletService.SyncThirdPartyPaymentMethods:output_type -> hyapp.wallet.v1.SyncThirdPartyPaymentMethodsResponse - 168, // 366: hyapp.wallet.v1.WalletService.ListH5RechargeOptions:output_type -> hyapp.wallet.v1.H5RechargeOptionsResponse - 177, // 367: hyapp.wallet.v1.WalletService.CreateH5RechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse - 177, // 368: hyapp.wallet.v1.WalletService.CreateTemporaryRechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse - 177, // 369: hyapp.wallet.v1.WalletService.GetTemporaryRechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse - 174, // 370: hyapp.wallet.v1.WalletService.ListTemporaryRechargeOrders:output_type -> hyapp.wallet.v1.ListTemporaryRechargeOrdersResponse - 177, // 371: hyapp.wallet.v1.WalletService.SubmitH5RechargeTx:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse - 177, // 372: hyapp.wallet.v1.WalletService.GetH5RechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse - 179, // 373: hyapp.wallet.v1.WalletService.VerifyCoinSellerRechargeReceipt:output_type -> hyapp.wallet.v1.VerifyCoinSellerRechargeReceiptResponse - 181, // 374: hyapp.wallet.v1.WalletService.HandleMifapayNotify:output_type -> hyapp.wallet.v1.HandleMifapayNotifyResponse - 183, // 375: hyapp.wallet.v1.WalletService.HandleV5PayNotify:output_type -> hyapp.wallet.v1.HandleV5PayNotifyResponse - 186, // 376: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:output_type -> hyapp.wallet.v1.GetDiamondExchangeConfigResponse - 189, // 377: hyapp.wallet.v1.WalletService.ListWalletTransactions:output_type -> hyapp.wallet.v1.ListWalletTransactionsResponse - 199, // 378: hyapp.wallet.v1.WalletService.ListVipPackages:output_type -> hyapp.wallet.v1.ListVipPackagesResponse - 211, // 379: hyapp.wallet.v1.WalletService.GetVipProgramConfig:output_type -> hyapp.wallet.v1.GetVipProgramConfigResponse - 201, // 380: hyapp.wallet.v1.WalletService.GetMyVip:output_type -> hyapp.wallet.v1.GetMyVipResponse - 233, // 381: hyapp.wallet.v1.WalletService.CheckVipBenefit:output_type -> hyapp.wallet.v1.CheckVipBenefitResponse - 203, // 382: hyapp.wallet.v1.WalletService.PurchaseVip:output_type -> hyapp.wallet.v1.PurchaseVipResponse - 205, // 383: hyapp.wallet.v1.WalletService.DebitCPBreakupFee:output_type -> hyapp.wallet.v1.DebitCPBreakupFeeResponse - 207, // 384: hyapp.wallet.v1.WalletService.DebitWheelDraw:output_type -> hyapp.wallet.v1.DebitWheelDrawResponse - 209, // 385: hyapp.wallet.v1.WalletService.GrantVip:output_type -> hyapp.wallet.v1.GrantVipResponse - 227, // 386: hyapp.wallet.v1.WalletService.GrantVipTrialCard:output_type -> hyapp.wallet.v1.GrantVipTrialCardResponse - 229, // 387: hyapp.wallet.v1.WalletService.EquipVipTrialCard:output_type -> hyapp.wallet.v1.EquipVipTrialCardResponse - 231, // 388: hyapp.wallet.v1.WalletService.UnequipVipTrialCard:output_type -> hyapp.wallet.v1.UnequipVipTrialCardResponse - 236, // 389: hyapp.wallet.v1.WalletService.ListAdminVipLevels:output_type -> hyapp.wallet.v1.ListAdminVipLevelsResponse - 238, // 390: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:output_type -> hyapp.wallet.v1.UpdateAdminVipLevelsResponse - 213, // 391: hyapp.wallet.v1.WalletService.UpdateAdminVipProgramConfig:output_type -> hyapp.wallet.v1.UpdateAdminVipProgramConfigResponse - 215, // 392: hyapp.wallet.v1.WalletService.UpdateMyVipSettings:output_type -> hyapp.wallet.v1.UpdateMyVipSettingsResponse - 221, // 393: hyapp.wallet.v1.WalletService.GetMyCurrentVipDailyCoinRebate:output_type -> hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateResponse - 223, // 394: hyapp.wallet.v1.WalletService.ListMyVipDailyCoinRebateStatuses:output_type -> hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesResponse - 225, // 395: hyapp.wallet.v1.WalletService.ClaimVipDailyCoinRebate:output_type -> hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse - 240, // 396: hyapp.wallet.v1.WalletService.CreditTaskReward:output_type -> hyapp.wallet.v1.CreditTaskRewardResponse - 242, // 397: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:output_type -> hyapp.wallet.v1.CreditLuckyGiftRewardResponse - 244, // 398: hyapp.wallet.v1.WalletService.CreditWheelReward:output_type -> hyapp.wallet.v1.CreditWheelRewardResponse - 246, // 399: hyapp.wallet.v1.WalletService.CreditRoomTurnoverReward:output_type -> hyapp.wallet.v1.CreditRoomTurnoverRewardResponse - 248, // 400: hyapp.wallet.v1.WalletService.CreditInviteActivityReward:output_type -> hyapp.wallet.v1.CreditInviteActivityRewardResponse - 250, // 401: hyapp.wallet.v1.WalletService.CreditAgencyOpeningReward:output_type -> hyapp.wallet.v1.CreditAgencyOpeningRewardResponse - 252, // 402: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:output_type -> hyapp.wallet.v1.ApplyGameCoinChangeResponse - 257, // 403: hyapp.wallet.v1.WalletService.GetRedPacketConfig:output_type -> hyapp.wallet.v1.GetRedPacketConfigResponse - 259, // 404: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:output_type -> hyapp.wallet.v1.UpdateRedPacketConfigResponse - 261, // 405: hyapp.wallet.v1.WalletService.CreateRedPacket:output_type -> hyapp.wallet.v1.CreateRedPacketResponse - 263, // 406: hyapp.wallet.v1.WalletService.ClaimRedPacket:output_type -> hyapp.wallet.v1.ClaimRedPacketResponse - 265, // 407: hyapp.wallet.v1.WalletService.ListRedPackets:output_type -> hyapp.wallet.v1.ListRedPacketsResponse - 267, // 408: hyapp.wallet.v1.WalletService.GetRedPacket:output_type -> hyapp.wallet.v1.GetRedPacketResponse - 269, // 409: hyapp.wallet.v1.WalletService.ExpireRedPackets:output_type -> hyapp.wallet.v1.ExpireRedPacketsResponse - 271, // 410: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:output_type -> hyapp.wallet.v1.RetryRedPacketRefundResponse - 281, // [281:411] is the sub-list for method output_type - 151, // [151:281] is the sub-list for method input_type - 151, // [151:151] is the sub-list for extension type_name - 151, // [151:151] is the sub-list for extension extendee - 0, // [0:151] is the sub-list for field type_name + 297, // 85: hyapp.wallet.v1.VipBenefit.presentation:type_name -> hyapp.wallet.v1.VipBenefitPresentation + 190, // 86: hyapp.wallet.v1.VipLevel.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem + 192, // 87: hyapp.wallet.v1.VipLevel.benefits:type_name -> hyapp.wallet.v1.VipBenefit + 194, // 88: hyapp.wallet.v1.VipState.paid_vip:type_name -> hyapp.wallet.v1.UserVip + 195, // 89: hyapp.wallet.v1.VipState.equipped_trial_card:type_name -> hyapp.wallet.v1.VipTrialCard + 194, // 90: hyapp.wallet.v1.VipState.effective_vip:type_name -> hyapp.wallet.v1.UserVip + 192, // 91: hyapp.wallet.v1.VipState.effective_benefits:type_name -> hyapp.wallet.v1.VipBenefit + 191, // 92: hyapp.wallet.v1.VipState.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig + 196, // 93: hyapp.wallet.v1.VipState.user_settings:type_name -> hyapp.wallet.v1.VipUserSettings + 194, // 94: hyapp.wallet.v1.ListVipPackagesResponse.current_vip:type_name -> hyapp.wallet.v1.UserVip + 193, // 95: hyapp.wallet.v1.ListVipPackagesResponse.packages:type_name -> hyapp.wallet.v1.VipLevel + 197, // 96: hyapp.wallet.v1.ListVipPackagesResponse.state:type_name -> hyapp.wallet.v1.VipState + 191, // 97: hyapp.wallet.v1.ListVipPackagesResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig + 194, // 98: hyapp.wallet.v1.GetMyVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip + 197, // 99: hyapp.wallet.v1.GetMyVipResponse.state:type_name -> hyapp.wallet.v1.VipState + 194, // 100: hyapp.wallet.v1.PurchaseVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip + 190, // 101: hyapp.wallet.v1.PurchaseVipResponse.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem + 197, // 102: hyapp.wallet.v1.PurchaseVipResponse.state:type_name -> hyapp.wallet.v1.VipState + 8, // 103: hyapp.wallet.v1.PurchaseVipResponse.coin_balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 104: hyapp.wallet.v1.DebitCPBreakupFeeResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 105: hyapp.wallet.v1.DebitWheelDrawResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 194, // 106: hyapp.wallet.v1.GrantVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip + 190, // 107: hyapp.wallet.v1.GrantVipResponse.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem + 197, // 108: hyapp.wallet.v1.GrantVipResponse.state:type_name -> hyapp.wallet.v1.VipState + 191, // 109: hyapp.wallet.v1.GetVipProgramConfigResponse.config:type_name -> hyapp.wallet.v1.VipProgramConfig + 192, // 110: hyapp.wallet.v1.GetVipProgramConfigResponse.benefits:type_name -> hyapp.wallet.v1.VipBenefit + 191, // 111: hyapp.wallet.v1.UpdateAdminVipProgramConfigRequest.config:type_name -> hyapp.wallet.v1.VipProgramConfig + 191, // 112: hyapp.wallet.v1.UpdateAdminVipProgramConfigResponse.config:type_name -> hyapp.wallet.v1.VipProgramConfig + 196, // 113: hyapp.wallet.v1.UpdateMyVipSettingsResponse.settings:type_name -> hyapp.wallet.v1.VipUserSettings + 216, // 114: hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse.run:type_name -> hyapp.wallet.v1.VipDailyCoinRebateRun + 217, // 115: hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateResponse.rebate:type_name -> hyapp.wallet.v1.VipDailyCoinRebate + 217, // 116: hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesResponse.rebates:type_name -> hyapp.wallet.v1.VipDailyCoinRebate + 217, // 117: hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse.rebate:type_name -> hyapp.wallet.v1.VipDailyCoinRebate + 8, // 118: hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse.coin_balance:type_name -> hyapp.wallet.v1.AssetBalance + 195, // 119: hyapp.wallet.v1.GrantVipTrialCardResponse.trial_card:type_name -> hyapp.wallet.v1.VipTrialCard + 197, // 120: hyapp.wallet.v1.GrantVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState + 195, // 121: hyapp.wallet.v1.EquipVipTrialCardResponse.trial_card:type_name -> hyapp.wallet.v1.VipTrialCard + 197, // 122: hyapp.wallet.v1.EquipVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState + 197, // 123: hyapp.wallet.v1.UnequipVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState + 192, // 124: hyapp.wallet.v1.CheckVipBenefitResponse.benefit:type_name -> hyapp.wallet.v1.VipBenefit + 197, // 125: hyapp.wallet.v1.CheckVipBenefitResponse.state:type_name -> hyapp.wallet.v1.VipState + 192, // 126: hyapp.wallet.v1.AdminVipLevelInput.benefits:type_name -> hyapp.wallet.v1.VipBenefit + 193, // 127: hyapp.wallet.v1.ListAdminVipLevelsResponse.levels:type_name -> hyapp.wallet.v1.VipLevel + 191, // 128: hyapp.wallet.v1.ListAdminVipLevelsResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig + 234, // 129: hyapp.wallet.v1.UpdateAdminVipLevelsRequest.levels:type_name -> hyapp.wallet.v1.AdminVipLevelInput + 193, // 130: hyapp.wallet.v1.UpdateAdminVipLevelsResponse.levels:type_name -> hyapp.wallet.v1.VipLevel + 191, // 131: hyapp.wallet.v1.UpdateAdminVipLevelsResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig + 8, // 132: hyapp.wallet.v1.CreditTaskRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 133: hyapp.wallet.v1.CreditLuckyGiftRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 134: hyapp.wallet.v1.CreditWheelRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 135: hyapp.wallet.v1.CreditRoomTurnoverRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 136: hyapp.wallet.v1.CreditInviteActivityRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 137: hyapp.wallet.v1.CreditAgencyOpeningRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 254, // 138: hyapp.wallet.v1.RedPacket.claims:type_name -> hyapp.wallet.v1.RedPacketClaim + 253, // 139: hyapp.wallet.v1.GetRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig + 253, // 140: hyapp.wallet.v1.UpdateRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig + 255, // 141: hyapp.wallet.v1.CreateRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket + 254, // 142: hyapp.wallet.v1.ClaimRedPacketResponse.claim:type_name -> hyapp.wallet.v1.RedPacketClaim + 255, // 143: hyapp.wallet.v1.ClaimRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket + 255, // 144: hyapp.wallet.v1.ListRedPacketsResponse.packets:type_name -> hyapp.wallet.v1.RedPacket + 255, // 145: hyapp.wallet.v1.GetRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket + 255, // 146: hyapp.wallet.v1.RetryRedPacketRefundResponse.packet:type_name -> hyapp.wallet.v1.RedPacket + 48, // 147: hyapp.wallet.v1.ResourceShopPurchaseOrder.resource:type_name -> hyapp.wallet.v1.Resource + 274, // 148: hyapp.wallet.v1.ListResourceShopPurchaseOrdersResponse.orders:type_name -> hyapp.wallet.v1.ResourceShopPurchaseOrder + 279, // 149: hyapp.wallet.v1.GetHostRevenueStatsResponse.stats:type_name -> hyapp.wallet.v1.HostRevenueStats + 282, // 150: hyapp.wallet.v1.GetAgencyPointShareStatsResponse.stats:type_name -> hyapp.wallet.v1.AgencyPointShareStats + 285, // 151: hyapp.wallet.v1.GetAgencyHostGiftStatsResponse.stats:type_name -> hyapp.wallet.v1.AgencyHostGiftStats + 288, // 152: hyapp.wallet.v1.ListPointWithdrawalCoinSellersResponse.sellers:type_name -> hyapp.wallet.v1.PointWithdrawalCoinSellerConfig + 295, // 153: hyapp.wallet.v1.VipBenefitPresentation.preview_items:type_name -> hyapp.wallet.v1.VipBenefitPreviewItem + 296, // 154: hyapp.wallet.v1.VipBenefitPresentation.numeric_reward:type_name -> hyapp.wallet.v1.VipBenefitNumericReward + 57, // 155: hyapp.wallet.v1.RevokeUserResourceResponse.resource:type_name -> hyapp.wallet.v1.UserResourceEntitlement + 272, // 156: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryDailySettlementBatch:input_type -> hyapp.wallet.v1.CronBatchRequest + 272, // 157: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryHalfMonthSettlementBatch:input_type -> hyapp.wallet.v1.CronBatchRequest + 272, // 158: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryMonthEndBatch:input_type -> hyapp.wallet.v1.CronBatchRequest + 218, // 159: hyapp.wallet.v1.WalletCronService.ProcessVipDailyCoinRebateBatch:input_type -> hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchRequest + 0, // 160: hyapp.wallet.v1.WalletService.DebitGift:input_type -> hyapp.wallet.v1.DebitGiftRequest + 1, // 161: hyapp.wallet.v1.WalletService.DebitDirectGift:input_type -> hyapp.wallet.v1.DebitDirectGiftRequest + 4, // 162: hyapp.wallet.v1.WalletService.BatchDebitGift:input_type -> hyapp.wallet.v1.BatchDebitGiftRequest + 7, // 163: hyapp.wallet.v1.WalletService.DebitRobotGift:input_type -> hyapp.wallet.v1.DebitRobotGiftRequest + 9, // 164: hyapp.wallet.v1.WalletService.GetBalances:input_type -> hyapp.wallet.v1.GetBalancesRequest + 13, // 165: hyapp.wallet.v1.WalletService.GetActiveHostSalaryPolicy:input_type -> hyapp.wallet.v1.GetActiveHostSalaryPolicyRequest + 16, // 166: hyapp.wallet.v1.WalletService.GetHostSalaryProgress:input_type -> hyapp.wallet.v1.GetHostSalaryProgressRequest + 280, // 167: hyapp.wallet.v1.WalletService.GetHostRevenueStats:input_type -> hyapp.wallet.v1.GetHostRevenueStatsRequest + 283, // 168: hyapp.wallet.v1.WalletService.GetAgencyPointShareStats:input_type -> hyapp.wallet.v1.GetAgencyPointShareStatsRequest + 286, // 169: hyapp.wallet.v1.WalletService.GetAgencyHostGiftStats:input_type -> hyapp.wallet.v1.GetAgencyHostGiftStatsRequest + 19, // 170: hyapp.wallet.v1.WalletService.GetTeamHostSalaryStats:input_type -> hyapp.wallet.v1.GetTeamHostSalaryStatsRequest + 21, // 171: hyapp.wallet.v1.WalletService.AdminCreditAsset:input_type -> hyapp.wallet.v1.AdminCreditAssetRequest + 23, // 172: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:input_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockRequest + 25, // 173: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:input_type -> hyapp.wallet.v1.TransferCoinFromSellerRequest + 27, // 174: hyapp.wallet.v1.WalletService.TransferCoinSellerStockToChild:input_type -> hyapp.wallet.v1.TransferCoinSellerStockToChildRequest + 30, // 175: hyapp.wallet.v1.WalletService.ListCoinSellerSalaryExchangeRateTiers:input_type -> hyapp.wallet.v1.ListCoinSellerSalaryExchangeRateTiersRequest + 32, // 176: hyapp.wallet.v1.WalletService.ExchangeSalaryToCoin:input_type -> hyapp.wallet.v1.ExchangeSalaryToCoinRequest + 34, // 177: hyapp.wallet.v1.WalletService.TransferSalaryToCoinSeller:input_type -> hyapp.wallet.v1.TransferSalaryToCoinSellerRequest + 289, // 178: hyapp.wallet.v1.WalletService.ListPointWithdrawalCoinSellers:input_type -> hyapp.wallet.v1.ListPointWithdrawalCoinSellersRequest + 291, // 179: hyapp.wallet.v1.WalletService.TransferPointToCoinSeller:input_type -> hyapp.wallet.v1.TransferPointToCoinSellerRequest + 293, // 180: hyapp.wallet.v1.WalletService.GetPointWithdrawalConfig:input_type -> hyapp.wallet.v1.GetPointWithdrawalConfigRequest + 36, // 181: hyapp.wallet.v1.WalletService.FreezeSalaryWithdrawal:input_type -> hyapp.wallet.v1.FreezeSalaryWithdrawalRequest + 38, // 182: hyapp.wallet.v1.WalletService.SettleSalaryWithdrawal:input_type -> hyapp.wallet.v1.SettleSalaryWithdrawalRequest + 40, // 183: hyapp.wallet.v1.WalletService.ReleaseSalaryWithdrawal:input_type -> hyapp.wallet.v1.ReleaseSalaryWithdrawalRequest + 42, // 184: hyapp.wallet.v1.WalletService.FreezePointWithdrawal:input_type -> hyapp.wallet.v1.FreezePointWithdrawalRequest + 44, // 185: hyapp.wallet.v1.WalletService.SettlePointWithdrawal:input_type -> hyapp.wallet.v1.SettlePointWithdrawalRequest + 46, // 186: hyapp.wallet.v1.WalletService.ReleasePointWithdrawal:input_type -> hyapp.wallet.v1.ReleasePointWithdrawalRequest + 61, // 187: hyapp.wallet.v1.WalletService.ListResources:input_type -> hyapp.wallet.v1.ListResourcesRequest + 63, // 188: hyapp.wallet.v1.WalletService.GetResource:input_type -> hyapp.wallet.v1.GetResourceRequest + 65, // 189: hyapp.wallet.v1.WalletService.CreateResource:input_type -> hyapp.wallet.v1.CreateResourceRequest + 66, // 190: hyapp.wallet.v1.WalletService.UpdateResource:input_type -> hyapp.wallet.v1.UpdateResourceRequest + 67, // 191: hyapp.wallet.v1.WalletService.SetResourceStatus:input_type -> hyapp.wallet.v1.SetResourceStatusRequest + 68, // 192: hyapp.wallet.v1.WalletService.DeleteResource:input_type -> hyapp.wallet.v1.DeleteResourceRequest + 69, // 193: hyapp.wallet.v1.WalletService.BatchDeleteResources:input_type -> hyapp.wallet.v1.BatchDeleteResourcesRequest + 72, // 194: hyapp.wallet.v1.WalletService.ListResourceGroups:input_type -> hyapp.wallet.v1.ListResourceGroupsRequest + 74, // 195: hyapp.wallet.v1.WalletService.GetResourceGroup:input_type -> hyapp.wallet.v1.GetResourceGroupRequest + 76, // 196: hyapp.wallet.v1.WalletService.CreateResourceGroup:input_type -> hyapp.wallet.v1.CreateResourceGroupRequest + 77, // 197: hyapp.wallet.v1.WalletService.UpdateResourceGroup:input_type -> hyapp.wallet.v1.UpdateResourceGroupRequest + 78, // 198: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:input_type -> hyapp.wallet.v1.SetResourceGroupStatusRequest + 52, // 199: hyapp.wallet.v1.WalletService.PinResourceGroupSnapshot:input_type -> hyapp.wallet.v1.PinResourceGroupSnapshotRequest + 80, // 200: hyapp.wallet.v1.WalletService.ListGiftConfigs:input_type -> hyapp.wallet.v1.ListGiftConfigsRequest + 82, // 201: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:input_type -> hyapp.wallet.v1.ListGiftTypeConfigsRequest + 277, // 202: hyapp.wallet.v1.WalletService.GetGiftCatalogVersion:input_type -> hyapp.wallet.v1.GetGiftCatalogVersionRequest + 86, // 203: hyapp.wallet.v1.WalletService.CreateGiftConfig:input_type -> hyapp.wallet.v1.CreateGiftConfigRequest + 87, // 204: hyapp.wallet.v1.WalletService.BatchCreateGiftConfigs:input_type -> hyapp.wallet.v1.BatchCreateGiftConfigsRequest + 89, // 205: hyapp.wallet.v1.WalletService.UpdateGiftConfig:input_type -> hyapp.wallet.v1.UpdateGiftConfigRequest + 90, // 206: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:input_type -> hyapp.wallet.v1.SetGiftConfigStatusRequest + 91, // 207: hyapp.wallet.v1.WalletService.DeleteGiftConfig:input_type -> hyapp.wallet.v1.DeleteGiftConfigRequest + 84, // 208: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:input_type -> hyapp.wallet.v1.UpsertGiftTypeConfigRequest + 93, // 209: hyapp.wallet.v1.WalletService.GrantResource:input_type -> hyapp.wallet.v1.GrantResourceRequest + 94, // 210: hyapp.wallet.v1.WalletService.GrantResourceGroup:input_type -> hyapp.wallet.v1.GrantResourceGroupRequest + 95, // 211: hyapp.wallet.v1.WalletService.GrantPinnedResourceGroup:input_type -> hyapp.wallet.v1.GrantPinnedResourceGroupRequest + 96, // 212: hyapp.wallet.v1.WalletService.RevokeResourceGrant:input_type -> hyapp.wallet.v1.RevokeResourceGrantRequest + 298, // 213: hyapp.wallet.v1.WalletService.RevokeUserResource:input_type -> hyapp.wallet.v1.RevokeUserResourceRequest + 98, // 214: hyapp.wallet.v1.WalletService.ListUserResources:input_type -> hyapp.wallet.v1.ListUserResourcesRequest + 100, // 215: hyapp.wallet.v1.WalletService.EquipUserResource:input_type -> hyapp.wallet.v1.EquipUserResourceRequest + 102, // 216: hyapp.wallet.v1.WalletService.UnequipUserResource:input_type -> hyapp.wallet.v1.UnequipUserResourceRequest + 104, // 217: hyapp.wallet.v1.WalletService.BatchGetUserEquippedResources:input_type -> hyapp.wallet.v1.BatchGetUserEquippedResourcesRequest + 107, // 218: hyapp.wallet.v1.WalletService.ListResourceGrants:input_type -> hyapp.wallet.v1.ListResourceGrantsRequest + 110, // 219: hyapp.wallet.v1.WalletService.ListResourceShopItems:input_type -> hyapp.wallet.v1.ListResourceShopItemsRequest + 112, // 220: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:input_type -> hyapp.wallet.v1.UpsertResourceShopItemsRequest + 114, // 221: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:input_type -> hyapp.wallet.v1.SetResourceShopItemStatusRequest + 275, // 222: hyapp.wallet.v1.WalletService.ListResourceShopPurchaseOrders:input_type -> hyapp.wallet.v1.ListResourceShopPurchaseOrdersRequest + 116, // 223: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:input_type -> hyapp.wallet.v1.PurchaseResourceShopItemRequest + 119, // 224: hyapp.wallet.v1.WalletService.ListRechargeBills:input_type -> hyapp.wallet.v1.ListRechargeBillsRequest + 121, // 225: hyapp.wallet.v1.WalletService.GetRechargeBillSummary:input_type -> hyapp.wallet.v1.GetRechargeBillSummaryRequest + 125, // 226: hyapp.wallet.v1.WalletService.BatchGetUserRechargeStats:input_type -> hyapp.wallet.v1.BatchGetUserRechargeStatsRequest + 127, // 227: hyapp.wallet.v1.WalletService.GetRechargeBillOverview:input_type -> hyapp.wallet.v1.GetRechargeBillOverviewRequest + 133, // 228: hyapp.wallet.v1.WalletService.RefreshGooglePaymentPrices:input_type -> hyapp.wallet.v1.RefreshGooglePaymentPricesRequest + 137, // 229: hyapp.wallet.v1.WalletService.GetWalletOverview:input_type -> hyapp.wallet.v1.GetWalletOverviewRequest + 140, // 230: hyapp.wallet.v1.WalletService.GetWalletValueSummary:input_type -> hyapp.wallet.v1.GetWalletValueSummaryRequest + 143, // 231: hyapp.wallet.v1.WalletService.GetUserGiftWall:input_type -> hyapp.wallet.v1.GetUserGiftWallRequest + 146, // 232: hyapp.wallet.v1.WalletService.ListRechargeProducts:input_type -> hyapp.wallet.v1.ListRechargeProductsRequest + 148, // 233: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:input_type -> hyapp.wallet.v1.ConfirmGooglePaymentRequest + 150, // 234: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:input_type -> hyapp.wallet.v1.ListAdminRechargeProductsRequest + 152, // 235: hyapp.wallet.v1.WalletService.CreateRechargeProduct:input_type -> hyapp.wallet.v1.CreateRechargeProductRequest + 153, // 236: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:input_type -> hyapp.wallet.v1.UpdateRechargeProductRequest + 154, // 237: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:input_type -> hyapp.wallet.v1.DeleteRechargeProductRequest + 159, // 238: hyapp.wallet.v1.WalletService.ListThirdPartyPaymentChannels:input_type -> hyapp.wallet.v1.ListThirdPartyPaymentChannelsRequest + 161, // 239: hyapp.wallet.v1.WalletService.SetThirdPartyPaymentMethodStatus:input_type -> hyapp.wallet.v1.SetThirdPartyPaymentMethodStatusRequest + 163, // 240: hyapp.wallet.v1.WalletService.UpdateThirdPartyPaymentRate:input_type -> hyapp.wallet.v1.UpdateThirdPartyPaymentRateRequest + 165, // 241: hyapp.wallet.v1.WalletService.SyncThirdPartyPaymentMethods:input_type -> hyapp.wallet.v1.SyncThirdPartyPaymentMethodsRequest + 167, // 242: hyapp.wallet.v1.WalletService.ListH5RechargeOptions:input_type -> hyapp.wallet.v1.H5RechargeOptionsRequest + 170, // 243: hyapp.wallet.v1.WalletService.CreateH5RechargeOrder:input_type -> hyapp.wallet.v1.CreateH5RechargeOrderRequest + 171, // 244: hyapp.wallet.v1.WalletService.CreateTemporaryRechargeOrder:input_type -> hyapp.wallet.v1.CreateTemporaryRechargeOrderRequest + 172, // 245: hyapp.wallet.v1.WalletService.GetTemporaryRechargeOrder:input_type -> hyapp.wallet.v1.GetTemporaryRechargeOrderRequest + 173, // 246: hyapp.wallet.v1.WalletService.ListTemporaryRechargeOrders:input_type -> hyapp.wallet.v1.ListTemporaryRechargeOrdersRequest + 175, // 247: hyapp.wallet.v1.WalletService.SubmitH5RechargeTx:input_type -> hyapp.wallet.v1.SubmitH5RechargeTxRequest + 176, // 248: hyapp.wallet.v1.WalletService.GetH5RechargeOrder:input_type -> hyapp.wallet.v1.GetH5RechargeOrderRequest + 178, // 249: hyapp.wallet.v1.WalletService.VerifyCoinSellerRechargeReceipt:input_type -> hyapp.wallet.v1.VerifyCoinSellerRechargeReceiptRequest + 180, // 250: hyapp.wallet.v1.WalletService.HandleMifapayNotify:input_type -> hyapp.wallet.v1.HandleMifapayNotifyRequest + 182, // 251: hyapp.wallet.v1.WalletService.HandleV5PayNotify:input_type -> hyapp.wallet.v1.HandleV5PayNotifyRequest + 185, // 252: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:input_type -> hyapp.wallet.v1.GetDiamondExchangeConfigRequest + 188, // 253: hyapp.wallet.v1.WalletService.ListWalletTransactions:input_type -> hyapp.wallet.v1.ListWalletTransactionsRequest + 198, // 254: hyapp.wallet.v1.WalletService.ListVipPackages:input_type -> hyapp.wallet.v1.ListVipPackagesRequest + 210, // 255: hyapp.wallet.v1.WalletService.GetVipProgramConfig:input_type -> hyapp.wallet.v1.GetVipProgramConfigRequest + 200, // 256: hyapp.wallet.v1.WalletService.GetMyVip:input_type -> hyapp.wallet.v1.GetMyVipRequest + 232, // 257: hyapp.wallet.v1.WalletService.CheckVipBenefit:input_type -> hyapp.wallet.v1.CheckVipBenefitRequest + 202, // 258: hyapp.wallet.v1.WalletService.PurchaseVip:input_type -> hyapp.wallet.v1.PurchaseVipRequest + 204, // 259: hyapp.wallet.v1.WalletService.DebitCPBreakupFee:input_type -> hyapp.wallet.v1.DebitCPBreakupFeeRequest + 206, // 260: hyapp.wallet.v1.WalletService.DebitWheelDraw:input_type -> hyapp.wallet.v1.DebitWheelDrawRequest + 208, // 261: hyapp.wallet.v1.WalletService.GrantVip:input_type -> hyapp.wallet.v1.GrantVipRequest + 226, // 262: hyapp.wallet.v1.WalletService.GrantVipTrialCard:input_type -> hyapp.wallet.v1.GrantVipTrialCardRequest + 228, // 263: hyapp.wallet.v1.WalletService.EquipVipTrialCard:input_type -> hyapp.wallet.v1.EquipVipTrialCardRequest + 230, // 264: hyapp.wallet.v1.WalletService.UnequipVipTrialCard:input_type -> hyapp.wallet.v1.UnequipVipTrialCardRequest + 235, // 265: hyapp.wallet.v1.WalletService.ListAdminVipLevels:input_type -> hyapp.wallet.v1.ListAdminVipLevelsRequest + 237, // 266: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:input_type -> hyapp.wallet.v1.UpdateAdminVipLevelsRequest + 212, // 267: hyapp.wallet.v1.WalletService.UpdateAdminVipProgramConfig:input_type -> hyapp.wallet.v1.UpdateAdminVipProgramConfigRequest + 214, // 268: hyapp.wallet.v1.WalletService.UpdateMyVipSettings:input_type -> hyapp.wallet.v1.UpdateMyVipSettingsRequest + 220, // 269: hyapp.wallet.v1.WalletService.GetMyCurrentVipDailyCoinRebate:input_type -> hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateRequest + 222, // 270: hyapp.wallet.v1.WalletService.ListMyVipDailyCoinRebateStatuses:input_type -> hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesRequest + 224, // 271: hyapp.wallet.v1.WalletService.ClaimVipDailyCoinRebate:input_type -> hyapp.wallet.v1.ClaimVipDailyCoinRebateRequest + 239, // 272: hyapp.wallet.v1.WalletService.CreditTaskReward:input_type -> hyapp.wallet.v1.CreditTaskRewardRequest + 241, // 273: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:input_type -> hyapp.wallet.v1.CreditLuckyGiftRewardRequest + 243, // 274: hyapp.wallet.v1.WalletService.CreditWheelReward:input_type -> hyapp.wallet.v1.CreditWheelRewardRequest + 245, // 275: hyapp.wallet.v1.WalletService.CreditRoomTurnoverReward:input_type -> hyapp.wallet.v1.CreditRoomTurnoverRewardRequest + 247, // 276: hyapp.wallet.v1.WalletService.CreditInviteActivityReward:input_type -> hyapp.wallet.v1.CreditInviteActivityRewardRequest + 249, // 277: hyapp.wallet.v1.WalletService.CreditAgencyOpeningReward:input_type -> hyapp.wallet.v1.CreditAgencyOpeningRewardRequest + 251, // 278: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:input_type -> hyapp.wallet.v1.ApplyGameCoinChangeRequest + 256, // 279: hyapp.wallet.v1.WalletService.GetRedPacketConfig:input_type -> hyapp.wallet.v1.GetRedPacketConfigRequest + 258, // 280: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:input_type -> hyapp.wallet.v1.UpdateRedPacketConfigRequest + 260, // 281: hyapp.wallet.v1.WalletService.CreateRedPacket:input_type -> hyapp.wallet.v1.CreateRedPacketRequest + 262, // 282: hyapp.wallet.v1.WalletService.ClaimRedPacket:input_type -> hyapp.wallet.v1.ClaimRedPacketRequest + 264, // 283: hyapp.wallet.v1.WalletService.ListRedPackets:input_type -> hyapp.wallet.v1.ListRedPacketsRequest + 266, // 284: hyapp.wallet.v1.WalletService.GetRedPacket:input_type -> hyapp.wallet.v1.GetRedPacketRequest + 268, // 285: hyapp.wallet.v1.WalletService.ExpireRedPackets:input_type -> hyapp.wallet.v1.ExpireRedPacketsRequest + 270, // 286: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:input_type -> hyapp.wallet.v1.RetryRedPacketRefundRequest + 273, // 287: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryDailySettlementBatch:output_type -> hyapp.wallet.v1.CronBatchResponse + 273, // 288: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryHalfMonthSettlementBatch:output_type -> hyapp.wallet.v1.CronBatchResponse + 273, // 289: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryMonthEndBatch:output_type -> hyapp.wallet.v1.CronBatchResponse + 219, // 290: hyapp.wallet.v1.WalletCronService.ProcessVipDailyCoinRebateBatch:output_type -> hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse + 2, // 291: hyapp.wallet.v1.WalletService.DebitGift:output_type -> hyapp.wallet.v1.DebitGiftResponse + 2, // 292: hyapp.wallet.v1.WalletService.DebitDirectGift:output_type -> hyapp.wallet.v1.DebitGiftResponse + 6, // 293: hyapp.wallet.v1.WalletService.BatchDebitGift:output_type -> hyapp.wallet.v1.BatchDebitGiftResponse + 2, // 294: hyapp.wallet.v1.WalletService.DebitRobotGift:output_type -> hyapp.wallet.v1.DebitGiftResponse + 10, // 295: hyapp.wallet.v1.WalletService.GetBalances:output_type -> hyapp.wallet.v1.GetBalancesResponse + 14, // 296: hyapp.wallet.v1.WalletService.GetActiveHostSalaryPolicy:output_type -> hyapp.wallet.v1.GetActiveHostSalaryPolicyResponse + 17, // 297: hyapp.wallet.v1.WalletService.GetHostSalaryProgress:output_type -> hyapp.wallet.v1.GetHostSalaryProgressResponse + 281, // 298: hyapp.wallet.v1.WalletService.GetHostRevenueStats:output_type -> hyapp.wallet.v1.GetHostRevenueStatsResponse + 284, // 299: hyapp.wallet.v1.WalletService.GetAgencyPointShareStats:output_type -> hyapp.wallet.v1.GetAgencyPointShareStatsResponse + 287, // 300: hyapp.wallet.v1.WalletService.GetAgencyHostGiftStats:output_type -> hyapp.wallet.v1.GetAgencyHostGiftStatsResponse + 20, // 301: hyapp.wallet.v1.WalletService.GetTeamHostSalaryStats:output_type -> hyapp.wallet.v1.GetTeamHostSalaryStatsResponse + 22, // 302: hyapp.wallet.v1.WalletService.AdminCreditAsset:output_type -> hyapp.wallet.v1.AdminCreditAssetResponse + 24, // 303: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:output_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockResponse + 26, // 304: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:output_type -> hyapp.wallet.v1.TransferCoinFromSellerResponse + 28, // 305: hyapp.wallet.v1.WalletService.TransferCoinSellerStockToChild:output_type -> hyapp.wallet.v1.TransferCoinSellerStockToChildResponse + 31, // 306: hyapp.wallet.v1.WalletService.ListCoinSellerSalaryExchangeRateTiers:output_type -> hyapp.wallet.v1.ListCoinSellerSalaryExchangeRateTiersResponse + 33, // 307: hyapp.wallet.v1.WalletService.ExchangeSalaryToCoin:output_type -> hyapp.wallet.v1.ExchangeSalaryToCoinResponse + 35, // 308: hyapp.wallet.v1.WalletService.TransferSalaryToCoinSeller:output_type -> hyapp.wallet.v1.TransferSalaryToCoinSellerResponse + 290, // 309: hyapp.wallet.v1.WalletService.ListPointWithdrawalCoinSellers:output_type -> hyapp.wallet.v1.ListPointWithdrawalCoinSellersResponse + 292, // 310: hyapp.wallet.v1.WalletService.TransferPointToCoinSeller:output_type -> hyapp.wallet.v1.TransferPointToCoinSellerResponse + 294, // 311: hyapp.wallet.v1.WalletService.GetPointWithdrawalConfig:output_type -> hyapp.wallet.v1.GetPointWithdrawalConfigResponse + 37, // 312: hyapp.wallet.v1.WalletService.FreezeSalaryWithdrawal:output_type -> hyapp.wallet.v1.FreezeSalaryWithdrawalResponse + 39, // 313: hyapp.wallet.v1.WalletService.SettleSalaryWithdrawal:output_type -> hyapp.wallet.v1.SettleSalaryWithdrawalResponse + 41, // 314: hyapp.wallet.v1.WalletService.ReleaseSalaryWithdrawal:output_type -> hyapp.wallet.v1.ReleaseSalaryWithdrawalResponse + 43, // 315: hyapp.wallet.v1.WalletService.FreezePointWithdrawal:output_type -> hyapp.wallet.v1.FreezePointWithdrawalResponse + 45, // 316: hyapp.wallet.v1.WalletService.SettlePointWithdrawal:output_type -> hyapp.wallet.v1.SettlePointWithdrawalResponse + 47, // 317: hyapp.wallet.v1.WalletService.ReleasePointWithdrawal:output_type -> hyapp.wallet.v1.ReleasePointWithdrawalResponse + 62, // 318: hyapp.wallet.v1.WalletService.ListResources:output_type -> hyapp.wallet.v1.ListResourcesResponse + 64, // 319: hyapp.wallet.v1.WalletService.GetResource:output_type -> hyapp.wallet.v1.GetResourceResponse + 71, // 320: hyapp.wallet.v1.WalletService.CreateResource:output_type -> hyapp.wallet.v1.ResourceResponse + 71, // 321: hyapp.wallet.v1.WalletService.UpdateResource:output_type -> hyapp.wallet.v1.ResourceResponse + 71, // 322: hyapp.wallet.v1.WalletService.SetResourceStatus:output_type -> hyapp.wallet.v1.ResourceResponse + 71, // 323: hyapp.wallet.v1.WalletService.DeleteResource:output_type -> hyapp.wallet.v1.ResourceResponse + 70, // 324: hyapp.wallet.v1.WalletService.BatchDeleteResources:output_type -> hyapp.wallet.v1.BatchDeleteResourcesResponse + 73, // 325: hyapp.wallet.v1.WalletService.ListResourceGroups:output_type -> hyapp.wallet.v1.ListResourceGroupsResponse + 75, // 326: hyapp.wallet.v1.WalletService.GetResourceGroup:output_type -> hyapp.wallet.v1.GetResourceGroupResponse + 79, // 327: hyapp.wallet.v1.WalletService.CreateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse + 79, // 328: hyapp.wallet.v1.WalletService.UpdateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse + 79, // 329: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:output_type -> hyapp.wallet.v1.ResourceGroupResponse + 53, // 330: hyapp.wallet.v1.WalletService.PinResourceGroupSnapshot:output_type -> hyapp.wallet.v1.PinResourceGroupSnapshotResponse + 81, // 331: hyapp.wallet.v1.WalletService.ListGiftConfigs:output_type -> hyapp.wallet.v1.ListGiftConfigsResponse + 83, // 332: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:output_type -> hyapp.wallet.v1.ListGiftTypeConfigsResponse + 278, // 333: hyapp.wallet.v1.WalletService.GetGiftCatalogVersion:output_type -> hyapp.wallet.v1.GetGiftCatalogVersionResponse + 92, // 334: hyapp.wallet.v1.WalletService.CreateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse + 88, // 335: hyapp.wallet.v1.WalletService.BatchCreateGiftConfigs:output_type -> hyapp.wallet.v1.BatchCreateGiftConfigsResponse + 92, // 336: hyapp.wallet.v1.WalletService.UpdateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse + 92, // 337: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:output_type -> hyapp.wallet.v1.GiftConfigResponse + 92, // 338: hyapp.wallet.v1.WalletService.DeleteGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse + 85, // 339: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:output_type -> hyapp.wallet.v1.GiftTypeConfigResponse + 97, // 340: hyapp.wallet.v1.WalletService.GrantResource:output_type -> hyapp.wallet.v1.ResourceGrantResponse + 97, // 341: hyapp.wallet.v1.WalletService.GrantResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse + 97, // 342: hyapp.wallet.v1.WalletService.GrantPinnedResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse + 97, // 343: hyapp.wallet.v1.WalletService.RevokeResourceGrant:output_type -> hyapp.wallet.v1.ResourceGrantResponse + 299, // 344: hyapp.wallet.v1.WalletService.RevokeUserResource:output_type -> hyapp.wallet.v1.RevokeUserResourceResponse + 99, // 345: hyapp.wallet.v1.WalletService.ListUserResources:output_type -> hyapp.wallet.v1.ListUserResourcesResponse + 101, // 346: hyapp.wallet.v1.WalletService.EquipUserResource:output_type -> hyapp.wallet.v1.EquipUserResourceResponse + 103, // 347: hyapp.wallet.v1.WalletService.UnequipUserResource:output_type -> hyapp.wallet.v1.UnequipUserResourceResponse + 106, // 348: hyapp.wallet.v1.WalletService.BatchGetUserEquippedResources:output_type -> hyapp.wallet.v1.BatchGetUserEquippedResourcesResponse + 108, // 349: hyapp.wallet.v1.WalletService.ListResourceGrants:output_type -> hyapp.wallet.v1.ListResourceGrantsResponse + 111, // 350: hyapp.wallet.v1.WalletService.ListResourceShopItems:output_type -> hyapp.wallet.v1.ListResourceShopItemsResponse + 113, // 351: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:output_type -> hyapp.wallet.v1.UpsertResourceShopItemsResponse + 115, // 352: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:output_type -> hyapp.wallet.v1.ResourceShopItemResponse + 276, // 353: hyapp.wallet.v1.WalletService.ListResourceShopPurchaseOrders:output_type -> hyapp.wallet.v1.ListResourceShopPurchaseOrdersResponse + 117, // 354: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:output_type -> hyapp.wallet.v1.PurchaseResourceShopItemResponse + 120, // 355: hyapp.wallet.v1.WalletService.ListRechargeBills:output_type -> hyapp.wallet.v1.ListRechargeBillsResponse + 123, // 356: hyapp.wallet.v1.WalletService.GetRechargeBillSummary:output_type -> hyapp.wallet.v1.GetRechargeBillSummaryResponse + 126, // 357: hyapp.wallet.v1.WalletService.BatchGetUserRechargeStats:output_type -> hyapp.wallet.v1.BatchGetUserRechargeStatsResponse + 132, // 358: hyapp.wallet.v1.WalletService.GetRechargeBillOverview:output_type -> hyapp.wallet.v1.GetRechargeBillOverviewResponse + 135, // 359: hyapp.wallet.v1.WalletService.RefreshGooglePaymentPrices:output_type -> hyapp.wallet.v1.RefreshGooglePaymentPricesResponse + 138, // 360: hyapp.wallet.v1.WalletService.GetWalletOverview:output_type -> hyapp.wallet.v1.GetWalletOverviewResponse + 141, // 361: hyapp.wallet.v1.WalletService.GetWalletValueSummary:output_type -> hyapp.wallet.v1.GetWalletValueSummaryResponse + 144, // 362: hyapp.wallet.v1.WalletService.GetUserGiftWall:output_type -> hyapp.wallet.v1.GetUserGiftWallResponse + 147, // 363: hyapp.wallet.v1.WalletService.ListRechargeProducts:output_type -> hyapp.wallet.v1.ListRechargeProductsResponse + 149, // 364: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:output_type -> hyapp.wallet.v1.ConfirmGooglePaymentResponse + 151, // 365: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:output_type -> hyapp.wallet.v1.ListAdminRechargeProductsResponse + 155, // 366: hyapp.wallet.v1.WalletService.CreateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse + 155, // 367: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse + 156, // 368: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:output_type -> hyapp.wallet.v1.DeleteRechargeProductResponse + 160, // 369: hyapp.wallet.v1.WalletService.ListThirdPartyPaymentChannels:output_type -> hyapp.wallet.v1.ListThirdPartyPaymentChannelsResponse + 162, // 370: hyapp.wallet.v1.WalletService.SetThirdPartyPaymentMethodStatus:output_type -> hyapp.wallet.v1.ThirdPartyPaymentMethodResponse + 162, // 371: hyapp.wallet.v1.WalletService.UpdateThirdPartyPaymentRate:output_type -> hyapp.wallet.v1.ThirdPartyPaymentMethodResponse + 166, // 372: hyapp.wallet.v1.WalletService.SyncThirdPartyPaymentMethods:output_type -> hyapp.wallet.v1.SyncThirdPartyPaymentMethodsResponse + 168, // 373: hyapp.wallet.v1.WalletService.ListH5RechargeOptions:output_type -> hyapp.wallet.v1.H5RechargeOptionsResponse + 177, // 374: hyapp.wallet.v1.WalletService.CreateH5RechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse + 177, // 375: hyapp.wallet.v1.WalletService.CreateTemporaryRechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse + 177, // 376: hyapp.wallet.v1.WalletService.GetTemporaryRechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse + 174, // 377: hyapp.wallet.v1.WalletService.ListTemporaryRechargeOrders:output_type -> hyapp.wallet.v1.ListTemporaryRechargeOrdersResponse + 177, // 378: hyapp.wallet.v1.WalletService.SubmitH5RechargeTx:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse + 177, // 379: hyapp.wallet.v1.WalletService.GetH5RechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse + 179, // 380: hyapp.wallet.v1.WalletService.VerifyCoinSellerRechargeReceipt:output_type -> hyapp.wallet.v1.VerifyCoinSellerRechargeReceiptResponse + 181, // 381: hyapp.wallet.v1.WalletService.HandleMifapayNotify:output_type -> hyapp.wallet.v1.HandleMifapayNotifyResponse + 183, // 382: hyapp.wallet.v1.WalletService.HandleV5PayNotify:output_type -> hyapp.wallet.v1.HandleV5PayNotifyResponse + 186, // 383: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:output_type -> hyapp.wallet.v1.GetDiamondExchangeConfigResponse + 189, // 384: hyapp.wallet.v1.WalletService.ListWalletTransactions:output_type -> hyapp.wallet.v1.ListWalletTransactionsResponse + 199, // 385: hyapp.wallet.v1.WalletService.ListVipPackages:output_type -> hyapp.wallet.v1.ListVipPackagesResponse + 211, // 386: hyapp.wallet.v1.WalletService.GetVipProgramConfig:output_type -> hyapp.wallet.v1.GetVipProgramConfigResponse + 201, // 387: hyapp.wallet.v1.WalletService.GetMyVip:output_type -> hyapp.wallet.v1.GetMyVipResponse + 233, // 388: hyapp.wallet.v1.WalletService.CheckVipBenefit:output_type -> hyapp.wallet.v1.CheckVipBenefitResponse + 203, // 389: hyapp.wallet.v1.WalletService.PurchaseVip:output_type -> hyapp.wallet.v1.PurchaseVipResponse + 205, // 390: hyapp.wallet.v1.WalletService.DebitCPBreakupFee:output_type -> hyapp.wallet.v1.DebitCPBreakupFeeResponse + 207, // 391: hyapp.wallet.v1.WalletService.DebitWheelDraw:output_type -> hyapp.wallet.v1.DebitWheelDrawResponse + 209, // 392: hyapp.wallet.v1.WalletService.GrantVip:output_type -> hyapp.wallet.v1.GrantVipResponse + 227, // 393: hyapp.wallet.v1.WalletService.GrantVipTrialCard:output_type -> hyapp.wallet.v1.GrantVipTrialCardResponse + 229, // 394: hyapp.wallet.v1.WalletService.EquipVipTrialCard:output_type -> hyapp.wallet.v1.EquipVipTrialCardResponse + 231, // 395: hyapp.wallet.v1.WalletService.UnequipVipTrialCard:output_type -> hyapp.wallet.v1.UnequipVipTrialCardResponse + 236, // 396: hyapp.wallet.v1.WalletService.ListAdminVipLevels:output_type -> hyapp.wallet.v1.ListAdminVipLevelsResponse + 238, // 397: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:output_type -> hyapp.wallet.v1.UpdateAdminVipLevelsResponse + 213, // 398: hyapp.wallet.v1.WalletService.UpdateAdminVipProgramConfig:output_type -> hyapp.wallet.v1.UpdateAdminVipProgramConfigResponse + 215, // 399: hyapp.wallet.v1.WalletService.UpdateMyVipSettings:output_type -> hyapp.wallet.v1.UpdateMyVipSettingsResponse + 221, // 400: hyapp.wallet.v1.WalletService.GetMyCurrentVipDailyCoinRebate:output_type -> hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateResponse + 223, // 401: hyapp.wallet.v1.WalletService.ListMyVipDailyCoinRebateStatuses:output_type -> hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesResponse + 225, // 402: hyapp.wallet.v1.WalletService.ClaimVipDailyCoinRebate:output_type -> hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse + 240, // 403: hyapp.wallet.v1.WalletService.CreditTaskReward:output_type -> hyapp.wallet.v1.CreditTaskRewardResponse + 242, // 404: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:output_type -> hyapp.wallet.v1.CreditLuckyGiftRewardResponse + 244, // 405: hyapp.wallet.v1.WalletService.CreditWheelReward:output_type -> hyapp.wallet.v1.CreditWheelRewardResponse + 246, // 406: hyapp.wallet.v1.WalletService.CreditRoomTurnoverReward:output_type -> hyapp.wallet.v1.CreditRoomTurnoverRewardResponse + 248, // 407: hyapp.wallet.v1.WalletService.CreditInviteActivityReward:output_type -> hyapp.wallet.v1.CreditInviteActivityRewardResponse + 250, // 408: hyapp.wallet.v1.WalletService.CreditAgencyOpeningReward:output_type -> hyapp.wallet.v1.CreditAgencyOpeningRewardResponse + 252, // 409: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:output_type -> hyapp.wallet.v1.ApplyGameCoinChangeResponse + 257, // 410: hyapp.wallet.v1.WalletService.GetRedPacketConfig:output_type -> hyapp.wallet.v1.GetRedPacketConfigResponse + 259, // 411: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:output_type -> hyapp.wallet.v1.UpdateRedPacketConfigResponse + 261, // 412: hyapp.wallet.v1.WalletService.CreateRedPacket:output_type -> hyapp.wallet.v1.CreateRedPacketResponse + 263, // 413: hyapp.wallet.v1.WalletService.ClaimRedPacket:output_type -> hyapp.wallet.v1.ClaimRedPacketResponse + 265, // 414: hyapp.wallet.v1.WalletService.ListRedPackets:output_type -> hyapp.wallet.v1.ListRedPacketsResponse + 267, // 415: hyapp.wallet.v1.WalletService.GetRedPacket:output_type -> hyapp.wallet.v1.GetRedPacketResponse + 269, // 416: hyapp.wallet.v1.WalletService.ExpireRedPackets:output_type -> hyapp.wallet.v1.ExpireRedPacketsResponse + 271, // 417: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:output_type -> hyapp.wallet.v1.RetryRedPacketRefundResponse + 287, // [287:418] is the sub-list for method output_type + 156, // [156:287] is the sub-list for method input_type + 156, // [156:156] is the sub-list for extension type_name + 156, // [156:156] is the sub-list for extension extendee + 0, // [0:156] is the sub-list for field type_name } func init() { file_proto_wallet_v1_wallet_proto_init() } @@ -29251,7 +29748,7 @@ func file_proto_wallet_v1_wallet_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_wallet_v1_wallet_proto_rawDesc), len(file_proto_wallet_v1_wallet_proto_rawDesc)), NumEnums: 0, - NumMessages: 296, + NumMessages: 301, NumExtensions: 0, NumServices: 2, }, diff --git a/api/proto/wallet/v1/wallet.proto b/api/proto/wallet/v1/wallet.proto index 0cf62e7a..5b088ee0 100644 --- a/api/proto/wallet/v1/wallet.proto +++ b/api/proto/wallet/v1/wallet.proto @@ -1187,6 +1187,8 @@ message ResourceShopItemInput { int64 effective_from_ms = 5; int64 effective_to_ms = 6; int32 sort_order = 7; + // coin_price 是当前售卖规格自己的金币价;新建传 0 使用资源列表价,更新已有规格传 0 保留当前售价。 + int64 coin_price = 8; } message ListResourceShopItemsRequest { @@ -2016,6 +2018,7 @@ message VipBenefit { string metadata_json = 12; int64 created_at_ms = 13; int64 updated_at_ms = 14; + VipBenefitPresentation presentation = 15; } message VipLevel { @@ -2073,13 +2076,16 @@ message VipTrialCard { } // VipUserSettings 是用户按 App 隔离的可关闭 VIP 功能偏好。没有持久记录时 wallet -// 仍返回两个默认开启值,updated_at_ms=0;设置本身不授予任何 VIP 权益。 +// 仍返回五个默认开启值,updated_at_ms=0;设置本身不授予任何 VIP 权益。 message VipUserSettings { string app_code = 1; int64 user_id = 2; bool room_entry_notice_enabled = 3; bool online_global_notice_enabled = 4; int64 updated_at_ms = 5; + bool hide_profile_data_enabled = 6; + bool anonymous_profile_visit_enabled = 7; + bool leaderboard_invisible_enabled = 8; } // VipState 同时返回付费会员、当前佩戴体验卡和最终生效会员,避免调用方自行合并两层状态。 @@ -2136,6 +2142,8 @@ message PurchaseVipResponse { int64 coin_balance_after = 5; repeated VipRewardItem reward_items = 6; VipState state = 7; + // coin_balance 是购买事务提交后的带版本余额快照;coin_balance_after 仅保留旧客户端兼容。 + AssetBalance coin_balance = 8; } message DebitCPBreakupFeeRequest { @@ -2220,6 +2228,9 @@ message UpdateMyVipSettingsRequest { int64 user_id = 3; optional bool room_entry_notice_enabled = 4; optional bool online_global_notice_enabled = 5; + optional bool hide_profile_data_enabled = 6; + optional bool anonymous_profile_visit_enabled = 7; + optional bool leaderboard_invisible_enabled = 8; } message UpdateMyVipSettingsResponse { @@ -2940,6 +2951,52 @@ message GetPointWithdrawalConfigResponse { string policy_instance_code = 5; } +// VipBenefitPreviewItem 是同一权益下的一项预览内容;独立稳定键允许 Flutter 在多图、动态资源间切换, +// 且不会把数组下标误当成资源身份。 +message VipBenefitPreviewItem { + string preview_id = 1; + string title = 2; + // media_type: image/animation;animation 使用 animation_url,静态兜底使用 preview_url/asset_url。 + string media_type = 3; + string asset_url = 4; + string preview_url = 5; + string animation_url = 6; + // composition_type: none/user_avatar_center/user_avatar_waveform,客户端据此把当前用户头像合成到预览中。 + string composition_type = 7; + int32 sort_order = 8; +} + +message VipBenefitNumericReward { + string label = 1; + int64 value = 2; + string unit = 3; + // period: once/daily/monthly,由服务端配置返回,客户端只展示不据此结算。 + string period = 4; +} + +// VipBenefitPresentation 是 Flutter 可直接渲染的结构化特权说明。metadata_json 继续保留后台扩展和 +// 历史兼容,但客户端不得按 benefit_code 自行解析 metadata_json 生成预览。 +message VipBenefitPresentation { + string description = 1; + repeated VipBenefitPreviewItem preview_items = 2; + VipBenefitNumericReward numeric_reward = 3; +} + +// RevokeUserResourceRequest targets one entitlement instead of its source grant. This keeps +// resource-group siblings intact when an operator removes a single badge/frame from a user. +message RevokeUserResourceRequest { + string request_id = 1; + string app_code = 2; + int64 user_id = 3; + string entitlement_id = 4; + string reason = 5; + int64 operator_user_id = 6; +} + +message RevokeUserResourceResponse { + UserResourceEntitlement resource = 1; +} + // WalletCronService 只给 cron-service 调用;账务状态仍由 wallet-service owner 修改。 service WalletCronService { rpc ProcessHostSalaryDailySettlementBatch(CronBatchRequest) returns (CronBatchResponse); @@ -3004,6 +3061,7 @@ service WalletService { rpc GrantResourceGroup(GrantResourceGroupRequest) returns (ResourceGrantResponse); rpc GrantPinnedResourceGroup(GrantPinnedResourceGroupRequest) returns (ResourceGrantResponse); rpc RevokeResourceGrant(RevokeResourceGrantRequest) returns (ResourceGrantResponse); + rpc RevokeUserResource(RevokeUserResourceRequest) returns (RevokeUserResourceResponse); rpc ListUserResources(ListUserResourcesRequest) returns (ListUserResourcesResponse); rpc EquipUserResource(EquipUserResourceRequest) returns (EquipUserResourceResponse); rpc UnequipUserResource(UnequipUserResourceRequest) returns (UnequipUserResourceResponse); diff --git a/api/proto/wallet/v1/wallet_grpc.pb.go b/api/proto/wallet/v1/wallet_grpc.pb.go index 141c0633..fe093be7 100644 --- a/api/proto/wallet/v1/wallet_grpc.pb.go +++ b/api/proto/wallet/v1/wallet_grpc.pb.go @@ -292,6 +292,7 @@ const ( WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup" WalletService_GrantPinnedResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantPinnedResourceGroup" WalletService_RevokeResourceGrant_FullMethodName = "/hyapp.wallet.v1.WalletService/RevokeResourceGrant" + WalletService_RevokeUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/RevokeUserResource" WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources" WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource" WalletService_UnequipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UnequipUserResource" @@ -427,6 +428,7 @@ type WalletServiceClient interface { GrantResourceGroup(ctx context.Context, in *GrantResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error) GrantPinnedResourceGroup(ctx context.Context, in *GrantPinnedResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error) RevokeResourceGrant(ctx context.Context, in *RevokeResourceGrantRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error) + RevokeUserResource(ctx context.Context, in *RevokeUserResourceRequest, opts ...grpc.CallOption) (*RevokeUserResourceResponse, error) ListUserResources(ctx context.Context, in *ListUserResourcesRequest, opts ...grpc.CallOption) (*ListUserResourcesResponse, error) EquipUserResource(ctx context.Context, in *EquipUserResourceRequest, opts ...grpc.CallOption) (*EquipUserResourceResponse, error) UnequipUserResource(ctx context.Context, in *UnequipUserResourceRequest, opts ...grpc.CallOption) (*UnequipUserResourceResponse, error) @@ -1040,6 +1042,16 @@ func (c *walletServiceClient) RevokeResourceGrant(ctx context.Context, in *Revok return out, nil } +func (c *walletServiceClient) RevokeUserResource(ctx context.Context, in *RevokeUserResourceRequest, opts ...grpc.CallOption) (*RevokeUserResourceResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RevokeUserResourceResponse) + err := c.cc.Invoke(ctx, WalletService_RevokeUserResource_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *walletServiceClient) ListUserResources(ctx context.Context, in *ListUserResourcesRequest, opts ...grpc.CallOption) (*ListUserResourcesResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListUserResourcesResponse) @@ -1830,6 +1842,7 @@ type WalletServiceServer interface { GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error) GrantPinnedResourceGroup(context.Context, *GrantPinnedResourceGroupRequest) (*ResourceGrantResponse, error) RevokeResourceGrant(context.Context, *RevokeResourceGrantRequest) (*ResourceGrantResponse, error) + RevokeUserResource(context.Context, *RevokeUserResourceRequest) (*RevokeUserResourceResponse, error) ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error) EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error) UnequipUserResource(context.Context, *UnequipUserResourceRequest) (*UnequipUserResourceResponse, error) @@ -2072,6 +2085,9 @@ func (UnimplementedWalletServiceServer) GrantPinnedResourceGroup(context.Context func (UnimplementedWalletServiceServer) RevokeResourceGrant(context.Context, *RevokeResourceGrantRequest) (*ResourceGrantResponse, error) { return nil, status.Error(codes.Unimplemented, "method RevokeResourceGrant not implemented") } +func (UnimplementedWalletServiceServer) RevokeUserResource(context.Context, *RevokeUserResourceRequest) (*RevokeUserResourceResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RevokeUserResource not implemented") +} func (UnimplementedWalletServiceServer) ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListUserResources not implemented") } @@ -3266,6 +3282,24 @@ func _WalletService_RevokeResourceGrant_Handler(srv interface{}, ctx context.Con return interceptor(ctx, in, info, handler) } +func _WalletService_RevokeUserResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RevokeUserResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletServiceServer).RevokeUserResource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WalletService_RevokeUserResource_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletServiceServer).RevokeUserResource(ctx, req.(*RevokeUserResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _WalletService_ListUserResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListUserResourcesRequest) if err := dec(in); err != nil { @@ -4799,6 +4833,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{ MethodName: "RevokeResourceGrant", Handler: _WalletService_RevokeResourceGrant_Handler, }, + { + MethodName: "RevokeUserResource", + Handler: _WalletService_RevokeUserResource_Handler, + }, { MethodName: "ListUserResources", Handler: _WalletService_ListUserResources_Handler, diff --git a/docs/Fami_VIP_Flutter对接方案.md b/docs/Fami_VIP_Flutter对接方案.md index 18c81fff..8e787fda 100644 --- a/docs/Fami_VIP_Flutter对接方案.md +++ b/docs/Fami_VIP_Flutter对接方案.md @@ -2,6 +2,14 @@ 本文定义 Flutter 与当前已落地 VIP 后端的对接契约。客户端不要重复实现 VIP 状态机,也不要按 App、VIP 等级或最低解锁等级推导权限。界面和文案统一使用“VIP”,不再使用“SVIP”。 +## 0. 当前稳定契约 + +- `GET /vip/me`、`GET /vip/packages`、购买、佩戴体验卡、卸下体验卡的 `state` 完全同构,固定包含 `paid_vip`、`equipped_trial_card`、`effective_vip`、`effective_source`、`effective_benefits`、`evaluated_at_ms`、完整 `program_config`、完整 `user_settings`。写成功后整体替换 store,禁止局部合并。 +- `user_settings` 固定包含 5 项开关:进房通知、上线全服通知、隐藏个人数据、匿名访问主页、榜单隐身;均按 App 隔离、无记录默认开启。 +- 权益富展示读取结构化 `benefits[].presentation`。它覆盖说明文案、多项静态/动态预览、用户头像合成方式和数值奖励;Flutter 不解析 `metadata_json` 生成 UI。 +- VIP 购买后的余额读取带 `version` 的 `coin_balance`;`coin_balance_after` 仅兼容旧客户端。历史幂等回放若 `version=0`,必须重新请求钱包余额。 +- 业务分支只判断稳定 `code`,不得解析 `message`。精确 JSON、`presentation` Schema 和错误码矩阵以 [新版 VIP 权限策略 Flutter 客户端对接文档](./新版VIP权限策略_Flutter客户端对接文档.md) 为唯一协议定义;本文后续较短 JSON 只解释场景,不定义裁剪响应。 + ## 1. 通用约定 - 地址前缀:`/api/v1`,实际 host 按环境配置。 @@ -82,6 +90,20 @@ class VipEntitlementSnapshot { return has('online_global_notice') && settings.onlineGlobalNoticeEnabled; } + + bool allowsHideProfileData() { + return has('hide_profile_data') && settings.hideProfileDataEnabled; + } + + bool allowsAnonymousProfileVisit() { + return has('anonymous_profile_visit') && + settings.anonymousProfileVisitEnabled; + } + + bool allowsLeaderboardInvisible() { + return has('leaderboard_invisible') && + settings.leaderboardInvisibleEnabled; + } } ``` @@ -106,7 +128,7 @@ final benefitHandlers = { | 功能入口 | `store.has(code)` 决定正常态/锁态和升级引导 | 点击后的写接口再次鉴权 | | 装扮权益 | 同时检查权益和服务端资源字段;素材为空时使用无图兜底 | 保存/佩戴仍调用资源 owner 接口 | | 进房通知 | 本人的设置页读 store;房间内展示读 Join/房间 IM 快照 | room-service 在 Join 时校验 | -| 防踢、防禁言 | 不根据目标用户本地 VIP 快照禁用管理按钮 | 正常调用房管接口,处理 `PERMISSION_DENIED` | +| 防踢、防禁言 | 不根据目标用户本地 VIP 快照禁用管理按钮 | 正常调用房管接口,分别处理 `VIP_ANTI_KICK`、`VIP_ANTI_MUTE` | | 金币返现 | `daily_coin_rebate` 可用于显示入口,金额/状态不在本地推导 | current/statuses/claim 接口决定资格和入账 | | 上线全服通知 | 权益和开关满足时发起 `/vip/online-notice` | activity/notice 链路异步投递 | @@ -176,7 +198,7 @@ final benefitHandlers = { | 体验卡背包 | `GET /api/v1/users/me/resources?resource_type=vip_trial_card` | 只更新卡列表 | | 佩戴体验卡 | `POST /api/v1/vip/trial-cards/{entitlement_id}/equip` | 成功后使用响应 `state` | | 卸下体验卡 | `DELETE /api/v1/vip/trial-cards/equipped` | 成功后使用响应 `state` | -| VIP 用户开关 | `GET/PATCH /api/v1/vip/settings` | 更新 `user_settings`,不改变权益集 | +| VIP 功能开关 | `GET/PATCH /api/v1/vip/settings` | 更新 `user_settings`,不改变权益集 | | 金币返现 | `/api/v1/vip/coin-rebates/*` | 使用服务端记录和余额回执 | | 上线全服通知 | `POST /api/v1/vip/online-notice` | 只创建异步播报事件 | @@ -250,6 +272,22 @@ final benefitHandlers = { "auto_equip": false, "sort_order": 110, "metadata_json": "", + "presentation": { + "description": "进入房间时展示动态背景和用户头像", + "preview_items": [ + { + "preview_id": "room_background_gold", + "title": "鎏金房间背景", + "media_type": "animation", + "asset_url": "https://cdn.example/vip/room-bg.png", + "preview_url": "https://cdn.example/vip/room-bg-preview.png", + "animation_url": "https://cdn.example/vip/room-bg.svga", + "composition_type": "user_avatar_center", + "sort_order": 10 + } + ], + "numeric_reward": null + }, "created_at_ms": 0, "updated_at_ms": 0 } @@ -302,6 +340,9 @@ final benefitHandlers = { "user_id": 10001, "room_entry_notice_enabled": true, "online_global_notice_enabled": true, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, "updated_at_ms": 0 } } @@ -412,7 +453,8 @@ final benefitHandlers = { "execution_scope": "room", "auto_equip": false, "sort_order": 290, - "metadata_json": "" + "metadata_json": "", + "presentation": null } ], "evaluated_at_ms": 1780101000000, @@ -426,6 +468,9 @@ final benefitHandlers = { "user_id": 10001, "room_entry_notice_enabled": true, "online_global_notice_enabled": true, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, "updated_at_ms": 0 } } @@ -447,7 +492,7 @@ final benefitHandlers = { - 当体验卡被佩戴时,不限制卡等级高低,它会覆盖付费 VIP 成为 `effective_vip`;卸下或过期后自然回落到仍有效的 `paid_vip`。 - 体验卡状态下,`daily_coin_rebate` 不会进入 `effective_benefits`。Flutter 不要只看卡等级猜测权益。 - 无 VIP 时,`effective_source="none"`、`effective_vip.active=false`、`effective_benefits=[]`。 -- `user_settings.updated_at_ms=0` 表示用户尚未修改,两个开关使用默认开启值;设置在无 VIP 时也保留,但不会单独授权。 +- `user_settings.updated_at_ms=0` 表示用户尚未修改,5 个开关使用默认开启值;设置在无 VIP 时也保留,但不会单独授权。 ## 8. 购买、续期和升级 @@ -490,6 +535,12 @@ final benefitHandlers = { }, "coin_spent": 4000, "coin_balance_after": 6000, + "coin_balance": { + "asset_type": "COIN", + "available_amount": 6000, + "frozen_amount": 0, + "version": 18 + }, "reward_items": [], "program_config": { "app_code": "fami", @@ -523,6 +574,16 @@ final benefitHandlers = { "app_code": "fami", "program_type": "tiered_privilege_v1", "config_version": 7 + }, + "user_settings": { + "app_code": "fami", + "user_id": 10001, + "room_entry_notice_enabled": true, + "online_global_notice_enabled": true, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, + "updated_at_ms": 0 } } } @@ -549,8 +610,9 @@ Flutter 收到后用 `event_id` 做会话内去重,再调 `GET /vip/me`。不 - Fami 同级续期:新截止时间 = 旧 `expires_at_ms` + `duration_ms`,保留剩余时间。 - Fami 高级升级:新截止时间 = 购买时间 + `duration_ms`,旧低等级剩余时间丢弃。例如 VIP3 剩 15 天购买 VIP4 30 天,结果是 VIP4 30 天。 - 购买更低等级由后端拒绝,错误 `VIP_DOWNGRADE_NOT_ALLOWED`。 -- 余额不足返回 `INSUFFICIENT_BALANCE`;等级未启用返回 `VIP_LEVEL_DISABLED`。 +- 余额不足返回 `VIP_INSUFFICIENT_COIN`;套餐不存在、停用或不可购买返回 `VIP_PACKAGE_NOT_PURCHASABLE`;体系停用返回 `VIP_PROGRAM_INACTIVE`。 - 如果购买时仍佩戴体验卡,购买结果会写入 `paid_vip`,但当前展示仍可能是 `effective_source="trial"`。购买成功页也必须以返回的 `state.effective_vip` 为准。 +- 购买成功后用 `coin_balance.version` 防止旧响应覆盖新余额。`version=0` 只可能来自升级前的历史幂等订单,必须刷新钱包余额。 ## 9. 体验卡背包列表 @@ -660,17 +722,73 @@ Flutter 收到后用 `event_id` 做会话内去重,再调 `GET /vip/me`。不 "program_config": { "app_code": "fami", "program_type": "tiered_privilege_v1", - "trial_card_enabled": true + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", + "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", + "config_version": 7 }, "state": { - "effective_source": "trial", + "paid_vip": null, + "equipped_trial_card": { + "trial_card_id": "vip_trial_card_b", + "entitlement_id": "ent_card_b", + "resource_id": 102, + "user_id": 10001, + "level": 4, + "name": "VIP4", + "status": "active", + "equipped": true, + "duration_ms": 1728000000, + "effective_at_ms": 1780200000000, + "expires_at_ms": 1781928000000, + "remaining_duration_ms": 1700000000, + "grant_source": "admin_grant", + "source_grant_id": "grant_b", + "created_at_ms": 1780200000000, + "updated_at_ms": 1780200000000 + }, "effective_vip": { + "user_id": 10001, "level": 4, "name": "VIP4", "active": true, - "expires_at_ms": 1781928000000 + "started_at_ms": 1780200000000, + "expires_at_ms": 1781928000000, + "updated_at_ms": 1780200000000, + "program_type": "tiered_privilege_v1", + "config_version": 7 }, - "effective_benefits": [] + "effective_source": "trial", + "effective_benefits": [], + "evaluated_at_ms": 1780228000000, + "program_config": { + "app_code": "fami", + "program_type": "tiered_privilege_v1", + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", + "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", + "config_version": 7 + }, + "user_settings": { + "app_code": "fami", + "user_id": 10001, + "room_entry_notice_enabled": true, + "online_global_notice_enabled": true, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, + "updated_at_ms": 0 + } }, "server_time_ms": 1780228000000 } @@ -710,22 +828,67 @@ Flutter 收到后用 `event_id` 做会话内去重,再调 `GET /vip/me`。不 "unequipped": true, "program_config": { "app_code": "fami", - "program_type": "tiered_privilege_v1" + "program_type": "tiered_privilege_v1", + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", + "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", + "config_version": 7 }, "state": { - "effective_source": "paid", "paid_vip": { + "user_id": 10001, "level": 3, "name": "VIP3", - "active": true + "active": true, + "started_at_ms": 1780000000000, + "expires_at_ms": 1782592000000, + "updated_at_ms": 1780000000000, + "program_type": "tiered_privilege_v1", + "config_version": 7 }, "equipped_trial_card": null, "effective_vip": { + "user_id": 10001, "level": 3, "name": "VIP3", - "active": true + "active": true, + "started_at_ms": 1780000000000, + "expires_at_ms": 1782592000000, + "updated_at_ms": 1780000000000, + "program_type": "tiered_privilege_v1", + "config_version": 7 }, - "effective_benefits": [] + "effective_source": "paid", + "effective_benefits": [], + "evaluated_at_ms": 1780229000000, + "program_config": { + "app_code": "fami", + "program_type": "tiered_privilege_v1", + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", + "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", + "config_version": 7 + }, + "user_settings": { + "app_code": "fami", + "user_id": 10001, + "room_entry_notice_enabled": true, + "online_global_notice_enabled": true, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, + "updated_at_ms": 0 + } }, "server_time_ms": 1780229000000 } @@ -898,7 +1061,7 @@ Fami(`trial_card`)发一张 30 天体验卡到背包,不自动佩戴: - 用 `event_id` 去重。重连/重复 Join 仍可收到进房展示事件,不要以本地 presence 猜测是否展示。 - 进房 VIP 查询失败时后端会降级为普通进房,不阻断进房主链路。 -## 14. VIP 通知开关 +## 14. VIP 功能开关 ### 接口地址 @@ -909,17 +1072,15 @@ Fami(`trial_card`)发一张 30 天体验卡到背包,不自动佩戴: ### 参数 -GET 无参数。PATCH 是局部更新,两个字段至少传一个;显式 `false` 不能被当成未传: +GET 无参数。PATCH 是局部更新,5 个字段至少传一个;显式 `false` 不能被当成未传: ```json { - "room_entry_notice_enabled": false -} -``` - -```json -{ - "online_global_notice_enabled": true + "room_entry_notice_enabled": false, + "online_global_notice_enabled": true, + "hide_profile_data_enabled": false, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true } ``` @@ -940,6 +1101,9 @@ GET: "user_id": 10001, "room_entry_notice_enabled": true, "online_global_notice_enabled": false, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, "updated_at_ms": 1780300000000 }, "evaluated_at_ms": 1780300001000 @@ -960,6 +1124,9 @@ PATCH: "user_id": 10001, "room_entry_notice_enabled": false, "online_global_notice_enabled": false, + "hide_profile_data_enabled": false, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, "updated_at_ms": 1780300010000 }, "server_time_ms": 1780300010000 @@ -967,7 +1134,7 @@ PATCH: } ``` -用户从未修改过时,两项默认为 `true`,`updated_at_ms=0`。PATCH 只改本次提交的字段,不会覆盖另一项。 +用户从未修改过时,5 项均默认为 `true`,`updated_at_ms=0`。PATCH 只改本次提交的字段,不会覆盖其他项。 ### 相关 IM @@ -978,6 +1145,7 @@ PATCH: - 开关是用户偏好,不是权益;无 VIP 时也可保存,但不会产生进房或上线播报。 - 进房展示必须同时满足 `effective_benefits` 含 `room_entry_notice` 且 `room_entry_notice_enabled=true`。 - 上线展示必须同时满足 `online_global_notice` 权益和开关;最终以触发接口的服务端判定为准。 +- 隐藏个人数据、匿名访问主页、榜单隐身分别要求对应权益和同名开关同时成立;客户端不得只看设置值授权。 - 设置按 App 隔离,Fami 的修改不影响 Lalu。 ## 15. 每日 VIP 金币返现 @@ -1246,7 +1414,7 @@ Flutter 先把 `action_param` 再解析为 JSON。点击前可将页面上所有 - 同进程同 UTC 日首次进入 App,且 `/vip/me` 中具有 `online_global_notice` 资格、`online_global_notice_enabled=true` 时触发。成功后当日不再产生新 command。 - 超时/5xx 可用同一 `command_id` 重试;不能在同进程同日换 command 规避幂等。 - 杀掉进程后内存清空,新进程产生新 `process_boot_id` 和 `command_id`,同日可再播一次,这是已确认产品规则。 -- 权益过期、后台停用、体验卡不允许或用户关闭开关时,服务端返回 `PERMISSION_DENIED`;Flutter 不做本地飘屏,可重拉 `/vip/me`。 +- 权益过期、后台停用、体验卡不允许或用户关闭开关时,服务端返回 `VIP_BENEFIT_REQUIRED`;Flutter 不做本地飘屏,可重拉 `/vip/me`。 - 不把“每日一次”放到服务端持久化去重,否则无法满足杀进程后同日可再展示。 ## 17. 权益场景和完成边界 @@ -1258,10 +1426,10 @@ Flutter 先把 `action_param` 再解析为 JSON。点击前可将页面上所有 | 同级续期 | 不在本地计算,以响应为准 | 已强制,从旧截止时间累加 | | 体验卡自由切换 | 按 `entitlement_id` 佩戴,每次直接替换全局 `state` | 已强制,不限等级,不作废旧卡,不改绝对截止时间 | | 体验卡权益 | 只渲染 `effective_benefits` 资格 | 已强制过滤 `trial_enabled=false`,金币返现不给体验卡 | -| VIP 通知开关 | 读 `state.user_settings`,用 PATCH 局部修改 | 已按 App 持久化,无记录默认两项开启,开关不单独授权 | +| VIP 功能开关 | 读 `state.user_settings`,用 PATCH 局部修改 | 已按 App 持久化,无记录默认 5 项开启,开关不单独授权 | | 进房高亮通知 | 本人首屏读 Join HTTP `effective_vip`,其他成员消费当前房间 `room_user_joined` | 后端已同时校验权益与用户开关,并透传 Join HTTP 和当前房间 IM;不发全服 | -| 防踢 | 房管操作返回 `PERMISSION_DENIED` 时显示不可踢,不做本地预判 | 已在 room-service 强制;只阻止普通房主/管理员的 KickUser | -| 防禁言 | 房管操作返回 `PERMISSION_DENIED` 时显示不可禁言,解禁始终可发起 | 已在 room-service 强制;只阻止普通房主/管理员新增禁言 | +| 防踢 | 房管操作返回 `VIP_ANTI_KICK` 时显示不可踢,不做本地预判 | 已在 room-service 强制;只阻止普通房主/管理员的 KickUser | +| 防禁言 | 房管操作返回 `VIP_ANTI_MUTE` 时显示不可禁言,解禁始终可发起 | 已在 room-service 强制;只阻止普通房主/管理员新增禁言 | | 平台封禁/风控/超级管理 | Flutter 不作 VIP 免责提示 | 独立 `SystemEvictUser` 治理链路会绕过防踢,VIP 不能绕过平台治理 | | 自定义房间背景 | 按现有房间背景页面调用 | `tiered_privilege_v1` 已由 room-service 强制校验 `custom_room_background` | | 金币返现 | 消费 system inbox,批量恢复状态,手动领取后使用响应余额 | 已完成 UTC 日资格、异步系统消息、24 小时窗口和 wallet 原子入账;体验卡不参与 | @@ -1277,4 +1445,4 @@ Flutter 先把 `action_param` 再解析为 JSON。点击前可将页面上所有 5. 所有房间、个人页和消息展示都使用 `effective_vip`,不要用 `paid_vip` 覆盖体验卡展示。 6. system inbox 遇到 `action_type=vip_coin_rebate_claim` 时,批量用 statuses 恢复真实状态;领取成功后替换余额与返现状态。 7. IM 登录并加入全局播报群后,按进程内 UTC 日规则触发 `/vip/online-notice`;收到 `vip_online_notice` 时用 `event_id` 去重。 -8. 修改通知开关成功后,用 PATCH 响应替换本地 `user_settings`,不修改 `effective_benefits`。 +8. 修改 VIP 功能开关成功后,用 PATCH 响应替换本地 `user_settings`,不修改 `effective_benefits`。 diff --git a/docs/VIP系统架构.md b/docs/VIP系统架构.md index 5b82fe60..77254a7a 100644 --- a/docs/VIP系统架构.md +++ b/docs/VIP系统架构.md @@ -85,12 +85,19 @@ Fami 的 VIP1–VIP9 初始全部为 `disabled`。30 天只是不产生零时长 ### 用户功能开关 -`user_vip_settings` 按 `(app_code,user_id)` 保存两个用户偏好: +`user_vip_settings` 按 `(app_code,user_id)` 保存 5 个用户偏好: - `room_entry_notice_enabled`:进房 VIP 高亮通知; -- `online_global_notice_enabled`:上线 VIP 全服飘屏。 +- `online_global_notice_enabled`:上线 VIP 全服飘屏; +- `hide_profile_data_enabled`:隐藏个人数据; +- `anonymous_profile_visit_enabled`:匿名访问主页; +- `leaderboard_invisible_enabled`:榜单隐身。 -无记录时两项默认为 `true`,首次修改才写行。偏好不授予权益:无 VIP 或对应等级没有权益时,即使开关为开也不会放行。用户可在无 VIP、VIP 过期或切换体验卡时保留偏好;Fami 与 Lalu 互不影响。 +无记录时 5 项默认为 `true`,首次修改才写行。偏好不授予权益:无 VIP 或对应等级没有权益时,即使开关为开也不会放行。用户可在无 VIP、VIP 过期或切换体验卡时保留偏好;Fami 与 Lalu 互不影响。 + +### 权益展示投影 + +`vip_level_benefits.metadata_json.presentation` 是后台保存的扩展配置,wallet 在读取时解析并校验,跨服务和 HTTP 统一投影为结构化 `presentation`。它包含说明文案、多项预览、`image/animation` 媒体类型、`none/user_avatar_center/user_avatar_waveform` 合成方式,以及 `once/daily/monthly` 数值奖励。Flutter 只消费结构化字段,不按 `benefit_code` 解释原始 JSON;未知扩展因此不会变成客户端隐形配置表。 ### 每日金币返现 @@ -115,8 +122,11 @@ Fami 的 VIP1–VIP9 初始全部为 `disabled`。30 天只是不产生零时长 - `effective_vip`:最终展示和权限使用的 VIP; - `effective_source`:`paid`、`trial` 或 `none`; - `effective_benefits`:最终身份拥有的权益资格;可关闭的通知类权益还要与 `user_settings` 做 AND; +- `evaluated_at_ms`:本次状态合并的服务端时间; - `program_config`:当前 App 规则; -- `user_settings`:当前 App 下的进房/上线通知偏好。 +- `user_settings`:当前 App 下完整的 5 项功能偏好。 + +`GET /vip/me`、`GET /vip/packages`、购买、佩戴体验卡和卸下体验卡返回完全同构的 `VipState`。写接口成功响应不是 patch;Flutter 必须原子替换 store。灰度期间如旧节点返回缺字段状态,客户端应丢弃该局部对象并重拉 `/vip/me`。 program 启用时的优先级固定为:`trial_card_enabled=true` 且体验卡有效并已佩戴时覆盖展示;否则使用有效付费会员;二者都无效则为 `none`。关闭体验卡开关只停止它参与最终权限合并,背包和佩戴原始事实仍保留;关闭整个 program 时付费与卡片原始事实仍可查询,但最终身份为 `none`、不下发权益。 @@ -156,8 +166,8 @@ program 启用时的优先级固定为:`trial_card_enabled=true` 且体验卡 - `GET /vip/packages`:program、可购买等级、每级权益和完整 state。 - `GET /vip/me`:付费、体验卡、最终 VIP、权益资格和用户功能开关。 -- `POST /vip/purchase`:请求 `{command_id,level}`;价格、时长和资源由服务端读取。 -- `GET|PATCH /vip/settings`:读取或局部修改进房/上线通知偏好;空 PATCH 拒绝。 +- `POST /vip/purchase`:请求 `{command_id,level}`;价格、时长和资源由服务端读取;响应 `coin_balance` 是带 `version` 的账后快照,旧 `coin_balance_after` 仅兼容历史客户端。 +- `GET|PATCH /vip/settings`:读取或局部修改 5 项 VIP 功能偏好;空 PATCH 拒绝。 - `POST /vip/online-notice`:请求 `{command_id}`,服务端校验权益、开关和资料后写全服播报 outbox。 - `GET /vip/coin-rebates/current`:返回当前 UTC 日返现资格;无资格或 cron 尚未生成时 `found=false`。 - `POST /vip/coin-rebates/statuses`:按最多 100 个 `rebate_ids` 恢复系统消息真实状态,或用受限分页查历史。 @@ -175,6 +185,8 @@ program 启用时的优先级固定为:`trial_card_enabled=true` 且体验卡 - `POST /v1/admin/activity/vip-trial-card-grants` - `POST /v1/admin/activity/vip-grants`,仅用于 `direct_membership` program +App 业务错误通过 xerr 目录稳定映射:购买使用 `VIP_PROGRAM_INACTIVE`、`VIP_PACKAGE_NOT_PURCHASABLE`、`VIP_DOWNGRADE_NOT_ALLOWED`、`VIP_RECHARGE_REQUIRED`、`VIP_INSUFFICIENT_COIN`;体验卡使用 `VIP_TRIAL_CARD_NOT_FOUND`、`VIP_TRIAL_CARD_EXPIRED`;权益拒绝使用 `VIP_BENEFIT_REQUIRED`,普通房管防踢/防禁言分别使用 `VIP_ANTI_KICK`、`VIP_ANTI_MUTE`;返现使用 `VIP_COIN_REBATE_NOT_FOUND`、`VIP_COIN_REBATE_EXPIRED`。客户端不得解析 message。返现重复领取沿用幂等成功回执,不额外定义 already-claimed 错误。 + ## 权益落地边界 当前服务端已强制执行购买有效期、体验卡状态合并、用户通知开关、每日金币返现资格/消息/手动领取入账、自定义房间背景、防踢、防禁言、房内进场通知和上线全服通知。装扮类权益由已绑定的钱包资源与 Flutter 展示链路执行。 diff --git a/docs/新版VIP权限策略_Flutter客户端对接文档.md b/docs/新版VIP权限策略_Flutter客户端对接文档.md index 6ebc57ab..959a54f1 100644 --- a/docs/新版VIP权限策略_Flutter客户端对接文档.md +++ b/docs/新版VIP权限策略_Flutter客户端对接文档.md @@ -35,6 +35,20 @@ class VipEntitlementState { return has('online_global_notice') && settings.onlineGlobalNoticeEnabled; } + + bool allowsHideProfileData() { + return has('hide_profile_data') && settings.hideProfileDataEnabled; + } + + bool allowsAnonymousProfileVisit() { + return has('anonymous_profile_visit') && + settings.anonymousProfileVisitEnabled; + } + + bool allowsLeaderboardInvisible() { + return has('leaderboard_invisible') && + settings.leaderboardInvisibleEnabled; + } } ``` @@ -52,6 +66,8 @@ class VipEntitlementState { | `config_version` 不一致 | 用户权限以 `/vip/me.state` 为准,重新请求 `/vip/packages`,不得合并不同版本的权益数组 | | VIP 到期倒计时结束 | 可更新页面显示,但执行功能前仍以服务端接口结果为准 | +购买、佩戴体验卡、卸下体验卡成功时,`data.state` 与 `GET /api/v1/vip/me` 的 `data.state` **完全同构**,不是局部补丁。Flutter 必须整体替换 VIP store,禁止按字段合并。若灰度期间收到缺少下文任一顶层字段的旧节点响应,应放弃该响应中的 `state` 并立即重拉 `/vip/me`。 + ### 1.4 Fami 初始等级权益 下表只用于识别初始配置。Flutter 实际必须渲染 `/vip/packages` 返回的数据。 @@ -89,6 +105,52 @@ class VipEntitlementState { - `request_id` 只用于链路追踪。 - 带 `command_id` 的接口,网络重试必须复用原 `command_id`;新的业务动作必须生成新值。 +### 2.1 `VipState` 唯一结构 + +`GET /vip/me`、`GET /vip/packages`、购买、佩戴体验卡和卸下体验卡返回的 `state` 使用同一结构,固定包含以下 8 个顶层字段: + +| 字段 | 含义 | +|---|---| +| `paid_vip` | 当前付费 VIP;没有时为 `null` | +| `equipped_trial_card` | 当前佩戴的体验卡;没有时为 `null` | +| `effective_vip` | 付费 VIP 与体验卡合并后的当前展示身份 | +| `effective_source` | `paid`、`trial`、`none` | +| `effective_benefits` | 服务端计算后的最终权益集合 | +| `evaluated_at_ms` | 本次状态计算时间 | +| `program_config` | 本次计算使用的完整 App VIP 配置 | +| `user_settings` | 当前 App 下完整的 5 项用户开关 | + +`paid_vip`、`effective_vip` 非空时都返回完整 `UserVip` 字段:`user_id`、`level`、`name`、`active`、`started_at_ms`、`expires_at_ms`、`updated_at_ms`、`program_type`、`config_version`。`program_config` 返回完整字段,不允许写接口只返回 `app_code/program_type` 的裁剪对象。 + +### 2.2 特权展示协议 + +每项 `VipBenefit` 除执行字段外提供结构化 `presentation`,Flutter 不解析 `metadata_json`,也不按 `benefit_code` 维护隐形展示配置: + +```json +{ + "description": "进入房间时展示动态背景和用户头像", + "preview_items": [ + { + "preview_id": "room_background_gold", + "title": "鎏金房间背景", + "media_type": "animation", + "asset_url": "https://cdn.example/vip/room-bg.png", + "preview_url": "https://cdn.example/vip/room-bg-preview.png", + "animation_url": "https://cdn.example/vip/room-bg.svga", + "composition_type": "user_avatar_center", + "sort_order": 10 + } + ], + "numeric_reward": null +} +``` + +- `media_type`:`image` 或 `animation`;动态资源加载失败时依次使用 `preview_url`、`asset_url`。 +- `composition_type`:`none`、`user_avatar_center`、`user_avatar_waveform`,分别表示无合成、头像居中、头像与声波纹合成。 +- `preview_items` 可返回多项,按 `sort_order` 升序、`preview_id` 去重。 +- `numeric_reward` 用于数值权益,字段为 `label/value/unit/period`,其中 `period` 为 `once/daily/monthly`。该字段只用于展示,结算仍以后端接口为准。 +- 没有富预览时 `presentation` 为 `null`;Flutter 回退展示权益名称和通用图标。 + ## 3. VIP 套餐 ### 接口地址 @@ -144,7 +206,23 @@ class VipEntitlementState { "execution_scope": "room", "auto_equip": false, "sort_order": 110, - "metadata_json": "" + "metadata_json": "", + "presentation": { + "description": "进入房间时展示动态背景和用户头像", + "preview_items": [ + { + "preview_id": "room_background_gold", + "title": "鎏金房间背景", + "media_type": "animation", + "asset_url": "https://cdn.example/vip/room-bg.png", + "preview_url": "https://cdn.example/vip/room-bg-preview.png", + "animation_url": "https://cdn.example/vip/room-bg.svga", + "composition_type": "user_avatar_center", + "sort_order": 10 + } + ], + "numeric_reward": null + } } ], "config_version": 7 @@ -165,17 +243,27 @@ class VipEntitlementState { }, "state": { "paid_vip": { + "user_id": 10001, "level": 3, "name": "VIP3", "active": true, - "expires_at_ms": 1782592000000 + "started_at_ms": 1780000000000, + "expires_at_ms": 1782592000000, + "updated_at_ms": 1780000000000, + "program_type": "tiered_privilege_v1", + "config_version": 7 }, "equipped_trial_card": null, "effective_vip": { + "user_id": 10001, "level": 3, "name": "VIP3", "active": true, - "expires_at_ms": 1782592000000 + "started_at_ms": 1780000000000, + "expires_at_ms": 1782592000000, + "updated_at_ms": 1780000000000, + "program_type": "tiered_privilege_v1", + "config_version": 7 }, "effective_source": "paid", "effective_benefits": [ @@ -185,9 +273,27 @@ class VipEntitlementState { } ], "evaluated_at_ms": 1780000000000, + "program_config": { + "app_code": "fami", + "program_type": "tiered_privilege_v1", + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", + "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", + "config_version": 7 + }, "user_settings": { + "app_code": "fami", + "user_id": 10001, "room_entry_notice_enabled": true, "online_global_notice_enabled": true, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, "updated_at_ms": 0 } } @@ -235,11 +341,13 @@ class VipEntitlementState { }, "state": { "paid_vip": { + "user_id": 10001, "level": 3, "name": "VIP3", "active": true, "started_at_ms": 1780000000000, "expires_at_ms": 1782592000000, + "updated_at_ms": 1780000000000, "program_type": "tiered_privilege_v1", "config_version": 7 }, @@ -255,14 +363,20 @@ class VipEntitlementState { "duration_ms": 2592000000, "effective_at_ms": 1780100000000, "expires_at_ms": 1782692000000, - "remaining_duration_ms": 2591000000 + "remaining_duration_ms": 2591000000, + "grant_source": "admin", + "source_grant_id": "grant_xxx", + "created_at_ms": 1780100000000, + "updated_at_ms": 1780100000000 }, "effective_vip": { + "user_id": 10001, "level": 8, "name": "VIP8", "active": true, "started_at_ms": 1780100000000, "expires_at_ms": 1782692000000, + "updated_at_ms": 1780100000000, "program_type": "tiered_privilege_v1", "config_version": 7 }, @@ -279,13 +393,22 @@ class VipEntitlementState { "execution_scope": "room", "auto_equip": false, "sort_order": 290, - "metadata_json": "" + "metadata_json": "", + "presentation": null } ], "evaluated_at_ms": 1780101000000, "program_config": { "app_code": "fami", "program_type": "tiered_privilege_v1", + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", + "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", "config_version": 7 }, "user_settings": { @@ -293,6 +416,9 @@ class VipEntitlementState { "user_id": 10001, "room_entry_notice_enabled": true, "online_global_notice_enabled": true, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, "updated_at_ms": 0 } } @@ -346,35 +472,81 @@ class VipEntitlementState { }, "coin_spent": 4000, "coin_balance_after": 6000, + "coin_balance": { + "asset_type": "COIN", + "available_amount": 6000, + "frozen_amount": 0, + "version": 18 + }, "reward_items": [], "program_config": { "app_code": "fami", "program_type": "tiered_privilege_v1", + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", "config_version": 7 }, "state": { - "effective_source": "paid", "paid_vip": { + "user_id": 10001, "level": 4, "name": "VIP4", "active": true, - "expires_at_ms": 1782692000000 + "started_at_ms": 1780100000000, + "expires_at_ms": 1782692000000, + "updated_at_ms": 1780100000000, + "program_type": "tiered_privilege_v1", + "config_version": 7 }, "equipped_trial_card": null, "effective_vip": { + "user_id": 10001, "level": 4, "name": "VIP4", "active": true, - "expires_at_ms": 1782692000000 + "started_at_ms": 1780100000000, + "expires_at_ms": 1782692000000, + "updated_at_ms": 1780100000000, + "program_type": "tiered_privilege_v1", + "config_version": 7 }, + "effective_source": "paid", "effective_benefits": [ { "benefit_code": "custom_room_background", "status": "active" } ], - "evaluated_at_ms": 1780100000000 + "evaluated_at_ms": 1780100000000, + "program_config": { + "app_code": "fami", + "program_type": "tiered_privilege_v1", + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", + "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", + "config_version": 7 + }, + "user_settings": { + "app_code": "fami", + "user_id": 10001, + "room_entry_notice_enabled": true, + "online_global_notice_enabled": true, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, + "updated_at_ms": 0 + } } } } @@ -384,6 +556,8 @@ class VipEntitlementState { - Fami 高等级购买:立即替换低等级,从购买时间重新计时,低等级剩余时间丢弃。 - 低等级购买:服务端拒绝。 - 购买时佩戴体验卡:付费 VIP 会更新,但 `effective_vip` 仍可能来自体验卡,必须使用响应 `state`。 +- 钱包缓存只使用 `coin_balance`。应用时比较 `version`,低版本响应不得覆盖本地高版本余额;`coin_balance_after` 仅保留旧客户端兼容。 +- 历史幂等订单在升级前没有记录版本时可能返回 `coin_balance.version=0`,Flutter 必须调用现有钱包余额接口刷新,不得把版本 0 写入余额 store。 ## 6. VIP 体验卡 @@ -473,22 +647,92 @@ class VipEntitlementState { "duration_ms": 1728000000, "effective_at_ms": 1780200000000, "expires_at_ms": 1781928000000, - "remaining_duration_ms": 1700000000 + "remaining_duration_ms": 1700000000, + "grant_source": "admin", + "source_grant_id": "grant_b", + "created_at_ms": 1780200000000, + "updated_at_ms": 1780200000000 }, "program_config": { "app_code": "fami", "program_type": "tiered_privilege_v1", - "trial_card_enabled": true + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", + "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", + "config_version": 7 }, "state": { - "effective_source": "trial", + "paid_vip": { + "user_id": 10001, + "level": 3, + "name": "VIP3", + "active": true, + "started_at_ms": 1780000000000, + "expires_at_ms": 1782592000000, + "updated_at_ms": 1780000000000, + "program_type": "tiered_privilege_v1", + "config_version": 7 + }, + "equipped_trial_card": { + "trial_card_id": "vip_trial_card_b", + "entitlement_id": "ent_card_b", + "resource_id": 102, + "user_id": 10001, + "level": 4, + "name": "VIP4", + "status": "active", + "equipped": true, + "duration_ms": 1728000000, + "effective_at_ms": 1780200000000, + "expires_at_ms": 1781928000000, + "remaining_duration_ms": 1700000000, + "grant_source": "admin", + "source_grant_id": "grant_b", + "created_at_ms": 1780200000000, + "updated_at_ms": 1780200000000 + }, "effective_vip": { + "user_id": 10001, "level": 4, "name": "VIP4", "active": true, - "expires_at_ms": 1781928000000 + "started_at_ms": 1780200000000, + "expires_at_ms": 1781928000000, + "updated_at_ms": 1780200000000, + "program_type": "tiered_privilege_v1", + "config_version": 7 }, - "effective_benefits": [] + "effective_source": "trial", + "effective_benefits": [], + "evaluated_at_ms": 1780228000000, + "program_config": { + "app_code": "fami", + "program_type": "tiered_privilege_v1", + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", + "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", + "config_version": 7 + }, + "user_settings": { + "app_code": "fami", + "user_id": 10001, + "room_entry_notice_enabled": true, + "online_global_notice_enabled": true, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, + "updated_at_ms": 0 + } }, "server_time_ms": 1780228000000 } @@ -516,22 +760,67 @@ class VipEntitlementState { "unequipped": true, "program_config": { "app_code": "fami", - "program_type": "tiered_privilege_v1" + "program_type": "tiered_privilege_v1", + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", + "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", + "config_version": 7 }, "state": { - "effective_source": "paid", "paid_vip": { + "user_id": 10001, "level": 3, "name": "VIP3", - "active": true + "active": true, + "started_at_ms": 1780000000000, + "expires_at_ms": 1782592000000, + "updated_at_ms": 1780000000000, + "program_type": "tiered_privilege_v1", + "config_version": 7 }, "equipped_trial_card": null, "effective_vip": { + "user_id": 10001, "level": 3, "name": "VIP3", - "active": true + "active": true, + "started_at_ms": 1780000000000, + "expires_at_ms": 1782592000000, + "updated_at_ms": 1780000000000, + "program_type": "tiered_privilege_v1", + "config_version": 7 }, - "effective_benefits": [] + "effective_source": "paid", + "effective_benefits": [], + "evaluated_at_ms": 1780229000000, + "program_config": { + "app_code": "fami", + "program_type": "tiered_privilege_v1", + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", + "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", + "config_version": 7 + }, + "user_settings": { + "app_code": "fami", + "user_id": 10001, + "room_entry_notice_enabled": true, + "online_global_notice_enabled": true, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, + "updated_at_ms": 0 + } }, "server_time_ms": 1780229000000 } @@ -543,7 +832,7 @@ class VipEntitlementState { - 每张卡的剩余时间按绝对 `expires_at_ms` 计算。 - 佩戴和卸下成功后立即使用响应 `state` 替换本地 VIP 状态。 -## 7. VIP 通知开关 +## 7. VIP 功能开关 ### 7.1 查询开关 @@ -568,6 +857,9 @@ class VipEntitlementState { "user_id": 10001, "room_entry_notice_enabled": true, "online_global_notice_enabled": false, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, "updated_at_ms": 1780300000000 }, "evaluated_at_ms": 1780300001000 @@ -588,7 +880,10 @@ class VipEntitlementState { ```json { "room_entry_notice_enabled": false, - "online_global_notice_enabled": true + "online_global_notice_enabled": true, + "hide_profile_data_enabled": false, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true } ``` @@ -605,6 +900,9 @@ class VipEntitlementState { "user_id": 10001, "room_entry_notice_enabled": false, "online_global_notice_enabled": true, + "hide_profile_data_enabled": false, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, "updated_at_ms": 1780300010000 }, "server_time_ms": 1780300010000 @@ -612,6 +910,8 @@ class VipEntitlementState { } ``` +5 个字段都使用 PATCH 语义:不传表示保持原值,显式 `false` 表示关闭。没有持久记录时均默认 `true`;设置只限制用户主动启用对应效果,不会单独授予 VIP 权益。服务端执行时使用“当前有效权益 AND 用户设置”判定,Flutter 不能只看开关。 + ## 8. 每日 VIP 金币返现 ### 8.1 查询当前返现 @@ -1050,3 +1350,35 @@ Flutter 将 `action_param` 解析为 JSON,打开页面后调用 `/vip/coin-reb 接口:`GET /api/v1/im/usersig`。 Flutter 使用返回的 UserSig 登录腾讯云 IM,并加入响应 `join_groups` 中 `type=global_broadcast` 的群组后,再消费 `vip_online_notice`。 + +## 13. 稳定业务错误码 + +失败仍使用统一 envelope,Flutter 只判断 `code`,不得解析 `message`: + +```json +{ + "code": "VIP_INSUFFICIENT_COIN", + "message": "insufficient coin balance", + "request_id": "req_purchase", + "data": null +} +``` + +| HTTP | `code` | 场景 | Flutter 处理 | +|---:|---|---|---| +| 409 | `VIP_PROGRAM_INACTIVE` | 当前 App 的 VIP 体系未启用 | 关闭购买/体验卡操作并刷新配置 | +| 409 | `VIP_PACKAGE_NOT_PURCHASABLE` | 套餐不存在、停用或不在当前体系范围 | 刷新 `/vip/packages` | +| 409 | `VIP_DOWNGRADE_NOT_ALLOWED` | 购买等级低于当前付费等级 | 提示不可降级,刷新 `/vip/me` | +| 409 | `VIP_RECHARGE_REQUIRED` | 未达到套餐充值门槛 | 展示充值引导 | +| 409 | `VIP_INSUFFICIENT_COIN` | 购买金币余额不足 | 展示金币充值入口 | +| 404 | `VIP_TRIAL_CARD_NOT_FOUND` | 卡不存在、不属于本人或背包权益已撤销 | 刷新体验卡列表和 `/vip/me` | +| 409 | `VIP_TRIAL_CARD_EXPIRED` | 卡或背包权益已越过绝对截止时间 | 刷新体验卡列表和 `/vip/me` | +| 403 | `VIP_BENEFIT_REQUIRED` | 操作需要的权益未生效或被用户关闭 | 刷新 `/vip/me`,保持入口锁态 | +| 403 | `VIP_ANTI_KICK` | 普通房主/管理员踢人被目标用户防踢拦截 | 提示目标不可被踢出,不改变房间本地状态 | +| 403 | `VIP_ANTI_MUTE` | 普通房主/管理员禁言被目标用户防禁言拦截 | 提示目标不可被禁言,不改变房间本地状态 | +| 404 | `VIP_COIN_REBATE_NOT_FOUND` | 返现不存在或不属于当前用户/App | 刷新返现列表 | +| 409 | `VIP_COIN_REBATE_EXPIRED` | 已越过 UTC 次日 0 点的领取结束边界 | 标记过期并刷新返现列表 | +| 409 | `IDEMPOTENCY_CONFLICT` | 同一 `command_id` 被用于不同业务参数 | 停止重试,为新动作生成新 ID | +| 409 | `LEDGER_CONFLICT` | 钱包并发版本冲突 | 刷新余额和 `/vip/me` 后由用户重新发起 | + +返现重复领取采用幂等成功语义:相同 `command_id` 重试返回首次成功回执,不返回 `VIP_REBATE_ALREADY_CLAIMED`。因此客户端不应实现该错误分支。 diff --git a/pkg/xerr/catalog.go b/pkg/xerr/catalog.go index 3c1d954f..f116853e 100644 --- a/pkg/xerr/catalog.go +++ b/pkg/xerr/catalog.go @@ -84,6 +84,14 @@ var catalog = map[Code]Spec{ VIPLevelNotFound: spec(codes.NotFound, httpStatusNotFound, VIPLevelNotFound, "not found"), VIPLevelDisabled: spec(codes.FailedPrecondition, httpStatusConflict, VIPLevelDisabled, "vip level is disabled"), VIPDowngradeNotAllowed: spec(codes.FailedPrecondition, httpStatusConflict, VIPDowngradeNotAllowed, "vip downgrade is not allowed"), + VIPProgramInactive: spec(codes.FailedPrecondition, httpStatusConflict, VIPProgramInactive, "vip program is inactive"), + VIPPackageNotPurchasable: spec(codes.FailedPrecondition, httpStatusConflict, VIPPackageNotPurchasable, "vip package is not purchasable"), + VIPInsufficientCoin: spec(codes.FailedPrecondition, httpStatusConflict, VIPInsufficientCoin, "insufficient coin balance"), + VIPTrialCardNotFound: spec(codes.NotFound, httpStatusNotFound, VIPTrialCardNotFound, "vip trial card not found"), + VIPTrialCardExpired: spec(codes.FailedPrecondition, httpStatusConflict, VIPTrialCardExpired, "vip trial card expired"), + VIPBenefitRequired: spec(codes.PermissionDenied, httpStatusForbidden, VIPBenefitRequired, "vip benefit is required"), + VIPAntiKick: spec(codes.PermissionDenied, httpStatusForbidden, VIPAntiKick, "target user cannot be kicked"), + VIPAntiMute: spec(codes.PermissionDenied, httpStatusForbidden, VIPAntiMute, "target user cannot be muted"), VIPRechargeRequired: spec(codes.FailedPrecondition, httpStatusConflict, VIPRechargeRequired, "vip recharge is required"), VIPCoinRebateNotFound: spec(codes.NotFound, httpStatusNotFound, VIPCoinRebateNotFound, "vip coin rebate not found"), VIPCoinRebateExpired: spec(codes.FailedPrecondition, httpStatusConflict, VIPCoinRebateExpired, "vip coin rebate expired"), diff --git a/pkg/xerr/errors.go b/pkg/xerr/errors.go index 91d92bcc..ff7c1c9e 100644 --- a/pkg/xerr/errors.go +++ b/pkg/xerr/errors.go @@ -102,6 +102,22 @@ const ( VIPLevelDisabled Code = "VIP_LEVEL_DISABLED" // VIPDowngradeNotAllowed 表示用户当前有效 VIP 等级高于本次购买目标等级。 VIPDowngradeNotAllowed Code = "VIP_DOWNGRADE_NOT_ALLOWED" + // VIPProgramInactive 表示当前 App 未启用可执行的 VIP 体系。 + VIPProgramInactive Code = "VIP_PROGRAM_INACTIVE" + // VIPPackageNotPurchasable 表示目标套餐不存在、停用或不在当前体系等级范围内。 + VIPPackageNotPurchasable Code = "VIP_PACKAGE_NOT_PURCHASABLE" + // VIPInsufficientCoin 表示 VIP 购买事务中的 COIN 余额不足。 + VIPInsufficientCoin Code = "VIP_INSUFFICIENT_COIN" + // VIPTrialCardNotFound 表示体验卡实例不存在、不属于当前用户或其背包权益已撤销。 + VIPTrialCardNotFound Code = "VIP_TRIAL_CARD_NOT_FOUND" + // VIPTrialCardExpired 表示体验卡或对应背包权益已越过绝对过期时间。 + VIPTrialCardExpired Code = "VIP_TRIAL_CARD_EXPIRED" + // VIPBenefitRequired 表示当前操作需要一项未生效或已被用户关闭的 VIP 权益。 + VIPBenefitRequired Code = "VIP_BENEFIT_REQUIRED" + // VIPAntiKick 表示普通房主/管理员踢人动作被目标用户的防踢权益拦截。 + VIPAntiKick Code = "VIP_ANTI_KICK" + // VIPAntiMute 表示普通房主/管理员禁言动作被目标用户的防禁言权益拦截。 + VIPAntiMute Code = "VIP_ANTI_MUTE" // VIPRechargeRequired 表示目标 VIP 等级需要用户先达到累计充值门槛。 VIPRechargeRequired Code = "VIP_RECHARGE_REQUIRED" // VIPCoinRebateNotFound 表示返现不存在,或不属于当前 App/用户。 diff --git a/pkg/xerr/grpc_test.go b/pkg/xerr/grpc_test.go index a569bfa5..7af1eee5 100644 --- a/pkg/xerr/grpc_test.go +++ b/pkg/xerr/grpc_test.go @@ -135,6 +135,14 @@ func TestCatalogMappings(t *testing.T) { publicCode: string(CPAlreadyExistsOther), publicMessage: "The other party already has a CP", }, + { + name: "vip anti kick keeps actionable forbidden code", + code: VIPAntiKick, + grpcCode: codes.PermissionDenied, + httpStatus: httpStatusForbidden, + publicCode: string(VIPAntiKick), + publicMessage: "target user cannot be kicked", + }, { name: "region mismatch keeps forbidden transport but exposes concrete app prompt", code: RegionMismatch, diff --git a/scripts/apply-local-mysql-initdb.sh b/scripts/apply-local-mysql-initdb.sh index c23340d5..e919be9f 100755 --- a/scripts/apply-local-mysql-initdb.sh +++ b/scripts/apply-local-mysql-initdb.sh @@ -29,12 +29,12 @@ SQL_FILES=( ) # initdb only creates missing tables; a reused local MySQL volume therefore -# needs incremental migrations as well. Keep the owned user-service migrations -# in this local bootstrap path so authentication schema changes (for example -# login_audit client-version fields) cannot leave a locally logged-in client -# receiving a misleading 401 during token refresh. -USER_MIGRATION_FILES=( +# needs incremental migrations as well. Keep owner migrations here when reused +# volumes need more than CREATE TABLE IF NOT EXISTS can provide; otherwise new +# binaries may start against a structurally valid-looking but stale table. +INCREMENTAL_MIGRATION_FILES=( "services/user-service/deploy/mysql/migrations/012_login_audit_client_version.sql" + "services/wallet-service/deploy/mysql/migrations/002_resource_shop_price_tiers.sql" ) # Keep MySQL as the only required dependency for this script. Business services @@ -71,7 +71,7 @@ for sql_file in "${SQL_FILES[@]}"; do docker compose exec -T mysql mysql --default-character-set=utf8mb4 -h 127.0.0.1 -uroot -p"${MYSQL_ROOT_PASSWORD}" < "${sql_file}" done -for sql_file in "${USER_MIGRATION_FILES[@]}"; do +for sql_file in "${INCREMENTAL_MIGRATION_FILES[@]}"; do if [[ ! -f "${sql_file}" ]]; then printf 'skipping mysql migration because it is absent or not a regular file: %s\n' "${sql_file}" continue diff --git a/scripts/mysql/062_vip_user_settings.sql b/scripts/mysql/062_vip_user_settings.sql index 7b5f77d5..d4a72b3d 100644 --- a/scripts/mysql/062_vip_user_settings.sql +++ b/scripts/mysql/062_vip_user_settings.sql @@ -9,6 +9,9 @@ CREATE TABLE IF NOT EXISTS user_vip_settings ( user_id BIGINT NOT NULL COMMENT '用户 ID', room_entry_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启进房通知', online_global_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启上线全服通知', + hide_profile_data_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启隐藏个人数据', + anonymous_profile_visit_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启匿名访问主页', + leaderboard_invisible_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启榜单隐身', created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', PRIMARY KEY (app_code, user_id) diff --git a/scripts/mysql/067_vip_user_privacy_settings.sql b/scripts/mysql/067_vip_user_privacy_settings.sql new file mode 100644 index 00000000..c14f343e --- /dev/null +++ b/scripts/mysql/067_vip_user_privacy_settings.sql @@ -0,0 +1,33 @@ +SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE hyapp_wallet; + +-- user_vip_settings 以 (app_code,user_id) 为主键且新增列均为常量默认值。MySQL 8.4 可用 +-- ALGORITHM=INSTANT 仅修改数据字典,不扫描或重写历史行;MySQL 8.4 的 INSTANT 算法不接受 +-- 显式 LOCK 子句,因此只保留算法硬约束,避免语法失败且禁止静默退化为 COPY/INPLACE。 +SET @ddl := IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_settings' AND COLUMN_NAME = 'hide_profile_data_enabled') = 0, + 'ALTER TABLE user_vip_settings ADD COLUMN hide_profile_data_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT ''是否开启隐藏个人数据'', ALGORITHM=INSTANT', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @ddl := IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_settings' AND COLUMN_NAME = 'anonymous_profile_visit_enabled') = 0, + 'ALTER TABLE user_vip_settings ADD COLUMN anonymous_profile_visit_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT ''是否开启匿名访问主页'', ALGORITHM=INSTANT', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @ddl := IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_settings' AND COLUMN_NAME = 'leaderboard_invisible_enabled') = 0, + 'ALTER TABLE user_vip_settings ADD COLUMN leaderboard_invisible_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT ''是否开启榜单隐身'', ALGORITHM=INSTANT', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/server/admin/internal/integration/walletclient/client.go b/server/admin/internal/integration/walletclient/client.go index bbb5bb10..88ad2839 100644 --- a/server/admin/internal/integration/walletclient/client.go +++ b/server/admin/internal/integration/walletclient/client.go @@ -33,6 +33,7 @@ type Client interface { GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error) GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) RevokeResourceGrant(ctx context.Context, req *walletv1.RevokeResourceGrantRequest) (*walletv1.ResourceGrantResponse, error) + RevokeUserResource(ctx context.Context, req *walletv1.RevokeUserResourceRequest) (*walletv1.RevokeUserResourceResponse, error) EquipUserResource(ctx context.Context, req *walletv1.EquipUserResourceRequest) (*walletv1.EquipUserResourceResponse, error) ListResourceGrants(ctx context.Context, req *walletv1.ListResourceGrantsRequest) (*walletv1.ListResourceGrantsResponse, error) ListResourceShopItems(ctx context.Context, req *walletv1.ListResourceShopItemsRequest) (*walletv1.ListResourceShopItemsResponse, error) @@ -173,6 +174,10 @@ func (c *GRPCClient) RevokeResourceGrant(ctx context.Context, req *walletv1.Revo return c.client.RevokeResourceGrant(ctx, req) } +func (c *GRPCClient) RevokeUserResource(ctx context.Context, req *walletv1.RevokeUserResourceRequest) (*walletv1.RevokeUserResourceResponse, error) { + return c.client.RevokeUserResource(ctx, req) +} + func (c *GRPCClient) EquipUserResource(ctx context.Context, req *walletv1.EquipUserResourceRequest) (*walletv1.EquipUserResourceResponse, error) { return c.client.EquipUserResource(ctx, req) } diff --git a/server/admin/internal/model/models.go b/server/admin/internal/model/models.go index 9c68a347..a569cd45 100644 --- a/server/admin/internal/model/models.go +++ b/server/admin/internal/model/models.go @@ -46,9 +46,9 @@ const ( // use an isolated credential lifecycle and can never exchange this row for a main-admin JWT. type ExternalAdminAccount struct { ID uint64 `gorm:"primaryKey" json:"id"` - AppCode string `gorm:"size:32;uniqueIndex:uk_external_admin_accounts_app_username;uniqueIndex:uk_external_admin_accounts_app_linked_user;index:idx_external_admin_accounts_app_status_updated,priority:1;not null" json:"appCode"` + AppCode string `gorm:"size:32;uniqueIndex:uk_external_admin_accounts_app_linked_user;index:idx_external_admin_accounts_app_status_updated,priority:1;not null" json:"appCode"` LinkedAppUserID int64 `gorm:"column:linked_app_user_id;uniqueIndex:uk_external_admin_accounts_app_linked_user;not null" json:"-"` - Username string `gorm:"size:64;uniqueIndex:uk_external_admin_accounts_app_username;not null" json:"username"` + Username string `gorm:"size:64;uniqueIndex:uk_external_admin_accounts_username;not null" json:"username"` PasswordHash string `gorm:"size:255;not null" json:"-"` PermissionsJSON string `gorm:"column:permissions_json;type:json;not null" json:"-"` PermissionRevision uint64 `gorm:"column:permission_revision;not null;default:1" json:"permissionRevision"` diff --git a/server/admin/internal/modules/resource/handler.go b/server/admin/internal/modules/resource/handler.go index ddb0f3c3..e8ddd647 100644 --- a/server/admin/internal/modules/resource/handler.go +++ b/server/admin/internal/modules/resource/handler.go @@ -827,6 +827,48 @@ func (h *Handler) RevokeResourceGrant(c *gin.Context) { response.OK(c, grant) } +func (h *Handler) RevokeUserResource(c *gin.Context) { + userID, err := strconv.ParseInt(strings.TrimSpace(c.Param("user_id")), 10, 64) + if err != nil || userID <= 0 { + response.BadRequest(c, "用户 ID 不正确") + return + } + entitlementID := strings.TrimSpace(c.Param("entitlement_id")) + if entitlementID == "" { + response.BadRequest(c, "用户素材权益不存在") + return + } + var req revokeUserResourceRequest + if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Reason) == "" { + response.BadRequest(c, "请填写素材撤回原因") + return + } + + resp, err := h.wallet.RevokeUserResource(c.Request.Context(), &walletv1.RevokeUserResourceRequest{ + RequestId: middleware.CurrentRequestID(c), + AppCode: appctx.FromContext(c.Request.Context()), + UserId: userID, + EntitlementId: entitlementID, + Reason: strings.TrimSpace(req.Reason), + OperatorUserId: actorID(c), + }) + if err != nil { + response.BadRequest(c, err.Error()) + return + } + resource := resp.GetResource() + // 审计对象使用 entitlement 而不是 source grant:同一资源组发放可包含多个素材,必须能追溯本次只撤回了哪一个。 + h.auditLog(c, "revoke-user-resource", "user_resource_entitlements", entitlementID, "success", fmt.Sprintf("target_user_id=%d resource_id=%d", userID, resource.GetResourceId())) + response.OK(c, gin.H{ + "entitlementId": resource.GetEntitlementId(), + "resourceId": resource.GetResourceId(), + "status": resource.GetStatus(), + "remainingQuantity": resource.GetRemainingQuantity(), + "equipped": resource.GetEquipped(), + "updatedAtMs": resource.GetUpdatedAtMs(), + }) +} + func (h *Handler) LookupResourceGrantTarget(c *gin.Context) { keyword := strings.TrimSpace(firstQuery(c, "user_id", "userId", "keyword")) if keyword == "" { diff --git a/server/admin/internal/modules/resource/handler_test.go b/server/admin/internal/modules/resource/handler_test.go index 66d6d18e..268e0bdc 100644 --- a/server/admin/internal/modules/resource/handler_test.go +++ b/server/admin/internal/modules/resource/handler_test.go @@ -409,6 +409,29 @@ func TestListResourceGrantsResolvesDisplayIDBeforeWalletFilter(t *testing.T) { } } +func TestRevokeUserResourceTargetsOneEntitlement(t *testing.T) { + wallet := &mockResourceWallet{} + router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil)) + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodPost, "/admin/users/318705991371722752/resources/ent-host-badge/revoke", strings.NewReader(`{"reason":"remove host badge"}`)) + request.Header.Set("Content-Type", "application/json") + router.ServeHTTP(recorder, request) + + if recorder.Code != http.StatusOK { + t.Fatalf("revoke user resource status mismatch: %d %s", recorder.Code, recorder.Body.String()) + } + if len(wallet.revokeUserResourceRequests) != 1 { + t.Fatalf("expected one precise revoke request, got %d", len(wallet.revokeUserResourceRequests)) + } + req := wallet.revokeUserResourceRequests[0] + if req.GetUserId() != 318705991371722752 || req.GetEntitlementId() != "ent-host-badge" || req.GetReason() != "remove host badge" || req.GetOperatorUserId() != 7 { + t.Fatalf("precise revoke request mismatch: %+v", req) + } + if !strings.Contains(recorder.Body.String(), `"entitlementId":"ent-host-badge"`) || !strings.Contains(recorder.Body.String(), `"status":"revoked"`) { + t.Fatalf("precise revoke response mismatch: %s", recorder.Body.String()) + } +} + func newResourceHandlerTestRouter(handler *Handler) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() @@ -447,17 +470,18 @@ func newResourceHandlerTestRouter(handler *Handler) *gin.Engine { type mockResourceWallet struct { walletclient.Client - resources map[int64]*walletv1.Resource - resourceGroups map[int64]*walletv1.ResourceGroup - updates []*walletv1.UpdateResourceRequest - deletedResources []*walletv1.DeleteResourceRequest - batchDeletedResources []*walletv1.BatchDeleteResourcesRequest - batchCreatedGifts []*walletv1.BatchCreateGiftConfigsRequest - deletedGifts []*walletv1.DeleteGiftConfigRequest - listGrantRequests []*walletv1.ListResourceGrantsRequest - listResourceRequests []*walletv1.ListResourcesRequest - listedResources []*walletv1.Resource - grantRequests []*walletv1.GrantResourceRequest + resources map[int64]*walletv1.Resource + resourceGroups map[int64]*walletv1.ResourceGroup + updates []*walletv1.UpdateResourceRequest + deletedResources []*walletv1.DeleteResourceRequest + batchDeletedResources []*walletv1.BatchDeleteResourcesRequest + batchCreatedGifts []*walletv1.BatchCreateGiftConfigsRequest + deletedGifts []*walletv1.DeleteGiftConfigRequest + listGrantRequests []*walletv1.ListResourceGrantsRequest + listResourceRequests []*walletv1.ListResourcesRequest + listedResources []*walletv1.Resource + grantRequests []*walletv1.GrantResourceRequest + revokeUserResourceRequests []*walletv1.RevokeUserResourceRequest } func (m *mockResourceWallet) ListResources(_ context.Context, req *walletv1.ListResourcesRequest) (*walletv1.ListResourcesResponse, error) { @@ -473,6 +497,18 @@ func (m *mockResourceWallet) GrantResource(_ context.Context, req *walletv1.Gran }}, nil } +func (m *mockResourceWallet) RevokeUserResource(_ context.Context, req *walletv1.RevokeUserResourceRequest) (*walletv1.RevokeUserResourceResponse, error) { + m.revokeUserResourceRequests = append(m.revokeUserResourceRequests, req) + return &walletv1.RevokeUserResourceResponse{Resource: &walletv1.UserResourceEntitlement{ + EntitlementId: req.GetEntitlementId(), + UserId: req.GetUserId(), + ResourceId: 91, + Status: "revoked", + RemainingQuantity: 0, + Equipped: false, + }}, nil +} + func (m *mockResourceWallet) GetResource(ctx context.Context, req *walletv1.GetResourceRequest) (*walletv1.GetResourceResponse, error) { resource := m.resources[req.GetResourceId()] if resource == nil { diff --git a/server/admin/internal/modules/resource/request.go b/server/admin/internal/modules/resource/request.go index c10ae0be..b96e45eb 100644 --- a/server/admin/internal/modules/resource/request.go +++ b/server/admin/internal/modules/resource/request.go @@ -235,6 +235,10 @@ type grantGroupRequest struct { Reason string `json:"reason"` } +type revokeUserResourceRequest struct { + Reason string `json:"reason"` +} + type resourceShopItemsRequest struct { Items []resourceShopItemRequest `json:"items"` } @@ -244,6 +248,7 @@ type resourceShopItemRequest struct { ResourceID int64 `json:"resourceId"` Status string `json:"status"` DurationDays int32 `json:"durationDays"` + CoinPrice int64 `json:"coinPrice"` EffectiveFromMS int64 `json:"effectiveFromMs"` EffectiveToMS int64 `json:"effectiveToMs"` SortOrder int32 `json:"sortOrder"` @@ -558,6 +563,7 @@ func resourceShopItemInputs(items []resourceShopItemRequest) []*walletv1.Resourc ResourceId: item.ResourceID, Status: strings.TrimSpace(item.Status), DurationDays: item.DurationDays, + CoinPrice: item.CoinPrice, EffectiveFromMs: item.EffectiveFromMS, EffectiveToMs: item.EffectiveToMS, SortOrder: item.SortOrder, diff --git a/server/admin/internal/modules/resource/routes.go b/server/admin/internal/modules/resource/routes.go index 0c4b6e76..facddfe7 100644 --- a/server/admin/internal/modules/resource/routes.go +++ b/server/admin/internal/modules/resource/routes.go @@ -49,6 +49,7 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) { protected.POST("/admin/resource-grants/resource", middleware.RequirePermission("resource-grant:create"), h.GrantResource) protected.POST("/admin/resource-grants/group", middleware.RequirePermission("resource-grant:create"), h.GrantResourceGroup) protected.POST("/admin/resource-grants/:grant_id/revoke", middleware.RequirePermission("resource-grant:revoke"), h.RevokeResourceGrant) + protected.POST("/admin/users/:user_id/resources/:entitlement_id/revoke", middleware.RequirePermission("resource-grant:revoke"), h.RevokeUserResource) // VIP 赠送复用同一目标用户解析入口;仅有 vip-config:grant 的运营也必须能把短号解析成内部 user_id。 protected.GET("/admin/resource-grants/target", middleware.RequireAnyPermission("resource-grant:create", "vip-config:grant"), h.LookupResourceGrantTarget) protected.GET("/admin/resource-grants", middleware.RequirePermission("resource-grant:view"), h.ListResourceGrants) diff --git a/server/admin/internal/modules/vipconfig/handler.go b/server/admin/internal/modules/vipconfig/handler.go index 7c1395a3..cecbfe1d 100644 --- a/server/admin/internal/modules/vipconfig/handler.go +++ b/server/admin/internal/modules/vipconfig/handler.go @@ -120,20 +120,45 @@ type vipProgramConfigDTO struct { } type vipBenefitDTO struct { - BenefitCode string `json:"benefitCode"` - Name string `json:"name"` - BenefitType string `json:"benefitType"` - UnlockLevel int32 `json:"unlockLevel"` - Status string `json:"status"` - TrialEnabled bool `json:"trialEnabled"` - ResourceID int64 `json:"resourceId"` - ResourceType string `json:"resourceType"` - ExecutionScope string `json:"executionScope"` - AutoEquip bool `json:"autoEquip"` - SortOrder int32 `json:"sortOrder"` - MetadataJSON string `json:"metadataJson"` - CreatedAtMS int64 `json:"createdAtMs"` - UpdatedAtMS int64 `json:"updatedAtMs"` + BenefitCode string `json:"benefitCode"` + Name string `json:"name"` + BenefitType string `json:"benefitType"` + UnlockLevel int32 `json:"unlockLevel"` + Status string `json:"status"` + TrialEnabled bool `json:"trialEnabled"` + ResourceID int64 `json:"resourceId"` + ResourceType string `json:"resourceType"` + ExecutionScope string `json:"executionScope"` + AutoEquip bool `json:"autoEquip"` + SortOrder int32 `json:"sortOrder"` + MetadataJSON string `json:"metadataJson"` + CreatedAtMS int64 `json:"createdAtMs"` + UpdatedAtMS int64 `json:"updatedAtMs"` + Presentation *vipBenefitPresentationDTO `json:"presentation"` +} + +type vipBenefitPresentationDTO struct { + Description string `json:"description"` + PreviewItems []vipBenefitPreviewItemDTO `json:"previewItems"` + NumericReward *vipBenefitNumericRewardDTO `json:"numericReward"` +} + +type vipBenefitPreviewItemDTO struct { + PreviewID string `json:"previewId"` + Title string `json:"title"` + MediaType string `json:"mediaType"` + AssetURL string `json:"assetUrl"` + PreviewURL string `json:"previewUrl"` + AnimationURL string `json:"animationUrl"` + CompositionType string `json:"compositionType"` + SortOrder int32 `json:"sortOrder"` +} + +type vipBenefitNumericRewardDTO struct { + Label string `json:"label"` + Value int64 `json:"value"` + Unit string `json:"unit"` + Period string `json:"period"` } type vipTrialCardDTO struct { @@ -163,6 +188,18 @@ type vipStateDTO struct { EffectiveBenefits []vipBenefitDTO `json:"effectiveBenefits"` EvaluatedAtMS int64 `json:"evaluatedAtMs"` ProgramConfig vipProgramConfigDTO `json:"programConfig"` + UserSettings vipUserSettingsDTO `json:"userSettings"` +} + +type vipUserSettingsDTO struct { + AppCode string `json:"appCode"` + UserID string `json:"userId"` + RoomEntryNoticeEnabled bool `json:"roomEntryNoticeEnabled"` + OnlineGlobalNoticeEnabled bool `json:"onlineGlobalNoticeEnabled"` + HideProfileDataEnabled bool `json:"hideProfileDataEnabled"` + AnonymousProfileVisitEnabled bool `json:"anonymousProfileVisitEnabled"` + LeaderboardInvisibleEnabled bool `json:"leaderboardInvisibleEnabled"` + UpdatedAtMS int64 `json:"updatedAtMs"` } type vipRewardItemDTO struct { @@ -470,6 +507,7 @@ func vipBenefitsFromProto(items []*walletv1.VipBenefit) []vipBenefitDTO { MetadataJSON: item.GetMetadataJson(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), + Presentation: vipBenefitPresentationFromProto(item.GetPresentation()), }) } return benefits @@ -489,9 +527,61 @@ func (item vipBenefitDTO) toProto() *walletv1.VipBenefit { AutoEquip: item.AutoEquip, SortOrder: item.SortOrder, MetadataJson: strings.TrimSpace(item.MetadataJSON), + Presentation: item.Presentation.toProto(), } } +func vipBenefitPresentationFromProto(item *walletv1.VipBenefitPresentation) *vipBenefitPresentationDTO { + if item == nil { + return nil + } + result := &vipBenefitPresentationDTO{ + Description: item.GetDescription(), + PreviewItems: make([]vipBenefitPreviewItemDTO, 0, len(item.GetPreviewItems())), + } + for _, preview := range item.GetPreviewItems() { + if preview == nil { + continue + } + result.PreviewItems = append(result.PreviewItems, vipBenefitPreviewItemDTO{ + PreviewID: preview.GetPreviewId(), Title: preview.GetTitle(), MediaType: preview.GetMediaType(), + AssetURL: preview.GetAssetUrl(), PreviewURL: preview.GetPreviewUrl(), AnimationURL: preview.GetAnimationUrl(), + CompositionType: preview.GetCompositionType(), SortOrder: preview.GetSortOrder(), + }) + } + if reward := item.GetNumericReward(); reward != nil { + result.NumericReward = &vipBenefitNumericRewardDTO{ + Label: reward.GetLabel(), Value: reward.GetValue(), Unit: reward.GetUnit(), Period: reward.GetPeriod(), + } + } + return result +} + +func (item *vipBenefitPresentationDTO) toProto() *walletv1.VipBenefitPresentation { + if item == nil { + return nil + } + result := &walletv1.VipBenefitPresentation{ + Description: strings.TrimSpace(item.Description), + PreviewItems: make([]*walletv1.VipBenefitPreviewItem, 0, len(item.PreviewItems)), + } + for _, preview := range item.PreviewItems { + result.PreviewItems = append(result.PreviewItems, &walletv1.VipBenefitPreviewItem{ + PreviewId: strings.TrimSpace(preview.PreviewID), Title: strings.TrimSpace(preview.Title), + MediaType: strings.TrimSpace(preview.MediaType), AssetUrl: strings.TrimSpace(preview.AssetURL), + PreviewUrl: strings.TrimSpace(preview.PreviewURL), AnimationUrl: strings.TrimSpace(preview.AnimationURL), + CompositionType: strings.TrimSpace(preview.CompositionType), SortOrder: preview.SortOrder, + }) + } + if item.NumericReward != nil { + result.NumericReward = &walletv1.VipBenefitNumericReward{ + Label: strings.TrimSpace(item.NumericReward.Label), Value: item.NumericReward.Value, + Unit: strings.TrimSpace(item.NumericReward.Unit), Period: strings.TrimSpace(item.NumericReward.Period), + } + } + return result +} + func grantVipFromProto(resp *walletv1.GrantVipResponse) grantVipDTO { if resp == nil { return grantVipDTO{} @@ -568,6 +658,22 @@ func vipStateFromProto(item *walletv1.VipState) vipStateDTO { EffectiveBenefits: vipBenefitsFromProto(item.GetEffectiveBenefits()), EvaluatedAtMS: item.GetEvaluatedAtMs(), ProgramConfig: vipProgramConfigFromProto(item.GetProgramConfig()), + UserSettings: vipUserSettingsFromProto(item.GetUserSettings()), + } +} + +func vipUserSettingsFromProto(item *walletv1.VipUserSettings) vipUserSettingsDTO { + if item == nil { + return vipUserSettingsDTO{} + } + return vipUserSettingsDTO{ + AppCode: item.GetAppCode(), UserID: strconv.FormatInt(item.GetUserId(), 10), + RoomEntryNoticeEnabled: item.GetRoomEntryNoticeEnabled(), + OnlineGlobalNoticeEnabled: item.GetOnlineGlobalNoticeEnabled(), + HideProfileDataEnabled: item.GetHideProfileDataEnabled(), + AnonymousProfileVisitEnabled: item.GetAnonymousProfileVisitEnabled(), + LeaderboardInvisibleEnabled: item.GetLeaderboardInvisibleEnabled(), + UpdatedAtMS: item.GetUpdatedAtMs(), } } diff --git a/server/admin/internal/modules/vipconfig/handler_test.go b/server/admin/internal/modules/vipconfig/handler_test.go index 1da5f5f8..6a6346d3 100644 --- a/server/admin/internal/modules/vipconfig/handler_test.go +++ b/server/admin/internal/modules/vipconfig/handler_test.go @@ -45,6 +45,12 @@ func TestGetProgramReturnsCamelCaseCompleteConfigAndBenefits(t *testing.T) { MetadataJson: "{}", CreatedAtMs: 101, UpdatedAtMs: 201, + Presentation: &walletv1.VipBenefitPresentation{ + Description: "普通房管无法踢出", + PreviewItems: []*walletv1.VipBenefitPreviewItem{{ + PreviewId: "anti-kick", MediaType: "image", PreviewUrl: "https://cdn.example/anti-kick.png", CompositionType: "none", + }}, + }, }}, ServerTimeMs: 300, }, @@ -60,6 +66,8 @@ func TestGetProgramReturnsCamelCaseCompleteConfigAndBenefits(t *testing.T) { `"updatedByAdminId":77`, `"benefitCode":"anti_kick"`, `"executionScope":"room"`, + `"description":"普通房管无法踢出"`, + `"compositionType":"none"`, `"serverTimeMs":300`, } { if !strings.Contains(body, fragment) { @@ -105,7 +113,7 @@ func TestUpdateLevelsForwardsExplicitBenefitsAndReturnsProgram(t *testing.T) { ProgramConfig: &walletv1.VipProgramConfig{AppCode: "fami", ProgramType: "tiered_privilege_v1", LevelCount: 9}, ServerTimeMs: 500, }} - body := `{"levels":[{"level":2,"name":"VIP2","status":"active","priceCoin":2000,"durationMs":2592000000,"rewardResourceGroupId":22,"sortOrder":20,"requiredRechargeCoinAmount":123,"benefits":[{"benefitCode":"room_entry_notice","name":"进房通知","benefitType":"function","unlockLevel":2,"status":"active","trialEnabled":true,"resourceId":0,"resourceType":"","executionScope":"room","autoEquip":false,"sortOrder":10,"metadataJson":"{}"}]}]}` + body := `{"levels":[{"level":2,"name":"VIP2","status":"active","priceCoin":2000,"durationMs":2592000000,"rewardResourceGroupId":22,"sortOrder":20,"requiredRechargeCoinAmount":123,"benefits":[{"benefitCode":"room_entry_notice","name":"进房通知","benefitType":"function","unlockLevel":2,"status":"active","trialEnabled":true,"resourceId":0,"resourceType":"","executionScope":"room","autoEquip":false,"sortOrder":10,"metadataJson":"{}","presentation":{"description":"当前房间进房动效","previewItems":[{"previewId":"entry-main","mediaType":"animation","animationUrl":"https://cdn.example/entry.svga","compositionType":"user_avatar_center","sortOrder":10}]}}]}]}` recorder := serveVIPConfigRequest(t, wallet, http.MethodPut, "/admin/activity/vip-levels", body) if recorder.Code != http.StatusOK { t.Fatalf("update levels status = %d, body=%s", recorder.Code, recorder.Body.String()) @@ -118,6 +126,10 @@ func TestUpdateLevelsForwardsExplicitBenefitsAndReturnsProgram(t *testing.T) { if level.GetRequiredRechargeCoinAmount() != 0 || len(level.GetBenefits()) != 1 || level.GetBenefits()[0].GetBenefitCode() != "room_entry_notice" { t.Fatalf("explicit level benefits mismatch: %+v", level) } + presentation := level.GetBenefits()[0].GetPresentation() + if presentation.GetDescription() != "当前房间进房动效" || len(presentation.GetPreviewItems()) != 1 || presentation.GetPreviewItems()[0].GetCompositionType() != "user_avatar_center" { + t.Fatalf("structured benefit presentation mismatch: %+v", presentation) + } if !strings.Contains(recorder.Body.String(), `"programConfig":{"appCode":"fami","programType":"tiered_privilege_v1","levelCount":9`) { t.Fatalf("level response must include program config: %s", recorder.Body.String()) } diff --git a/services/gateway-service/internal/transport/http/vip_handler_test.go b/services/gateway-service/internal/transport/http/vip_handler_test.go index 06ed461b..a4b20dcd 100644 --- a/services/gateway-service/internal/transport/http/vip_handler_test.go +++ b/services/gateway-service/internal/transport/http/vip_handler_test.go @@ -184,6 +184,9 @@ func TestTriggerVIPOnlineNoticeDenialDoesNotReadProfileOrBroadcast(t *testing.T) if recorder.Code != http.StatusForbidden || profileClient.lastGet != nil || broadcastClient.lastGlobal != nil { t.Fatalf("denied request leaked into downstream calls: status=%d profile=%+v broadcast=%+v body=%s", recorder.Code, profileClient.lastGet, broadcastClient.lastGlobal, recorder.Body.String()) } + if !strings.Contains(recorder.Body.String(), `"code":"VIP_BENEFIT_REQUIRED"`) { + t.Fatalf("denied notice must expose stable VIP code: %s", recorder.Body.String()) + } } func TestTriggerVIPOnlineNoticeUserSettingDenialDoesNotReadProfileOrBroadcast(t *testing.T) { @@ -214,12 +217,16 @@ func TestVIPSettingsHTTPContractPreservesExplicitFalseAndAuthScope(t *testing.T) UserSettings: &walletv1.VipUserSettings{ AppCode: "fami", UserId: 42, RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: false, UpdatedAtMs: 1_799_999_999_000, + HideProfileDataEnabled: true, AnonymousProfileVisitEnabled: false, + LeaderboardInvisibleEnabled: true, }, }}, updateVIPSettingsResp: &walletv1.UpdateMyVipSettingsResponse{ Settings: &walletv1.VipUserSettings{ AppCode: "fami", UserId: 42, RoomEntryNoticeEnabled: false, OnlineGlobalNoticeEnabled: false, UpdatedAtMs: 1_800_000_000_100, + HideProfileDataEnabled: false, AnonymousProfileVisitEnabled: false, + LeaderboardInvisibleEnabled: true, }, ServerTimeMs: 1_800_000_000_200, }, @@ -240,10 +247,13 @@ func TestVIPSettingsHTTPContractPreservesExplicitFalseAndAuthScope(t *testing.T) var getEnvelope struct { Data struct { Settings struct { - AppCode string `json:"app_code"` - UserID int64 `json:"user_id"` - RoomEntryNoticeEnabled bool `json:"room_entry_notice_enabled"` - OnlineGlobalNoticeEnabled bool `json:"online_global_notice_enabled"` + AppCode string `json:"app_code"` + UserID int64 `json:"user_id"` + RoomEntryNoticeEnabled bool `json:"room_entry_notice_enabled"` + OnlineGlobalNoticeEnabled bool `json:"online_global_notice_enabled"` + HideProfileDataEnabled bool `json:"hide_profile_data_enabled"` + AnonymousProfileVisitEnabled bool `json:"anonymous_profile_visit_enabled"` + LeaderboardInvisibleEnabled bool `json:"leaderboard_invisible_enabled"` } `json:"settings"` EvaluatedAtMS int64 `json:"evaluated_at_ms"` } `json:"data"` @@ -251,11 +261,11 @@ func TestVIPSettingsHTTPContractPreservesExplicitFalseAndAuthScope(t *testing.T) if err := json.NewDecoder(getRecorder.Body).Decode(&getEnvelope); err != nil { t.Fatalf("decode GET settings response failed: %v", err) } - if getEnvelope.Data.Settings.AppCode != "fami" || getEnvelope.Data.Settings.UserID != 42 || !getEnvelope.Data.Settings.RoomEntryNoticeEnabled || getEnvelope.Data.Settings.OnlineGlobalNoticeEnabled || getEnvelope.Data.EvaluatedAtMS != 1_800_000_000_000 { + if getEnvelope.Data.Settings.AppCode != "fami" || getEnvelope.Data.Settings.UserID != 42 || !getEnvelope.Data.Settings.RoomEntryNoticeEnabled || getEnvelope.Data.Settings.OnlineGlobalNoticeEnabled || !getEnvelope.Data.Settings.HideProfileDataEnabled || getEnvelope.Data.Settings.AnonymousProfileVisitEnabled || !getEnvelope.Data.Settings.LeaderboardInvisibleEnabled || getEnvelope.Data.EvaluatedAtMS != 1_800_000_000_000 { t.Fatalf("GET settings response mismatch: %+v", getEnvelope.Data) } - patchRequest := httptest.NewRequest(http.MethodPatch, "/api/v1/vip/settings", bytes.NewReader([]byte(`{"room_entry_notice_enabled":false}`))) + patchRequest := httptest.NewRequest(http.MethodPatch, "/api/v1/vip/settings", bytes.NewReader([]byte(`{"room_entry_notice_enabled":false,"hide_profile_data_enabled":false}`))) patchRequest.Header.Set("Authorization", token) patchRecorder := httptest.NewRecorder() router.ServeHTTP(patchRecorder, patchRequest) @@ -263,7 +273,7 @@ func TestVIPSettingsHTTPContractPreservesExplicitFalseAndAuthScope(t *testing.T) t.Fatalf("PATCH settings status mismatch: got=%d body=%s", patchRecorder.Code, patchRecorder.Body.String()) } patch := walletClient.lastUpdateVIPSettings - if patch == nil || patch.GetAppCode() != "fami" || patch.GetUserId() != 42 || patch.RoomEntryNoticeEnabled == nil || patch.GetRoomEntryNoticeEnabled() || patch.OnlineGlobalNoticeEnabled != nil || patch.GetRequestId() != patchRecorder.Header().Get("X-Request-ID") { + if patch == nil || patch.GetAppCode() != "fami" || patch.GetUserId() != 42 || patch.RoomEntryNoticeEnabled == nil || patch.GetRoomEntryNoticeEnabled() || patch.HideProfileDataEnabled == nil || patch.GetHideProfileDataEnabled() || patch.OnlineGlobalNoticeEnabled != nil || patch.AnonymousProfileVisitEnabled != nil || patch.LeaderboardInvisibleEnabled != nil || patch.GetRequestId() != patchRecorder.Header().Get("X-Request-ID") { t.Fatalf("PATCH settings lost explicit false or auth scope: %+v", patch) } @@ -505,7 +515,13 @@ func TestListVIPPackagesAddsProgramStateAndLevelBenefits(t *testing.T) { CurrentVip: &walletv1.UserVip{UserId: 42, Level: 2, Active: true}, Packages: []*walletv1.VipLevel{{ Level: 3, Name: "VIP3", PriceCoin: 300, DurationMs: 2_592_000_000, CanPurchase: true, ConfigVersion: 7, - Benefits: []*walletv1.VipBenefit{{BenefitCode: "voice_wave", Name: "语音波纹", ExecutionScope: "room", TrialEnabled: true}}, + Benefits: []*walletv1.VipBenefit{{ + BenefitCode: "voice_wave", Name: "语音波纹", ExecutionScope: "room", TrialEnabled: true, + Presentation: &walletv1.VipBenefitPresentation{ + Description: "动态声波纹预览", + PreviewItems: []*walletv1.VipBenefitPreviewItem{{PreviewId: "wave-main", MediaType: "animation", AnimationUrl: "https://cdn.example/wave.svga", CompositionType: "user_avatar_waveform"}}, + }, + }}, }}, State: &walletv1.VipState{EffectiveVip: &walletv1.UserVip{UserId: 42, Level: 2, Active: true}, EffectiveSource: "paid", ProgramConfig: config}, ProgramConfig: config, @@ -537,7 +553,13 @@ func TestListVIPPackagesAddsProgramStateAndLevelBenefits(t *testing.T) { Packages []struct { ConfigVersion int64 `json:"config_version"` Benefits []struct { - BenefitCode string `json:"benefit_code"` + BenefitCode string `json:"benefit_code"` + Presentation struct { + Description string `json:"description"` + PreviewItems []struct { + CompositionType string `json:"composition_type"` + } `json:"preview_items"` + } `json:"presentation"` } `json:"benefits"` } `json:"packages"` } `json:"data"` @@ -551,13 +573,17 @@ func TestListVIPPackagesAddsProgramStateAndLevelBenefits(t *testing.T) { if envelope.Data.Packages[0].ConfigVersion != 7 || len(envelope.Data.Packages[0].Benefits) != 1 || envelope.Data.Packages[0].Benefits[0].BenefitCode != "voice_wave" { t.Fatalf("package benefits mismatch: %+v", envelope.Data.Packages) } + if envelope.Data.Packages[0].Benefits[0].Presentation.Description != "动态声波纹预览" || len(envelope.Data.Packages[0].Benefits[0].Presentation.PreviewItems) != 1 || envelope.Data.Packages[0].Benefits[0].Presentation.PreviewItems[0].CompositionType != "user_avatar_waveform" { + t.Fatalf("structured presentation mismatch: %+v", envelope.Data.Packages[0].Benefits[0].Presentation) + } } func TestPurchaseVIPKeepsLegacyResultAndAddsState(t *testing.T) { config := &walletv1.VipProgramConfig{AppCode: "fami", ProgramType: "tiered_privilege_v1", UpgradeExpiryPolicy: "replace_from_now", ConfigVersion: 8} walletClient := &fakeWalletClient{purchaseVipResp: &walletv1.PurchaseVipResponse{ OrderId: "vip-order-1", TransactionId: "vip-tx-1", Vip: &walletv1.UserVip{UserId: 42, Level: 4, Active: true, ExpiresAtMs: 4_000}, CoinSpent: 400, CoinBalanceAfter: 600, - State: &walletv1.VipState{PaidVip: &walletv1.UserVip{UserId: 42, Level: 4, Active: true}, EffectiveVip: &walletv1.UserVip{UserId: 42, Level: 4, Active: true}, EffectiveSource: "paid", ProgramConfig: config}, + CoinBalance: &walletv1.AssetBalance{AssetType: "COIN", AvailableAmount: 600, FrozenAmount: 0, Version: 18}, + State: &walletv1.VipState{PaidVip: &walletv1.UserVip{UserId: 42, Level: 4, Active: true}, EffectiveVip: &walletv1.UserVip{UserId: 42, Level: 4, Active: true}, EffectiveSource: "paid", ProgramConfig: config, UserSettings: &walletv1.VipUserSettings{AppCode: "fami", UserId: 42, RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true, HideProfileDataEnabled: true, AnonymousProfileVisitEnabled: true, LeaderboardInvisibleEnabled: true}}, }} router := newVIPTestRouter(walletClient) request := httptest.NewRequest(http.MethodPost, "/api/v1/vip/purchase", bytes.NewReader([]byte(`{"command_id":"vip-buy-4","level":4}`))) @@ -569,6 +595,7 @@ func TestPurchaseVIPKeepsLegacyResultAndAddsState(t *testing.T) { if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } + assertCompleteVIPStateJSON(t, recorder.Body.Bytes()) if req := walletClient.lastPurchaseVip; req == nil || req.GetCommandId() != "vip-buy-4" || req.GetAppCode() != "fami" || req.GetUserId() != 42 || req.GetLevel() != 4 { t.Fatalf("PurchaseVip request mismatch: %+v", req) } @@ -585,12 +612,16 @@ func TestPurchaseVIPKeepsLegacyResultAndAddsState(t *testing.T) { State struct { EffectiveSource string `json:"effective_source"` } `json:"state"` + CoinBalance struct { + AvailableAmount int64 `json:"available_amount"` + Version int64 `json:"version"` + } `json:"coin_balance"` } `json:"data"` } if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil { t.Fatalf("decode response failed: %v", err) } - if envelope.Data.OrderID != "vip-order-1" || envelope.Data.TransactionID != "vip-tx-1" || envelope.Data.VIP.Level != 4 || envelope.Data.ProgramConfig.UpgradeExpiryPolicy != "replace_from_now" || envelope.Data.State.EffectiveSource != "paid" { + if envelope.Data.OrderID != "vip-order-1" || envelope.Data.TransactionID != "vip-tx-1" || envelope.Data.VIP.Level != 4 || envelope.Data.ProgramConfig.UpgradeExpiryPolicy != "replace_from_now" || envelope.Data.State.EffectiveSource != "paid" || envelope.Data.CoinBalance.AvailableAmount != 600 || envelope.Data.CoinBalance.Version != 18 { t.Fatalf("purchase response mismatch: %+v", envelope.Data) } } @@ -612,6 +643,7 @@ func TestEquipVIPTrialCardUsesAuthenticatedEntitlement(t *testing.T) { if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } + assertCompleteVIPStateJSON(t, recorder.Body.Bytes()) if req := walletClient.lastTrialEquip; req == nil || req.GetRequestId() != recorder.Header().Get("X-Request-ID") || req.GetAppCode() != "fami" || req.GetUserId() != 42 || req.GetEntitlementId() != "ent-card-9" { t.Fatalf("EquipVipTrialCard request mismatch: %+v", req) } @@ -651,6 +683,7 @@ func TestUnequipVIPTrialCardRestoresPaidState(t *testing.T) { if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } + assertCompleteVIPStateJSON(t, recorder.Body.Bytes()) if req := walletClient.lastTrialUnequip; req == nil || req.GetRequestId() != recorder.Header().Get("X-Request-ID") || req.GetAppCode() != "fami" || req.GetUserId() != 42 { t.Fatalf("UnequipVipTrialCard request mismatch: %+v", req) } @@ -672,3 +705,22 @@ func TestUnequipVIPTrialCardRestoresPaidState(t *testing.T) { t.Fatalf("unequip response mismatch: %+v", envelope.Data) } } + +// assertCompleteVIPStateJSON 锁定所有 VIP 写接口的 state 与 /vip/me.state 同构;即使值为空, +// 字段也必须存在,Flutter 才能用响应原子替换本地状态而不是做不安全的局部合并。 +func assertCompleteVIPStateJSON(t *testing.T, body []byte) { + t.Helper() + var envelope struct { + Data struct { + State map[string]json.RawMessage `json:"state"` + } `json:"data"` + } + if err := json.Unmarshal(body, &envelope); err != nil { + t.Fatalf("decode complete state envelope: %v", err) + } + for _, field := range []string{"paid_vip", "equipped_trial_card", "effective_vip", "effective_source", "effective_benefits", "evaluated_at_ms", "program_config", "user_settings"} { + if _, exists := envelope.Data.State[field]; !exists { + t.Fatalf("VIP state missing %s: %s", field, body) + } + } +} diff --git a/services/gateway-service/internal/transport/http/walletapi/app_wallet_handler.go b/services/gateway-service/internal/transport/http/walletapi/app_wallet_handler.go index 12a595e1..dfa330be 100644 --- a/services/gateway-service/internal/transport/http/walletapi/app_wallet_handler.go +++ b/services/gateway-service/internal/transport/http/walletapi/app_wallet_handler.go @@ -215,20 +215,45 @@ type vipProgramConfigData struct { // vipBenefitData 是最终生效权益的资格描述。客户端不得根据 unlock_level 自行继承; // 对可关闭通知还需结合 state.user_settings,真正执行前以 CheckVipBenefit 为准。 type vipBenefitData struct { - BenefitCode string `json:"benefit_code"` - Name string `json:"name"` - BenefitType string `json:"benefit_type"` - UnlockLevel int32 `json:"unlock_level"` - Status string `json:"status"` - TrialEnabled bool `json:"trial_enabled"` - ResourceID int64 `json:"resource_id"` - ResourceType string `json:"resource_type"` - ExecutionScope string `json:"execution_scope"` - AutoEquip bool `json:"auto_equip"` - SortOrder int32 `json:"sort_order"` - MetadataJSON string `json:"metadata_json"` - CreatedAtMS int64 `json:"created_at_ms"` - UpdatedAtMS int64 `json:"updated_at_ms"` + BenefitCode string `json:"benefit_code"` + Name string `json:"name"` + BenefitType string `json:"benefit_type"` + UnlockLevel int32 `json:"unlock_level"` + Status string `json:"status"` + TrialEnabled bool `json:"trial_enabled"` + ResourceID int64 `json:"resource_id"` + ResourceType string `json:"resource_type"` + ExecutionScope string `json:"execution_scope"` + AutoEquip bool `json:"auto_equip"` + SortOrder int32 `json:"sort_order"` + MetadataJSON string `json:"metadata_json"` + CreatedAtMS int64 `json:"created_at_ms"` + UpdatedAtMS int64 `json:"updated_at_ms"` + Presentation *vipBenefitPresentationData `json:"presentation"` +} + +type vipBenefitPresentationData struct { + Description string `json:"description"` + PreviewItems []vipBenefitPreviewItemData `json:"preview_items"` + NumericReward *vipBenefitNumericRewardData `json:"numeric_reward"` +} + +type vipBenefitPreviewItemData struct { + PreviewID string `json:"preview_id"` + Title string `json:"title"` + MediaType string `json:"media_type"` + AssetURL string `json:"asset_url"` + PreviewURL string `json:"preview_url"` + AnimationURL string `json:"animation_url"` + CompositionType string `json:"composition_type"` + SortOrder int32 `json:"sort_order"` +} + +type vipBenefitNumericRewardData struct { + Label string `json:"label"` + Value int64 `json:"value"` + Unit string `json:"unit"` + Period string `json:"period"` } type vipTrialCardData struct { @@ -262,11 +287,14 @@ type vipStateData struct { } type vipUserSettingsData struct { - AppCode string `json:"app_code"` - UserID int64 `json:"user_id"` - RoomEntryNoticeEnabled bool `json:"room_entry_notice_enabled"` - OnlineGlobalNoticeEnabled bool `json:"online_global_notice_enabled"` - UpdatedAtMS int64 `json:"updated_at_ms"` + AppCode string `json:"app_code"` + UserID int64 `json:"user_id"` + RoomEntryNoticeEnabled bool `json:"room_entry_notice_enabled"` + OnlineGlobalNoticeEnabled bool `json:"online_global_notice_enabled"` + HideProfileDataEnabled bool `json:"hide_profile_data_enabled"` + AnonymousProfileVisitEnabled bool `json:"anonymous_profile_visit_enabled"` + LeaderboardInvisibleEnabled bool `json:"leaderboard_invisible_enabled"` + UpdatedAtMS int64 `json:"updated_at_ms"` } type vipRewardItemData struct { diff --git a/services/gateway-service/internal/transport/http/walletapi/vip_handler.go b/services/gateway-service/internal/transport/http/walletapi/vip_handler.go index 587aecef..b376d4f3 100644 --- a/services/gateway-service/internal/transport/http/walletapi/vip_handler.go +++ b/services/gateway-service/internal/transport/http/walletapi/vip_handler.go @@ -11,6 +11,7 @@ import ( userv1 "hyapp.local/api/proto/user/v1" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" + "hyapp/pkg/xerr" "hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/transport/http/httpkit" ) @@ -131,6 +132,7 @@ func (h *Handler) purchaseVIP(writer http.ResponseWriter, request *http.Request) "vip": vipFromProto(resp.GetVip()), "coin_spent": resp.GetCoinSpent(), "coin_balance_after": resp.GetCoinBalanceAfter(), + "coin_balance": balanceFromProto(resp.GetCoinBalance()), "reward_items": vipRewardItemsFromProto(resp.GetRewardItems()), "program_config": vipProgramConfigFromProto(resp.GetState().GetProgramConfig()), "state": vipStateFromProto(resp.GetState()), @@ -230,7 +232,8 @@ func (h *Handler) triggerVIPOnlineNotice(writer http.ResponseWriter, request *ht if !benefitResp.GetAllowed() { // 客户端展示态可能滞后,执行前必须重新查 effective benefit;体验卡排除、过期和后台停用 // 都以 wallet-service 此刻的判定为准,不能仅比较 VIP9 等级。 - httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied") + statusCode, code, message := httpkit.MapReasonToHTTP(xerr.VIPBenefitRequired) + httpkit.WriteError(writer, request, statusCode, code, message) return } equippedResp, err := h.walletClient.BatchGetUserEquippedResources(ctx, &walletv1.BatchGetUserEquippedResourcesRequest{ @@ -434,11 +437,38 @@ func vipBenefitsFromProto(items []*walletv1.VipBenefit) []vipBenefitData { MetadataJSON: item.GetMetadataJson(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), + Presentation: vipBenefitPresentationFromProto(item.GetPresentation()), }) } return result } +func vipBenefitPresentationFromProto(item *walletv1.VipBenefitPresentation) *vipBenefitPresentationData { + if item == nil { + return nil + } + result := &vipBenefitPresentationData{ + Description: item.GetDescription(), + PreviewItems: make([]vipBenefitPreviewItemData, 0, len(item.GetPreviewItems())), + } + for _, preview := range item.GetPreviewItems() { + if preview == nil { + continue + } + result.PreviewItems = append(result.PreviewItems, vipBenefitPreviewItemData{ + PreviewID: preview.GetPreviewId(), Title: preview.GetTitle(), MediaType: preview.GetMediaType(), + AssetURL: preview.GetAssetUrl(), PreviewURL: preview.GetPreviewUrl(), AnimationURL: preview.GetAnimationUrl(), + CompositionType: preview.GetCompositionType(), SortOrder: preview.GetSortOrder(), + }) + } + if reward := item.GetNumericReward(); reward != nil { + result.NumericReward = &vipBenefitNumericRewardData{ + Label: reward.GetLabel(), Value: reward.GetValue(), Unit: reward.GetUnit(), Period: reward.GetPeriod(), + } + } + return result +} + func vipTrialCardFromProto(card *walletv1.VipTrialCard) *vipTrialCardData { if card == nil { return nil @@ -469,6 +499,8 @@ func vipStateFromProto(state *walletv1.VipState) vipStateData { EffectiveBenefits: []vipBenefitData{}, UserSettings: vipUserSettingsData{ RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true, + HideProfileDataEnabled: true, AnonymousProfileVisitEnabled: true, + LeaderboardInvisibleEnabled: true, }, } } @@ -487,13 +519,20 @@ func vipStateFromProto(state *walletv1.VipState) vipStateData { func vipUserSettingsFromProto(settings *walletv1.VipUserSettings) vipUserSettingsData { if settings == nil { // wallet 滚动升级期间旧实例没有 tag 8;与 wallet owner 的缺行语义一致,按默认开启兼容。 - return vipUserSettingsData{RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true} + return vipUserSettingsData{ + RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true, + HideProfileDataEnabled: true, AnonymousProfileVisitEnabled: true, + LeaderboardInvisibleEnabled: true, + } } return vipUserSettingsData{ AppCode: settings.GetAppCode(), UserID: settings.GetUserId(), - RoomEntryNoticeEnabled: settings.GetRoomEntryNoticeEnabled(), - OnlineGlobalNoticeEnabled: settings.GetOnlineGlobalNoticeEnabled(), - UpdatedAtMS: settings.GetUpdatedAtMs(), + RoomEntryNoticeEnabled: settings.GetRoomEntryNoticeEnabled(), + OnlineGlobalNoticeEnabled: settings.GetOnlineGlobalNoticeEnabled(), + HideProfileDataEnabled: settings.GetHideProfileDataEnabled(), + AnonymousProfileVisitEnabled: settings.GetAnonymousProfileVisitEnabled(), + LeaderboardInvisibleEnabled: settings.GetLeaderboardInvisibleEnabled(), + UpdatedAtMS: settings.GetUpdatedAtMs(), } } diff --git a/services/gateway-service/internal/transport/http/walletapi/vip_settings_handler.go b/services/gateway-service/internal/transport/http/walletapi/vip_settings_handler.go index dddbc381..f689b5cf 100644 --- a/services/gateway-service/internal/transport/http/walletapi/vip_settings_handler.go +++ b/services/gateway-service/internal/transport/http/walletapi/vip_settings_handler.go @@ -11,8 +11,11 @@ import ( type vipSettingsPatchBody struct { // 指针保留 PATCH 的三态:未提交、显式 true、显式 false。 - RoomEntryNoticeEnabled *bool `json:"room_entry_notice_enabled"` - OnlineGlobalNoticeEnabled *bool `json:"online_global_notice_enabled"` + RoomEntryNoticeEnabled *bool `json:"room_entry_notice_enabled"` + OnlineGlobalNoticeEnabled *bool `json:"online_global_notice_enabled"` + HideProfileDataEnabled *bool `json:"hide_profile_data_enabled"` + AnonymousProfileVisitEnabled *bool `json:"anonymous_profile_visit_enabled"` + LeaderboardInvisibleEnabled *bool `json:"leaderboard_invisible_enabled"` } // handleVIPSettings 在同一路径分发 GET/PATCH,避免向当前 ServeMux 重复注册相同 pattern。 @@ -70,7 +73,9 @@ func (h *Handler) patchVIPSettings(writer http.ResponseWriter, request *http.Req if !httpkit.Decode(writer, request, &body) { return } - if body.RoomEntryNoticeEnabled == nil && body.OnlineGlobalNoticeEnabled == nil { + if body.RoomEntryNoticeEnabled == nil && body.OnlineGlobalNoticeEnabled == nil && + body.HideProfileDataEnabled == nil && body.AnonymousProfileVisitEnabled == nil && + body.LeaderboardInvisibleEnabled == nil { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } @@ -78,7 +83,10 @@ func (h *Handler) patchVIPSettings(writer http.ResponseWriter, request *http.Req resp, err := h.walletClient.UpdateMyVipSettings(ctx, &walletv1.UpdateMyVipSettingsRequest{ RequestId: httpkit.RequestIDFromContext(ctx), AppCode: appcode.FromContext(ctx), UserId: auth.UserIDFromContext(ctx), RoomEntryNoticeEnabled: body.RoomEntryNoticeEnabled, - OnlineGlobalNoticeEnabled: body.OnlineGlobalNoticeEnabled, + OnlineGlobalNoticeEnabled: body.OnlineGlobalNoticeEnabled, + HideProfileDataEnabled: body.HideProfileDataEnabled, + AnonymousProfileVisitEnabled: body.AnonymousProfileVisitEnabled, + LeaderboardInvisibleEnabled: body.LeaderboardInvisibleEnabled, }) if err != nil { httpkit.WriteRPCError(writer, request, err) diff --git a/services/room-service/internal/room/service/vip.go b/services/room-service/internal/room/service/vip.go index 1c37eff5..6cf20563 100644 --- a/services/room-service/internal/room/service/vip.go +++ b/services/room-service/internal/room/service/vip.go @@ -118,7 +118,7 @@ func (s *Service) requireCustomRoomBackgroundBenefit(ctx context.Context, reques return nil } if !access.hasBenefit(vipBenefitCustomRoomBackground) { - return xerr.New(xerr.PermissionDenied, "custom room background vip benefit is required") + return xerr.New(xerr.VIPBenefitRequired, "custom room background vip benefit is required") } return nil } @@ -131,7 +131,14 @@ func (s *Service) requireTargetWithoutVIPBenefit(ctx context.Context, requestID return err } if access.hasBenefit(benefitCode) { - return xerr.New(xerr.PermissionDenied, "target vip benefit prevents this room moderation action") + switch normalizeVIPCode(benefitCode) { + case vipBenefitAntiKick: + return xerr.New(xerr.VIPAntiKick, "target vip benefit prevents kick") + case vipBenefitAntiMute: + return xerr.New(xerr.VIPAntiMute, "target vip benefit prevents mute") + default: + return xerr.New(xerr.VIPBenefitRequired, "target vip benefit prevents this room moderation action") + } } return nil } diff --git a/services/room-service/internal/room/service/vip_test.go b/services/room-service/internal/room/service/vip_test.go index b280e93b..b8ddcabf 100644 --- a/services/room-service/internal/room/service/vip_test.go +++ b/services/room-service/internal/room/service/vip_test.go @@ -118,7 +118,7 @@ func TestTieredVIPCustomRoomBackgroundRequiresEffectiveBenefit(t *testing.T) { Meta: vipRoomMeta(roomID, ownerID, "save-denied"), RoomId: roomID, ImageUrl: "https://cdn.example.com/fami-vip-denied.png", - }); !xerr.IsCode(err, xerr.PermissionDenied) { + }); !xerr.IsCode(err, xerr.VIPBenefitRequired) { t.Fatalf("tiered app without custom_room_background must be denied: %v", err) } @@ -136,7 +136,7 @@ func TestTieredVIPCustomRoomBackgroundRequiresEffectiveBenefit(t *testing.T) { if _, err := svc.SetRoomBackground(ctx, &roomv1.SetRoomBackgroundRequest{ Meta: vipRoomMeta(roomID, ownerID, "set-denied"), BackgroundId: second.GetBackgroundId(), - }); !xerr.IsCode(err, xerr.PermissionDenied) { + }); !xerr.IsCode(err, xerr.VIPBenefitRequired) { t.Fatalf("expired custom_room_background benefit must block later set: %v", err) } } @@ -161,13 +161,13 @@ func TestVIPModerationProtectionBlocksRoomManagersButNotSystemEvict(t *testing.T Meta: vipRoomMeta(roomID, ownerID, "mute"), TargetUserId: targetID, Muted: true, - }); !xerr.IsCode(err, xerr.PermissionDenied) { + }); !xerr.IsCode(err, xerr.VIPAntiMute) { t.Fatalf("anti_mute must block ordinary room owner: %v", err) } if _, err := svc.KickUser(ctx, &roomv1.KickUserRequest{ Meta: vipRoomMeta(roomID, ownerID, "kick"), TargetUserId: targetID, - }); !xerr.IsCode(err, xerr.PermissionDenied) { + }); !xerr.IsCode(err, xerr.VIPAntiKick) { t.Fatalf("anti_kick must block ordinary room owner: %v", err) } 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 3e4a3215..bc5b03be 100644 --- a/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql +++ b/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql @@ -1153,6 +1153,7 @@ CREATE TABLE IF NOT EXISTS resource_shop_items ( resource_id BIGINT NOT NULL COMMENT '售卖资源 ID', status VARCHAR(32) NOT NULL COMMENT '业务状态', duration_days INT NOT NULL COMMENT '售卖天数,只允许 1/3/7', + coin_price BIGINT NOT NULL DEFAULT 0 COMMENT '该售卖天数的独立金币价格;未自定义时复制资源列表价格', effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效开始时间,UTC epoch ms,0 表示立即生效', effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效结束时间,UTC epoch ms,0 表示长期有效', sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前', @@ -1160,7 +1161,7 @@ CREATE TABLE IF NOT EXISTS resource_shop_items ( updated_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新人用户 ID', created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', - UNIQUE KEY uk_resource_shop_items_resource (app_code, resource_id), + UNIQUE KEY uk_resource_shop_items_resource_duration (app_code, resource_id, duration_days), KEY idx_resource_shop_items_status_sort (app_code, status, sort_order), KEY idx_resource_shop_items_effective (app_code, status, effective_from_ms, effective_to_ms) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='道具商店售卖资源表'; @@ -1685,6 +1686,9 @@ CREATE TABLE IF NOT EXISTS user_vip_settings ( user_id BIGINT NOT NULL COMMENT '用户 ID', room_entry_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启进房通知', online_global_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启上线全服通知', + hide_profile_data_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启隐藏个人数据', + anonymous_profile_visit_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启匿名访问主页', + leaderboard_invisible_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启榜单隐身', created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', PRIMARY KEY (app_code, user_id) diff --git a/services/wallet-service/deploy/mysql/migrations/002_resource_shop_price_tiers.sql b/services/wallet-service/deploy/mysql/migrations/002_resource_shop_price_tiers.sql new file mode 100644 index 00000000..ec7738b0 --- /dev/null +++ b/services/wallet-service/deploy/mysql/migrations/002_resource_shop_price_tiers.sql @@ -0,0 +1,48 @@ +-- 道具商店从“每个资源一个动态价格”迁移为“每个资源可有 1/3/7 天独立定价规格”。 +-- 先增加带默认值的列,保证滚动发布期间旧代码仍可写入;随后把历史行回填为上线前实际展示/扣费价, +-- 因而迁移不会把既有 3/7 天商品突然降为资源列表原价。配置表数据量很小,但执行前仍应检查长事务, +-- 避免 ALTER TABLE 等待 metadata lock。 +SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE hyapp_wallet; + +SET @ddl := IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resource_shop_items' AND COLUMN_NAME = 'coin_price') = 0, + 'ALTER TABLE resource_shop_items ADD COLUMN coin_price BIGINT NOT NULL DEFAULT 0 COMMENT ''该售卖天数的独立金币价格;未自定义时复制资源列表价格'' AFTER duration_days', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 旧实现按 resources.coin_price * duration_days 实时计价。乘法上界保护只处理异常脏数据;正常 1/3/7 +-- 天配置会完整保留迁移前价格,之后资源目录调价不再隐式影响已上架规格。 +UPDATE resource_shop_items AS si +JOIN resources AS r ON r.app_code = si.app_code AND r.resource_id = si.resource_id +SET si.coin_price = CASE + WHEN si.duration_days > 0 AND r.coin_price <= 9223372036854775807 DIV si.duration_days + THEN r.coin_price * si.duration_days + ELSE r.coin_price +END +WHERE si.coin_price = 0; + +SET @ddl := IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resource_shop_items' AND INDEX_NAME = 'uk_resource_shop_items_resource_duration') = 0, + 'ALTER TABLE resource_shop_items ADD UNIQUE INDEX uk_resource_shop_items_resource_duration (app_code, resource_id, duration_days)', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @ddl := IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resource_shop_items' AND INDEX_NAME = 'uk_resource_shop_items_resource') > 0, + 'ALTER TABLE resource_shop_items DROP INDEX uk_resource_shop_items_resource', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/services/wallet-service/internal/domain/ledger/vip.go b/services/wallet-service/internal/domain/ledger/vip.go index d173982b..db141c23 100644 --- a/services/wallet-service/internal/domain/ledger/vip.go +++ b/services/wallet-service/internal/domain/ledger/vip.go @@ -128,6 +128,36 @@ type VipBenefit struct { MetadataJSON string CreatedAtMS int64 UpdatedAtMS int64 + Presentation *VipBenefitPresentation +} + +// VipBenefitPresentation 是 App 可直接渲染的权益表现契约。它从 metadata_json.presentation +// 投影而来,避免 Flutter 按 benefit_code 维护一份隐形 JSON 解析规则。 +type VipBenefitPresentation struct { + Description string `json:"description"` + PreviewItems []VipBenefitPreviewItem `json:"preview_items"` + NumericReward *VipBenefitNumericReward `json:"numeric_reward,omitempty"` +} + +// VipBenefitPreviewItem 描述一个静态或动态预览。CompositionType 明确头像合成方式, +// 头像框与声波纹无需再由客户端根据权益编码猜测布局。 +type VipBenefitPreviewItem struct { + PreviewID string `json:"preview_id"` + Title string `json:"title"` + MediaType string `json:"media_type"` + AssetURL string `json:"asset_url"` + PreviewURL string `json:"preview_url"` + AnimationURL string `json:"animation_url"` + CompositionType string `json:"composition_type"` + SortOrder int32 `json:"sort_order"` +} + +// VipBenefitNumericReward 只负责数值展示;真实发放和结算仍由对应 owner service 执行。 +type VipBenefitNumericReward struct { + Label string `json:"label"` + Value int64 `json:"value"` + Unit string `json:"unit"` + Period string `json:"period"` } // VipRewardItem 是 VIP 资源组权益的轻量展示投影。 @@ -201,28 +231,39 @@ type VipTrialCard struct { // VipUserSettings 是用户可关闭的 VIP 展示偏好。默认值必须由 wallet owner 统一构造, // 不能让 room/gateway 因为数据库尚未物化记录而各自猜不同默认值。 type VipUserSettings struct { - AppCode string - UserID int64 - RoomEntryNoticeEnabled bool - OnlineGlobalNoticeEnabled bool - UpdatedAtMS int64 + AppCode string + UserID int64 + RoomEntryNoticeEnabled bool + OnlineGlobalNoticeEnabled bool + HideProfileDataEnabled bool + AnonymousProfileVisitEnabled bool + LeaderboardInvisibleEnabled bool + UpdatedAtMS int64 } -// DefaultVipUserSettings 返回尚未物化时的产品默认:两项通知均开启,UpdatedAtMS=0。 +// DefaultVipUserSettings 返回尚未物化时的产品默认:五项能力均开启,UpdatedAtMS=0。 func DefaultVipUserSettings(appCode string, userID int64) VipUserSettings { return VipUserSettings{ AppCode: appCode, UserID: userID, RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true, + HideProfileDataEnabled: true, AnonymousProfileVisitEnabled: true, + LeaderboardInvisibleEnabled: true, } } -// AllowsBenefitSetting 只处理用户可关闭的两项权益;其它权益不受偏好表影响。 +// AllowsBenefitSetting 只处理用户可关闭的五项权益;其它权益不受偏好表影响。 func (settings VipUserSettings) AllowsBenefitSetting(benefitCode string) bool { switch strings.ToLower(strings.TrimSpace(benefitCode)) { case VipBenefitCodeRoomEntryNotice: return settings.RoomEntryNoticeEnabled case VipBenefitCodeOnlineGlobalNotice: return settings.OnlineGlobalNoticeEnabled + case VipBenefitCodeHideProfileData: + return settings.HideProfileDataEnabled + case VipBenefitCodeAnonymousProfileVisit: + return settings.AnonymousProfileVisitEnabled + case VipBenefitCodeLeaderboardInvisible: + return settings.LeaderboardInvisibleEnabled default: return true } @@ -256,6 +297,7 @@ type PurchaseVipReceipt struct { Vip UserVip CoinSpent int64 CoinBalanceAfter int64 + CoinBalance AssetBalance RewardItems []VipRewardItem State VipState } @@ -345,10 +387,13 @@ type AdminVipProgramConfigCommand struct { // UpdateVipUserSettingsCommand 使用指针表达 partial update;nil 字段必须保留数据库原值。 type UpdateVipUserSettingsCommand struct { - AppCode string - UserID int64 - RoomEntryNoticeEnabled *bool - OnlineGlobalNoticeEnabled *bool + AppCode string + UserID int64 + RoomEntryNoticeEnabled *bool + OnlineGlobalNoticeEnabled *bool + HideProfileDataEnabled *bool + AnonymousProfileVisitEnabled *bool + LeaderboardInvisibleEnabled *bool } // VipDailyCoinRebateRun 是单 App、单 UTC 日唯一的配置快照与分页游标。 diff --git a/services/wallet-service/internal/domain/ledger/vip_presentation.go b/services/wallet-service/internal/domain/ledger/vip_presentation.go new file mode 100644 index 00000000..a0a57de8 --- /dev/null +++ b/services/wallet-service/internal/domain/ledger/vip_presentation.go @@ -0,0 +1,145 @@ +package ledger + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "unicode/utf8" +) + +const ( + vipPresentationDescriptionMaxRunes = 1000 + vipPresentationPreviewMaxCount = 20 + vipPresentationTextMaxRunes = 128 + vipPresentationURLMaxRunes = 2048 +) + +// ParseVipBenefitPresentation 只解析 metadata_json.presentation 这一段稳定展示契约。 +// metadata_json 的其它业务字段(例如 coin_amount、message)由对应 owner 保留处理。 +func ParseVipBenefitPresentation(metadataJSON string) (*VipBenefitPresentation, error) { + metadataJSON = strings.TrimSpace(metadataJSON) + if metadataJSON == "" { + return nil, nil + } + var metadata struct { + Presentation *VipBenefitPresentation `json:"presentation"` + } + if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil { + return nil, err + } + if metadata.Presentation == nil { + return nil, nil + } + if err := ValidateVipBenefitPresentation(*metadata.Presentation); err != nil { + return nil, err + } + return metadata.Presentation, nil +} + +// MergeVipBenefitPresentation 将后台结构化输入归一到现有 JSON 列,避免为纯展示字段增加双写事实。 +// presentation 为空对象表示显式清除;nil 表示沿用 metadata_json 中已有值。 +func MergeVipBenefitPresentation(metadataJSON string, presentation *VipBenefitPresentation) (string, error) { + metadataJSON = strings.TrimSpace(metadataJSON) + if presentation == nil { + if metadataJSON == "" { + return "", nil + } + if !json.Valid([]byte(metadataJSON)) { + return "", errors.New("metadata_json is invalid") + } + return metadataJSON, nil + } + if err := ValidateVipBenefitPresentation(*presentation); err != nil { + return "", err + } + + metadata := make(map[string]json.RawMessage) + if metadataJSON != "" { + if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil || metadata == nil { + return "", errors.New("metadata_json must be an object") + } + } + if vipBenefitPresentationIsZero(*presentation) { + delete(metadata, "presentation") + } else { + encoded, err := json.Marshal(presentation) + if err != nil { + return "", err + } + metadata["presentation"] = encoded + } + if len(metadata) == 0 { + return "", nil + } + encoded, err := json.Marshal(metadata) + return string(encoded), err +} + +// ValidateVipBenefitPresentation 锁定 Flutter 可直接渲染的边界,防止后台把任意 JSON +// 重新变成客户端按 benefit_code 猜类型的隐式协议。 +func ValidateVipBenefitPresentation(presentation VipBenefitPresentation) error { + presentation.Description = strings.TrimSpace(presentation.Description) + if utf8.RuneCountInString(presentation.Description) > vipPresentationDescriptionMaxRunes { + return errors.New("presentation description is too long") + } + if len(presentation.PreviewItems) > vipPresentationPreviewMaxCount { + return errors.New("presentation has too many preview items") + } + seen := make(map[string]struct{}, len(presentation.PreviewItems)) + for index, item := range presentation.PreviewItems { + item.PreviewID = strings.TrimSpace(item.PreviewID) + item.Title = strings.TrimSpace(item.Title) + item.MediaType = strings.ToLower(strings.TrimSpace(item.MediaType)) + item.CompositionType = strings.ToLower(strings.TrimSpace(item.CompositionType)) + if item.PreviewID == "" || utf8.RuneCountInString(item.PreviewID) > vipPresentationTextMaxRunes { + return fmt.Errorf("preview_items[%d].preview_id is invalid", index) + } + if _, exists := seen[item.PreviewID]; exists { + return fmt.Errorf("preview_items[%d].preview_id is duplicated", index) + } + seen[item.PreviewID] = struct{}{} + if utf8.RuneCountInString(item.Title) > vipPresentationTextMaxRunes { + return fmt.Errorf("preview_items[%d].title is too long", index) + } + if item.MediaType != "image" && item.MediaType != "animation" { + return fmt.Errorf("preview_items[%d].media_type is invalid", index) + } + switch item.CompositionType { + case "none", "user_avatar_center", "user_avatar_waveform": + default: + return fmt.Errorf("preview_items[%d].composition_type is invalid", index) + } + if exceedsRuneLimit(item.AssetURL, vipPresentationURLMaxRunes) || exceedsRuneLimit(item.PreviewURL, vipPresentationURLMaxRunes) || exceedsRuneLimit(item.AnimationURL, vipPresentationURLMaxRunes) { + return fmt.Errorf("preview_items[%d] url is too long", index) + } + if item.MediaType == "animation" && strings.TrimSpace(item.AnimationURL) == "" { + return fmt.Errorf("preview_items[%d].animation_url is required", index) + } + if item.MediaType == "image" && strings.TrimSpace(item.PreviewURL) == "" && strings.TrimSpace(item.AssetURL) == "" { + return fmt.Errorf("preview_items[%d] image url is required", index) + } + } + if reward := presentation.NumericReward; reward != nil { + reward.Label = strings.TrimSpace(reward.Label) + reward.Unit = strings.TrimSpace(reward.Unit) + reward.Period = strings.ToLower(strings.TrimSpace(reward.Period)) + if reward.Value <= 0 || reward.Label == "" || reward.Unit == "" || utf8.RuneCountInString(reward.Label) > vipPresentationTextMaxRunes || utf8.RuneCountInString(reward.Unit) > vipPresentationTextMaxRunes { + return errors.New("numeric_reward is invalid") + } + switch reward.Period { + case "once", "daily", "monthly": + default: + return errors.New("numeric_reward.period is invalid") + } + } + return nil +} + +func vipBenefitPresentationIsZero(presentation VipBenefitPresentation) bool { + return strings.TrimSpace(presentation.Description) == "" && len(presentation.PreviewItems) == 0 && presentation.NumericReward == nil +} + +func exceedsRuneLimit(value string, limit int) bool { + return utf8.RuneCountInString(strings.TrimSpace(value)) > limit +} diff --git a/services/wallet-service/internal/domain/ledger/vip_presentation_test.go b/services/wallet-service/internal/domain/ledger/vip_presentation_test.go new file mode 100644 index 00000000..26c2d697 --- /dev/null +++ b/services/wallet-service/internal/domain/ledger/vip_presentation_test.go @@ -0,0 +1,46 @@ +package ledger + +import "testing" + +func TestVipBenefitPresentationRoundTripPreservesOtherMetadata(t *testing.T) { + presentation := &VipBenefitPresentation{ + Description: "头像框会自动合成当前用户头像", + PreviewItems: []VipBenefitPreviewItem{{ + PreviewID: "frame-main", MediaType: "animation", AnimationURL: "https://cdn.example/frame.svga", + PreviewURL: "https://cdn.example/frame.png", CompositionType: "user_avatar_center", SortOrder: 10, + }}, + NumericReward: &VipBenefitNumericReward{Label: "每日返现", Value: 100, Unit: "金币", Period: "daily"}, + } + metadata, err := MergeVipBenefitPresentation(`{"coin_amount":100}`, presentation) + if err != nil { + t.Fatalf("merge presentation failed: %v", err) + } + parsed, err := ParseVipBenefitPresentation(metadata) + if err != nil || parsed == nil { + t.Fatalf("parse presentation failed: parsed=%+v err=%v", parsed, err) + } + if parsed.Description != presentation.Description || len(parsed.PreviewItems) != 1 || parsed.PreviewItems[0].CompositionType != "user_avatar_center" || parsed.NumericReward == nil || parsed.NumericReward.Value != 100 { + t.Fatalf("presentation round trip mismatch: %+v", parsed) + } + if metadata == "" || !containsJSONFragment(metadata, `"coin_amount":100`) { + t.Fatalf("merge must preserve owner metadata: %s", metadata) + } +} + +func TestVipBenefitPresentationRejectsImplicitClientRules(t *testing.T) { + invalid := VipBenefitPresentation{PreviewItems: []VipBenefitPreviewItem{{ + PreviewID: "wave", MediaType: "animation", CompositionType: "guess_from_benefit_code", + }}} + if err := ValidateVipBenefitPresentation(invalid); err == nil { + t.Fatal("unknown composition type must be rejected") + } +} + +func containsJSONFragment(value string, fragment string) bool { + for index := 0; index+len(fragment) <= len(value); index++ { + if value[index:index+len(fragment)] == fragment { + return true + } + } + return false +} diff --git a/services/wallet-service/internal/domain/resource/entitlement.go b/services/wallet-service/internal/domain/resource/entitlement.go index 8fb02e9d..9fa50aae 100644 --- a/services/wallet-service/internal/domain/resource/entitlement.go +++ b/services/wallet-service/internal/domain/resource/entitlement.go @@ -30,6 +30,15 @@ type ListUserResourcesQuery struct { ActiveOnly bool } +type RevokeUserResourceCommand struct { + RequestID string + AppCode string + UserID int64 + EntitlementID string + Reason string + OperatorUserID int64 +} + type EquipUserResourceCommand struct { // RequestID 在通用背包命中 VIP 体验卡时作为专用状态机的 outbox 幂等事件键; // 普通装扮仍不把 request_id 当业务幂等键。 diff --git a/services/wallet-service/internal/domain/resource/shop.go b/services/wallet-service/internal/domain/resource/shop.go index 3fea677f..29005801 100644 --- a/services/wallet-service/internal/domain/resource/shop.go +++ b/services/wallet-service/internal/domain/resource/shop.go @@ -43,10 +43,12 @@ type ResourceShopPurchaseOrder struct { } type ResourceShopItemInput struct { - ShopItemID int64 - ResourceID int64 - Status string - DurationDays int32 + ShopItemID int64 + ResourceID int64 + Status string + DurationDays int32 + // CoinPrice 新建为 0 时使用资源列表价,更新已有规格为 0 时保留当前售价;落库后各规格价格互相独立。 + CoinPrice int64 EffectiveFromMS int64 EffectiveToMS int64 SortOrder int32 diff --git a/services/wallet-service/internal/service/wallet/ports/repository.go b/services/wallet-service/internal/service/wallet/ports/repository.go index 36dda1af..01384f68 100644 --- a/services/wallet-service/internal/service/wallet/ports/repository.go +++ b/services/wallet-service/internal/service/wallet/ports/repository.go @@ -215,6 +215,7 @@ type ResourceGrantStore interface { // ResourceEquipmentStore 管理用户资源权益和穿戴状态。 type ResourceEquipmentStore interface { ListUserResources(ctx context.Context, query resourcedomain.ListUserResourcesQuery) ([]resourcedomain.UserResourceEntitlement, error) + RevokeUserResource(ctx context.Context, command resourcedomain.RevokeUserResourceCommand) (resourcedomain.UserResourceEntitlement, error) EquipUserResource(ctx context.Context, command resourcedomain.EquipUserResourceCommand) (resourcedomain.UserResourceEntitlement, error) UnequipUserResource(ctx context.Context, command resourcedomain.UnequipUserResourceCommand) (resourcedomain.UnequipUserResourceResult, error) BatchGetUserEquippedResources(ctx context.Context, query resourcedomain.BatchGetUserEquippedResourcesQuery) ([]resourcedomain.UserEquippedResources, error) diff --git a/services/wallet-service/internal/service/wallet/resource.go b/services/wallet-service/internal/service/wallet/resource.go index 808dd568..3f0f4c1a 100644 --- a/services/wallet-service/internal/service/wallet/resource.go +++ b/services/wallet-service/internal/service/wallet/resource.go @@ -280,6 +280,15 @@ func (s *Service) ListUserResources(ctx context.Context, query resourcedomain.Li return s.repository.ListUserResources(ctx, query) } +func (s *Service) RevokeUserResource(ctx context.Context, command resourcedomain.RevokeUserResourceCommand) (resourcedomain.UserResourceEntitlement, error) { + if s.repository == nil { + return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") + } + command.AppCode = appcode.Normalize(command.AppCode) + ctx = appcode.WithContext(ctx, command.AppCode) + return s.repository.RevokeUserResource(ctx, command) +} + func (s *Service) EquipUserResource(ctx context.Context, command resourcedomain.EquipUserResourceCommand) (resourcedomain.UserResourceEntitlement, error) { if s.repository == nil { return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") diff --git a/services/wallet-service/internal/service/wallet/service_test.go b/services/wallet-service/internal/service/wallet/service_test.go index 1c2f89d0..39732c0f 100644 --- a/services/wallet-service/internal/service/wallet/service_test.go +++ b/services/wallet-service/internal/service/wallet/service_test.go @@ -5491,6 +5491,132 @@ func TestRevokeResourceGroupGrantReversesWalletCreditAndEntitlement(t *testing.T } } +// TestRevokeUserResourceKeepsResourceGroupSiblings proves the admin detail action removes only the +// selected entitlement. Reusing grant-level revoke here would also remove every sibling in the group. +func TestRevokeUserResourceKeepsResourceGroupSiblings(t *testing.T) { + repository := mysqltest.NewRepository(t) + svc := walletservice.New(repository) + ctx := context.Background() + + hostBadge, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{ + ResourceCode: "host_badge_precise_revoke", + ResourceType: resourcedomain.TypeBadge, + Name: "Host Badge", + Status: resourcedomain.StatusActive, + Grantable: true, + GrantStrategy: resourcedomain.GrantStrategySetActiveFlag, + UsageScopes: []string{"profile"}, + OperatorUserID: 90001, + }) + if err != nil { + t.Fatalf("create host badge failed: %v", err) + } + avatarFrame, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{ + ResourceCode: "host_frame_precise_revoke", + ResourceType: resourcedomain.TypeAvatarFrame, + Name: "Host Frame", + Status: resourcedomain.StatusActive, + Grantable: true, + GrantStrategy: resourcedomain.GrantStrategySetActiveFlag, + UsageScopes: []string{"profile"}, + OperatorUserID: 90001, + }) + if err != nil { + t.Fatalf("create host frame failed: %v", err) + } + group, err := svc.CreateResourceGroup(ctx, resourcedomain.ResourceGroupCommand{ + GroupCode: "host_assets_precise_revoke", + Name: "Host Assets", + Status: resourcedomain.StatusActive, + Items: []resourcedomain.ResourceGroupItemInput{ + {ResourceID: hostBadge.ResourceID, Quantity: 1, SortOrder: 1}, + {ResourceID: avatarFrame.ResourceID, Quantity: 1, SortOrder: 2}, + }, + OperatorUserID: 90001, + }) + if err != nil { + t.Fatalf("create host resource group failed: %v", err) + } + grant, err := svc.GrantResourceGroup(ctx, resourcedomain.GrantResourceGroupCommand{ + CommandID: "cmd-grant-host-assets-precise-revoke", + TargetUserID: 42106, + GroupID: group.GroupID, + Reason: "test precise user resource revoke", + OperatorUserID: 90001, + GrantSource: resourcedomain.GrantSourceAdmin, + }) + if err != nil { + t.Fatalf("grant host resource group failed: %v", err) + } + + var badgeEntitlementID, frameEntitlementID string + for _, item := range grant.Items { + switch item.ResourceID { + case hostBadge.ResourceID: + badgeEntitlementID = item.EntitlementID + case avatarFrame.ResourceID: + frameEntitlementID = item.EntitlementID + } + } + if badgeEntitlementID == "" || frameEntitlementID == "" { + t.Fatalf("grant entitlements are incomplete: %+v", grant.Items) + } + for resourceID, entitlementID := range map[int64]string{ + hostBadge.ResourceID: badgeEntitlementID, + avatarFrame.ResourceID: frameEntitlementID, + } { + if _, err := svc.EquipUserResource(ctx, resourcedomain.EquipUserResourceCommand{ + UserID: 42106, ResourceID: resourceID, EntitlementID: entitlementID, + }); err != nil { + t.Fatalf("equip resource %d failed: %v", resourceID, err) + } + } + + revoked, err := svc.RevokeUserResource(ctx, resourcedomain.RevokeUserResourceCommand{ + RequestID: "request-revoke-host-badge", + UserID: 42106, + EntitlementID: badgeEntitlementID, + Reason: "remove host badge", + OperatorUserID: 90002, + }) + if err != nil { + t.Fatalf("RevokeUserResource failed: %v", err) + } + if revoked.Status != resourcedomain.GrantStatusRevoked || revoked.RemainingQuantity != 0 || revoked.Equipped { + t.Fatalf("revoked entitlement state mismatch: %+v", revoked) + } + active, err := svc.ListUserResources(ctx, resourcedomain.ListUserResourcesQuery{UserID: 42106, ActiveOnly: true}) + if err != nil { + t.Fatalf("list active resources failed: %v", err) + } + if len(active) != 1 || active[0].EntitlementID != frameEntitlementID { + t.Fatalf("resource-group sibling must remain active: %+v", active) + } + if got := repository.CountRows("resource_grants", "grant_id = ? AND status = ?", grant.GrantID, resourcedomain.GrantStatusDone); got != 1 { + t.Fatalf("precise revoke must not revoke the source group grant, got %d", got) + } + if got := repository.CountRows("user_resource_equipment", "user_id = ? AND entitlement_id = ?", int64(42106), badgeEntitlementID); got != 0 { + t.Fatalf("revoked badge equipment row must be removed, got %d", got) + } + if got := repository.CountRows("user_resource_equipment", "user_id = ? AND entitlement_id = ?", int64(42106), frameEntitlementID); got != 1 { + t.Fatalf("sibling frame equipment row must remain, got %d", got) + } + + outboxCount := repository.CountRows("wallet_outbox", "event_type = ? AND user_id = ?", "UserResourceChanged", int64(42106)) + if _, err := svc.RevokeUserResource(ctx, resourcedomain.RevokeUserResourceCommand{ + RequestID: "request-revoke-host-badge-retry", + UserID: 42106, + EntitlementID: badgeEntitlementID, + Reason: "remove host badge retry", + OperatorUserID: 90003, + }); err != nil { + t.Fatalf("idempotent RevokeUserResource retry failed: %v", err) + } + if got := repository.CountRows("wallet_outbox", "event_type = ? AND user_id = ?", "UserResourceChanged", int64(42106)); got != outboxCount { + t.Fatalf("idempotent retry must not duplicate outbox facts: got %d want %d", got, outboxCount) + } +} + // TestRevokeResourceGroupGrantClampsWalletDebitWhenBalanceInsufficient 验证撤回金币时余额不足也不能扣成负数。 func TestRevokeResourceGroupGrantClampsWalletDebitWhenBalanceInsufficient(t *testing.T) { repository := mysqltest.NewRepository(t) @@ -5855,27 +5981,58 @@ func TestPurchaseResourceShopItemDebitsCoinAndGrantsEntitlement(t *testing.T) { t.Fatalf("create shop resource failed: %v", err) } shopItems, err := svc.UpsertResourceShopItems(ctx, resourcedomain.ResourceShopItemsCommand{ - Items: []resourcedomain.ResourceShopItemInput{{ - ResourceID: resource.ResourceID, - Status: resourcedomain.StatusActive, - DurationDays: resourcedomain.ShopDurationThreeDays, - SortOrder: 1, - }}, + Items: []resourcedomain.ResourceShopItemInput{ + { + ResourceID: resource.ResourceID, + Status: resourcedomain.StatusActive, + DurationDays: resourcedomain.ShopDurationOneDay, + SortOrder: 1, + }, + { + ResourceID: resource.ResourceID, + Status: resourcedomain.StatusActive, + DurationDays: resourcedomain.ShopDurationThreeDays, + CoinPrice: 250, + SortOrder: 2, + }, + { + ResourceID: resource.ResourceID, + Status: resourcedomain.StatusActive, + DurationDays: resourcedomain.ShopDurationSevenDays, + CoinPrice: 600, + SortOrder: 3, + }, + }, OperatorUserID: 90001, }) if err != nil { t.Fatalf("upsert shop item failed: %v", err) } + if len(shopItems) != 3 { + t.Fatalf("one resource should keep three sale durations, got %+v", shopItems) + } + var purchaseItem resourcedomain.ResourceShopItem + for _, item := range shopItems { + if item.DurationDays == resourcedomain.ShopDurationOneDay && item.CoinPrice != resource.CoinPrice { + t.Fatalf("omitted sale price should copy resource price: %+v", item) + } + if item.DurationDays == resourcedomain.ShopDurationThreeDays { + purchaseItem = item + } + } + if purchaseItem.ShopItemID == 0 || purchaseItem.CoinPrice != 250 { + t.Fatalf("custom three-day sale price mismatch: %+v", purchaseItem) + } receipt, err := svc.PurchaseResourceShopItem(ctx, resourcedomain.ResourceShopPurchaseCommand{ CommandID: "cmd-shop-badge", UserID: 53001, - ShopItemID: shopItems[0].ShopItemID, + ShopItemID: purchaseItem.ShopItemID, }) if err != nil { t.Fatalf("PurchaseResourceShopItem failed: %v", err) } - if receipt.OrderID == "" || receipt.TransactionID == "" || receipt.GrantID == "" || receipt.CoinSpent != 300 || receipt.Balance.AvailableAmount != 700 { + if receipt.OrderID == "" || receipt.TransactionID == "" || receipt.GrantID == "" || receipt.CoinSpent != 250 || receipt.Balance.AvailableAmount != 750 { t.Fatalf("resource shop purchase receipt mismatch: %+v", receipt) } if receipt.Resource.EntitlementID == "" || receipt.Resource.ResourceID != resource.ResourceID || receipt.Resource.ExpiresAtMS <= time.Now().UnixMilli() { @@ -5884,16 +6041,72 @@ func TestPurchaseResourceShopItemDebitsCoinAndGrantsEntitlement(t *testing.T) { if !receipt.Resource.Equipped { t.Fatalf("resource shop badge purchase should auto equip receipt resource: %+v", receipt.Resource) } + if _, err := svc.UpsertResourceShopItems(ctx, resourcedomain.ResourceShopItemsCommand{ + Items: []resourcedomain.ResourceShopItemInput{{ + ShopItemID: purchaseItem.ShopItemID, + ResourceID: resource.ResourceID, + Status: resourcedomain.StatusActive, + DurationDays: resourcedomain.ShopDurationThreeDays, + CoinPrice: 275, + SortOrder: 2, + }}, + OperatorUserID: 90001, + }); err != nil { + t.Fatalf("update shop item price failed: %v", err) + } + preservedItems, err := svc.UpsertResourceShopItems(ctx, resourcedomain.ResourceShopItemsCommand{ + Items: []resourcedomain.ResourceShopItemInput{{ + ShopItemID: purchaseItem.ShopItemID, + ResourceID: resource.ResourceID, + Status: resourcedomain.StatusActive, + DurationDays: resourcedomain.ShopDurationThreeDays, + SortOrder: 4, + }}, + OperatorUserID: 90002, + }) + if err != nil { + t.Fatalf("update shop item without price failed: %v", err) + } + var preservedPrice int64 + for _, item := range preservedItems { + if item.ShopItemID == purchaseItem.ShopItemID { + preservedPrice = item.CoinPrice + } + } + if preservedPrice != 275 { + t.Fatalf("omitted price on an existing item must preserve the custom price, got %d", preservedPrice) + } + naturalKeyItems, err := svc.UpsertResourceShopItems(ctx, resourcedomain.ResourceShopItemsCommand{ + Items: []resourcedomain.ResourceShopItemInput{{ + ResourceID: resource.ResourceID, + Status: resourcedomain.StatusActive, + DurationDays: resourcedomain.ShopDurationThreeDays, + SortOrder: 5, + }}, + OperatorUserID: 90003, + }) + if err != nil { + t.Fatalf("update shop item by natural key without price failed: %v", err) + } + preservedPrice = 0 + for _, item := range naturalKeyItems { + if item.ShopItemID == purchaseItem.ShopItemID { + preservedPrice = item.CoinPrice + } + } + if preservedPrice != 275 { + t.Fatalf("natural-key update without price must preserve the custom price, got %d", preservedPrice) + } again, err := svc.PurchaseResourceShopItem(ctx, resourcedomain.ResourceShopPurchaseCommand{ CommandID: "cmd-shop-badge", UserID: 53001, - ShopItemID: shopItems[0].ShopItemID, + ShopItemID: purchaseItem.ShopItemID, }) if err != nil { t.Fatalf("PurchaseResourceShopItem retry failed: %v", err) } - if again.OrderID != receipt.OrderID || again.TransactionID != receipt.TransactionID || again.Balance.AvailableAmount != receipt.Balance.AvailableAmount { + if again.OrderID != receipt.OrderID || again.TransactionID != receipt.TransactionID || again.Balance.AvailableAmount != receipt.Balance.AvailableAmount || again.ShopItem.CoinPrice != receipt.CoinSpent { t.Fatalf("resource shop idempotency mismatch: first=%+v again=%+v", receipt, again) } if got := repository.CountRows("resource_shop_purchase_orders", "user_id = ?", int64(53001)); got != 1 { @@ -5902,7 +6115,7 @@ func TestPurchaseResourceShopItemDebitsCoinAndGrantsEntitlement(t *testing.T) { if got := repository.CountRows("wallet_transactions", "command_id = ? AND biz_type = ?", "cmd-shop-badge", "resource_shop_purchase"); got != 1 { t.Fatalf("resource shop purchase should write one wallet transaction, got %d", got) } - if got := repository.CountRows("wallet_entries", "transaction_id = ? AND available_delta = ?", receipt.TransactionID, int64(-300)); got != 1 { + if got := repository.CountRows("wallet_entries", "transaction_id = ? AND available_delta = ?", receipt.TransactionID, int64(-250)); got != 1 { t.Fatalf("resource shop purchase should write one debit entry, got %d", got) } if got := repository.CountRows("resource_grants", "grant_id = ?", receipt.GrantID); got != 1 { @@ -6600,6 +6813,9 @@ func TestPurchaseVipDebitsCoinAndGrantsReward(t *testing.T) { if receipt.OrderID == "" || receipt.TransactionID == "" || receipt.CoinSpent != 1000 || receipt.CoinBalanceAfter != 19000 { t.Fatalf("vip receipt mismatch: %+v", receipt) } + if receipt.CoinBalance.AssetType != ledger.AssetCoin || receipt.CoinBalance.AvailableAmount != 19000 || receipt.CoinBalance.FrozenAmount != 0 || receipt.CoinBalance.Version <= 0 { + t.Fatalf("vip purchase must return versioned coin balance: %+v", receipt.CoinBalance) + } if !receipt.Vip.Active || receipt.Vip.Level != 1 || receipt.Vip.ExpiresAtMS <= receipt.Vip.StartedAtMS { t.Fatalf("vip membership mismatch: %+v", receipt.Vip) } @@ -6624,7 +6840,7 @@ func TestPurchaseVipDebitsCoinAndGrantsReward(t *testing.T) { if err != nil { t.Fatalf("PurchaseVip retry failed: %v", err) } - if again.OrderID != receipt.OrderID || again.CoinBalanceAfter != receipt.CoinBalanceAfter { + if again.OrderID != receipt.OrderID || again.CoinBalanceAfter != receipt.CoinBalanceAfter || again.CoinBalance.Version != receipt.CoinBalance.Version { t.Fatalf("vip idempotency mismatch: first=%+v again=%+v", receipt, again) } if _, err := svc.PurchaseVip(context.Background(), ledger.PurchaseVipCommand{ @@ -6653,6 +6869,14 @@ func TestPurchaseVipDebitsCoinAndGrantsReward(t *testing.T) { if !xerr.IsCode(err, xerr.VIPDowngradeNotAllowed) { t.Fatalf("expected VIP_DOWNGRADE_NOT_ALLOWED, got %v", err) } + _, err = svc.PurchaseVip(context.Background(), ledger.PurchaseVipCommand{ + CommandID: "cmd-vip-insufficient", + UserID: 51002, + Level: 1, + }) + if !xerr.IsCode(err, xerr.VIPInsufficientCoin) { + t.Fatalf("expected VIP_INSUFFICIENT_COIN, got %v", err) + } } func TestPurchaseVipRenewalAlignsRewardEntitlementExpiry(t *testing.T) { diff --git a/services/wallet-service/internal/service/wallet/vip.go b/services/wallet-service/internal/service/wallet/vip.go index 751fe8d4..2e290bfc 100644 --- a/services/wallet-service/internal/service/wallet/vip.go +++ b/services/wallet-service/internal/service/wallet/vip.go @@ -230,10 +230,12 @@ func (s *Service) UpdateAdminVipProgramConfig(ctx context.Context, command ledge return s.repository.UpdateAdminVipProgramConfig(ctx, command) } -// UpdateVipUserSettings 保存用户可关闭的 VIP 通知偏好。用户当前没有 VIP 也允许保存, +// UpdateVipUserSettings 保存用户可关闭的 VIP 展示与隐私偏好。用户当前没有 VIP 也允许保存, // 但偏好不会授予权益;实际执行仍由 CheckVipBenefit 同时校验 effective benefit。 func (s *Service) UpdateVipUserSettings(ctx context.Context, command ledger.UpdateVipUserSettingsCommand) (ledger.VipUserSettings, error) { - if command.UserID <= 0 || (command.RoomEntryNoticeEnabled == nil && command.OnlineGlobalNoticeEnabled == nil) { + if command.UserID <= 0 || (command.RoomEntryNoticeEnabled == nil && command.OnlineGlobalNoticeEnabled == nil && + command.HideProfileDataEnabled == nil && command.AnonymousProfileVisitEnabled == nil && + command.LeaderboardInvisibleEnabled == nil) { return ledger.VipUserSettings{}, xerr.New(xerr.InvalidArgument, "vip user settings update is incomplete") } if s.repository == nil { diff --git a/services/wallet-service/internal/service/wallet/vip_configurable_integration_test.go b/services/wallet-service/internal/service/wallet/vip_configurable_integration_test.go index 016f9d05..72f7ab9e 100644 --- a/services/wallet-service/internal/service/wallet/vip_configurable_integration_test.go +++ b/services/wallet-service/internal/service/wallet/vip_configurable_integration_test.go @@ -144,7 +144,7 @@ func TestFamiVIPReplaceAndTrialCardSwitch(t *testing.T) { if _, err := service.EquipUserResource(ctx, resourcedomain.EquipUserResourceCommand{ AppCode: "fami", RequestID: "generic-equip-disabled", UserID: userID, ResourceID: card3.TrialCard.ResourceID, EntitlementID: card3.TrialCard.EntitlementID, - }); !xerr.IsCode(err, xerr.Conflict) { + }); !xerr.IsCode(err, xerr.VIPProgramInactive) { t.Fatalf("generic backpack equip must preserve trial_card_enabled validation, got %v", err) } if got := repository.CountRows("user_resource_equipment", "app_code = ? AND user_id = ? AND resource_type = ?", "fami", userID, resourcedomain.TypeVIPTrialCard); got != 0 { @@ -270,6 +270,19 @@ func TestFamiVIPReplaceAndTrialCardSwitch(t *testing.T) { if err != nil || !paidRebate.Allowed { t.Fatalf("paid VIP6 must retain configured rebate benefit: allowed=%v err=%v result=%+v", paidRebate.Allowed, err, paidRebate) } + + // 体验卡错误必须稳定区分“实例不存在”和“绝对时间已过期”,Flutter 不解析 message 猜分支。 + if _, _, err := service.EquipVipTrialCard(ctx, ledger.EquipVipTrialCardCommand{ + AppCode: "fami", RequestID: "equip-missing-trial", UserID: userID, EntitlementID: "missing-entitlement", + }); !xerr.IsCode(err, xerr.VIPTrialCardNotFound) { + t.Fatalf("missing trial card must return VIP_TRIAL_CARD_NOT_FOUND, got %v", err) + } + repository.ExpireEntitlement(card6.TrialCard.EntitlementID, time.Now().Add(-time.Minute).UnixMilli()) + if _, _, err := service.EquipVipTrialCard(ctx, ledger.EquipVipTrialCardCommand{ + AppCode: "fami", RequestID: "equip-expired-trial", UserID: userID, EntitlementID: card6.TrialCard.EntitlementID, + }); !xerr.IsCode(err, xerr.VIPTrialCardExpired) { + t.Fatalf("expired trial card must return VIP_TRIAL_CARD_EXPIRED, got %v", err) + } } // TestVipPurchaseConcurrentReplayWithExactBalance 验证幂等守卫发生在账户/会员锁之前: @@ -309,6 +322,9 @@ func TestVipPurchaseConcurrentReplayWithExactBalance(t *testing.T) { if first.receipt.CoinBalanceAfter != 0 || second.receipt.CoinBalanceAfter != 0 { t.Fatalf("idempotent purchase must debit exact balance once: first=%d second=%d", first.receipt.CoinBalanceAfter, second.receipt.CoinBalanceAfter) } + if first.receipt.CoinBalance.Version <= 0 || first.receipt.CoinBalance.Version != second.receipt.CoinBalance.Version { + t.Fatalf("idempotent purchase must preserve original balance version: first=%+v second=%+v", first.receipt.CoinBalance, second.receipt.CoinBalance) + } if got := repository.CountRows("vip_purchase_orders", "app_code = ? AND command_id = ?", "lalu", commandID); got != 1 { t.Fatalf("concurrent replay persisted %d VIP orders, want 1", got) } @@ -399,7 +415,7 @@ func TestVipProgramCanCreateAndResizeAppLevels(t *testing.T) { } repository.SetBalanceForApp("future_app", 880002, 1_000) _, err = service.PurchaseVip(ctx, ledger.PurchaseVipCommand{AppCode: "future_app", CommandID: "buy-hidden-level", UserID: 880002, Level: 3}) - if !xerr.IsCode(err, xerr.VIPLevelNotFound) { + if !xerr.IsCode(err, xerr.VIPPackageNotPurchasable) { t.Fatalf("level above current level_count must be unreachable, got %v", err) } } diff --git a/services/wallet-service/internal/service/wallet/vip_user_settings_integration_test.go b/services/wallet-service/internal/service/wallet/vip_user_settings_integration_test.go index 96168131..62fb6c06 100644 --- a/services/wallet-service/internal/service/wallet/vip_user_settings_integration_test.go +++ b/services/wallet-service/internal/service/wallet/vip_user_settings_integration_test.go @@ -24,7 +24,9 @@ func TestVipUserSettingsDefaultPartialUpdateAndAppScope(t *testing.T) { if err != nil { t.Fatalf("get default VIP settings failed: %v", err) } - if !defaultState.UserSettings.RoomEntryNoticeEnabled || !defaultState.UserSettings.OnlineGlobalNoticeEnabled || defaultState.UserSettings.UpdatedAtMS != 0 { + if !defaultState.UserSettings.RoomEntryNoticeEnabled || !defaultState.UserSettings.OnlineGlobalNoticeEnabled || + !defaultState.UserSettings.HideProfileDataEnabled || !defaultState.UserSettings.AnonymousProfileVisitEnabled || + !defaultState.UserSettings.LeaderboardInvisibleEnabled || defaultState.UserSettings.UpdatedAtMS != 0 { t.Fatalf("missing settings row must project default enabled: %+v", defaultState.UserSettings) } if got := repository.CountRows("user_vip_settings", "app_code = ? AND user_id = ?", "fami", paidUserID); got != 0 { @@ -38,6 +40,13 @@ func TestVipUserSettingsDefaultPartialUpdateAndAppScope(t *testing.T) { if err != nil || settings.RoomEntryNoticeEnabled || !settings.OnlineGlobalNoticeEnabled || settings.UpdatedAtMS <= 0 { t.Fatalf("first partial settings update mismatch: settings=%+v err=%v", settings, err) } + hideProfileOff := false + settings, err = service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{ + AppCode: "fami", UserID: paidUserID, HideProfileDataEnabled: &hideProfileOff, + }) + if err != nil || settings.HideProfileDataEnabled || !settings.AnonymousProfileVisitEnabled || !settings.LeaderboardInvisibleEnabled { + t.Fatalf("privacy partial update must preserve other switches: settings=%+v err=%v", settings, err) + } onlineOff := false settings, err = service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{ AppCode: "fami", UserID: paidUserID, OnlineGlobalNoticeEnabled: &onlineOff, @@ -46,7 +55,9 @@ func TestVipUserSettingsDefaultPartialUpdateAndAppScope(t *testing.T) { t.Fatalf("second partial update must preserve room switch: settings=%+v err=%v", settings, err) } laluState, err := service.GetVipState(laluCtx, paidUserID) - if err != nil || !laluState.UserSettings.RoomEntryNoticeEnabled || !laluState.UserSettings.OnlineGlobalNoticeEnabled || laluState.UserSettings.UpdatedAtMS != 0 { + if err != nil || !laluState.UserSettings.RoomEntryNoticeEnabled || !laluState.UserSettings.OnlineGlobalNoticeEnabled || + !laluState.UserSettings.HideProfileDataEnabled || !laluState.UserSettings.AnonymousProfileVisitEnabled || + !laluState.UserSettings.LeaderboardInvisibleEnabled || laluState.UserSettings.UpdatedAtMS != 0 { t.Fatalf("same user settings must remain App scoped: state=%+v err=%v", laluState.UserSettings, err) } @@ -98,6 +109,10 @@ func TestVipUserSettingsDefaultPartialUpdateAndAppScope(t *testing.T) { t.Fatalf("paid VIP user setting gate mismatch for %s: result=%+v err=%v", code, result, checkErr) } } + hideProfileCheck, err := service.CheckVipBenefit(famiCtx, paidUserID, ledger.VipBenefitCodeHideProfileData) + if err != nil || hideProfileCheck.Allowed || hideProfileCheck.DenialReason != "user_setting_disabled" { + t.Fatalf("hide profile setting gate mismatch: result=%+v err=%v", hideProfileCheck, err) + } roomOn := true settings, err = service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{ AppCode: "fami", UserID: paidUserID, RoomEntryNoticeEnabled: &roomOn, diff --git a/services/wallet-service/internal/storage/mysql/resource_entitlement_repository.go b/services/wallet-service/internal/storage/mysql/resource_entitlement_repository.go index 85d5c16b..f27a551f 100644 --- a/services/wallet-service/internal/storage/mysql/resource_entitlement_repository.go +++ b/services/wallet-service/internal/storage/mysql/resource_entitlement_repository.go @@ -8,6 +8,7 @@ import ( "fmt" "hyapp/pkg/appcode" "hyapp/pkg/xerr" + "hyapp/services/wallet-service/internal/domain/ledger" resourcedomain "hyapp/services/wallet-service/internal/domain/resource" "math" "strings" @@ -63,6 +64,109 @@ func (r *Repository) ListUserResources(ctx context.Context, query resourcedomain return items, rows.Err() } +// RevokeUserResource forcibly invalidates exactly one entitlement. It deliberately does not mutate +// the source grant because a resource-group grant can contain sibling assets that must stay active. +func (r *Repository) RevokeUserResource(ctx context.Context, command resourcedomain.RevokeUserResourceCommand) (resourcedomain.UserResourceEntitlement, error) { + if r == nil || r.db == nil { + return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + ctx = contextWithCommandApp(ctx, command.AppCode) + command.AppCode = appcode.FromContext(ctx) + command.RequestID = strings.TrimSpace(command.RequestID) + command.EntitlementID = strings.TrimSpace(command.EntitlementID) + command.Reason = strings.TrimSpace(command.Reason) + if command.UserID <= 0 || command.EntitlementID == "" || command.Reason == "" || command.OperatorUserID <= 0 { + return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InvalidArgument, "user resource revoke command is incomplete") + } + + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } + defer func() { _ = tx.Rollback() }() + + entitlement, err := r.getUserResourceEntitlementForUpdateTx(ctx, tx, command.EntitlementID) + if err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } + if entitlement.UserID != command.UserID { + // Do not reveal that another user's entitlement exists when a mismatched user path is supplied. + return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.NotFound, "user resource entitlement not found") + } + + nowMs := time.Now().UnixMilli() + changed := entitlement.Status == resourcedomain.StatusActive || entitlement.RemainingQuantity > 0 + if changed { + result, err := tx.ExecContext(ctx, ` + UPDATE user_resource_entitlements + SET status = ?, remaining_quantity = 0, updated_at_ms = ? + WHERE app_code = ? AND entitlement_id = ? AND user_id = ?`, + resourcedomain.GrantStatusRevoked, nowMs, command.AppCode, command.EntitlementID, command.UserID, + ) + if err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } + affected, err := result.RowsAffected() + if err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } + if affected != 1 { + return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.LedgerConflict, "user resource revoke state conflict") + } + } + + // Removing equipment is part of the same transaction as entitlement invalidation, so clients can + // never observe a revoked badge/frame still selected as the user's current appearance. + if _, err := tx.ExecContext(ctx, ` + DELETE FROM user_resource_equipment + WHERE app_code = ? AND user_id = ? AND entitlement_id = ?`, + command.AppCode, command.UserID, command.EntitlementID, + ); err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } + if _, err := tx.ExecContext(ctx, ` + UPDATE user_vip_trial_cards + SET status = ?, updated_at_ms = ? + WHERE app_code = ? AND user_id = ? AND entitlement_id = ? AND status = ?`, + ledger.VipTrialCardStatusRevoked, nowMs, command.AppCode, command.UserID, + command.EntitlementID, ledger.VipTrialCardStatusActive, + ); err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } + + if changed { + eventCommandID := "revoke-user-resource:" + stableHash(fmt.Sprintf("%s|%d|%s", command.AppCode, command.UserID, command.EntitlementID)) + if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ + resourceOutboxEvent("UserResourceChanged", eventCommandID, command.UserID, entitlement.ResourceID, map[string]any{ + "action": "revoke", + "app_code": command.AppCode, + "user_id": command.UserID, + "resource_id": entitlement.ResourceID, + "resource_type": entitlement.Resource.ResourceType, + "entitlement_id": command.EntitlementID, + "source_grant_id": entitlement.SourceGrantID, + "status": resourcedomain.GrantStatusRevoked, + "remaining_quantity": 0, + "revoked_by_user_id": command.OperatorUserID, + "revoke_reason": command.Reason, + "updated_at_ms": nowMs, + "event_command": eventCommandID, + }, nowMs), + }); err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } + } + + revoked, err := r.getUserResourceEntitlementTx(ctx, tx, command.EntitlementID) + if err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } + if err := tx.Commit(); err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } + return revoked, nil +} + func (r *Repository) EquipUserResource(ctx context.Context, command resourcedomain.EquipUserResourceCommand) (resourcedomain.UserResourceEntitlement, error) { if r == nil || r.db == nil { return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") diff --git a/services/wallet-service/internal/storage/mysql/resource_shop_repository.go b/services/wallet-service/internal/storage/mysql/resource_shop_repository.go index 46d8ac12..300584a0 100644 --- a/services/wallet-service/internal/storage/mysql/resource_shop_repository.go +++ b/services/wallet-service/internal/storage/mysql/resource_shop_repository.go @@ -9,11 +9,13 @@ import ( "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" resourcedomain "hyapp/services/wallet-service/internal/domain/resource" + "sort" "strings" "time" ) -// ListResourceShopItems 返回 App 道具商店和后台配置共用的售卖视图;价格始终由资源一日价乘以售卖天数派生。 +// ListResourceShopItems 返回 App 道具商店和后台配置共用的售卖规格;价格优先取规格自己的持久值。 +// 滚动发布期间旧实例仍可能写出 0,这类行必须沿用旧版“资源价 × 天数”,不能误套新建规格的默认价。 func (r *Repository) ListResourceShopItems(ctx context.Context, query resourcedomain.ListResourceShopItemsQuery) ([]resourcedomain.ResourceShopItem, int64, error) { if r == nil || r.db == nil { return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured") @@ -90,7 +92,8 @@ func (r *Repository) ListResourceShopPurchaseOrders(ctx context.Context, query r return items, total, rows.Err() } -// UpsertResourceShopItems 批量添加或重配售卖资源;同一个资源在商店只保留一个当前售卖配置。 +// UpsertResourceShopItems 批量添加或重配售卖规格;同一资源可同时配置 1/3/7 天, +// 但相同资源和相同天数只能保留一个规格,购买端继续用独立 shop_item_id 选择具体档位。 func (r *Repository) UpsertResourceShopItems(ctx context.Context, command resourcedomain.ResourceShopItemsCommand) ([]resourcedomain.ResourceShopItem, error) { if r == nil || r.db == nil { return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured") @@ -108,13 +111,29 @@ func (r *Repository) UpsertResourceShopItems(ctx context.Context, command resour defer func() { _ = tx.Rollback() }() nowMs := time.Now().UnixMilli() - resourceIDs := make([]int64, 0, len(command.Items)) - seen := make(map[int64]struct{}, len(command.Items)) - for _, item := range command.Items { - if _, exists := seen[item.ResourceID]; exists { - return nil, xerr.New(xerr.InvalidArgument, "resource shop contains duplicate resource") + items := append([]resourcedomain.ResourceShopItemInput(nil), command.Items...) + // 所有写事务统一按 resource_id 再按规格身份加锁。购买同样先锁资源再锁商品,避免改价与购买形成反向等待。 + sort.SliceStable(items, func(left, right int) bool { + if items[left].ResourceID != items[right].ResourceID { + return items[left].ResourceID < items[right].ResourceID } - seen[item.ResourceID] = struct{}{} + if items[left].ShopItemID != items[right].ShopItemID { + return items[left].ShopItemID < items[right].ShopItemID + } + return items[left].DurationDays < items[right].DurationDays + }) + resourceIDs := make([]int64, 0, len(items)) + type resourceShopTierKey struct { + resourceID int64 + durationDays int32 + } + seen := make(map[resourceShopTierKey]struct{}, len(items)) + for _, item := range items { + tierKey := resourceShopTierKey{resourceID: item.ResourceID, durationDays: item.DurationDays} + if _, exists := seen[tierKey]; exists { + return nil, xerr.New(xerr.InvalidArgument, "resource shop contains duplicate resource duration") + } + seen[tierKey] = struct{}{} resource, err := r.getResourceForUpdate(ctx, tx, item.ResourceID) if err != nil { return nil, err @@ -122,22 +141,69 @@ func (r *Repository) UpsertResourceShopItems(ctx context.Context, command resour if err := validateResourceShopResource(resource); err != nil { return nil, err } + if item.ShopItemID > 0 { + current, err := r.getResourceShopItemTx(ctx, tx, item.ShopItemID, true) + if err != nil { + return nil, err + } + // shop_item_id 是既有规格的稳定身份,只允许在同一资源内调整天数和价格,防止误把另一资源的条目搬过来。 + if current.ResourceID != item.ResourceID { + return nil, xerr.New(xerr.InvalidArgument, "resource shop item does not match resource") + } + // 旧管理端不会发送新增的 coin_price;更新已有规格时 0 必须保留当前售价,不能把自定义价重置为资源价。 + coinPrice := item.CoinPrice + if coinPrice == 0 { + coinPrice = current.CoinPrice + } + if _, err := tx.ExecContext(ctx, ` + UPDATE resource_shop_items + SET status = ?, duration_days = ?, coin_price = ?, effective_from_ms = ?, effective_to_ms = ?, + sort_order = ?, updated_by_user_id = ?, updated_at_ms = ? + WHERE app_code = ? AND shop_item_id = ?`, + item.Status, item.DurationDays, coinPrice, item.EffectiveFromMS, item.EffectiveToMS, + item.SortOrder, command.OperatorUserID, nowMs, command.AppCode, item.ShopItemID, + ); err != nil { + if isMySQLDuplicateError(err) { + return nil, xerr.New(xerr.Conflict, "resource shop duration already exists") + } + return nil, err + } + resourceIDs = append(resourceIDs, item.ResourceID) + continue + } + // 新规格未传价格时复制资源列表价一次;落库后规格价格独立,不随资源目录调价变化。 + coinPrice := item.CoinPrice + if coinPrice == 0 { + current, exists, err := r.getResourceShopItemByTierTx(ctx, tx, item.ResourceID, item.DurationDays, true) + if err != nil { + return nil, err + } + if exists { + // 兼容未发送 shop_item_id 的旧调用:自然键已存在时 0 表示未提供价格,必须保留当前售价。 + coinPrice = current.CoinPrice + } else { + coinPrice = resource.CoinPrice + } + } if _, err := tx.ExecContext(ctx, ` INSERT INTO resource_shop_items ( - app_code, resource_id, status, duration_days, effective_from_ms, effective_to_ms, sort_order, + app_code, resource_id, status, duration_days, coin_price, effective_from_ms, effective_to_ms, sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = VALUES(status), - duration_days = VALUES(duration_days), + coin_price = VALUES(coin_price), effective_from_ms = VALUES(effective_from_ms), effective_to_ms = VALUES(effective_to_ms), sort_order = VALUES(sort_order), updated_by_user_id = VALUES(updated_by_user_id), updated_at_ms = VALUES(updated_at_ms)`, - command.AppCode, item.ResourceID, item.Status, item.DurationDays, item.EffectiveFromMS, item.EffectiveToMS, item.SortOrder, + command.AppCode, item.ResourceID, item.Status, item.DurationDays, coinPrice, item.EffectiveFromMS, item.EffectiveToMS, item.SortOrder, command.OperatorUserID, command.OperatorUserID, nowMs, nowMs, ); err != nil { + if isMySQLDuplicateError(err) { + return nil, xerr.New(xerr.Conflict, "resource shop duration already exists") + } return nil, err } resourceIDs = append(resourceIDs, item.ResourceID) @@ -167,7 +233,7 @@ func (r *Repository) SetResourceShopItemStatus(ctx context.Context, command reso } defer func() { _ = tx.Rollback() }() - item, err := r.getResourceShopItemTx(ctx, tx, command.ShopItemID, true) + item, err := r.lockResourceShopItemTx(ctx, tx, command.ShopItemID) if err != nil { return resourcedomain.ResourceShopItem{}, err } @@ -221,7 +287,7 @@ func (r *Repository) PurchaseResourceShopItem(ctx context.Context, command resou } nowMs := time.Now().UnixMilli() - item, err := r.getResourceShopItemTx(ctx, tx, command.ShopItemID, true) + item, err := r.lockResourceShopItemTx(ctx, tx, command.ShopItemID) if err != nil { return resourcedomain.ResourceShopPurchaseReceipt{}, err } @@ -345,6 +411,39 @@ func (r *Repository) getResourceShopItemTx(ctx context.Context, tx *sql.Tx, shop return r.getResourceShopItemWithQuerier(ctx, tx, shopItemID, lock) } +// lockResourceShopItemTx 先无锁定位不可变的 resource_id,再统一按“资源 -> 商品”顺序取写锁。 +// 改价、启停和购买都走这里,避免某条链路先锁商品、另一条链路先锁资源而形成循环等待。 +func (r *Repository) lockResourceShopItemTx(ctx context.Context, tx *sql.Tx, shopItemID int64) (resourcedomain.ResourceShopItem, error) { + candidate, err := r.getResourceShopItemTx(ctx, tx, shopItemID, false) + if err != nil { + return resourcedomain.ResourceShopItem{}, err + } + if _, err := r.getResourceForUpdate(ctx, tx, candidate.ResourceID); err != nil { + return resourcedomain.ResourceShopItem{}, err + } + item, err := r.getResourceShopItemTx(ctx, tx, shopItemID, true) + if err != nil { + return resourcedomain.ResourceShopItem{}, err + } + // resource_id 按业务约束不可变;若被旁路修改,不能在未锁定的新资源行上继续启停、改价或扣费。 + if item.ResourceID != candidate.ResourceID { + return resourcedomain.ResourceShopItem{}, xerr.New(xerr.Conflict, "resource shop item changed while locking") + } + return item, nil +} + +func (r *Repository) getResourceShopItemByTierTx(ctx context.Context, tx *sql.Tx, resourceID int64, durationDays int32, lock bool) (resourcedomain.ResourceShopItem, bool, error) { + query := resourceShopItemSelectSQL() + ` WHERE si.app_code = ? AND si.resource_id = ? AND si.duration_days = ?` + if lock { + query += ` FOR UPDATE` + } + item, err := scanResourceShopItem(tx.QueryRowContext(ctx, query, appcode.FromContext(ctx), resourceID, durationDays)) + if errors.Is(err, sql.ErrNoRows) { + return resourcedomain.ResourceShopItem{}, false, nil + } + return item, err == nil, err +} + func (r *Repository) getResourceShopItemWithQuerier(ctx context.Context, querier sqlRowQuerier, shopItemID int64, lock bool) (resourcedomain.ResourceShopItem, error) { query := resourceShopItemSelectSQL() + ` WHERE si.app_code = ? AND si.shop_item_id = ?` if lock { @@ -361,15 +460,16 @@ func (r *Repository) getResourceShopItemWithQuerier(ctx context.Context, querier func (r *Repository) resourceShopPurchaseReceiptForTransaction(ctx context.Context, tx *sql.Tx, transactionID string, userID int64) (resourcedomain.ResourceShopPurchaseReceipt, error) { var orderID string var shopItemID int64 + var durationDays int32 var priceCoin int64 var grantID string var entitlementID string err := tx.QueryRowContext(ctx, ` - SELECT order_id, shop_item_id, price_coin, resource_grant_id, entitlement_id + SELECT order_id, shop_item_id, duration_days, price_coin, resource_grant_id, entitlement_id FROM resource_shop_purchase_orders WHERE app_code = ? AND wallet_transaction_id = ? AND user_id = ?`, appcode.FromContext(ctx), transactionID, userID, - ).Scan(&orderID, &shopItemID, &priceCoin, &grantID, &entitlementID) + ).Scan(&orderID, &shopItemID, &durationDays, &priceCoin, &grantID, &entitlementID) if err != nil { return resourcedomain.ResourceShopPurchaseReceipt{}, err } @@ -377,6 +477,9 @@ func (r *Repository) resourceShopPurchaseReceiptForTransaction(ctx context.Conte if err != nil { return resourcedomain.ResourceShopPurchaseReceipt{}, err } + // 幂等重试必须返回原订单规格;运营在首单后改价或改配置时,不能让回执里的商品价格与 coin_spent 自相矛盾。 + item.DurationDays = durationDays + item.CoinPrice = priceCoin resource, err := r.getUserResourceEntitlementTx(ctx, tx, entitlementID) if err != nil { return resourcedomain.ResourceShopPurchaseReceipt{}, err @@ -532,9 +635,9 @@ func scanResourceShopPurchaseOrder(scanner scanTarget) (resourcedomain.ResourceS func resourceShopItemSelectSQL() string { return ` - SELECT si.app_code, si.shop_item_id, si.resource_id, si.status, si.duration_days, - r.price_type, - CASE WHEN r.price_type = 'coin' THEN r.coin_price * si.duration_days ELSE 0 END AS coin_price, + SELECT si.app_code, si.shop_item_id, si.resource_id, si.status, si.duration_days, + r.price_type, + CASE WHEN si.coin_price > 0 THEN si.coin_price ELSE r.coin_price * si.duration_days END AS coin_price, si.effective_from_ms, si.effective_to_ms, si.sort_order, si.created_by_user_id, si.updated_by_user_id, si.created_at_ms, si.updated_at_ms, ` + resourceColumnsWithAlias("r") + ` @@ -651,6 +754,9 @@ func validateResourceShopItemsCommand(command resourcedomain.ResourceShopItemsCo if !resourcedomain.ValidShopDurationDays(item.DurationDays) { return xerr.New(xerr.InvalidArgument, "resource shop duration is invalid") } + if item.CoinPrice < 0 { + return xerr.New(xerr.InvalidArgument, "resource shop coin price is invalid") + } if item.EffectiveFromMS < 0 || item.EffectiveToMS < 0 || (item.EffectiveFromMS > 0 && item.EffectiveToMS > 0 && item.EffectiveToMS <= item.EffectiveFromMS) { return xerr.New(xerr.InvalidArgument, "resource shop effective time is invalid") } 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 205456b7..ba5253ba 100644 --- a/services/wallet-service/internal/storage/mysql/vip_admin_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_admin_repository.go @@ -198,6 +198,13 @@ func normalizeAdminVipLevelCommands(levels []ledger.AdminVipLevelCommand, progra benefit.ResourceType = resourcedomain.NormalizeResourceType(benefit.ResourceType) benefit.ExecutionScope = strings.ToLower(strings.TrimSpace(benefit.ExecutionScope)) benefit.MetadataJSON = strings.TrimSpace(benefit.MetadataJSON) + // 后台可直接提交结构化 presentation;钱包仍把它与 coin_amount/message 等扩展字段 + // 原子保存在同一 JSON 列,避免展示配置出现双写版本。 + mergedMetadata, mergeErr := ledger.MergeVipBenefitPresentation(benefit.MetadataJSON, benefit.Presentation) + if mergeErr != nil { + return nil, xerr.New(xerr.InvalidArgument, "vip benefit presentation is invalid") + } + benefit.MetadataJSON = mergedMetadata // 体验卡永远不参与金币返现;保存时强制收敛,运行时仍会按编码再次兜底。 if benefit.BenefitCode == ledger.VipBenefitCodeDailyCoinRebate { benefit.TrialEnabled = false @@ -265,6 +272,9 @@ func (r *Repository) validateAdminVipBenefit(ctx context.Context, tx *sql.Tx, le if benefit.MetadataJSON != "" && !json.Valid([]byte(benefit.MetadataJSON)) { return xerr.New(xerr.InvalidArgument, "vip benefit metadata_json is invalid") } + if _, err := ledger.ParseVipBenefitPresentation(benefit.MetadataJSON); err != nil { + return xerr.New(xerr.InvalidArgument, "vip benefit presentation is invalid") + } if benefit.BenefitCode == ledger.VipBenefitCodeDailyCoinRebate { if benefit.ExecutionScope != ledger.VipExecutionScopeWallet { return xerr.New(xerr.InvalidArgument, "daily_coin_rebate execution_scope must be wallet") diff --git a/services/wallet-service/internal/storage/mysql/vip_program_repository.go b/services/wallet-service/internal/storage/mysql/vip_program_repository.go index 3a01a948..2598d73c 100644 --- a/services/wallet-service/internal/storage/mysql/vip_program_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_program_repository.go @@ -80,7 +80,7 @@ func (r *Repository) GetVipState(ctx context.Context, userID int64) (ledger.VipS func (r *Repository) requireVipProgramConfig(ctx context.Context, querier vipSnapshotQuerier) (ledger.VipProgramConfig, error) { config, err := r.queryVipProgramConfig(ctx, querier.QueryRowContext(ctx, vipProgramSelectSQL(), appcode.FromContext(ctx))) if errors.Is(err, sql.ErrNoRows) { - return ledger.VipProgramConfig{}, xerr.New(xerr.NotFound, "vip program config not found") + return ledger.VipProgramConfig{}, xerr.New(xerr.VIPProgramInactive, "vip program config not found") } return config, err } @@ -282,6 +282,11 @@ func scanVipBenefit(scanner vipProgramScanner) (ledger.VipBenefit, error) { if metadata.Valid { benefit.MetadataJSON = metadata.String } + if err == nil { + // presentation 是 metadata_json 的稳定结构化投影;读取时统一解析,HTTP/gRPC 调用方 + // 无需按 benefit_code 重复维护 JSON schema。 + benefit.Presentation, err = ledger.ParseVipBenefitPresentation(benefit.MetadataJSON) + } return benefit, err } 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 b5877ebc..eb5741d2 100644 --- a/services/wallet-service/internal/storage/mysql/vip_purchase_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_purchase_repository.go @@ -43,23 +43,26 @@ func (r *Repository) PurchaseVip(ctx context.Context, command ledger.PurchaseVip program, err := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, command.AppCode)) if errors.Is(err, sql.ErrNoRows) { - return ledger.PurchaseVipReceipt{}, xerr.New(xerr.NotFound, "vip program config not found") + return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPProgramInactive, "vip program config not found") } if err != nil { return ledger.PurchaseVipReceipt{}, err } if program.Status != ledger.VipStatusActive { - return ledger.PurchaseVipReceipt{}, xerr.New(xerr.Conflict, "vip program is disabled") + return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPProgramInactive, "vip program is disabled") } if command.Level > program.LevelCount { - return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPLevelNotFound, "vip level exceeds program level_count") + return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPPackageNotPurchasable, "vip level exceeds program level_count") } level, err := r.getVipLevelForUpdate(ctx, tx, command.Level) if err != nil { + if xerr.IsCode(err, xerr.VIPLevelNotFound) { + return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPPackageNotPurchasable, "vip package not found") + } return ledger.PurchaseVipReceipt{}, err } if level.Status != ledger.VipStatusActive { - return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPLevelDisabled, "vip level is disabled") + return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPPackageNotPurchasable, "vip package is disabled") } current, err := r.ensureUserVipForUpdate(ctx, tx, command.UserID, nowMs) if err != nil { @@ -85,22 +88,25 @@ func (r *Repository) PurchaseVip(ctx context.Context, command ledger.PurchaseVip return ledger.PurchaseVipReceipt{}, err } if account.AvailableAmount < level.PriceCoin { - return ledger.PurchaseVipReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance") + return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPInsufficientCoin, "insufficient coin balance") } transactionID := transactionID(command.AppCode, command.CommandID) 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, - "vip_name": level.Name, - "previous_level": previousLevel, - "price_coin": level.PriceCoin, - "expires_at_ms": newExpiresAt, - "balance_after": coinBalanceAfter, + "app_code": command.AppCode, + "order_id": orderID, + "user_id": command.UserID, + "target_level": command.Level, + "vip_name": level.Name, + "previous_level": previousLevel, + "price_coin": level.PriceCoin, + "expires_at_ms": newExpiresAt, + "balance_after": coinBalanceAfter, + // 余额版本必须与本次交易后的快照一起固化,幂等重放不能读取已被后续交易推进的当前版本。 + "balance_version": account.Version + 1, + "frozen_after": account.FrozenAmount, "reward_group_id": level.RewardResourceGroupID, "duration_ms": level.DurationMS, "previous_expires_at_ms": current.ExpiresAtMS, @@ -203,8 +209,13 @@ func (r *Repository) PurchaseVip(ctx context.Context, command ledger.PurchaseVip Vip: activation.VIP, CoinSpent: level.PriceCoin, CoinBalanceAfter: coinBalanceAfter, - RewardItems: activation.RewardItems, - State: state, + CoinBalance: ledger.AssetBalance{ + AppCode: command.AppCode, UserID: command.UserID, AssetType: ledger.AssetCoin, + AvailableAmount: coinBalanceAfter, FrozenAmount: account.FrozenAmount, + Version: account.Version + 1, UpdatedAtMs: nowMs, + }, + RewardItems: activation.RewardItems, + State: state, }, nil } @@ -518,9 +529,10 @@ func (r *Repository) vipPurchaseReceiptForTransaction(ctx context.Context, tx *s return ledger.PurchaseVipReceipt{}, err } var metadata struct { - VIPName string `json:"vip_name"` - StartedAtMS int64 `json:"started_at_ms"` - ExpiresAtMS int64 `json:"expires_at_ms"` + VIPName string `json:"vip_name"` + StartedAtMS int64 `json:"started_at_ms"` + ExpiresAtMS int64 `json:"expires_at_ms"` + BalanceVersion int64 `json:"balance_version"` } _ = json.Unmarshal([]byte(txRow.MetadataJSON), &metadata) if expiresAtMS <= 0 { @@ -552,6 +564,9 @@ func (r *Repository) vipPurchaseReceiptForTransaction(ctx context.Context, tx *s if err != nil { return ledger.PurchaseVipReceipt{}, err } + // 2026-07-15 之后的新交易始终从交易 metadata 还原原始版本。旧交易没有该字段时保持 0, + // Flutter 看到 version=0 必须走余额查询,不能拿当前账户版本伪装成本次历史回执。 + balance.Version = metadata.BalanceVersion rewards, err := r.rewardItemsByGrant(ctx, tx, grantID, expiresAtMS) if err != nil { return ledger.PurchaseVipReceipt{}, err @@ -562,6 +577,7 @@ func (r *Repository) vipPurchaseReceiptForTransaction(ctx context.Context, tx *s Vip: vip, CoinSpent: priceCoin, CoinBalanceAfter: balance.AvailableAmount, + CoinBalance: balance, RewardItems: rewards, }, nil } diff --git a/services/wallet-service/internal/storage/mysql/vip_trial_card_repository.go b/services/wallet-service/internal/storage/mysql/vip_trial_card_repository.go index a2de54cd..48b2b716 100644 --- a/services/wallet-service/internal/storage/mysql/vip_trial_card_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_trial_card_repository.go @@ -238,29 +238,38 @@ func (r *Repository) EquipVipTrialCard(ctx context.Context, command ledger.Equip defer func() { _ = tx.Rollback() }() nowMS := time.Now().UnixMilli() program, err := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL(), command.AppCode)) + if errors.Is(err, sql.ErrNoRows) { + return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPProgramInactive, "vip program config not found") + } if err != nil { return ledger.VipTrialCard{}, ledger.VipState{}, err } if program.Status != ledger.VipStatusActive || !program.TrialCardEnabled { - return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.Conflict, "vip trial card is disabled for this app") + return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPProgramInactive, "vip trial card is disabled for this app") } // 锁序必须与 RevokeResourceGrant 保持 entitlement -> trial_card;反向先锁 card 会和后台撤销形成死锁。 // entitlement 同时承担并发有效性门禁,避免撤销在校验后抢先提交又被重新写回 equipment。 entitlement, err := r.getUserResourceEntitlementForUpdateTx(ctx, tx, command.EntitlementID) if err != nil { + if xerr.IsCode(err, xerr.NotFound) { + return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPTrialCardNotFound, "vip trial card entitlement not found") + } return ledger.VipTrialCard{}, ledger.VipState{}, err } card, err := r.queryVipTrialCardForUpdate(ctx, tx, command.UserID, command.EntitlementID, nowMS) if errors.Is(err, sql.ErrNoRows) { - return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.NotFound, "vip trial card not found") + return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPTrialCardNotFound, "vip trial card not found") } if err != nil { return ledger.VipTrialCard{}, ledger.VipState{}, err } + if card.ExpiresAtMS <= nowMS || (entitlement.ExpiresAtMS > 0 && entitlement.ExpiresAtMS <= nowMS) { + return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPTrialCardExpired, "vip trial card expired") + } if entitlement.UserID != command.UserID || entitlement.ResourceID != card.ResourceID || entitlement.Status != resourcedomain.StatusActive || entitlement.EffectiveAtMS > nowMS || - (entitlement.ExpiresAtMS > 0 && entitlement.ExpiresAtMS <= nowMS) || entitlement.RemainingQuantity <= 0 { - return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.Conflict, "vip trial card entitlement is inactive") + card.Status != ledger.VipTrialCardStatusActive || card.EffectiveAtMS > nowMS || entitlement.RemainingQuantity <= 0 { + return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPTrialCardNotFound, "vip trial card entitlement is inactive") } if resourcedomain.NormalizeResourceType(entitlement.Resource.ResourceType) != resourcedomain.TypeVIPTrialCard { return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.Conflict, "vip trial card resource type mismatch") @@ -435,10 +444,9 @@ func (r *Repository) queryVipTrialCardForUpdate(ctx context.Context, tx *sql.Tx, duration_ms, effective_at_ms, expires_at_ms, grant_source, source_grant_id, created_at_ms, updated_at_ms FROM user_vip_trial_cards - WHERE app_code = ? AND user_id = ? AND entitlement_id = ? AND status = ? - AND effective_at_ms <= ? AND expires_at_ms > ? + WHERE app_code = ? AND user_id = ? AND entitlement_id = ? FOR UPDATE`, - appcode.FromContext(ctx), userID, entitlementID, ledger.VipTrialCardStatusActive, nowMS, nowMS, + appcode.FromContext(ctx), userID, entitlementID, ) return scanVipTrialCard(row, nowMS) } diff --git a/services/wallet-service/internal/storage/mysql/vip_user_settings_repository.go b/services/wallet-service/internal/storage/mysql/vip_user_settings_repository.go index 0bcd846e..07887e63 100644 --- a/services/wallet-service/internal/storage/mysql/vip_user_settings_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_user_settings_repository.go @@ -19,7 +19,9 @@ func (r *Repository) UpdateVipUserSettings(ctx context.Context, command ledger.U } ctx = contextWithCommandApp(ctx, command.AppCode) command.AppCode = appcode.FromContext(ctx) - if command.UserID <= 0 || (command.RoomEntryNoticeEnabled == nil && command.OnlineGlobalNoticeEnabled == nil) { + if command.UserID <= 0 || (command.RoomEntryNoticeEnabled == nil && command.OnlineGlobalNoticeEnabled == nil && + command.HideProfileDataEnabled == nil && command.AnonymousProfileVisitEnabled == nil && + command.LeaderboardInvisibleEnabled == nil) { return ledger.VipUserSettings{}, xerr.New(xerr.InvalidArgument, "vip user settings update is incomplete") } @@ -39,17 +41,39 @@ func (r *Repository) UpdateVipUserSettings(ctx context.Context, command ledger.U if onlineProvided { onlineValue = *command.OnlineGlobalNoticeEnabled } + hideProfileValue := true + hideProfileProvided := command.HideProfileDataEnabled != nil + if hideProfileProvided { + hideProfileValue = *command.HideProfileDataEnabled + } + anonymousVisitValue := true + anonymousVisitProvided := command.AnonymousProfileVisitEnabled != nil + if anonymousVisitProvided { + anonymousVisitValue = *command.AnonymousProfileVisitEnabled + } + leaderboardInvisibleValue := true + leaderboardInvisibleProvided := command.LeaderboardInvisibleEnabled != nil + if leaderboardInvisibleProvided { + leaderboardInvisibleValue = *command.LeaderboardInvisibleEnabled + } if _, err := tx.ExecContext(ctx, ` INSERT INTO user_vip_settings ( app_code, user_id, room_entry_notice_enabled, online_global_notice_enabled, + hide_profile_data_enabled, anonymous_profile_visit_enabled, leaderboard_invisible_enabled, created_at_ms, updated_at_ms - ) VALUES (?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE room_entry_notice_enabled = IF(?, ?, room_entry_notice_enabled), online_global_notice_enabled = IF(?, ?, online_global_notice_enabled), + hide_profile_data_enabled = IF(?, ?, hide_profile_data_enabled), + anonymous_profile_visit_enabled = IF(?, ?, anonymous_profile_visit_enabled), + leaderboard_invisible_enabled = IF(?, ?, leaderboard_invisible_enabled), updated_at_ms = ?`, - command.AppCode, command.UserID, roomValue, onlineValue, nowMS, nowMS, - roomProvided, roomValue, onlineProvided, onlineValue, nowMS, + command.AppCode, command.UserID, roomValue, onlineValue, hideProfileValue, + anonymousVisitValue, leaderboardInvisibleValue, nowMS, nowMS, + roomProvided, roomValue, onlineProvided, onlineValue, + hideProfileProvided, hideProfileValue, anonymousVisitProvided, anonymousVisitValue, + leaderboardInvisibleProvided, leaderboardInvisibleValue, nowMS, ); err != nil { return ledger.VipUserSettings{}, err } @@ -70,7 +94,8 @@ func (r *Repository) queryVipUserSettings(ctx context.Context, querier interface }, userID int64, lock bool) (ledger.VipUserSettings, error) { query := ` SELECT app_code, user_id, room_entry_notice_enabled, - online_global_notice_enabled, updated_at_ms + online_global_notice_enabled, hide_profile_data_enabled, + anonymous_profile_visit_enabled, leaderboard_invisible_enabled, updated_at_ms FROM user_vip_settings WHERE app_code = ? AND user_id = ?` if lock { @@ -79,7 +104,9 @@ func (r *Repository) queryVipUserSettings(ctx context.Context, querier interface var settings ledger.VipUserSettings if err := querier.QueryRowContext(ctx, query, appcode.FromContext(ctx), userID).Scan( &settings.AppCode, &settings.UserID, &settings.RoomEntryNoticeEnabled, - &settings.OnlineGlobalNoticeEnabled, &settings.UpdatedAtMS, + &settings.OnlineGlobalNoticeEnabled, &settings.HideProfileDataEnabled, + &settings.AnonymousProfileVisitEnabled, &settings.LeaderboardInvisibleEnabled, + &settings.UpdatedAtMS, ); err != nil { if errors.Is(err, sql.ErrNoRows) { return ledger.DefaultVipUserSettings(appcode.FromContext(ctx), userID), nil diff --git a/services/wallet-service/internal/transport/grpc/resource.go b/services/wallet-service/internal/transport/grpc/resource.go index 08442f16..69ebdf72 100644 --- a/services/wallet-service/internal/transport/grpc/resource.go +++ b/services/wallet-service/internal/transport/grpc/resource.go @@ -349,6 +349,21 @@ func (s *Server) RevokeResourceGrant(ctx context.Context, req *walletv1.RevokeRe return &walletv1.ResourceGrantResponse{Grant: resourceGrantToProto(grant)}, nil } +func (s *Server) RevokeUserResource(ctx context.Context, req *walletv1.RevokeUserResourceRequest) (*walletv1.RevokeUserResourceResponse, error) { + resource, err := s.svc.RevokeUserResource(ctx, resourcedomain.RevokeUserResourceCommand{ + RequestID: req.GetRequestId(), + AppCode: req.GetAppCode(), + UserID: req.GetUserId(), + EntitlementID: req.GetEntitlementId(), + Reason: req.GetReason(), + OperatorUserID: req.GetOperatorUserId(), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + return &walletv1.RevokeUserResourceResponse{Resource: userResourceToProto(resource)}, nil +} + func (s *Server) ListUserResources(ctx context.Context, req *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error) { items, err := s.svc.ListUserResources(ctx, resourcedomain.ListUserResourcesQuery{ AppCode: req.GetAppCode(), diff --git a/services/wallet-service/internal/transport/grpc/resource_command_mapper.go b/services/wallet-service/internal/transport/grpc/resource_command_mapper.go index c245653b..69a18c98 100644 --- a/services/wallet-service/internal/transport/grpc/resource_command_mapper.go +++ b/services/wallet-service/internal/transport/grpc/resource_command_mapper.go @@ -164,6 +164,7 @@ func resourceShopItemInputs(items []*walletv1.ResourceShopItemInput) []resourced ResourceID: item.GetResourceId(), Status: item.GetStatus(), DurationDays: item.GetDurationDays(), + CoinPrice: item.GetCoinPrice(), EffectiveFromMS: item.GetEffectiveFromMs(), EffectiveToMS: item.GetEffectiveToMs(), SortOrder: item.GetSortOrder(), diff --git a/services/wallet-service/internal/transport/grpc/vip.go b/services/wallet-service/internal/transport/grpc/vip.go index 5d8a2782..8e0305d5 100644 --- a/services/wallet-service/internal/transport/grpc/vip.go +++ b/services/wallet-service/internal/transport/grpc/vip.go @@ -90,6 +90,7 @@ func (s *Server) PurchaseVip(ctx context.Context, req *walletv1.PurchaseVipReque CoinBalanceAfter: receipt.CoinBalanceAfter, RewardItems: vipRewardItemsToProto(receipt.RewardItems), State: vipStateToProto(receipt.State), + CoinBalance: balanceToProto(receipt.CoinBalance), }, nil } @@ -255,9 +256,26 @@ func (s *Server) UpdateMyVipSettings(ctx context.Context, req *walletv1.UpdateMy value := req.GetOnlineGlobalNoticeEnabled() onlineGlobalNoticeEnabled = &value } + var hideProfileDataEnabled *bool + if req.HideProfileDataEnabled != nil { + value := req.GetHideProfileDataEnabled() + hideProfileDataEnabled = &value + } + var anonymousProfileVisitEnabled *bool + if req.AnonymousProfileVisitEnabled != nil { + value := req.GetAnonymousProfileVisitEnabled() + anonymousProfileVisitEnabled = &value + } + var leaderboardInvisibleEnabled *bool + if req.LeaderboardInvisibleEnabled != nil { + value := req.GetLeaderboardInvisibleEnabled() + leaderboardInvisibleEnabled = &value + } settings, err := s.svc.UpdateVipUserSettings(ctx, ledger.UpdateVipUserSettingsCommand{ AppCode: req.GetAppCode(), UserID: req.GetUserId(), RoomEntryNoticeEnabled: roomEntryNoticeEnabled, OnlineGlobalNoticeEnabled: onlineGlobalNoticeEnabled, + HideProfileDataEnabled: hideProfileDataEnabled, AnonymousProfileVisitEnabled: anonymousProfileVisitEnabled, + LeaderboardInvisibleEnabled: leaderboardInvisibleEnabled, }) if err != nil { return nil, xerr.ToGRPCError(err) @@ -355,9 +373,12 @@ func vipDailyCoinRebateToProto(rebate ledger.VipDailyCoinRebate) *walletv1.VipDa func vipUserSettingsToProto(settings ledger.VipUserSettings) *walletv1.VipUserSettings { return &walletv1.VipUserSettings{ AppCode: settings.AppCode, UserId: settings.UserID, - RoomEntryNoticeEnabled: settings.RoomEntryNoticeEnabled, - OnlineGlobalNoticeEnabled: settings.OnlineGlobalNoticeEnabled, - UpdatedAtMs: settings.UpdatedAtMS, + RoomEntryNoticeEnabled: settings.RoomEntryNoticeEnabled, + OnlineGlobalNoticeEnabled: settings.OnlineGlobalNoticeEnabled, + HideProfileDataEnabled: settings.HideProfileDataEnabled, + AnonymousProfileVisitEnabled: settings.AnonymousProfileVisitEnabled, + LeaderboardInvisibleEnabled: settings.LeaderboardInvisibleEnabled, + UpdatedAtMs: settings.UpdatedAtMS, } } @@ -439,6 +460,7 @@ func vipBenefitToProto(benefit ledger.VipBenefit) *walletv1.VipBenefit { MetadataJson: benefit.MetadataJSON, CreatedAtMs: benefit.CreatedAtMS, UpdatedAtMs: benefit.UpdatedAtMS, + Presentation: vipBenefitPresentationToProto(benefit.Presentation), } } @@ -471,11 +493,62 @@ func vipBenefitsFromProto(benefits []*walletv1.VipBenefit) []ledger.VipBenefit { AutoEquip: benefit.GetAutoEquip(), SortOrder: benefit.GetSortOrder(), MetadataJSON: benefit.GetMetadataJson(), + Presentation: vipBenefitPresentationFromProto(benefit.GetPresentation()), }) } return items } +func vipBenefitPresentationToProto(presentation *ledger.VipBenefitPresentation) *walletv1.VipBenefitPresentation { + if presentation == nil { + return nil + } + result := &walletv1.VipBenefitPresentation{ + Description: presentation.Description, + PreviewItems: make([]*walletv1.VipBenefitPreviewItem, 0, len(presentation.PreviewItems)), + } + for _, item := range presentation.PreviewItems { + result.PreviewItems = append(result.PreviewItems, &walletv1.VipBenefitPreviewItem{ + PreviewId: item.PreviewID, Title: item.Title, MediaType: item.MediaType, + AssetUrl: item.AssetURL, PreviewUrl: item.PreviewURL, AnimationUrl: item.AnimationURL, + CompositionType: item.CompositionType, SortOrder: item.SortOrder, + }) + } + if presentation.NumericReward != nil { + result.NumericReward = &walletv1.VipBenefitNumericReward{ + Label: presentation.NumericReward.Label, Value: presentation.NumericReward.Value, + Unit: presentation.NumericReward.Unit, Period: presentation.NumericReward.Period, + } + } + return result +} + +func vipBenefitPresentationFromProto(presentation *walletv1.VipBenefitPresentation) *ledger.VipBenefitPresentation { + if presentation == nil { + return nil + } + result := &ledger.VipBenefitPresentation{ + Description: presentation.GetDescription(), + PreviewItems: make([]ledger.VipBenefitPreviewItem, 0, len(presentation.GetPreviewItems())), + } + for _, item := range presentation.GetPreviewItems() { + if item == nil { + continue + } + result.PreviewItems = append(result.PreviewItems, ledger.VipBenefitPreviewItem{ + PreviewID: item.GetPreviewId(), Title: item.GetTitle(), MediaType: item.GetMediaType(), + AssetURL: item.GetAssetUrl(), PreviewURL: item.GetPreviewUrl(), AnimationURL: item.GetAnimationUrl(), + CompositionType: item.GetCompositionType(), SortOrder: item.GetSortOrder(), + }) + } + if reward := presentation.GetNumericReward(); reward != nil { + result.NumericReward = &ledger.VipBenefitNumericReward{ + Label: reward.GetLabel(), Value: reward.GetValue(), Unit: reward.GetUnit(), Period: reward.GetPeriod(), + } + } + return result +} + func vipTrialCardToProto(card ledger.VipTrialCard) *walletv1.VipTrialCard { if card.TrialCardID == "" { return nil