bindbox-game/internal/service/title/effects_resolver.go
邹方成 6ee627139c
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 40s
feat: 新增支付测试小程序与微信支付集成
feat(pay): 添加支付API基础结构
feat(miniapp): 创建支付测试小程序页面与配置
feat(wechatpay): 配置微信支付参数与证书
fix(guild): 修复成员列表查询条件
docs: 更新代码规范文档与需求文档
style: 统一前后端枚举显示与注释格式
refactor(admin): 重构用户奖励发放接口参数处理
test(title): 添加称号效果参数验证测试
2025-11-17 00:42:08 +08:00

189 lines
5.2 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 title
import (
"context"
"encoding/json"
"time"
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/repository/mysql/model"
)
// Service 提供头衔效果解析与领取型权益限流的服务
// Params:
// - ctx: 上下文
// - 依赖通过构造函数注入
// Returns:
// - 头衔相关功能的服务实例
type Service interface {
// ResolveActiveEffects 解析用户在指定上下文(issue/activity/category)下的激活头衔效果
// Params:
// - ctx: 上下文
// - userID: 用户ID
// - scope: 事件作用域(可空字段用于精细过滤)
// Returns:
// - 解析后的效果列表已按scopes过滤并仅保留启用效果
ResolveActiveEffects(ctx context.Context, userID int64, scope EffectScope) ([]*model.SystemTitleEffects, error)
// AssignUserTitle 为用户分配或激活称号,并保证仅有一个激活称号
// Params:
// - ctx: 上下文
// - userID: 用户ID
// - titleID: 称号ID
// - expiresAt: 可选过期时间
// - remark: 备注
// Returns:
// - 错误信息(若已拥有且未过期则返回错误)
AssignUserTitle(ctx context.Context, userID int64, titleID int64, expiresAt *time.Time, remark string) error
ValidateEffectParams(effectType int32, raw string) (string, error)
}
type service struct {
logger logger.CustomLogger
readDB *dao.Query
writeDB *dao.Query
}
// New 创建头衔服务实例
// Params:
// - l: 日志器
// - db: 数据库仓库
// Returns:
// - 头衔服务实例
func New(l logger.CustomLogger, db mysql.Repo) Service {
return &service{
logger: l,
readDB: dao.Use(db.GetDbR()),
writeDB: dao.Use(db.GetDbW()),
}
}
// EffectScope 事件上下文作用域
// 用于按活动/期/分类过滤效果生效范围
type EffectScope struct {
ActivityID *int64
IssueID *int64
ActivityCategory *int64
}
// scopesPayload 用于解析SystemTitleEffects.ScopesJSON
type scopesPayload struct {
ActivityIDs []int64 `json:"activity_ids"`
IssueIDs []int64 `json:"issue_ids"`
CategoryIDs []int64 `json:"category_ids"`
Exclude struct {
ActivityIDs []int64 `json:"activity_ids"`
IssueIDs []int64 `json:"issue_ids"`
CategoryIDs []int64 `json:"category_ids"`
} `json:"exclude"`
}
// ResolveActiveEffects 查询用户激活头衔并解析效果返回在给定scope内生效的效果
// Params:
// - ctx: 上下文
// - userID: 用户ID
// - scope: 事件作用域
// Returns:
// - 生效效果列表
func (s *service) ResolveActiveEffects(ctx context.Context, userID int64, scope EffectScope) ([]*model.SystemTitleEffects, error) {
now := time.Now()
titles, err := s.readDB.UserTitles.WithContext(ctx).
Where(s.readDB.UserTitles.UserID.Eq(userID)).
Where(s.readDB.UserTitles.Active.Eq(1)).
Find()
if err != nil {
return nil, err
}
if len(titles) == 0 {
return []*model.SystemTitleEffects{}, nil
}
var selID int64
var selAt time.Time
for _, ut := range titles {
if ut.ExpiresAt.IsZero() || ut.ExpiresAt.After(now) {
if selID == 0 || ut.ObtainedAt.After(selAt) {
selID = ut.TitleID
selAt = ut.ObtainedAt
}
}
}
if selID == 0 {
return []*model.SystemTitleEffects{}, nil
}
effects, err := s.readDB.SystemTitleEffects.WithContext(ctx).
Where(s.readDB.SystemTitleEffects.TitleID.Eq(selID)).
Where(s.readDB.SystemTitleEffects.Status.Eq(1)).
Order(s.readDB.SystemTitleEffects.Sort).
Find()
if err != nil {
return nil, err
}
if len(effects) == 0 {
return []*model.SystemTitleEffects{}, nil
}
var result []*model.SystemTitleEffects
for _, ef := range effects {
if ef.ScopesJSON == "" {
result = append(result, ef)
continue
}
var sc scopesPayload
if err := json.Unmarshal([]byte(ef.ScopesJSON), &sc); err != nil {
// 解析失败时按“全局生效”处理
result = append(result, ef)
continue
}
if !scopeMatch(scope, sc) {
continue
}
result = append(result, ef)
}
return result, nil
}
// scopeMatch 判断效果scope是否命中当前事件上下文
// Params:
// - scope: 事件作用域
// - sc: 效果配置的scope载荷
// Returns:
// - 是否命中
func scopeMatch(scope EffectScope, sc scopesPayload) bool {
// 排除优先
if scope.ActivityID != nil && containsInt64(sc.Exclude.ActivityIDs, *scope.ActivityID) {
return false
}
if scope.IssueID != nil && containsInt64(sc.Exclude.IssueIDs, *scope.IssueID) {
return false
}
if scope.ActivityCategory != nil && containsInt64(sc.Exclude.CategoryIDs, *scope.ActivityCategory) {
return false
}
// 包含判定(未配置即视为全局)
if scope.ActivityID != nil && len(sc.ActivityIDs) > 0 && !containsInt64(sc.ActivityIDs, *scope.ActivityID) {
return false
}
if scope.IssueID != nil && len(sc.IssueIDs) > 0 && !containsInt64(sc.IssueIDs, *scope.IssueID) {
return false
}
if scope.ActivityCategory != nil && len(sc.CategoryIDs) > 0 && !containsInt64(sc.CategoryIDs, *scope.ActivityCategory) {
return false
}
return true
}
// containsInt64 判断切片是否包含指定值
// Params:
// - arr: 切片
// - v: 值
// Returns:
// - 是否包含
func containsInt64(arr []int64, v int64) bool {
for _, x := range arr {
if x == v {
return true
}
}
return false
}