142 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 tagnil 必须兼容为产品默认开启。
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))
}