# Daily-Task 配置驱动改造 — 实施计划 > **配套文档**:`docs/superpowers/specs/2026-07-21-daily-task-config-driven-design.md`(设计方案,已审过 12 处修复) > **状态**:待实施(计划已批准) > **工作量**:约 1 周(8 阶段 × 0.5~1.5d) ## Context 每日任务系统当前完成判定硬编码在前端(`Header.vue` 报 `daily_login`、`asset-detail.vue` 报 `daily_browse_asset`,原 `exhibition.vue` 错位实现待迁移),后端用 `def.TaskKey == eventType` 内联匹配并带 `TODO` 注释,无法表达"次数 / 多事件"语义。`daily_mint` 在 `task_definitions` 中 active 但无后端 emit;`daily_place_asset` 同样 active 但全仓库无对应 emit 点,用户永远无法完成(已确认本地库 `user_daily_task_progress` 中 0 行)—— 属悬空任务。 目标:把完成判定搬到后端 `ProcessTaskEvent` 引擎(方案 A:单列 `trigger_event` + `target_count`),加 MQ `task:event` 统一事件流,让新增每日任务只需 SQL 配置而不改前端 / 后端代码。 预期结果: - 加新每日任务 = SQL 一行(`INSERT INTO task_definitions ...`)+ 后端业务源头 emit 一次;前端零改动 - 修复 `daily_mint` / `daily_place_asset` 悬空 - 现有 4 个每日任务行为不变(前端保留 `Header.vue` / `asset-detail.vue` / `myWorks.vue` 的 `reportEvent` 调用;`exhibition.vue` 错位调用删除) --- ## 用户已确认的关键决策 | 问题 | 决定 | |---|---| | 上线策略 | **全量上线**(不加 feature flag / 灰度) | | 前端 daily_browse_asset 缓存 | **当前无缓存**,依赖后端幂等(`status='completed'` 后跳过) | | assetService MQ 依赖 | **新增 mq.Init + EnqueueTaskEvent helper**(沿用 galleryService 模式) | --- ## 实施步骤(8 阶段,可独立 commit) ### Phase A — 数据库 schema 变更(0.5d) **新建 migration** `backend/migrations/2026_07_21_004_daily_task_trigger.sql`: - `ALTER TABLE task_definitions ADD COLUMN trigger_event varchar(64)` + `ADD COLUMN target_count int NOT NULL DEFAULT 1` - `ALTER TABLE user_daily_task_progress ADD COLUMN progress int NOT NULL DEFAULT 0` - Backfill:`UPDATE task_definitions SET trigger_event='daily_mint', target_count=1 WHERE id=3 AND task_type='daily'` + 同上 `WHERE id=4 AND task_key='daily_place_asset'`。 - ★ 修订 2026-07-27:原计划只 backfill id=3/4 是**错的**——`daily_login` (id=1) / `daily_browse_asset` (id=2) 虽走前端 reportEvent,但 ProcessTaskEvent 引擎按 `trigger_event = ?` 严格匹配(spec §4 单一隔离单元),NULL 不会命中,daily-tasks 任务永远 pending。补充迁移 `backend/migrations/2026_07_27_005_daily_task_browse_login_trigger.sql` 回填 id=1/2 的 trigger_event=task_key。`onboarding_complete` (id=5) 不属于 daily,留 NULL。 **同步 `docker/init-db.sql`**(line 3564 task_definitions CREATE TABLE、line 3744 user_daily_task_progress CREATE TABLE、line 124667 COPY 数据):加新列;为新 COPY 行加 `trigger_event` / `target_count` 值(id=1,2,3,4 填对应 task_key + 1,id=5 onboarding 留 NULL) **Critical files**: - `backend/migrations/2026_07_21_004_daily_task_trigger.sql`(新) - `docker/init-db.sql`(line 3564, 3744, 124664 三处同步) **验证**:`psql -f backend/migrations/2026_07_21_004_daily_task_trigger.sql` + `\d task_definitions` `\d user_daily_task_progress` 确认列存在 + backfill 4 行值正确 --- ### Phase B — GORM model + Repository 层(0.5d) **修改 `backend/services/taskService/model/task_models.go`**: - `TaskDefinition` 加 `TriggerEvent string` + `TargetCount int` 字段 - `UserDailyTaskProgress` 加 `Progress int` 字段 **修改 `backend/services/taskService/repository/daily_task_repo.go`**: - `DailyTaskRepository` interface 新增 `IncrementProgress(progress *UserDailyTaskProgress, def *TaskDefinition) error`(实现位于 line ~140) - **事务内** `UPDATE user_daily_task_progress SET progress = progress + 1, status=?, completed_at=? WHERE id=? AND status='pending'`(避免 Save 全量覆写) - `ListActiveDailyTaskDefinitions` 查询条件追加 `trigger_event = ?` 参数化(line 111 起,WHERE 子句改为 `Where("trigger_event = ? AND ...", eventType, ...)`) - `ResetAllDailyTasks` 的 `Updates` map 加 `"progress": 0`(line 131) - **接口签名变更**:现有 `ListActiveDailyTaskDefinitions(starID)` 改为 `ListActiveDailyTaskDefinitions(starID, eventType string)` —— 单一参数变化,全仓调用点同步(grep 验证;只有 `daily_task_service.go` 调用) **Critical files**: - `backend/services/taskService/model/task_models.go` - `backend/services/taskService/repository/daily_task_repo.go` - `backend/services/taskService/service/daily_task_service.go`(ListActiveDailyTaskDefinitions 调用点同步) **验证**:`cd backend/services/taskService && go build ./...`;手动 `INSERT INTO task_definitions (..., trigger_event='test_xxx', ...)` + 调 `ListActiveDailyTaskDefinitions(1, "test_xxx")` 返回结果对得上 --- ### Phase C — 事件目录 + MQ 基础设施(0.5d) **新建 `backend/pkg/mq/tasks/task_events.go`**: ```go package tasks const ( EventDailyLogin = "daily_login" EventDailyBrowseAsset = "daily_browse_asset" EventDailyMint = "daily_mint" EventDailyPlaceAsset = "daily_place_asset" ) type TaskEventPayload struct { UserID int64 `json:"user_id"` StarID int64 `json:"star_id"` EventType string `json:"event_type"` // 取自 EventDailyXxx 常量 } ``` **修改 `backend/pkg/mq/tasks/registry.go`**: - 加 `TypeTaskEvent = "task:event"` 常量(与现有 `TypeRevenueExhibition` 风格一致) **Critical files**: - `backend/pkg/mq/tasks/task_events.go`(新) - `backend/pkg/mq/tasks/registry.go`(追加 1 行常量) **验证**:`go build ./...`;`grep -rn "EventDailyMint\|TypeTaskEvent" backend/` 确认可被 import --- ### Phase D — 完成引擎 `ProcessTaskEvent`(1.5d) **修改 `backend/services/taskService/service/daily_task_service.go`**: 1. 接口 `DailyTaskService`(line 20-25)新增方法签名: ```go ProcessTaskEvent(ctx context.Context, userID, starID int64, eventType string) (*TaskEventResult, error) ``` 2. 新增 struct(line 27 后): ```go type TaskEventResult struct { CompletedTaskKeys []string } ``` 3. **重写 `ReportEvent` 方法**(line 99-165):删除原内联 `def.TaskKey != eventType` 循环,整体替换为委托给 `ProcessTaskEvent`: ```go func (s *dailyTaskService) ReportEvent(ctx context.Context, userID, starID int64, eventType string) (*pb.ReportEventResponse, error) { result, err := s.ProcessTaskEvent(ctx, userID, starID, eventType) if err != nil { return &pb.ReportEventResponse{Base: &pbCommon.BaseResponse{Code: uint32(codes.Internal)}, Success: false}, err } 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 } ``` 4. **新增 `ProcessTaskEvent` 方法**(line ~166 后): ```go 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)) defs, err := s.dailyRepo.ListActiveDailyTaskDefinitions(starID, eventType) // ★ Phase B 改后的签名 if err != nil { return nil, err } result := &TaskEventResult{} for _, def := range defs { progress, err := s.dailyRepo.GetOrCreateDailyProgress(userID, starID, def.TaskKey, def) if err != nil { logger.Logger.Error("ProcessTaskEvent: get/create failed", zap.Error(err), zap.String("task_key", def.TaskKey)) continue } if progress.Status == "completed" || progress.Status == "claimed" { continue // 当天幂等 } if err := s.dailyRepo.IncrementProgress(progress, def); err != nil { logger.Logger.Error("ProcessTaskEvent: increment failed", zap.Error(err)) continue } if progress.Progress >= def.TargetCount { progress.Status = "completed" now := time.Now().UnixMilli() progress.CompletedAt = &now if err := s.dailyRepo.UpdateDailyProgress(progress); err != nil { logger.Logger.Error("ProcessTaskEvent: status update failed", zap.Error(err)) continue } result.CompletedTaskKeys = append(result.CompletedTaskKeys, def.TaskKey) } } return result, nil } ``` **修改 `backend/services/taskService/provider/task_mobile_provider.go` line 62**: - `return p.dailySvc.ReportEvent(...)` 保持不变(provider 仍叫 `ReportEvent`,内部委托给 `ProcessTaskEvent`) **Critical files**: - `backend/services/taskService/service/daily_task_service.go` - `backend/services/taskService/provider/task_mobile_provider.go`(不改,确认接口稳定即可) **验证**:`go build ./...`;单测(Phase G)覆盖 5 个 case --- ### Phase E — MQ consumer 注册(0.5d) **修改 `backend/services/taskService/mq/consumer.go`**: ```go func newHandleTaskEvent(dailySvc service.DailyTaskService) adapter.TaskHandler { return func(ctx context.Context, t *adapter.Task) error { var p tasks.TaskEventPayload if err := tasks.UnmarshalPayload(t.Payload, &p); err != nil { return fmt.Errorf("unmarshal: %w", err) } if _, err := dailySvc.ProcessTaskEvent(ctx, p.UserID, p.StarID, p.EventType); err != nil { logger.Logger.Error("handle task:event: ProcessTaskEvent failed", zap.String("event_type", p.EventType), zap.Error(err)) return err } return nil } } ``` **修改 `RegisterHandlers` 签名**(line 26):增加 `dailySvc service.DailyTaskService` 参数 ```go func RegisterHandlers(revenueSvc service.RevenueService, dailySvc service.DailyTaskService) error { // ... 现有注册保留 if err := tc.RegisterTask(tasks.TypeTaskEvent, newHandleTaskEvent(dailySvc), adapter.TaskRegisterOptions{ MaxRetry: 3, Queue: queueconsts.QueueDefault, }); err != nil { return fmt.Errorf("register task:event: %w", err) } } ``` **修改 `backend/services/taskService/main.go` line 195**: - `taskmq.RegisterHandlers(revenueSvc)` 改为 `taskmq.RegisterHandlers(revenueSvc, dailySvc)`(`dailySvc` 已在 line 161 构造) **Critical files**: - `backend/services/taskService/mq/consumer.go` - `backend/services/taskService/main.go`(line 195) **验证**:`go build ./...`;本地起 MQ + 手 enqueue `task:event` 消息,确认 taskService 消费 --- ### Phase F — 触发源 emit 接入(1d) > **修订(2026-07-27)**:`daily_place_asset` 改为前端 emit(`myWorks.vue` `placeAssetToGalleryApi` 成功后);`daily_browse_asset` 由 `exhibition.vue` 迁移到 `asset-detail.vue`。`galleryService` 不再需要新增 emit 代码。 #### F.1 assetService(铸造成功 emit `daily_mint` — 后端) **新建 `backend/services/assetService/mq/producer.go`**: ```go package mq import ( "context" "github.com/topfans/backend/pkg/logger" "github.com/topfans/backend/pkg/mq/adapter" "github.com/topfans/backend/pkg/mq/tasks" "go.uber.org/zap" ) // EnqueueTaskEvent 业务事件 emit(修复 daily_mint 悬空) func EnqueueTaskEvent(ctx context.Context, userID, starID int64, eventType string) error { payload, err := tasks.MarshalToPayload(tasks.TaskEventPayload{ UserID: userID, StarID: starID, EventType: eventType, }) if err != nil { return err } task := adapter.Task{ Type: tasks.TypeTaskEvent, Payload: payload, MaxRetry: 3, } if _, err := adapter.Get().TaskProducer().Enqueue(ctx, task); err != nil { logger.Logger.Warn("EnqueueTaskEvent failed", zap.Error(err), zap.String("event_type", eventType)) return err } return nil } ``` **修改 `backend/services/assetService/main.go`**: - 新增 4 个 flag(参照 galleryService line 46-49):`mqRedisAddr / mqRedisDB / mqRedisPassword / mqConcurrency` - 新增 3 个 import:`pkg/mq` / `pkg/mq/asynq` / `pkg/queue/consts` - 新增 `mq.Init(mq.Config{...})` 块(参照 galleryService line 213-236),**只 init 不注册 handler**(assetService 是 producer-only) **修改 `backend/services/assetService/service/mint_service.go`**: - 新增 import `assetmq "github.com/topfans/backend/services/assetService/mq"` - line 603(`statistic.Get().TrackEvent(...)` 之后,`return response, nil` 之前)插入: ```go // 业务事件:触发 daily_mint 每日任务 if err := assetmq.EnqueueTaskEvent(context.Background(), userID, starID, "daily_mint"); err != nil { logger.Logger.Warn("emit task:event daily_mint failed", zap.Error(err)) } ``` - **同样补到 `finalizeMintOrderRecovery`(line 1324)和 `reconcileOneMintOrder` 补单成功分支**,避免 reconcile 路径漏发 #### F.2 前端 emit 接入(替换原 galleryService emit 方案) **`frontend/pages/asset-detail/asset-detail.vue`**:在 onLoad 钩子(line 851 之后)新增: ```js // 触发每日首次浏览藏品任务 const starIdBrowse = uni.getStorageSync('star_id') || 1 reportEvent('daily_browse_asset', starIdBrowse).catch(err => { console.error('上报浏览事件失败:', err) }) ``` 顶部 import 加 `import { reportEvent } from '@/utils/task-api.js'`。 **`frontend/pages/profile/myWorks.vue`**:在 `placeAssetToGalleryApi` 成功后(line 347 后)插入: ```js // 触发每日首次上架任务 const starIdPlace = uni.getStorageSync('star_id') || 1 reportEvent('daily_place_asset', starIdPlace).catch(err => { console.error('上报上架事件失败:', err) }) ``` 顶部 import 加 `import { reportEvent } from '@/utils/task-api.js'`。 **`frontend/pages/exhibition/exhibition.vue`**:删除 line 1564-1568 错位的 `reportEvent('daily_browse_asset', starId)` 调用块(含上方注释 "上报每日首次预览事件"),并删除顶部 `import { reportEvent } from '@/utils/task-api.js'`(如该文件不再使用 reportEvent)。 **Critical files**: - `backend/services/assetService/mq/producer.go`(新) - `backend/services/assetService/main.go` - `backend/services/assetService/service/mint_service.go` - `frontend/pages/asset-detail/asset-detail.vue` - `frontend/pages/profile/myWorks.vue` - `frontend/pages/exhibition/exhibition.vue` **验证**: - 本地 dev 环境铸造一个藏品 → 后端日志看到 `EnqueueTaskEvent daily_mint` → Asynq Web UI 队列收到 `task:event` → taskService consumer 处理 → `user_daily_task_progress` 中 `daily_mint` 进度 +1 - 上架一枚藏品 → 前端 `myWorks.vue` 调 `reportEvent('daily_place_asset')` → taskService 后端 `daily_place_asset` 进度 +1 - 进入任一藏品详情页 → 前端 `asset-detail.vue` 调 `reportEvent('daily_browse_asset')` → `daily_browse_asset` 进度 +1 --- ### Phase G — 单测覆盖(1d) **新建 `backend/services/taskService/service/daily_task_service_test.go`**(参照 `like_bet_repo_test.go` + `revenue_service_test.go` 的混合模式): - **TestMain + setupTestDB + cleanupTestRecords**(sentinel user_id < 0, star_id = -77, task_key 前缀 `test_xxx_`) - 用 `db.AutoMigrate(&model.TaskDefinition{}, &model.UserDailyTaskProgress{})` 准备 schema - 5 个 case(覆盖 spec §8 测试清单): 1. `TestProcessTaskEvent_FirstCompletion`:target=1 + emit daily_login → status=completed 2. `TestProcessTaskEvent_Counting`:target=3 + emit 3 次 daily_browse_asset → progress=1,2,3 → 第三次 completed,第四次 no-op 3. `TestProcessTaskEvent_AlreadyCompleted`:emit 时 status='completed' → skip 4. `TestProcessTaskEvent_UnknownEvent`:emit "test_unknown_event" → no-op, no error 5. `TestProcessTaskEvent_GlobalAndStarSpecific`:star_id=NULL 定义 + star_id=87 定义并存 → emit 时两者都 +1 - **每个 case 末尾 `defer cleanupTestRecords`** 清理 sentinel 行 **新建 `backend/services/taskService/repository/daily_task_repo_test.go`**: - `TestIncrementProgress`:pending → progress=1 → completed - `TestIncrementProgress_AlreadyCompleted`:status='completed' → UPDATE 行 0 影响 - `TestResetAllDailyTasks`:completed/claimed → pending, progress=0 - `TestListActiveDailyTaskDefinitions_WithTriggerEvent`:trigger_event='daily_login' 只返回对应行 **Critical files**: - `backend/services/taskService/service/daily_task_service_test.go`(新) - `backend/services/taskService/repository/daily_task_repo_test.go`(新) **验证**:`cd backend/services/taskService && go test ./...`;要求本地 dev PostgreSQL 可连(`localhost:15432`,与 `.env.dev` 一致),否则 `t.Skipf` --- ### Phase H — 前端清理 + 部署(0.5d) **修改 `frontend/pages/tasks/daily-tasks.vue`** line 62-63 + 闭合 ``:删除 `task-progress` 渲染段(约 3 行),proto 字段不存在(已确认) **部署顺序**: 1. 数据库 migration 先上线(`ALTER TABLE` 无破坏,列加 + 默认值) 2. 后端 3 个服务滚动重启(taskService / assetService / galleryService) 3. 前端发版(删 3 行死代码) **回滚预案**: - migration 回滚:`ALTER TABLE task_definitions DROP COLUMN trigger_event, DROP COLUMN target_count`;`ALTER TABLE user_daily_task_progress DROP COLUMN progress` - 代码回滚:git revert 各 commit - 数据无损:所有新列都有默认值 + backfill 已固化 **Critical files**: - `frontend/pages/tasks/daily-tasks.vue`(删除 line 62-63 + 闭合标签) --- ## 关键文件清单(汇总) ### 新增 - `backend/migrations/2026_07_21_004_daily_task_trigger.sql` - `backend/pkg/mq/tasks/task_events.go` - `backend/services/assetService/mq/producer.go` - `backend/services/taskService/service/daily_task_service_test.go` - `backend/services/taskService/repository/daily_task_repo_test.go` ### 修改 - `docker/init-db.sql` - `backend/services/taskService/model/task_models.go` - `backend/services/taskService/repository/daily_task_repo.go` - `backend/services/taskService/service/daily_task_service.go` - `backend/services/taskService/mq/consumer.go` - `backend/services/taskService/main.go` - `backend/services/assetService/main.go` - `backend/services/assetService/service/mint_service.go` - `frontend/pages/asset-detail/asset-detail.vue`(新增 `reportEvent('daily_browse_asset')`) - `frontend/pages/profile/myWorks.vue`(新增 `reportEvent('daily_place_asset')`) - `frontend/pages/exhibition/exhibition.vue`(**删除**错位的 `reportEvent('daily_browse_asset')`) - `frontend/pages/tasks/daily-tasks.vue` ### 不修改(确认无变化) - `backend/services/taskService/provider/task_mobile_provider.go`(line 62 保留 `ReportEvent` 调用,内部委托已在新 `daily_task_service.go` 完成) - `backend/services/taskService/worker/daily_reset_worker.go`(已通过 repo 层 `ResetAllDailyTasks` 加 `progress: 0` 间接修复) - `backend/services/galleryService/mq/producer.go`(**不修改**:`daily_place_asset` 改为前端 emit,不需要新增 `EnqueueTaskEvent` helper) - `backend/services/galleryService/service/exhibition_service.go`(**不修改**) - `backend/services/galleryService/main.go`(**不修改**) - `frontend/utils/task-api.js`、`frontend/pages/components/Header.vue`(保留前端 `reportEvent` 调用) --- ## 验证方法(端到端) 1. **本地 dev 环境全流程**: ```bash # Phase A psql -h localhost -p 15432 -U postgres -d top-fans -f backend/migrations/2026_07_21_004_daily_task_trigger.sql # Phase B-F cd backend && go build ./... # 重启 taskService / assetService / galleryService(dev 环境) # Phase G cd backend/services/taskService && go test ./... # Phase H # uniapp 编译 + 模拟器测试 ``` 2. **手测 daily_mint 端到端**: - App 内铸造一个藏品 → 后端日志查 `[asset.mint] emit task:event daily_mint` - Asynq Web UI(`http://localhost:8081/queues`)查 `default` 队列出现 `task:event` 任务 - taskService 日志查 `[task:event] ProcessTaskEvent ... task_completed: daily_mint` - `SELECT * FROM user_daily_task_progress WHERE task_key='daily_mint'` 应有 status='completed' 行 3. **手测 daily_place_asset 端到端**:上架一个藏品到展位 → 同样流程 4. **手测 daily_login(前端 reportEvent 路径)**:打开 App 进 home → `task-api.js` 调 `/api/v1/tasks/report-event` → 后端 `ReportEvent` RPC 委托 `ProcessTaskEvent` → `daily_login` 进度更新 5. **手测 daily_browse_asset**:进入任一藏品详情页 → `asset-detail.vue` 调 `reportEvent('daily_browse_asset')` → 后端处理 → 进度更新 6. **重置验证**:手动调 `ResetAllDailyTasks` 或等 05:00 Asia/Shanghai → `progress=0, status='pending'`(查询验证) 7. **回滚演练**:本地先演练 migration 回滚 SQL,确认无破坏 --- ## 风险与缓解 | 风险 | 概率 | 缓解 | |---|---|---| | MQ emit 失败导致任务永远完不成 | 低 | Emit 走 Asynq `MaxRetry=3`;前端可重新触发(daily_login / daily_browse_asset);`EnqueueTaskEvent` 失败仅 Warn 不阻塞主路径 | | Backfill 数据偏差 | 低 | Migration 用显式 `WHERE id IN (3,4)` 而非全表 UPDATE;手动验证 4 行值 | | assetService 新增 MQ 依赖增加启动失败面 | 中 | `mq.Init` 失败仅 Warn 日志(仿 galleryService),不影响 RPC;失败时 daily_mint 永远完不成,但任务保留 active 状态待修复后补发 | | `IncrementProgress` 事务并发导致进度丢失 | 低 | GORM 默认事务隔离;如需严格一致后续 Stage 2 加 `SELECT FOR UPDATE` | | Reset 时 `progress` 字段未加导致 daily 重置不全 | 高(已规避) | Phase B `ResetAllDailyTasks` 必须同步加 `progress: 0` | | daily_browse_asset 重复触发(同一天进入详情页多次) | 低 | 后端 `completed` 后跳过;无前端缓存依赖,行为正确 | --- ## 任务清单(commit 粒度建议) 按 8 阶段独立 commit,每个 commit 通过 `go build` + 单测: 1. `feat(task): migration + backfill daily_task trigger_event` (Phase A) 2. `feat(task): GORM model + repository schema support` (Phase B) 3. `feat(mq): task:event task type + business event constants` (Phase C) 4. `feat(task): ProcessTaskEvent engine + ReportEvent delegation` (Phase D) 5. `feat(task): register task:event MQ consumer` (Phase E) 6. `feat(asset,frontend): emit task:event on mint success + frontend daily_place/browse_asset` (Phase F) 7. `test(task): ProcessTaskEvent + IncrementProgress coverage` (Phase G) 8. `chore(frontend): remove dead task-progress rendering` (Phase H) 每个 commit 完成后跑 `go build ./...` + `go test ./...`,全绿后再继续。 --- ## 关键参考(已读过) - 设计文档:`docs/superpowers/specs/2026-07-21-daily-task-config-driven-design.md`(12 处修复已落地) - `backend/services/taskService/service/daily_task_service.go:99-165` —— 当前 ReportEvent 内联实现(待删除) - `backend/pkg/mq/tasks/registry.go:30-88` —— 现有 TaskType 常量风格 - `backend/services/galleryService/mq/producer.go:62-99` —— EnqueueExhibitSettled 模式(EnqueueTaskEvent 参考) - `backend/services/userService/mq/consumer.go:36-47` —— RegisterHandlers 模式(task:event handler 参考) - `backend/services/taskService/repository/like_bet_repo_test.go:16-78` —— 测试模式(TestMain + setupTestDB + sentinel cleanup) - `docker/init-db.sql:124664-124670` —— task_definitions 现有 5 行 seed(id=1,2,3,4 是 daily) --- ## 不在本次范围(明确延后) - proto 加 `progress` / `target_count` 字段 + 前端展示进度条(spec §9.1) - 计数型任务 distinct 去重(spec §5、§7.1) - A→B 升级:task_triggers 表 + JSON 规则(spec §7) - 用户服务 `daily_login` 后端化(当前 daily_login 必须前端,因为"每日首次活跃"语义后端观测不到)