topfans/docs/superpowers/specs/2026-07-21-daily-task-config-driven-design.md
2026-07-21 21:14:22 +08:00

15 KiB
Raw Blame History

每日任务配置驱动 + 后端事件驱动触发 设计方案

★ MVP 优先:本文档是可落地的 MVP 实施方案(方案 A。计数模型是二元模型的低成本超集予以采纳多对多映射方案 B与规则引擎方案 C不在本次实现,仅作为 §7 平滑升级路线图。

文档说明

  • 适用范围taskService 每日任务(task_type='daily')的完成/触发机制改造及其前端联动。不含引导任务onboarding与收益revenue逻辑。
  • 工作量估算:约 1 周2 列 + 1 列 migration、1 个完成引擎方法、MQ 事件接入、3~4 处 emit 点、重置改一行、service 单测)。
  • 前置版本/历史:现状为前端硬编码上报 + 后端 def.TaskKey == eventType 内联匹配;daily_mint / daily_place_asset 无 emit 点,属悬空任务(本方案顺带修复)。
  • 目标读者:后端 taskService 开发、前端 App 开发、DBA。

一、方案概述(必读)

要解决的问题

业务问题

  • 每日任务列表会"不定时更改":大多数时候是改文案 / 奖励 / 次数 / 顺序 / 上下架(复用已有用户行为),偶尔引入全新的完成行为。
  • 诉求:每次改动前端不做大改

技术问题

  • 完成判定硬编码在前端(Header.vuedaily_loginexhibition.vuedaily_browse_asset),加任务就得改前端,且客户端可伪造。
  • 后端匹配是内联的 def.TaskKey == eventType,带 TODO,无法表达"次数""多事件"。
  • daily_mint / daily_place_assettask_definitions 中 active但全仓库无对应上报点 → 用户永远无法完成(已由实连本地库 top-fans 确认:这两个 task_key 在 user_daily_task_progress 中 0 行)。

整体实现路径

阶段 内容 估时
1. 数据模型 task_definitionstrigger_event/target_countuser_daily_task_progressprogressmigration + backfill 0.5d
2. 事件目录 共享事件常量文件 + 治理规则 0.5d
3. 完成引擎 ProcessTaskEvent 方法,替换内联匹配 1.5d
4. 事件接入 MQ task:event 消费 + ReportEvent 改为生产者;铸造/上架/登录/浏览源头 emit 2d
5. 重置 ResetAllDailyTasks 增加 progress=0 0.25d
6. 测试 ProcessTaskEvent service 单测 1d

关键决策

  1. 触发源 = 后端事件驱动为主,前端通用上报兜底(详见 §4。抗刷、可覆盖纯后端行为且新任务复用已有事件时前端零改动。
  2. 映射机制 = 方案 Atask_definitions 单列 trigger_event + target_count(详见 §2、§4。最低复杂度覆盖 90% 场景。
  3. 计数模型target_count=1 等价"首次">1 为计数型MVP 不做 distinct 去重(详见 §5
  4. 单一隔离单元:所有匹配逻辑封在 ProcessTaskEvent 一个方法内,成为 A→B 升级的唯一改动点(详见 §4、§7

核心架构图TL;DR

[后端服务: 登录/铸造成功/上架成功] ──publish──┐
                                            ▼
[前端纯 UI 动作(如浏览详情)] ─reportEvent─▶ gateway ─▶ MQ 统一事件
                                            │   task:event { user_id, star_id, event_type }
                                            ▼
                       taskService MQ consumer  ┐
                       ReportEvent RPC(兜底)     ├─▶ DailyTaskService.ProcessTaskEvent()
                                                 ┘        │
                                                          ▼
              查 active daily 定义 where trigger_event = event_type
                          → GetOrCreate 进度 → progress += 1
                          → progress >= target_count ? status=completed
                                                          │
                        (领取仍是独立步骤: ClaimDailyTask → 发水晶 → claimed)

二、数据模型变更

所有变更需写 migrationbackend/migrations/),并在 docker/init-db.sql 同步。

2.1 task_definitions 新增 2 列

类型 约束 说明
trigger_event varchar(50) 可空 驱动该任务的事件名,取自 §3 事件目录
target_count int not null default 1 完成所需次数;=1 即"首次",等价现状

对应 GORM model model.TaskDefinition 增加字段:

TriggerEvent string `gorm:"column:trigger_event;size:50"`
TargetCount  int    `gorm:"column:target_count;default:1"`

Backfill存量 4 行)trigger_event = task_keytarget_count = 1

UPDATE task_definitions SET trigger_event = task_key WHERE task_type = 'daily' AND trigger_event IS NULL;
UPDATE task_definitions SET target_count = 1 WHERE target_count IS NULL OR target_count = 0;

2.2 user_daily_task_progress 新增 1 列

类型 约束 说明
progress int not null default 0 当前累计次数;progress >= target_count → completed

对应 model UserDailyTaskProgress 增加:

Progress int `gorm:"column:progress;default:0"`

状态机不变:pending → completed → claimed,每日重置回 pending

序列规范提醒:如后续脚本手动 INSERT 指定 id须按 CLAUDE.md 规则重置对应 _id_seq


三、事件目录(单一事实来源)

在后端建共享常量文件(建议 backend/pkg/mq/tasks/task_events.goservices/taskService/model),集中定义有限、稳定的事件枚举:

const (
    EventDailyLogin       = "daily_login"        // 每日首次登录
    EventDailyBrowseAsset = "daily_browse_asset" // 每日首次浏览藏品详情
    EventDailyMint        = "daily_mint"          // 每日首次铸造
    EventDailyPlaceAsset  = "daily_place_asset"   // 每日首次上架作品
)

治理规则

  1. 任务配置的 trigger_event 只能引用目录中已存在的事件
  2. 新增事件B 类新行为)= 加一个常量 + 在该行为的源头 emit 一次;此后该事件可被任意数量的新任务复用(纯配置)。
  3. 事件命名与含义在本节表格维护,改动需同步本文档。
  4. 每个事件标注归属路径(后端 emit / 前端 reportEvent遵守 §5 F4 单一路径规则:
事件 含义 归属路径
daily_login 每日首次登录 后端auth/登录服务 emitstar_id 从 JWT/token 中取)
daily_browse_asset 每日首次浏览藏品详情 前端 reportEvent(纯 UI 动作)
daily_mint 每日首次铸造 后端(铸造成功处 emit修复悬空
daily_place_asset 每日首次上架作品 后端(上架成功处 emit修复悬空

F3 概念区分:业务事件枚举(上表 daily_login…,供 trigger_event 引用)与 MQ 传输层的任务类型是两回事。MQ 任务类型常量 TypeTaskEvent = "task:event" 及其 payload struct TaskEventPayload{UserID, StarID, EventType} 应放在 backend/pkg/mq/tasks/registry.go(沿用现有 revenue: / gallery: 命名风格);业务事件枚举放 task_events.go。一条 task:event MQ 消息的 payload 里携带某个业务 EventType


四、完成引擎(唯一隔离单元)

4.1 新方法

DailyTaskService 新增:

// TaskEventResult 供 ReportEvent RPC 回填响应
type TaskEventResult struct {
    CompletedTaskKeys []string // 本次事件导致 completed 的任务(可能 0~N 个)
}

ProcessTaskEvent(ctx context.Context, userID, starID int64, eventType string) (*TaskEventResult, error)

取代 daily_task_service.go 现有 ReportEvent 内那段 def.TaskKey == eventType 内联循环。

F1 说明:返回结果而非仅 error,是因为 ReportEvent RPC 的 ReportEventResponse 需要 TaskKey/TaskCompleted/Messageproto 现有字段)。委托后由 ReportEventTaskEventResult 回填:TaskCompleted = len(CompletedTaskKeys) > 0TaskKey = 首个 completed。MQ consumer 则忽略返回值只关心 error

4.2 两个入口,一个引擎

入口 来源 说明
MQ consumer 后端服务发布的 task:event 抗刷、覆盖纯后端行为
ReportEvent RPC兜底 前端通用上报(纯 UI 动作,如浏览详情) 改为"发同一条 MQ 事件 / 或直接调 ProcessTaskEvent",不再自己做匹配

前端 task-api.js 保留唯一reportEvent(eventType, starId),不随任务增减而改动。

4.3 引擎逻辑

输入 {userID, starID, eventType}

  1. 查定义:is_active = true AND task_type = 'daily' AND trigger_event = eventType AND (star_id = ? OR star_id IS NULL)
  2. 逐个 GetOrCreateDailyProgress;若状态已是 completed / claimed → 跳过(当天幂等)。
  3. progress += 1;若 progress >= def.TargetCountstatus = "completed"completed_at = now
  4. UpdateDailyProgress 保存。

隔离保证:事件→任务的映射被封在"第 1 步查询 + 本方法"内。这是 §7 升级 B 的唯一改动点。

F5 多命中说明:同一 trigger_event 可能同时命中「全局任务star_id IS NULL」与「该 star 专属任务」,此时两者都会各自 +1 —— 这是预期行为(专属任务是全局任务的叠加,而非替代)。若运营需要"专属覆盖全局",属 §7 范畴MVP 不做。


五、幂等与去重

  • target_count = 1:天然幂等,completed 后跳过,无需额外处理。
  • 计数型(target_count > 1MVP 每个合格事件 +1,封顶 target_count。语义为"做 N 次"不做 distinct 去重(即"浏览 3 次",而非"3 个不同藏品")。
  • MQ 重试consumer MaxRetry = 3(沿用现有 revenue handler 模式);引擎对 target=1 幂等,重试安全。计数型在无去重前提下,重试可能多计——通过"仅在业务成功后 emit 一次 + 合理 MaxRetry"控制;严格 exactly-once 归入 §7。
  • "N 个不同对象"distinct 去重):需要 event_id(如 asset_id+ 幂等表,归入 §7 升级路径,不进 MVP。

F4 单一路径规则(强约束):同一个用户动作只能经一条路径 emit —— 要么后端 MQ 发布,要么前端 reportEvent不可两者都发。否则计数型任务(target_count > 1)会重复 +1。约定能被后端观测的动作登录/铸造/上架)一律走后端 emit 且前端再上报;纯 UI 动作(浏览详情)才走前端 reportEvent。事件目录§3需标注每个事件的归属路径。


六、每日重置

DailyResetWorker 逻辑不变05:00 Asia/Shanghai、pg_try_advisory_lock 防多实例)。仅 ResetAllDailyTasks()Updates map 增加一项:

Updates(map[string]interface{}{
    "status":       "pending",
    "progress":     0,          // 新增
    "completed_at": nil,
    "claimed_at":   nil,
    "updated_at":   now,
})

七、平滑升级路径(写入文档,不实现)

7.1 A → B一个任务多种触发 / 每事件独立规则

  • 新增表 task_triggers(id, task_id, event_type, increment, dedup_key, created_at),支持一任务多事件、一事件多任务、每触发独立增量与去重键。
  • 引擎 §4.3 第 1 步的查询从"读 task_definitions.trigger_event 列"改为"读 task_triggers join task_definitions"。
  • 因匹配逻辑已隔离在 ProcessTaskEvent 一个方法内,只改这一处 + 加一张表trigger_event 列降级为冗余快捷方式或废弃。

7.2 B → C复杂规则时间窗、distinct 计数、连续行为)

  • 在 trigger 上加 JSON rule 表达式列(如 {"event":"browse","distinct_by":"asset_id","count":3,"window":"1d"}),引擎接一个规则求值器。
  • 属 Stage 2+,业务复杂度真正上来后再评估,避免 YAGNI。

八、错误处理与测试

错误处理

  • 定义缺失 / 未激活 → 静默跳过 + logger.Info
  • 发奖仍在独立的 ClaimDailyTask / ClaimAllDailyTasks 步骤(不变),与完成解耦。
  • 引擎内单条任务更新失败不影响其他任务(逐个处理,记 error 日志)。

测试(沿用现有 *_test.go 事务回滚 / 测试容器模式)

  • ProcessTaskEvent service 单测覆盖:
    • 首次完成(target=1
    • 计数累加与封顶(target=31→2→3 completed第 4 次 no-op
    • 已 completed / claimed 时再来事件 → 跳过
    • 未知 / 无匹配事件 → no-op
    • per-star 定义与全局定义并存时的匹配
  • repositoryprogress 累加、ResetAllDailyTasks 重置 progress=0

九、受影响文件清单

文件 改动
backend/migrations/2026_07_21_*_daily_task_trigger.sql 新增:加列 + backfill
docker/init-db.sql 同步表结构 + 种子 trigger_event/target_count
backend/services/taskService/model/task_models.go TaskDefinition + UserDailyTaskProgress 加字段
backend/services/taskService/service/daily_task_service.go 新增 ProcessTaskEventReportEvent 改为委托
backend/services/taskService/repository/daily_task_repo.go 进度累加;ResetAllDailyTasksprogress
backend/pkg/mq/tasks/task_events.go(新) 业务事件枚举常量(daily_login…
backend/pkg/mq/tasks/registry.go 加 MQ 任务类型 TypeTaskEvent="task:event" + TaskEventPayload struct
backend/services/taskService/mq/consumer.go 注册 task:event handler → ProcessTaskEvent
铸造 / 上架 / 登录服务源头 emit task:event(修复 daily_mint/daily_place_asset 悬空;遵守 §5 F4 单一路径)
backend/services/taskService/worker/daily_reset_worker.go 无需改(调 repo
frontend/utils/task-api.js 无需改(保留唯一 reportEvent
frontend/pages/tasks/daily-tasks.vue 默认无需改(已配置驱动)

9.1 若需前端展示进度条 "N/M"(可选,非 MVP 默认)

计数型任务若要在前端显示 2/3 进度,DailyTaskItem proto 当前无 progress/target 字段(实测仅 TaskKey/StarId/Name/Description/CrystalReward/Status/CanClaim需额外

文件 改动
backend/pkg/proto/task/*.proto DailyTaskItemprogress / target_count 字段
(重新生成) protoc 重新生成 task.pb.go / task.triple.go
daily_task_service.go GetDailyTasks 映射时填充 progress / target_count
frontend/pages/tasks/daily-tasks.vue 渲染进度 progress/target_count

决策(已定稿)MVP 不展示进度条,前端完全零改动,progress 仅后端内部计数用于判完成。本 §9.1 的 proto 改动作为将来需要进度展示时的参考,不在本次实现范围。