2026-07-17 16:55:22 +08:00

196 lines
8.4 KiB
Go
Raw Permalink 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"
"hyapp/services/room-service/internal/integration"
)
const (
// tieredPrivilegeVIPProgram 是 P1 权益制 VIP 的稳定策略名。App 选择哪套体系由 wallet 配置决定,
// room-service 禁止再按 fami/lalu 写死分支,否则后续 App 无法复用同一房间权限实现。
tieredPrivilegeVIPProgram = "tiered_privilege_v1"
vipBenefitCustomRoomBackground = "custom_room_background"
vipBenefitRoomImageMessage = "room_image_message"
vipBenefitRoomBorder = "room_border"
vipBenefitColoredRoomName = "colored_room_name"
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 执行自定义背景门禁。
// action 由调用点传入精确动作Flutter 才能区分上传、保存和切换失败并保留对应本地状态。
// legacy_timed 保持 Lalu 原行为;新 App 只需在 wallet 切换 program_type无需再修改 room-service。
func (s *Service) requireCustomRoomBackgroundBenefit(ctx context.Context, requestID string, ownerUserID int64, action string) error {
return s.requireVIPBenefit(ctx, requestID, ownerUserID, vipBenefitCustomRoomBackground, action)
}
// requireVIPBenefit 优先调用 wallet 的单项权益判定,并把升级页需要的等级事实放进错误 metadata。
// legacy_timed App 沿用旧行为P1 App 在 wallet 不可用或没有权益时都失败关闭,避免故障窗口越权。
func (s *Service) requireVIPBenefit(ctx context.Context, requestID string, userID int64, benefitCode string, action string) error {
if checker, ok := s.wallet.(integration.WalletVIPBenefitClient); ok {
lookupCtx, cancel := context.WithTimeout(ctx, roomVIPLookupTimeout)
defer cancel()
response, err := checker.CheckVipBenefit(lookupCtx, &walletv1.CheckVipBenefitRequest{
RequestId: strings.TrimSpace(requestID),
AppCode: appcode.FromContext(ctx),
UserId: userID,
BenefitCode: normalizeVIPCode(benefitCode),
})
if err != nil {
// 对外错误只暴露稳定的业务语义,底层 gRPC 状态和地址不能进入 HTTP envelope。
return xerr.New(xerr.Unavailable, "check vip benefit failed")
}
if response == nil || response.GetState() == nil || response.GetState().GetProgramConfig() == nil {
return xerr.New(xerr.Unavailable, "wallet vip benefit state is incomplete")
}
if normalizeVIPCode(response.GetState().GetProgramConfig().GetProgramType()) != tieredPrivilegeVIPProgram {
return nil
}
if response.GetAllowed() {
return nil
}
return xerr.NewWithMetadata(xerr.VIPBenefitRequired, "vip benefit is required", map[string]string{
"benefit_code": normalizeVIPCode(benefitCode),
"required_level": fmt.Sprintf("%d", response.GetRequiredLevel()),
"current_effective_level": fmt.Sprintf("%d", response.GetCurrentEffectiveLevel()),
"action": strings.TrimSpace(action),
})
}
// 兼容滚动升级和现有测试 fake旧 client 仍可从 effective_benefits 得到同一授权事实。
access, err := s.loadEffectiveVIPAccess(ctx, requestID, userID)
if err != nil {
// 权限事实不可用时必须失败关闭;放行会让无权益用户在 wallet 故障窗口获得可持久化背景权限。
return err
}
if access.programType != tieredPrivilegeVIPProgram {
return nil
}
if !access.hasBenefit(benefitCode) {
return xerr.NewWithMetadata(xerr.VIPBenefitRequired, "vip benefit is required", map[string]string{
"benefit_code": normalizeVIPCode(benefitCode),
"required_level": "0",
"current_effective_level": fmt.Sprintf("%d", access.level),
"action": strings.TrimSpace(action),
})
}
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) {
switch normalizeVIPCode(benefitCode) {
case vipBenefitAntiKick:
return xerr.New(xerr.VIPAntiKick, "target vip benefit prevents kick")
case vipBenefitAntiMute:
return xerr.New(xerr.VIPAntiMute, "target vip benefit prevents mute")
default:
return xerr.New(xerr.VIPBenefitRequired, "target vip benefit prevents this room moderation action")
}
}
return nil
}
func normalizeVIPCode(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}