package wallet import ( "context" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" "strings" ) // ListVipPackages 返回同一配置快照下的最终 VIP 状态和可购买套餐。 func (s *Service) ListVipPackages(ctx context.Context, userID int64) (ledger.VipState, []ledger.VipLevel, error) { if userID <= 0 { return ledger.VipState{}, nil, xerr.New(xerr.InvalidArgument, "user_id is required") } if s.repository == nil { return ledger.VipState{}, nil, xerr.New(xerr.Unavailable, "wallet repository is not configured") } ctx = appcode.WithContext(ctx, appcode.FromContext(ctx)) return s.repository.ListVipPackages(ctx, userID) } // GetMyVip 返回当前用户会员状态;过期会员会按 active=false 投影。 func (s *Service) GetMyVip(ctx context.Context, userID int64) (ledger.UserVip, error) { if userID <= 0 { return ledger.UserVip{}, xerr.New(xerr.InvalidArgument, "user_id is required") } if s.repository == nil { return ledger.UserVip{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } ctx = appcode.WithContext(ctx, appcode.FromContext(ctx)) return s.repository.GetMyVip(ctx, userID) } // GetVipProgramConfig 返回当前 App 的完整 VIP 规则和权益目录;不存在配置时直接失败, // 避免业务在缺少迁移或错误 app_code 时悄悄回退到另一套体系。 func (s *Service) GetVipProgramConfig(ctx context.Context) (ledger.VipProgramConfig, []ledger.VipBenefit, error) { if s.repository == nil { return ledger.VipProgramConfig{}, nil, xerr.New(xerr.Unavailable, "wallet repository is not configured") } ctx = appcode.WithContext(ctx, appcode.FromContext(ctx)) return s.repository.GetVipProgramConfig(ctx) } // GetVipState 统一返回付费、体验卡和最终生效状态,客户端与 owner service 都不能自行按等级合并。 func (s *Service) GetVipState(ctx context.Context, userID int64) (ledger.VipState, error) { if userID <= 0 { return ledger.VipState{}, xerr.New(xerr.InvalidArgument, "user_id is required") } if s.repository == nil { return ledger.VipState{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } ctx = appcode.WithContext(ctx, appcode.FromContext(ctx)) return s.repository.GetVipState(ctx, userID) } // CheckVipBenefit 是房间、用户资料和通知 owner 执行单项权益时的服务端判定入口。 func (s *Service) CheckVipBenefit(ctx context.Context, userID int64, benefitCode string) (ledger.CheckVipBenefitResult, error) { benefitCode = strings.ToLower(strings.TrimSpace(benefitCode)) if userID <= 0 || benefitCode == "" { return ledger.CheckVipBenefitResult{}, xerr.New(xerr.InvalidArgument, "user_id and benefit_code are required") } if s.repository == nil { return ledger.CheckVipBenefitResult{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } ctx = appcode.WithContext(ctx, appcode.FromContext(ctx)) return s.repository.CheckVipBenefit(ctx, userID, benefitCode) } // PurchaseVip 执行 VIP 购买、升级或续期;降级由 repository 在锁内按当前会员状态拒绝。 func (s *Service) PurchaseVip(ctx context.Context, command ledger.PurchaseVipCommand) (ledger.PurchaseVipReceipt, error) { command.CommandID = strings.TrimSpace(command.CommandID) if command.CommandID == "" || command.UserID <= 0 || command.Level <= 0 { return ledger.PurchaseVipReceipt{}, xerr.New(xerr.InvalidArgument, "vip purchase command is incomplete") } if len(command.CommandID) > 128 { return ledger.PurchaseVipReceipt{}, xerr.New(xerr.InvalidArgument, "command_id is too long") } if s.repository == nil { return ledger.PurchaseVipReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.PurchaseVip(ctx, command) } // GrantVip 是活动和后台赠送 VIP 的统一入口;购买仍走 PurchaseVip 完成扣费。 func (s *Service) GrantVip(ctx context.Context, command ledger.GrantVipCommand) (ledger.GrantVipReceipt, error) { command.CommandID = strings.TrimSpace(command.CommandID) if command.CommandID == "" || command.TargetUserID <= 0 || command.Level <= 0 { return ledger.GrantVipReceipt{}, xerr.New(xerr.InvalidArgument, "vip grant command is incomplete") } if len(command.CommandID) > 128 { return ledger.GrantVipReceipt{}, xerr.New(xerr.InvalidArgument, "command_id is too long") } command.GrantSource = ledger.NormalizeVipGrantSource(command.GrantSource) switch command.GrantSource { case ledger.VipGrantSourceAdmin, ledger.VipGrantSourceManagerCenter: if command.OperatorUserID <= 0 { return ledger.GrantVipReceipt{}, xerr.New(xerr.InvalidArgument, "operator_user_id is required") } case ledger.VipGrantSourceActivity: if command.OperatorUserID < 0 { return ledger.GrantVipReceipt{}, xerr.New(xerr.InvalidArgument, "operator_user_id is invalid") } default: return ledger.GrantVipReceipt{}, xerr.New(xerr.InvalidArgument, "vip grant_source is invalid") } command.Reason = strings.TrimSpace(command.Reason) if command.Reason == "" { command.Reason = command.GrantSource } if len(command.Reason) > 256 { return ledger.GrantVipReceipt{}, xerr.New(xerr.InvalidArgument, "reason is too long") } if s.repository == nil { return ledger.GrantVipReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.GrantVip(ctx, command) } // GrantVipTrialCard 发放独立绝对计时的背包卡片;发卡不自动佩戴,避免后台操作改变用户当前展示。 func (s *Service) GrantVipTrialCard(ctx context.Context, command ledger.GrantVipTrialCardCommand) (ledger.GrantVipTrialCardReceipt, error) { command.CommandID = strings.TrimSpace(command.CommandID) command.Reason = strings.TrimSpace(command.Reason) command.GrantSource = ledger.NormalizeVipGrantSource(command.GrantSource) if command.CommandID == "" || command.TargetUserID <= 0 || command.Level <= 0 || command.DurationMS <= 0 { return ledger.GrantVipTrialCardReceipt{}, xerr.New(xerr.InvalidArgument, "vip trial card command is incomplete") } if len(command.CommandID) > 128 || len(command.Reason) > 256 || command.ResourceID < 0 { return ledger.GrantVipTrialCardReceipt{}, xerr.New(xerr.InvalidArgument, "vip trial card command is invalid") } switch command.GrantSource { case ledger.VipGrantSourceAdmin, ledger.VipGrantSourceManagerCenter: if command.OperatorUserID <= 0 { return ledger.GrantVipTrialCardReceipt{}, xerr.New(xerr.InvalidArgument, "operator_user_id is required") } case ledger.VipGrantSourceActivity: if command.OperatorUserID < 0 { return ledger.GrantVipTrialCardReceipt{}, xerr.New(xerr.InvalidArgument, "operator_user_id is invalid") } default: return ledger.GrantVipTrialCardReceipt{}, xerr.New(xerr.InvalidArgument, "vip grant_source is invalid") } if command.Reason == "" { command.Reason = command.GrantSource } if s.repository == nil { return ledger.GrantVipTrialCardReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.GrantVipTrialCard(ctx, command) } // EquipVipTrialCard 只切换通用装备指针,任一卡片的 effective/expires 时间都不会变化。 func (s *Service) EquipVipTrialCard(ctx context.Context, command ledger.EquipVipTrialCardCommand) (ledger.VipTrialCard, ledger.VipState, error) { command.RequestID = strings.TrimSpace(command.RequestID) command.EntitlementID = strings.TrimSpace(command.EntitlementID) if command.RequestID == "" || command.UserID <= 0 || command.EntitlementID == "" { return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.InvalidArgument, "vip trial equip command is incomplete") } if len(command.RequestID) > 128 || len(command.EntitlementID) > 96 { return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.InvalidArgument, "vip trial equip command is invalid") } if s.repository == nil { return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.EquipVipTrialCard(ctx, command) } // UnequipVipTrialCard 清理当前体验卡装备记录,最终状态立即回落到仍有效的付费会员。 func (s *Service) UnequipVipTrialCard(ctx context.Context, command ledger.UnequipVipTrialCardCommand) (bool, ledger.VipState, error) { command.RequestID = strings.TrimSpace(command.RequestID) if command.RequestID == "" || command.UserID <= 0 { return false, ledger.VipState{}, xerr.New(xerr.InvalidArgument, "vip trial unequip command is incomplete") } if len(command.RequestID) > 128 { return false, ledger.VipState{}, xerr.New(xerr.InvalidArgument, "request_id is too long") } if s.repository == nil { return false, ledger.VipState{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.UnequipVipTrialCard(ctx, command) } // ListAdminVipLevels 返回后台 VIP program 与动态等级的同一数据库快照。 func (s *Service) ListAdminVipLevels(ctx context.Context) (ledger.VipProgramConfig, []ledger.VipLevel, error) { if s.repository == nil { return ledger.VipProgramConfig{}, nil, xerr.New(xerr.Unavailable, "wallet repository is not configured") } ctx = appcode.WithContext(ctx, appcode.FromContext(ctx)) return s.repository.ListAdminVipLevels(ctx) } // UpdateAdminVipLevels 保存后台 VIP 等级配置;完整校验下沉到 repository,保证和购买事务一致。 func (s *Service) UpdateAdminVipLevels(ctx context.Context, levels []ledger.AdminVipLevelCommand, operatorUserID int64) (ledger.VipProgramConfig, []ledger.VipLevel, error) { if operatorUserID <= 0 { return ledger.VipProgramConfig{}, nil, xerr.New(xerr.InvalidArgument, "operator_user_id is required") } if s.repository == nil { return ledger.VipProgramConfig{}, nil, xerr.New(xerr.Unavailable, "wallet repository is not configured") } normalized := make([]ledger.AdminVipLevelCommand, 0, len(levels)) for _, level := range levels { level.Name = strings.TrimSpace(level.Name) level.Status = strings.ToLower(strings.TrimSpace(level.Status)) normalized = append(normalized, level) } ctx = appcode.WithContext(ctx, appcode.FromContext(ctx)) return s.repository.UpdateAdminVipLevels(ctx, normalized, operatorUserID) } // UpdateAdminVipProgramConfig 保存 App 级状态机配置;版本递增与字段白名单由 repository 在锁内处理。 func (s *Service) UpdateAdminVipProgramConfig(ctx context.Context, command ledger.AdminVipProgramConfigCommand) (ledger.VipProgramConfig, error) { if command.OperatorUserID <= 0 { return ledger.VipProgramConfig{}, xerr.New(xerr.InvalidArgument, "operator_user_id is required") } if s.repository == nil { return ledger.VipProgramConfig{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.Config.AppCode = appcode.Normalize(command.Config.AppCode) ctx = appcode.WithContext(ctx, command.Config.AppCode) return s.repository.UpdateAdminVipProgramConfig(ctx, command) } // 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) { return ledger.VipUserSettings{}, xerr.New(xerr.InvalidArgument, "vip user settings update is incomplete") } if s.repository == nil { return ledger.VipUserSettings{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.UpdateVipUserSettings(ctx, command) }