package grpc import ( "context" "time" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" ) // ListVipPackages 返回当前 VIP 和可购买包列表。 func (s *Server) ListVipPackages(ctx context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) state, levels, err := s.svc.ListVipPackages(ctx, req.GetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.ListVipPackagesResponse{ CurrentVip: vipToProto(state.EffectiveVip), Packages: make([]*walletv1.VipLevel, 0, len(levels)), State: vipStateToProto(state), ProgramConfig: vipProgramConfigToProto(state.ProgramConfig), } for _, level := range levels { resp.Packages = append(resp.Packages, vipLevelToProto(level)) } return resp, nil } // GetMyVip 返回当前登录用户 VIP 状态。 func (s *Server) GetMyVip(ctx context.Context, req *walletv1.GetMyVipRequest) (*walletv1.GetMyVipResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) state, err := s.svc.GetVipState(ctx, req.GetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } // 旧 vip 字段保留为最终生效投影,付费会员原始事实由 state.paid_vip 明确返回。 return &walletv1.GetMyVipResponse{Vip: vipToProto(state.EffectiveVip), State: vipStateToProto(state)}, nil } // GetVipProgramConfig 返回按 App 隔离的状态机配置和最高等级权益目录。 func (s *Server) GetVipProgramConfig(ctx context.Context, req *walletv1.GetVipProgramConfigRequest) (*walletv1.GetVipProgramConfigResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) config, benefits, err := s.svc.GetVipProgramConfig(ctx) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.GetVipProgramConfigResponse{ Config: vipProgramConfigToProto(config), Benefits: vipBenefitsToProto(benefits), ServerTimeMs: time.Now().UnixMilli(), }, nil } // CheckVipBenefit 供权益 owner 精确判断单项能力,不允许调用方退化成等级比较。 func (s *Server) CheckVipBenefit(ctx context.Context, req *walletv1.CheckVipBenefitRequest) (*walletv1.CheckVipBenefitResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) result, err := s.svc.CheckVipBenefit(ctx, req.GetUserId(), req.GetBenefitCode()) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.CheckVipBenefitResponse{ Allowed: result.Allowed, Benefit: vipBenefitToProto(result.Benefit), State: vipStateToProto(result.State), DenialReason: result.DenialReason, EvaluatedAtMs: result.EvaluatedAtMS, }, nil } // PurchaseVip 购买、续期或升级 VIP。 func (s *Server) PurchaseVip(ctx context.Context, req *walletv1.PurchaseVipRequest) (*walletv1.PurchaseVipResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.PurchaseVip(ctx, ledger.PurchaseVipCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), UserID: req.GetUserId(), Level: req.GetLevel(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.PurchaseVipResponse{ OrderId: receipt.OrderID, TransactionId: receipt.TransactionID, Vip: vipToProto(receipt.Vip), CoinSpent: receipt.CoinSpent, CoinBalanceAfter: receipt.CoinBalanceAfter, RewardItems: vipRewardItemsToProto(receipt.RewardItems), State: vipStateToProto(receipt.State), }, nil } // GrantVip 统一处理活动和后台赠送 VIP。 func (s *Server) GrantVip(ctx context.Context, req *walletv1.GrantVipRequest) (*walletv1.GrantVipResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.GrantVip(ctx, ledger.GrantVipCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), TargetUserID: req.GetTargetUserId(), Level: req.GetLevel(), GrantSource: req.GetGrantSource(), OperatorUserID: req.GetOperatorUserId(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.GrantVipResponse{ TransactionId: receipt.TransactionID, Vip: vipToProto(receipt.Vip), RewardItems: vipRewardItemsToProto(receipt.RewardItems), ServerTimeMs: time.Now().UnixMilli(), State: vipStateToProto(receipt.State), }, nil } // GrantVipTrialCard 发卡但不自动佩戴,resource_id 省略时由钱包按 level 解析同 App 预置资源。 func (s *Server) GrantVipTrialCard(ctx context.Context, req *walletv1.GrantVipTrialCardRequest) (*walletv1.GrantVipTrialCardResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) resourceID := int64(0) if req.ResourceId != nil { resourceID = req.GetResourceId() } receipt, err := s.svc.GrantVipTrialCard(ctx, ledger.GrantVipTrialCardCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), TargetUserID: req.GetTargetUserId(), Level: req.GetLevel(), DurationMS: req.GetDurationMs(), ResourceID: resourceID, GrantSource: req.GetGrantSource(), OperatorUserID: req.GetOperatorUserId(), Reason: req.GetReason(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.GrantVipTrialCardResponse{ TrialCard: vipTrialCardToProto(receipt.TrialCard), State: vipStateToProto(receipt.State), ServerTimeMs: receipt.ServerTimeMS, }, nil } // EquipVipTrialCard 精确佩戴一个 entitlement;同类型旧装备仅解除指针,库存与到期时间保持不变。 func (s *Server) EquipVipTrialCard(ctx context.Context, req *walletv1.EquipVipTrialCardRequest) (*walletv1.EquipVipTrialCardResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) card, state, err := s.svc.EquipVipTrialCard(ctx, ledger.EquipVipTrialCardCommand{ AppCode: req.GetAppCode(), RequestID: req.GetRequestId(), UserID: req.GetUserId(), EntitlementID: req.GetEntitlementId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.EquipVipTrialCardResponse{ TrialCard: vipTrialCardToProto(card), State: vipStateToProto(state), ServerTimeMs: time.Now().UnixMilli(), }, nil } // UnequipVipTrialCard 卸下当前体验卡并返回回落后的最终 VIP 状态。 func (s *Server) UnequipVipTrialCard(ctx context.Context, req *walletv1.UnequipVipTrialCardRequest) (*walletv1.UnequipVipTrialCardResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) unequipped, state, err := s.svc.UnequipVipTrialCard(ctx, ledger.UnequipVipTrialCardCommand{ AppCode: req.GetAppCode(), RequestID: req.GetRequestId(), UserID: req.GetUserId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.UnequipVipTrialCardResponse{ Unequipped: unequipped, State: vipStateToProto(state), ServerTimeMs: time.Now().UnixMilli(), }, nil } // ListAdminVipLevels 返回后台 VIP 配置。 func (s *Server) ListAdminVipLevels(ctx context.Context, req *walletv1.ListAdminVipLevelsRequest) (*walletv1.ListAdminVipLevelsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) config, levels, err := s.svc.ListAdminVipLevels(ctx) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.ListAdminVipLevelsResponse{Levels: make([]*walletv1.VipLevel, 0, len(levels)), ServerTimeMs: time.Now().UnixMilli(), ProgramConfig: vipProgramConfigToProto(config)} for _, level := range levels { resp.Levels = append(resp.Levels, vipLevelToProto(level)) } return resp, nil } // UpdateAdminVipLevels 保存后台 VIP 配置。 func (s *Server) UpdateAdminVipLevels(ctx context.Context, req *walletv1.UpdateAdminVipLevelsRequest) (*walletv1.UpdateAdminVipLevelsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) commands := make([]ledger.AdminVipLevelCommand, 0, len(req.GetLevels())) for _, item := range req.GetLevels() { if item == nil { continue } commands = append(commands, ledger.AdminVipLevelCommand{ Level: item.GetLevel(), Name: item.GetName(), Status: item.GetStatus(), PriceCoin: item.GetPriceCoin(), DurationMS: item.GetDurationMs(), RewardResourceGroupID: item.GetRewardResourceGroupId(), RequiredRechargeCoinAmount: item.GetRequiredRechargeCoinAmount(), SortOrder: item.GetSortOrder(), Benefits: vipBenefitsFromProto(item.GetBenefits()), }) } config, levels, err := s.svc.UpdateAdminVipLevels(ctx, commands, req.GetOperatorUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &walletv1.UpdateAdminVipLevelsResponse{Levels: make([]*walletv1.VipLevel, 0, len(levels)), ServerTimeMs: time.Now().UnixMilli(), ProgramConfig: vipProgramConfigToProto(config)} for _, level := range levels { resp.Levels = append(resp.Levels, vipLevelToProto(level)) } return resp, nil } // UpdateAdminVipProgramConfig 保存 App 级策略,审计操作者只接受请求外层的服务端身份字段。 func (s *Server) UpdateAdminVipProgramConfig(ctx context.Context, req *walletv1.UpdateAdminVipProgramConfigRequest) (*walletv1.UpdateAdminVipProgramConfigResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) config := vipProgramConfigFromProto(req.GetConfig()) config.AppCode = req.GetAppCode() updated, err := s.svc.UpdateAdminVipProgramConfig(ctx, ledger.AdminVipProgramConfigCommand{ Config: config, OperatorUserID: req.GetOperatorUserId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.UpdateAdminVipProgramConfigResponse{Config: vipProgramConfigToProto(updated), ServerTimeMs: time.Now().UnixMilli()}, nil } // UpdateMyVipSettings 保留 proto optional 的 partial update 语义;未提交字段不能被零值 false 覆盖。 func (s *Server) UpdateMyVipSettings(ctx context.Context, req *walletv1.UpdateMyVipSettingsRequest) (*walletv1.UpdateMyVipSettingsResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) var roomEntryNoticeEnabled *bool if req.RoomEntryNoticeEnabled != nil { value := req.GetRoomEntryNoticeEnabled() roomEntryNoticeEnabled = &value } var onlineGlobalNoticeEnabled *bool if req.OnlineGlobalNoticeEnabled != nil { value := req.GetOnlineGlobalNoticeEnabled() onlineGlobalNoticeEnabled = &value } settings, err := s.svc.UpdateVipUserSettings(ctx, ledger.UpdateVipUserSettingsCommand{ AppCode: req.GetAppCode(), UserID: req.GetUserId(), RoomEntryNoticeEnabled: roomEntryNoticeEnabled, OnlineGlobalNoticeEnabled: onlineGlobalNoticeEnabled, }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.UpdateMyVipSettingsResponse{ Settings: vipUserSettingsToProto(settings), ServerTimeMs: time.Now().UTC().UnixMilli(), }, nil } // GetMyCurrentVipDailyCoinRebate 返回当前 UTC 日可领取/已领取事实;未生成以 found=false 表达。 func (s *Server) GetMyCurrentVipDailyCoinRebate(ctx context.Context, req *walletv1.GetMyCurrentVipDailyCoinRebateRequest) (*walletv1.GetMyCurrentVipDailyCoinRebateResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) rebate, found, err := s.svc.GetMyCurrentVipDailyCoinRebate(ctx, req.GetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } response := &walletv1.GetMyCurrentVipDailyCoinRebateResponse{Found: found, ServerTimeMs: time.Now().UTC().UnixMilli()} if found { response.Rebate = vipDailyCoinRebateToProto(rebate) } return response, nil } // ListMyVipDailyCoinRebateStatuses 同时支持历史分页和通知按 rebate_ids 精确恢复;repository 始终附加 user_id 条件。 func (s *Server) ListMyVipDailyCoinRebateStatuses(ctx context.Context, req *walletv1.ListMyVipDailyCoinRebateStatusesRequest) (*walletv1.ListMyVipDailyCoinRebateStatusesResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) items, total, err := s.svc.ListMyVipDailyCoinRebateStatuses(ctx, ledger.ListVipDailyCoinRebateStatusesQuery{ AppCode: req.GetAppCode(), UserID: req.GetUserId(), Page: int(req.GetPage()), PageSize: int(req.GetPageSize()), RebateIDs: append([]string(nil), req.GetRebateIds()...), }) if err != nil { return nil, xerr.ToGRPCError(err) } response := &walletv1.ListMyVipDailyCoinRebateStatusesResponse{ Rebates: make([]*walletv1.VipDailyCoinRebate, 0, len(items)), Total: total, ServerTimeMs: time.Now().UTC().UnixMilli(), } for _, item := range items { response.Rebates = append(response.Rebates, vipDailyCoinRebateToProto(item)) } return response, nil } // ClaimVipDailyCoinRebate 不接收金额;wallet owner 从锁内返现行读取并返回稳定账后余额。 func (s *Server) ClaimVipDailyCoinRebate(ctx context.Context, req *walletv1.ClaimVipDailyCoinRebateRequest) (*walletv1.ClaimVipDailyCoinRebateResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) receipt, err := s.svc.ClaimVipDailyCoinRebate(ctx, ledger.ClaimVipDailyCoinRebateCommand{ AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), UserID: req.GetUserId(), RebateID: req.GetRebateId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &walletv1.ClaimVipDailyCoinRebateResponse{ Rebate: vipDailyCoinRebateToProto(receipt.Rebate), TransactionId: receipt.TransactionID, CoinBalance: balanceToProto(receipt.CoinBalance), ServerTimeMs: time.Now().UTC().UnixMilli(), }, nil } func vipToProto(vip ledger.UserVip) *walletv1.UserVip { return &walletv1.UserVip{ UserId: vip.UserID, Level: vip.Level, Name: vip.Name, Active: vip.Active, StartedAtMs: vip.StartedAtMS, ExpiresAtMs: vip.ExpiresAtMS, UpdatedAtMs: vip.UpdatedAtMS, ProgramType: vip.ProgramType, ConfigVersion: vip.ConfigVersion, } } func vipDailyCoinRebateRunToProto(run ledger.VipDailyCoinRebateRun) *walletv1.VipDailyCoinRebateRun { return &walletv1.VipDailyCoinRebateRun{ RunId: run.RunID, TaskDay: run.TaskDay, DayStartMs: run.DayStartMS, DayEndMs: run.DayEndMS, ConfigVersion: run.ConfigVersion, LevelAmountsJson: run.LevelAmountsJSON, Status: run.Status, LastUserId: run.LastUserID, ScannedCount: run.ScannedCount, CreatedCount: run.CreatedCount, CreatedAtMs: run.CreatedAtMS, UpdatedAtMs: run.UpdatedAtMS, CompletedAtMs: run.CompletedAtMS, } } func vipDailyCoinRebateToProto(rebate ledger.VipDailyCoinRebate) *walletv1.VipDailyCoinRebate { if rebate.RebateID == "" { return nil } return &walletv1.VipDailyCoinRebate{ RebateId: rebate.RebateID, UserId: rebate.UserID, TaskDay: rebate.TaskDay, VipLevel: rebate.VipLevel, VipName: rebate.VipName, CoinAmount: rebate.CoinAmount, Status: rebate.Status, AvailableAtMs: rebate.AvailableAtMS, ExpiresAtMs: rebate.ExpiresAtMS, ConfigVersion: rebate.ConfigVersion, ClaimedAtMs: rebate.ClaimedAtMS, WalletTransactionId: rebate.WalletTransactionID, CreatedAtMs: rebate.CreatedAtMS, UpdatedAtMs: rebate.UpdatedAtMS, } } 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, } } func vipLevelToProto(level ledger.VipLevel) *walletv1.VipLevel { return &walletv1.VipLevel{ Level: level.Level, Name: level.Name, Status: level.Status, PriceCoin: level.PriceCoin, DurationMs: level.DurationMS, RewardResourceGroupId: level.RewardResourceGroupID, RewardItems: vipRewardItemsToProto(level.RewardItems), CanPurchase: level.CanPurchase, SortOrder: level.SortOrder, RechargeGateRequired: level.RechargeGateRequired, RequiredRechargeCoinAmount: level.RequiredRechargeCoinAmount, UserRechargeCoinAmount: level.UserRechargeCoinAmount, PurchaseLockedReason: level.PurchaseLockedReason, CreatedAtMs: level.CreatedAtMS, UpdatedAtMs: level.UpdatedAtMS, Benefits: vipBenefitsToProto(level.Benefits), ConfigVersion: level.ConfigVersion, } } func vipProgramConfigToProto(config ledger.VipProgramConfig) *walletv1.VipProgramConfig { return &walletv1.VipProgramConfig{ AppCode: config.AppCode, ProgramType: config.ProgramType, LevelCount: config.LevelCount, SameLevelExpiryPolicy: config.SameLevelExpiryPolicy, UpgradeExpiryPolicy: config.UpgradeExpiryPolicy, DowngradePurchasePolicy: config.DowngradePurchasePolicy, BenefitInheritancePolicy: config.BenefitInheritancePolicy, GrantMode: config.GrantMode, TrialCardEnabled: config.TrialCardEnabled, Status: config.Status, ConfigVersion: config.ConfigVersion, UpdatedByAdminId: config.UpdatedByAdminID, CreatedAtMs: config.CreatedAtMS, UpdatedAtMs: config.UpdatedAtMS, } } func vipProgramConfigFromProto(config *walletv1.VipProgramConfig) ledger.VipProgramConfig { if config == nil { return ledger.VipProgramConfig{} } return ledger.VipProgramConfig{ AppCode: config.GetAppCode(), ProgramType: config.GetProgramType(), LevelCount: config.GetLevelCount(), SameLevelExpiryPolicy: config.GetSameLevelExpiryPolicy(), UpgradeExpiryPolicy: config.GetUpgradeExpiryPolicy(), DowngradePurchasePolicy: config.GetDowngradePurchasePolicy(), BenefitInheritancePolicy: config.GetBenefitInheritancePolicy(), GrantMode: config.GetGrantMode(), TrialCardEnabled: config.GetTrialCardEnabled(), Status: config.GetStatus(), } } func vipBenefitToProto(benefit ledger.VipBenefit) *walletv1.VipBenefit { if benefit.BenefitCode == "" { return nil } return &walletv1.VipBenefit{ BenefitCode: benefit.BenefitCode, Name: benefit.Name, BenefitType: benefit.BenefitType, UnlockLevel: benefit.UnlockLevel, Status: benefit.Status, TrialEnabled: benefit.TrialEnabled, ResourceId: benefit.ResourceID, ResourceType: benefit.ResourceType, ExecutionScope: benefit.ExecutionScope, AutoEquip: benefit.AutoEquip, SortOrder: benefit.SortOrder, MetadataJson: benefit.MetadataJSON, CreatedAtMs: benefit.CreatedAtMS, UpdatedAtMs: benefit.UpdatedAtMS, } } func vipBenefitsToProto(benefits []ledger.VipBenefit) []*walletv1.VipBenefit { items := make([]*walletv1.VipBenefit, 0, len(benefits)) for _, benefit := range benefits { if item := vipBenefitToProto(benefit); item != nil { items = append(items, item) } } return items } func vipBenefitsFromProto(benefits []*walletv1.VipBenefit) []ledger.VipBenefit { items := make([]ledger.VipBenefit, 0, len(benefits)) for _, benefit := range benefits { if benefit == nil { continue } items = append(items, ledger.VipBenefit{ BenefitCode: benefit.GetBenefitCode(), Name: benefit.GetName(), BenefitType: benefit.GetBenefitType(), UnlockLevel: benefit.GetUnlockLevel(), Status: benefit.GetStatus(), TrialEnabled: benefit.GetTrialEnabled(), ResourceID: benefit.GetResourceId(), ResourceType: benefit.GetResourceType(), ExecutionScope: benefit.GetExecutionScope(), AutoEquip: benefit.GetAutoEquip(), SortOrder: benefit.GetSortOrder(), MetadataJSON: benefit.GetMetadataJson(), }) } return items } func vipTrialCardToProto(card ledger.VipTrialCard) *walletv1.VipTrialCard { if card.TrialCardID == "" { return nil } return &walletv1.VipTrialCard{ TrialCardId: card.TrialCardID, EntitlementId: card.EntitlementID, ResourceId: card.ResourceID, UserId: card.UserID, Level: card.Level, Name: card.Name, Status: card.Status, Equipped: card.Equipped, DurationMs: card.DurationMS, EffectiveAtMs: card.EffectiveAtMS, ExpiresAtMs: card.ExpiresAtMS, RemainingDurationMs: card.RemainingDurationMS, GrantSource: card.GrantSource, SourceGrantId: card.SourceGrantID, CreatedAtMs: card.CreatedAtMS, UpdatedAtMs: card.UpdatedAtMS, } } func vipStateToProto(state ledger.VipState) *walletv1.VipState { return &walletv1.VipState{ PaidVip: vipToProto(state.PaidVip), EquippedTrialCard: func() *walletv1.VipTrialCard { if state.EquippedTrialCard == nil { return nil } return vipTrialCardToProto(*state.EquippedTrialCard) }(), EffectiveVip: vipToProto(state.EffectiveVip), EffectiveSource: state.EffectiveSource, EffectiveBenefits: vipBenefitsToProto(state.EffectiveBenefits), EvaluatedAtMs: state.EvaluatedAtMS, ProgramConfig: vipProgramConfigToProto(state.ProgramConfig), UserSettings: vipUserSettingsToProto(state.UserSettings), } } func vipRewardItemsToProto(items []ledger.VipRewardItem) []*walletv1.VipRewardItem { resp := make([]*walletv1.VipRewardItem, 0, len(items)) for _, item := range items { resp = append(resp, &walletv1.VipRewardItem{ ResourceId: item.ResourceID, ResourceCode: item.ResourceCode, ResourceType: item.ResourceType, Name: item.Name, Quantity: item.Quantity, ExpiresAtMs: item.ExpiresAtMS, AssetUrl: item.AssetURL, PreviewUrl: item.PreviewURL, AnimationUrl: item.AnimationURL, }) } return resp }