- 采集失败时按 latest_likes 分流:无数据才标 failed,已有历史数据保持 success 并注明沿用上次数据,collection_updated_at 不动;run 行照实记失败 - 种子 seedIfEmpty 末尾统一对账 claimed_quantity,杜绝演示数据口径漂移 - 新增 mysql/0021 对账迁移,修复 0020 物理合并遗留的生产计数漂移 - 运营端展示全面改读实时 dist_total(任务卡双面板/项目树/概览/进度), project-service 查询补 dist_total 子查询 - 走查实证:卡片 7/18 vs 详情已领取 2 两处口径打架;保存采集计划补采 失败会把历史成功行改写为 failed
54 lines
2.4 KiB
JavaScript
54 lines
2.4 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import { readFile } from "node:fs/promises";
|
||
import test from "node:test";
|
||
|
||
// claimed_quantity 口径:领取(+1)/释放(-1) 维护的不变式是
|
||
// claimed_quantity == 当前分发行数。种子与 0020 迁移都可能造成漂移,
|
||
// 这里钉住三道防线:种子末尾对账、0021 生产对账、UI 一律读实时 dist_total。
|
||
|
||
test("seed reconciles claimed_quantity with actual distribution rows", async () => {
|
||
const source = await readFile(
|
||
new URL("../lib/mvp-db.ts", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const reconcile = source.indexOf(
|
||
"SET claimed_quantity = (\n SELECT COUNT(*) FROM distributions d WHERE d.task_id = tasks.id",
|
||
);
|
||
assert.ok(reconcile > 0, "种子阶段缺少 claimed_quantity 对账 UPDATE");
|
||
const lastDistributionInsert = source.lastIndexOf(
|
||
"INSERT INTO distributions",
|
||
);
|
||
assert.ok(
|
||
reconcile > lastDistributionInsert,
|
||
"对账必须发生在所有分发 INSERT 之后",
|
||
);
|
||
const batchEnd = source.indexOf("await db.batch(batch);", reconcile);
|
||
assert.ok(batchEnd > reconcile, "对账语句必须在 seedIfEmpty 的 batch 内执行");
|
||
});
|
||
|
||
test("migration 0021 recalculates drifted claimed_quantity in production", async () => {
|
||
const migration = await readFile(
|
||
new URL("../mysql/0021_reconcile_claimed_quantity.sql", import.meta.url),
|
||
"utf8",
|
||
);
|
||
assert.match(migration, /UPDATE tasks\s+SET claimed_quantity = \(\s+SELECT COUNT\(\*\) FROM distributions d\s+WHERE d\.task_id = tasks\.id\s+\)/);
|
||
assert.match(migration, /WHERE claimed_quantity <> \(/);
|
||
});
|
||
|
||
test("admin surfaces read the live distribution count, not the counter", async () => {
|
||
const [adminApp, projectService] = await Promise.all([
|
||
readFile(new URL("../app/admin-app.tsx", import.meta.url), "utf8"),
|
||
readFile(new URL("../lib/project-service.ts", import.meta.url), "utf8"),
|
||
]);
|
||
|
||
// 任务卡双面板 + 概览统计
|
||
assert.match(adminApp, /panelMetric\(task\.dist_total \?\? 0, task\.quantity, "已领取"/);
|
||
assert.match(adminApp, /panelMetric\(task\.dist_submitted \?\? 0, task\.dist_total \?\? 0, "已收取"/);
|
||
assert.match(adminApp, /sum \+ \(task\.dist_total \?\? 0\)/);
|
||
// 项目树明细同样用实时口径
|
||
assert.match(projectService, /AS dist_total/);
|
||
assert.match(adminApp, /\{task\.dist_total\} \/ \{task\.quantity\}/);
|
||
// 展示层不再读反规范化计数器
|
||
assert.doesNotMatch(adminApp, /task\.claimed_quantity/);
|
||
});
|