fix(task): backfill id=1/2 trigger_event + cleanup misleading NULL comments
daily_browse_asset (id=2) / daily_login (id=1) 之前永远不触发,根因:
2026_07_21_004 迁移只 backfill 了 id=3/4 的 trigger_event=task_key,
id=1/2 留 NULL。但 ProcessTaskEvent 引擎按 trigger_event = ? 严格匹配
(spec §4 单一隔离单元),NULL 不会命中,前端 reportEvent 后引擎跑空
循环、daily-tasks 任务永远 pending。
修复:
- 新增 2026_07_27_005 迁移:回填 id=1/2 的 trigger_event=task_key(幂等)
- 修订原 004 迁移的错误注释('trigger_event 留 NULL'误导后人)
- model/task_models.go:删除 'NULL 仍走 def.TaskKey 兜底' 不实承诺
- impl plan §37/39:修订错描述('暂不依赖 trigger_event 列')
- docker/init-db.sql:124668/124671:重建 dev DB 时也带 trigger_event
- daily_task_repo_test.go:补 2 个回归测试
- NullTriggerEvent_NotMatched:trigger_event=NULL 时查不到
- TriggerEventEqualsTaskKey_Matches:trigger_event=task_key 时能命中
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4284775ed6
commit
89d5d12450
@ -17,9 +17,17 @@ ALTER TABLE task_definitions ADD COLUMN IF NOT EXISTS target_count int NOT NULL
|
||||
-- (2) user_daily_task_progress 新列
|
||||
ALTER TABLE user_daily_task_progress ADD COLUMN IF NOT EXISTS progress int NOT NULL DEFAULT 0;
|
||||
|
||||
-- (3) Backfill:仅给"后端 emit 路径"的两个 daily 任务填 trigger_event
|
||||
-- daily_login (id=1) 与 daily_browse_asset (id=2) 走前端 reportEvent,trigger_event 留 NULL
|
||||
-- onboarding_complete (id=5) 不属于 daily,trigger_event 留 NULL
|
||||
-- (3) Backfill:给"后端 emit 路径"的两个 daily 任务填 trigger_event。
|
||||
-- daily_mint (id=3) 由 assetService.CreateMintOrder 成功后 emit;
|
||||
-- daily_place_asset (id=4) 由前端 myWorks.vue 上架成功后 reportEvent。
|
||||
-- onboarding_complete (id=5) 不属于 daily,留 NULL。
|
||||
--
|
||||
-- ★ 修正 2026-07-27:
|
||||
-- 原 SQL 漏 backfill id=1 (daily_login) / id=2 (daily_browse_asset),
|
||||
-- 误以为"走前端 reportEvent 就不用填 trigger_event"——但 ProcessTaskEvent
|
||||
-- 按 trigger_event = ? 严格匹配(spec §4 单一隔离单元,无 NULL 兜底),
|
||||
-- NULL 永远查不到,引擎跑空循环,daily-tasks 任务永远 pending。
|
||||
-- 已在 2026_07_27_005 迁移中按 spec §2.1 原计划补回填。
|
||||
UPDATE task_definitions SET trigger_event = 'daily_mint', target_count = 1 WHERE id = 3 AND task_type = 'daily';
|
||||
UPDATE task_definitions SET trigger_event = 'daily_place_asset', target_count = 1 WHERE id = 4 AND task_type = 'daily';
|
||||
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
-- 2026_07_27_005_daily_task_browse_login_trigger.sql
|
||||
-- 修复 daily_login (id=1) / daily_browse_asset (id=2) 的 trigger_event 留 NULL bug:
|
||||
-- 原迁移 2026_07_21_004 只 backfill 了 id=3/4,id=1/2 留 NULL。
|
||||
-- 但 ProcessTaskEvent 引擎按 trigger_event = ? 严格匹配(spec §4 单一隔离单元),
|
||||
-- NULL 不会命中,导致前端 reportEvent 后引擎跑空循环、daily-tasks 页面任务永远 pending。
|
||||
--
|
||||
-- 本迁移按 spec §2.1 原计划回填:trigger_event = task_key。
|
||||
-- 触发源(spec §3 / §5 F4):
|
||||
-- - id=1 daily_login ← 前端 Header.vue reportEvent
|
||||
-- - id=2 daily_browse_asset ← 前端 asset-detail.vue reportEvent
|
||||
--
|
||||
-- 已正确填过的(id=3 daily_mint, id=4 daily_place_asset)不在本迁移处理范围。
|
||||
-- onboarding_complete (id=5) 不属于 daily,留 NULL。
|
||||
--
|
||||
-- 执行前请先备份:
|
||||
-- pg_dump -h <host> -U postgres -t task_definitions <db> > backup_daily_task_005.sql
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Backfill:仅回填当前 trigger_event 仍为 NULL 的 daily 任务(幂等)
|
||||
UPDATE task_definitions
|
||||
SET trigger_event = task_key,
|
||||
target_count = 1,
|
||||
updated_at = (EXTRACT(EPOCH FROM NOW()) * 1000)::bigint
|
||||
WHERE task_type = 'daily'
|
||||
AND trigger_event IS NULL
|
||||
AND id IN (1, 2);
|
||||
|
||||
-- 序列同步(CLAUDE.md 强制)
|
||||
SELECT setval('task_definitions_id_seq', (SELECT COALESCE(MAX(id),1) FROM task_definitions));
|
||||
|
||||
COMMIT;
|
||||
@ -14,7 +14,10 @@ type TaskDefinition struct {
|
||||
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 兜底(向后兼容)
|
||||
// ★ trigger_event 必须填;ProcessTaskEvent 引擎按 trigger_event = ? 严格匹配
|
||||
// (spec §4 单一隔离单元),NULL 不会命中。daily 任务的 trigger_event 由
|
||||
// 2026_07_27_005 迁移回填(与 task_key 一致);onboarding 任务 trigger_event 留 NULL。
|
||||
TriggerEvent string `gorm:"column:trigger_event;size:64"`
|
||||
TargetCount int `gorm:"column:target_count;default:1"` // =1 等价"首次",>1 为计数型
|
||||
}
|
||||
|
||||
|
||||
@ -326,4 +326,96 @@ func TestListActiveDailyTaskDefinitions_EmptyEventType_NoFilter(t *testing.T) {
|
||||
if !foundA || !foundB {
|
||||
t.Errorf("expected both defA and defB in no-filter results: A=%v B=%v", foundA, foundB)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 回归用例 2026-07-27:daily_login / daily_browse_asset 触发源 ====================
|
||||
|
||||
// seedTestDefinitionNullTrigger 同 seedTestDefinition,但显式把 trigger_event 置为 NULL。
|
||||
// 模拟 2026_07_21_004 迁移漏 backfill 的存量场景。
|
||||
func seedTestDefinitionNullTrigger(t *testing.T, db *gorm.DB, taskKey string) *model.TaskDefinition {
|
||||
def := &model.TaskDefinition{
|
||||
StarID: nil,
|
||||
TaskKey: taskKey,
|
||||
TaskType: "daily",
|
||||
Name: "test daily null-trigger",
|
||||
CrystalReward: 10,
|
||||
TriggerEvent: "", // GORM 零值 = 不写入或写 NULL;为保险显式用 sql.NullString 走 NULL
|
||||
TargetCount: 1,
|
||||
IsActive: true,
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
UpdatedAt: time.Now().UnixMilli(),
|
||||
}
|
||||
// 强制 NULL:直接 SQL 插入,绕开 GORM 零值处理
|
||||
if err := db.Exec(
|
||||
`INSERT INTO task_definitions
|
||||
(star_id, task_key, task_type, name, crystal_reward, trigger_event, target_count, is_active, created_at, updated_at)
|
||||
VALUES (NULL, ?, 'daily', ?, ?, NULL, ?, true, ?, ?)`,
|
||||
def.TaskKey, def.Name, def.CrystalReward, def.TargetCount, def.CreatedAt, def.UpdatedAt,
|
||||
).Error; err != nil {
|
||||
t.Fatalf("seed null-trigger task_definition: %v", err)
|
||||
}
|
||||
// 回读拿 ID
|
||||
if err := db.Where("task_key = ?", def.TaskKey).First(def).Error; err != nil {
|
||||
t.Fatalf("reread null-trigger task_definition: %v", err)
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// TestListActiveDailyTaskDefinitions_NullTriggerEvent_NotMatched 回归测试:
|
||||
// spec §4 单一隔离单元——ListActiveDailyTaskDefinitions 按 trigger_event = ? 严格匹配,
|
||||
// NULL 不会命中(即使 task_key == eventType 也不兜底)。这是 daily_browse_asset
|
||||
// 之前不触发的根因(迁移漏 backfill)。
|
||||
func TestListActiveDailyTaskDefinitions_NullTriggerEvent_NotMatched(t *testing.T) {
|
||||
db := setupDailyTaskDB(t)
|
||||
defer cleanupDailyTaskRecords(t, db)
|
||||
repo := NewDailyTaskRepository(db)
|
||||
|
||||
// 模拟 2026_07_21_004 漏 backfill 的存量:task_key=daily_browse_asset + trigger_event=NULL
|
||||
def := seedTestDefinitionNullTrigger(t, db, "test_daily_browse_asset_null_trigger")
|
||||
defer db.Delete(def)
|
||||
|
||||
// 验证:trigger_event 确实为 NULL
|
||||
if def.TriggerEvent != "" {
|
||||
t.Fatalf("seed sanity check failed: expected trigger_event=NULL/empty, got %q", def.TriggerEvent)
|
||||
}
|
||||
|
||||
// 核心断言:eventType="test_daily_browse_asset_null_trigger" 时查不到
|
||||
results, err := repo.ListActiveDailyTaskDefinitions(-77, def.TaskKey)
|
||||
if err != nil {
|
||||
t.Fatalf("ListActiveDailyTaskDefinitions: %v", err)
|
||||
}
|
||||
for _, d := range results {
|
||||
if d.ID == def.ID {
|
||||
t.Errorf("def with trigger_event=NULL MUST NOT be matched when eventType=%q (spec §4 单一隔离单元)", def.TaskKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestListActiveDailyTaskDefinitions_TriggerEventEqualsTaskKey_Matches 回归测试:
|
||||
// 2026_07_27_005 迁移回填后,trigger_event=task_key 的 def 应当被对应 eventType 命中。
|
||||
// 验证 happy path:daily_browse_asset / daily_login 修复后能正确触发。
|
||||
func TestListActiveDailyTaskDefinitions_TriggerEventEqualsTaskKey_Matches(t *testing.T) {
|
||||
db := setupDailyTaskDB(t)
|
||||
defer cleanupDailyTaskRecords(t, db)
|
||||
repo := NewDailyTaskRepository(db)
|
||||
|
||||
// 模拟 2026_07_27_005 迁移回填后的状态:trigger_event = task_key
|
||||
def := seedTestDefinition(t, db, "test_daily_browse_asset_matched", 1)
|
||||
defer db.Delete(def)
|
||||
|
||||
results, err := repo.ListActiveDailyTaskDefinitions(-77, def.TriggerEvent)
|
||||
if err != nil {
|
||||
t.Fatalf("ListActiveDailyTaskDefinitions: %v", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, d := range results {
|
||||
if d.ID == def.ID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected def with trigger_event=%q to be matched", def.TriggerEvent)
|
||||
}
|
||||
}
|
||||
@ -124665,10 +124665,10 @@ COPY public.system_configs (id, config_key, config_value, description, is_active
|
||||
--
|
||||
|
||||
COPY public.task_definitions (id, task_key, task_type, name, description, crystal_reward, exp_reward, sort_order, is_active, created_at, updated_at, star_id, trigger_event, target_count) FROM stdin;
|
||||
1 daily_login daily 每日首次登录 每天首次登录即可完成 20 20 1 t 1775528749856 1775528749856 \N \N 1
|
||||
1 daily_login daily 每日首次登录 每天首次登录即可完成 20 20 1 t 1775528749856 1775528749856 \N daily_login 1
|
||||
3 daily_mint daily 每日首次铸造 每天首次铸造藏品即可完成 20 60 3 t 1775528749856 1775528749856 \N daily_mint 1
|
||||
5 onboarding_complete onboarding 完成新手引导 完成新手引导教程 420 250 1 t 1775528749856 1775528749856 \N \N 1
|
||||
2 daily_browse_asset daily 每日首次浏览藏品 每天首次查看藏品详情即可完成 20 40 2 t 1775528749856 1776149064005 \N \N 1
|
||||
2 daily_browse_asset daily 每日首次浏览藏品 每天首次查看藏品详情即可完成 20 40 2 t 1775528749856 1776149064005 \N daily_browse_asset 1
|
||||
4 daily_place_asset daily 每日首次上架作品 每天首次将作品放入展位即可完成 20 80 4 t 1775528749856 1781157975963 \N daily_place_asset 1
|
||||
\.
|
||||
|
||||
|
||||
@ -34,9 +34,10 @@
|
||||
**新建 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'`(**只 backfill id=3 / id=4**,`daily_login` / `daily_browse_asset` 走前端 reportEvent 暂不依赖后端 trigger_event 列;`onboarding_complete` 不动)
|
||||
- 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 124664 COPY 数据):加新列;为新 COPY 行加 `trigger_event` / `target_count` 值(id=1,2 留 NULL,id=3,4 填对应 task_key + 1)
|
||||
**同步 `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`(新)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user