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 }