package service import ( "context" "fmt" "strings" "time" roomv1 "hyapp.local/api/proto/room/v1" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" "hyapp/pkg/xerr" ) const ( // tieredPrivilegeVIPProgram 是 P1 权益制 VIP 的稳定策略名。App 选择哪套体系由 wallet 配置决定, // room-service 禁止再按 fami/lalu 写死分支,否则后续 App 无法复用同一房间权限实现。 tieredPrivilegeVIPProgram = "tiered_privilege_v1" vipBenefitCustomRoomBackground = "custom_room_background" vipBenefitAntiKick = "anti_kick" vipBenefitAntiMute = "anti_mute" vipBenefitRoomEntryNotice = "room_entry_notice" // VIP 查询会进入进房展示或低频房管动作,必须给同步依赖设置短上限,避免 wallet 抖动长期占住房间请求。 roomVIPLookupTimeout = time.Second ) // effectiveVIPAccess 是 room-service 从 wallet effective state 裁剪出的房间权限快照。 // paid/trial 合并、等级继承和体验卡过滤都已经由 wallet 完成,这里只做精确 benefit_code membership 判断。 type effectiveVIPAccess struct { programType string level int32 name string benefits map[string]struct{} roomEntryNoticeSettingEnabled bool } func (access effectiveVIPAccess) hasBenefit(code string) bool { _, exists := access.benefits[normalizeVIPCode(code)] return exists } func (access effectiveVIPAccess) roomEntryNoticeEnabled() bool { return access.roomEntryNoticeSettingEnabled && access.hasBenefit(vipBenefitRoomEntryNotice) } func (access effectiveVIPAccess) roomSnapshot() *roomv1.RoomEffectiveVipSnapshot { return &roomv1.RoomEffectiveVipSnapshot{ ProgramType: access.programType, Level: access.level, Name: access.name, RoomEntryNoticeEnabled: access.roomEntryNoticeEnabled(), } } // loadEffectiveVIPAccess 调 wallet GetMyVip 一次并只接受完整 effective state。 // 空 VIP 是合法结果,但 program_config 不能为空;缺配置时无法判断当前 App 是否应执行 P1 权限,必须按上游失败处理。 func (s *Service) loadEffectiveVIPAccess(ctx context.Context, requestID string, userID int64) (effectiveVIPAccess, error) { if s == nil || s.wallet == nil { return effectiveVIPAccess{}, xerr.New(xerr.Unavailable, "wallet vip client is unavailable") } if userID <= 0 { return effectiveVIPAccess{}, xerr.New(xerr.InvalidArgument, "vip user_id is required") } lookupCtx, cancel := context.WithTimeout(ctx, roomVIPLookupTimeout) defer cancel() response, err := s.wallet.GetMyVip(lookupCtx, &walletv1.GetMyVipRequest{ RequestId: strings.TrimSpace(requestID), AppCode: appcode.FromContext(ctx), UserId: userID, }) if err != nil { return effectiveVIPAccess{}, xerr.New(xerr.Unavailable, fmt.Sprintf("get effective vip failed: %v", err)) } if response == nil || response.GetState() == nil || response.GetState().GetProgramConfig() == nil { return effectiveVIPAccess{}, xerr.New(xerr.Unavailable, "wallet vip state is incomplete") } state := response.GetState() access := effectiveVIPAccess{ programType: normalizeVIPCode(state.GetProgramConfig().GetProgramType()), benefits: make(map[string]struct{}, len(state.GetEffectiveBenefits())), roomEntryNoticeSettingEnabled: true, } // 旧 wallet 在滚动升级期间没有 user_settings tag;nil 必须兼容为产品默认开启。 if settings := state.GetUserSettings(); settings != nil { access.roomEntryNoticeSettingEnabled = settings.GetRoomEntryNoticeEnabled() } if access.programType == "" { return effectiveVIPAccess{}, xerr.New(xerr.Unavailable, "wallet vip program is missing") } if effective := state.GetEffectiveVip(); effective != nil && effective.GetActive() { access.level = effective.GetLevel() access.name = strings.TrimSpace(effective.GetName()) } for _, benefit := range state.GetEffectiveBenefits() { if benefit == nil { continue } if code := normalizeVIPCode(benefit.GetBenefitCode()); code != "" { access.benefits[code] = struct{}{} } } return access, nil } // requireCustomRoomBackgroundBenefit 只对配置为 P1 权益制的 App 执行自定义背景门禁。 // legacy_timed 保持 Lalu 原行为;新 App 只需在 wallet 切换 program_type,无需再修改 room-service。 func (s *Service) requireCustomRoomBackgroundBenefit(ctx context.Context, requestID string, ownerUserID int64) error { access, err := s.loadEffectiveVIPAccess(ctx, requestID, ownerUserID) if err != nil { // 权限事实不可用时必须失败关闭;放行会让无权益用户在 wallet 故障窗口获得可持久化背景权限。 return err } if access.programType != tieredPrivilegeVIPProgram { return nil } if !access.hasBenefit(vipBenefitCustomRoomBackground) { return xerr.New(xerr.PermissionDenied, "custom room background vip benefit is required") } return nil } // requireTargetWithoutVIPBenefit 保护普通房主/管理员治理入口;平台治理必须继续走 SystemEvictUser,不能复用该守卫。 func (s *Service) requireTargetWithoutVIPBenefit(ctx context.Context, requestID string, targetUserID int64, benefitCode string) error { access, err := s.loadEffectiveVIPAccess(ctx, requestID, targetUserID) if err != nil { // anti_kick/anti_mute 属于付费保护,wallet 不可判定时宁可拒绝普通房管动作,也不能错误剥夺已生效权益。 return err } if access.hasBenefit(benefitCode) { return xerr.New(xerr.PermissionDenied, "target vip benefit prevents this room moderation action") } return nil } func normalizeVIPCode(value string) string { return strings.ToLower(strings.TrimSpace(value)) }