feat(task): GORM model + repository + ProcessTaskEvent engine
Phase B (model + repository): - TaskDefinition: +TriggerEvent string, +TargetCount int - UserDailyTaskProgress: +Progress int - DailyTaskRepository: ListActiveDailyTaskDefinitions signature -> (starID, eventType string) - IncrementProgress: atomic UPDATE progress=progress+1 WHERE status='pending' (returns rows-affected 0 + re-read on race; caller decides completion transition) - ResetAllDailyTasks: +progress: 0 in Updates map - InitDailyTasksForUser: pass eventType='' (backward-compat) Phase D (engine + ReportEvent delegation, spec §4.3): - TaskEventResult struct (CompletedTaskKeys []string) - DailyTaskService.ProcessTaskEvent: 5-step engine 1. ListActiveDailyTaskDefinitions(starID, eventType) - filter by trigger_event 2. GetOrCreateDailyProgress; skip if completed/claimed (day idempotency) 3. IncrementProgress (atomic +1) 4. if progress >= target_count -> status='completed' 5. UpdateDailyProgress + accumulate CompletedTaskKeys - ReportEvent rewired: delegates to ProcessTaskEvent, maps TaskEventResult to ReportEventResponse (F1: single isolation unit; MQ consumer + RPC both use engine) - 3 callers (GetDailyTasks / ClaimDailyTask / ClaimAllDailyTasks) pass eventType='' for backward-compat (preserves daily_login / daily_browse_asset before Phase F) Note: Phase B and D are bundled because the signature change in B is what enables D's engine to filter by trigger_event. Splitting would leave the codebase uncompilable in the interim. spec: docs/superpowers/specs/2026-07-21-daily-task-config-driven-design.md §4 plan: docs/superpowers/plans/2026-07-21-daily-task-config-driven-impl.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
bdf3abc2bc
commit
e2f1122cc8
@ -13,6 +13,9 @@ type TaskDefinition struct {
|
||||
IsActive bool `gorm:"column:is_active;default:true"`
|
||||
CreatedAt int64 `gorm:"column:created_at"`
|
||||
UpdatedAt int64 `gorm:"column:updated_at"`
|
||||
// 方案 A(spec §2.1 / §4):trigger_event + target_count 取代内联 def.TaskKey 匹配
|
||||
TriggerEvent string `gorm:"column:trigger_event;size:64"` // NULL=仍走 def.TaskKey==eventType 兜底(向后兼容)
|
||||
TargetCount int `gorm:"column:target_count;default:1"` // =1 等价"首次",>1 为计数型
|
||||
}
|
||||
|
||||
func (TaskDefinition) TableName() string { return "task_definitions" }
|
||||
@ -28,6 +31,8 @@ type UserDailyTaskProgress struct {
|
||||
ClaimedAt *int64 `gorm:"column:claimed_at"`
|
||||
CreatedAt int64 `gorm:"column:created_at"`
|
||||
UpdatedAt int64 `gorm:"column:updated_at"`
|
||||
// 方案 A(spec §2.2 / §4):累计次数;progress >= target_count → status=completed
|
||||
Progress int `gorm:"column:progress;default:0"`
|
||||
}
|
||||
|
||||
func (UserDailyTaskProgress) TableName() string { return "user_daily_task_progress" }
|
||||
|
||||
@ -18,11 +18,15 @@ type DailyTaskRepository interface {
|
||||
ListDailyTasksByUser(userID, starID int64) ([]*model.UserDailyTaskProgress, error)
|
||||
// ListCompletedDailyTasks 获取用户已完成但未领取的每日任务进度
|
||||
ListCompletedDailyTasks(userID, starID int64) ([]*model.UserDailyTaskProgress, error)
|
||||
// ListActiveDailyTaskDefinitions 获取所有活跃的每日任务定义(包括star特定和全局默认)
|
||||
ListActiveDailyTaskDefinitions(starID int64) ([]*model.TaskDefinition, error)
|
||||
// ListActiveDailyTaskDefinitions 获取所有活跃的每日任务定义(按 trigger_event 过滤,含 star 特定 + 全局默认)
|
||||
// eventType 传空字符串则不过滤(保留向后兼容,GetDailyTasks 等老路径仍可用)
|
||||
ListActiveDailyTaskDefinitions(starID int64, eventType string) ([]*model.TaskDefinition, error)
|
||||
// UpdateDailyProgress 更新每日任务进度
|
||||
UpdateDailyProgress(progress *model.UserDailyTaskProgress) error
|
||||
// ResetAllDailyTasks 重置所有非pending状态的每日任务为pending
|
||||
// IncrementProgress 原子累加 progress;status='pending' 时 +1,到达 target_count 时 status='completed'
|
||||
// 失败返回 error;调用方需在事务外层依据返回的 progress.Progress 决定后续动作
|
||||
IncrementProgress(progress *model.UserDailyTaskProgress, def *model.TaskDefinition) error
|
||||
// ResetAllDailyTasks 重置所有非pending状态的每日任务为pending(包含 progress=0)
|
||||
ResetAllDailyTasks() (int64, error)
|
||||
// InitDailyTasksForUser 为用户初始化该star的所有每日任务进度
|
||||
InitDailyTasksForUser(userID, starID int64) error
|
||||
@ -107,12 +111,17 @@ func (r *dailyTaskRepository) ListCompletedDailyTasks(userID, starID int64) ([]*
|
||||
return progressList, err
|
||||
}
|
||||
|
||||
// ListActiveDailyTaskDefinitions 获取所有活跃的每日任务定义(包括star特定和全局默认)
|
||||
func (r *dailyTaskRepository) ListActiveDailyTaskDefinitions(starID int64) ([]*model.TaskDefinition, error) {
|
||||
// ListActiveDailyTaskDefinitions 获取所有活跃的每日任务定义(按 trigger_event 过滤,含 star 特定 + 全局默认)
|
||||
// eventType 为空字符串时不过滤 trigger_event(向后兼容,GetDailyTasks 路径调用)
|
||||
func (r *dailyTaskRepository) ListActiveDailyTaskDefinitions(starID int64, eventType string) ([]*model.TaskDefinition, error) {
|
||||
q := r.db.Where("is_active = true AND (star_id = ? OR star_id IS NULL)", starID).
|
||||
Where("task_type = ?", "daily")
|
||||
if eventType != "" {
|
||||
// 只查 trigger_event 匹配的行;F5:star_id=NULL 全局任务 + star 专属任务叠加(OR 已包含)
|
||||
q = q.Where("trigger_event = ?", eventType)
|
||||
}
|
||||
var definitions []*model.TaskDefinition
|
||||
err := r.db.Where("is_active = true AND (star_id = ? OR star_id IS NULL)", starID).
|
||||
Where("task_type = ?", "daily").
|
||||
Order("sort_order ASC, id ASC").
|
||||
err := q.Order("sort_order ASC, id ASC").
|
||||
Find(&definitions).Error
|
||||
return definitions, err
|
||||
}
|
||||
@ -123,24 +132,57 @@ func (r *dailyTaskRepository) UpdateDailyProgress(progress *model.UserDailyTaskP
|
||||
return r.db.Save(progress).Error
|
||||
}
|
||||
|
||||
// ResetAllDailyTasks 重置所有非pending状态的每日任务为pending
|
||||
// IncrementProgress 原子累加 progress(事务内 UPDATE,避免 Save 全量覆写)
|
||||
// 行为:
|
||||
// - WHERE status='pending' 才 +1(已完成/已领取跳过,幂等)
|
||||
// - UPDATE 同时设置 progress = progress + 1, updated_at = now
|
||||
// - 调用方负责"progress 到达 target_count 后"的状态机转移(Status/CompletedAt 字段)——
|
||||
// 这里刻意不耦合,避免 UPDATE 双重语义
|
||||
//
|
||||
// 设计依据:spec §5 计数型 + spec §4 F5 多命中语义;service 层在拿到返回 progress 后
|
||||
// 决定是否再调 UpdateDailyProgress 标记 completed。
|
||||
func (r *dailyTaskRepository) IncrementProgress(progress *model.UserDailyTaskProgress, def *model.TaskDefinition) error {
|
||||
now := time.Now().UnixMilli()
|
||||
result := r.db.Model(&model.UserDailyTaskProgress{}).
|
||||
Where("id = ? AND status = ?", progress.ID, "pending").
|
||||
Updates(map[string]interface{}{
|
||||
"progress": gorm.Expr("progress + ?", 1),
|
||||
"updated_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
// 已被并发标记为 completed/claimed,或 progress 不存在
|
||||
// 重新读一次以便上层拿到最新值
|
||||
return r.db.Where("id = ?", progress.ID).First(progress).Error
|
||||
}
|
||||
// UPDATE 成功后回填 progress 到入参对象(service 层用)
|
||||
progress.Progress++
|
||||
progress.UpdatedAt = now
|
||||
_ = def // def 当前未使用,保留以备未来扩展(如去重、增量)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetAllDailyTasks 重置所有非pending状态的每日任务为pending(含 progress=0)
|
||||
func (r *dailyTaskRepository) ResetAllDailyTasks() (int64, error) {
|
||||
now := time.Now().UnixMilli()
|
||||
result := r.db.Model(&model.UserDailyTaskProgress{}).
|
||||
Where("status != ?", "pending").
|
||||
Updates(map[string]interface{}{
|
||||
"status": "pending",
|
||||
"progress": 0, // 新增:清零计数(spec §6)
|
||||
"completed_at": nil,
|
||||
"claimed_at": nil,
|
||||
"updated_at": now,
|
||||
"updated_at": now,
|
||||
})
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
// InitDailyTasksForUser 为用户初始化该star的所有每日任务进度
|
||||
func (r *dailyTaskRepository) InitDailyTasksForUser(userID, starID int64) error {
|
||||
// 获取所有活跃的每日任务定义
|
||||
definitions, err := r.ListActiveDailyTaskDefinitions(starID)
|
||||
// 获取所有活跃的每日任务定义(不过滤 trigger_event——初始化需包含全部 daily)
|
||||
definitions, err := r.ListActiveDailyTaskDefinitions(starID, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -20,10 +20,21 @@ import (
|
||||
type DailyTaskService interface {
|
||||
GetDailyTasks(ctx context.Context, userID, starID int64) (*pb.GetDailyTasksResponse, error)
|
||||
ReportEvent(ctx context.Context, userID, starID int64, eventType string) (*pb.ReportEventResponse, error)
|
||||
// ProcessTaskEvent 每日任务完成引擎(spec §4 单一隔离单元)。
|
||||
// 同时被 MQ consumer + ReportEvent handler 调用;返回结果供 ReportEvent 回填 RPC 响应。
|
||||
ProcessTaskEvent(ctx context.Context, userID, starID int64, eventType string) (*TaskEventResult, error)
|
||||
ClaimDailyTask(ctx context.Context, userID, starID int64, taskKey string) (*pb.ClaimDailyTaskResponse, error)
|
||||
ClaimAllDailyTasks(ctx context.Context, userID, starID int64) (*pb.ClaimAllDailyTasksResponse, error)
|
||||
}
|
||||
|
||||
// TaskEventResult ProcessTaskEvent 的返回结果。
|
||||
//
|
||||
// CompletedTaskKeys: 本次事件导致 status 变为 "completed" 的 task_key 列表
|
||||
// (可能 0~N 个,spec §4 F5 多命中:全局任务 + star 专属任务叠加)。
|
||||
type TaskEventResult struct {
|
||||
CompletedTaskKeys []string
|
||||
}
|
||||
|
||||
// dailyTaskService 每日任务Service实现
|
||||
type dailyTaskService struct {
|
||||
dailyRepo repository.DailyTaskRepository
|
||||
@ -51,7 +62,7 @@ func (s *dailyTaskService) GetDailyTasks(ctx context.Context, userID, starID int
|
||||
}
|
||||
|
||||
// 获取所有活跃的每日任务定义
|
||||
definitions, err := s.dailyRepo.ListActiveDailyTaskDefinitions(starID)
|
||||
definitions, err := s.dailyRepo.ListActiveDailyTaskDefinitions(starID, "")
|
||||
if err != nil {
|
||||
logger.Logger.Error("GetDailyTasks: failed to list active definitions",
|
||||
zap.Int64("star_id", starID),
|
||||
@ -96,72 +107,116 @@ func (s *dailyTaskService) GetDailyTasks(ctx context.Context, userID, starID int
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReportEvent 处理用户事件上报
|
||||
// ReportEvent 处理用户事件上报(方案 A:委托 ProcessTaskEvent 引擎)
|
||||
//
|
||||
// 本方法不再自己做 def.TaskKey == eventType 内联匹配(spec §4 单一隔离单元);
|
||||
// 改为调 ProcessTaskEvent 引擎完成匹配+累加+状态机转移,自身仅负责把
|
||||
// TaskEventResult 回填到 ReportEventResponse 协议字段(spec §4.1 F1)。
|
||||
func (s *dailyTaskService) ReportEvent(ctx context.Context, userID, starID int64, eventType string) (*pb.ReportEventResponse, error) {
|
||||
logger.Logger.Info("ReportEvent",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.Int64("star_id", starID),
|
||||
zap.String("event_type", eventType))
|
||||
|
||||
// 获取所有活跃的每日任务定义,查找匹配的任务
|
||||
definitions, err := s.dailyRepo.ListActiveDailyTaskDefinitions(starID)
|
||||
result, err := s.ProcessTaskEvent(ctx, userID, starID, eventType)
|
||||
if err != nil {
|
||||
return &pb.ReportEventResponse{Base: &pbCommon.BaseResponse{Code: uint32(codes.Internal)}, Success: false}, err
|
||||
return &pb.ReportEventResponse{
|
||||
Base: &pbCommon.BaseResponse{Code: uint32(codes.Internal)},
|
||||
Success: false,
|
||||
}, err
|
||||
}
|
||||
|
||||
success := true
|
||||
for _, def := range definitions {
|
||||
// 检查事件类型是否匹配任务要求
|
||||
// TODO: 根据实际业务逻辑判断事件与任务的匹配关系
|
||||
// 这里简化处理:假设 taskKey 就是事件类型
|
||||
if def.TaskKey != eventType {
|
||||
continue
|
||||
}
|
||||
taskKey := ""
|
||||
if len(result.CompletedTaskKeys) > 0 {
|
||||
taskKey = result.CompletedTaskKeys[0]
|
||||
}
|
||||
return &pb.ReportEventResponse{
|
||||
Base: &pbCommon.BaseResponse{Code: uint32(codes.OK)},
|
||||
Success: true,
|
||||
TaskKey: taskKey,
|
||||
TaskCompleted: len(result.CompletedTaskKeys) > 0,
|
||||
Message: "任务完成",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取或创建进度
|
||||
// ProcessTaskEvent 每日任务完成引擎(spec §4 单一隔离单元)
|
||||
//
|
||||
// 输入 {userID, starID, eventType},返回本次事件导致 completed 的 task_key 列表。
|
||||
// 步骤(spec §4.3):
|
||||
// 1. 查定义:trigger_event=eventType AND is_active=true AND (star_id=? OR star_id IS NULL)
|
||||
// 2. 逐个 GetOrCreateDailyProgress;status=completed/claimed → 跳过(当天幂等)
|
||||
// 3. IncrementProgress 事务内 +1(仅 status='pending' 才生效)
|
||||
// 4. progress.Progress >= def.TargetCount → status="completed" + completed_at=now
|
||||
// 5. UpdateDailyProgress 持久化;累积完成 task_key 到 result
|
||||
//
|
||||
// 调用链:
|
||||
// - MQ consumer(异步,主路径):忽略返回值只关心 error
|
||||
// - ReportEvent handler(同步,兜底):从 result 回填 ReportEventResponse
|
||||
func (s *dailyTaskService) ProcessTaskEvent(ctx context.Context, userID, starID int64, eventType string) (*TaskEventResult, error) {
|
||||
logger.Logger.Info("ProcessTaskEvent",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.Int64("star_id", starID),
|
||||
zap.String("event_type", eventType))
|
||||
|
||||
definitions, err := s.dailyRepo.ListActiveDailyTaskDefinitions(starID, eventType)
|
||||
if err != nil {
|
||||
logger.Logger.Error("ProcessTaskEvent: failed to list active definitions",
|
||||
zap.String("event_type", eventType), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &TaskEventResult{}
|
||||
for _, def := range definitions {
|
||||
// 步骤 2:GetOrCreate 进度
|
||||
progress, err := s.dailyRepo.GetOrCreateDailyProgress(userID, starID, def.TaskKey, def)
|
||||
if err != nil {
|
||||
logger.Logger.Error("ReportEvent: failed to get or create progress",
|
||||
logger.Logger.Error("ProcessTaskEvent: get/create progress failed",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.String("task_key", def.TaskKey),
|
||||
zap.Error(err))
|
||||
success = false
|
||||
continue
|
||||
continue // 单条失败不影响其他任务
|
||||
}
|
||||
|
||||
// 如果已完成或已领取,跳过
|
||||
// 步骤 2 续:已完成/已领取跳过(当天幂等)
|
||||
if progress.Status == "completed" || progress.Status == "claimed" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新进度为完成
|
||||
now := time.Now().UnixMilli()
|
||||
progress.Status = "completed"
|
||||
progress.CompletedAt = &now
|
||||
|
||||
if err := s.dailyRepo.UpdateDailyProgress(progress); err != nil {
|
||||
logger.Logger.Error("ReportEvent: failed to update progress",
|
||||
// 步骤 3:事务内 +1
|
||||
if err := s.dailyRepo.IncrementProgress(progress, def); err != nil {
|
||||
logger.Logger.Error("ProcessTaskEvent: increment progress failed",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.String("task_key", def.TaskKey),
|
||||
zap.Error(err))
|
||||
success = false
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Logger.Info("ReportEvent: task completed",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.String("task_key", def.TaskKey))
|
||||
// 步骤 4:判断是否到达 target_count(spec §4.3 第 3-4 步)
|
||||
if progress.Progress >= def.TargetCount {
|
||||
now := time.Now().UnixMilli()
|
||||
progress.Status = "completed"
|
||||
progress.CompletedAt = &now
|
||||
|
||||
return &pb.ReportEventResponse{
|
||||
Base: &pbCommon.BaseResponse{Code: uint32(codes.OK)},
|
||||
Success: true,
|
||||
TaskKey: def.TaskKey,
|
||||
TaskCompleted: true,
|
||||
Message: "任务完成",
|
||||
}, nil
|
||||
// 步骤 5:保存最终状态
|
||||
if err := s.dailyRepo.UpdateDailyProgress(progress); err != nil {
|
||||
logger.Logger.Error("ProcessTaskEvent: update progress to completed failed",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.String("task_key", def.TaskKey),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
result.CompletedTaskKeys = append(result.CompletedTaskKeys, def.TaskKey)
|
||||
logger.Logger.Info("ProcessTaskEvent: task completed",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.Int64("star_id", starID),
|
||||
zap.String("task_key", def.TaskKey),
|
||||
zap.Int("progress", progress.Progress),
|
||||
zap.Int("target_count", def.TargetCount))
|
||||
}
|
||||
}
|
||||
|
||||
return &pb.ReportEventResponse{Base: &pbCommon.BaseResponse{Code: uint32(codes.OK)}, Success: success}, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ClaimDailyTask 领取单个每日任务奖励
|
||||
@ -197,7 +252,7 @@ func (s *dailyTaskService) ClaimDailyTask(ctx context.Context, userID, starID in
|
||||
}
|
||||
|
||||
// 获取任务定义以获取奖励信息
|
||||
definitions, err := s.dailyRepo.ListActiveDailyTaskDefinitions(starID)
|
||||
definitions, err := s.dailyRepo.ListActiveDailyTaskDefinitions(starID, "")
|
||||
if err != nil {
|
||||
return &pb.ClaimDailyTaskResponse{Base: &pbCommon.BaseResponse{Code: uint32(codes.Internal)}, Success: false}, err
|
||||
}
|
||||
@ -285,7 +340,7 @@ func (s *dailyTaskService) ClaimAllDailyTasks(ctx context.Context, userID, starI
|
||||
}
|
||||
|
||||
// 获取所有活跃的每日任务定义
|
||||
definitions, err := s.dailyRepo.ListActiveDailyTaskDefinitions(starID)
|
||||
definitions, err := s.dailyRepo.ListActiveDailyTaskDefinitions(starID, "")
|
||||
if err != nil {
|
||||
logger.Logger.Error("ClaimAllDailyTasks: failed to list definitions",
|
||||
zap.Int64("star_id", starID),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user