142 lines
5.7 KiB
Go
142 lines
5.7 KiB
Go
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))
|
||
}
|