Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 40s
feat(pay): 添加支付API基础结构 feat(miniapp): 创建支付测试小程序页面与配置 feat(wechatpay): 配置微信支付参数与证书 fix(guild): 修复成员列表查询条件 docs: 更新代码规范文档与需求文档 style: 统一前后端枚举显示与注释格式 refactor(admin): 重构用户奖励发放接口参数处理 test(title): 添加称号效果参数验证测试
189 lines
5.2 KiB
Go
189 lines
5.2 KiB
Go
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
|
||
}
|