分组聚合取代表 — 「按 score」不是「取第一个」¶
核心:把 N 行聚合到 1 行展示时(按 URL 去重 / 按域名 group / 按邮箱 group), 选哪一行作"代表"会影响 UI 显示。「取第一个看到的」永远是隐性 bug。
反模式(红信号)¶
ISSUE-0075 现场(v0.10.114 之前):
// ❌ 反模式:按 URL 去重,取第一遇到的 row 作代表
const websites = useMemo(() => {
const seen = new Set<string>();
const out: any[] = [];
for (const r of rows) {
if (!r.website) continue;
const key = r.website.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
out.push(r); // ← 永远是第一个,可能是 status=0 的"差代表"
}
return out;
}, [rows]);
为什么坏:
- rows 顺序由 order by id desc 决定
- 同 URL 多行时,「id 最大」≠「数据最充实」
- 用户新建任务推入大量 status=0 行 → id 最大 → 反而成了"代表"
- 而真正采过 emails 的老行 id 小 → 被舍弃
- UI 上"已完成"的行变成"待采集"
正模式(绿信号)¶
方案:用 score 选最佳代表¶
// ✅ 正模式:按价值 score 选代表
const score = (r: any) =>
(Array.isArray(r.emails) && r.emails.length > 0 ? 2 : 0) + // 有邮箱 +2
(r.scrape_status === 2 ? 1 : 0); // 已采 +1
// 自定义 score 函数,越大越值得作代表
const byKey = new Map<string, any>();
for (const r of rows) {
const key = r.website?.toLowerCase();
if (!key) continue;
const existing = byKey.get(key);
if (!existing || score(r) > score(existing)) {
byKey.set(key, r);
}
}
const out = Array.from(byKey.values());
score 设计原则:
- 越能让用户「这行有用」感觉强 → 分越高
- 不要单纯看数量(length)—— 看「字段完整度」
- 通常 已采(status=2) > 待采(status=0),有 emails > 无 emails
决策表¶
| 场景 | 怎么选 |
|---|---|
| 按 URL 去重官网列表 | score = emails 非空(2) + status=2(1) |
| 按邮箱去重 → 显示来源商家 | score = rating(0~5) + commentCount/1000 |
| 按电话去重 → 显示主联系商家 | score = website 非空(2) + emails.length |
| 按域名 group → 找代表 | score = friendly(2) > unknown(1) > antibot(0) |
| 任意 group 但无明确"好坏" | 用 id desc(最新优先)也比"第一个"明确 |
反例 vs 正例¶
// ❌ 反例 1:用 Array.find — 永远第一个
const rep = group.find(r => true);
// ❌ 反例 2:用 reduce 简单挑首个
const rep = group.reduce((acc, r) => acc || r, null);
// ❌ 反例 3:filter unique by key 取第一个
const seen = new Set();
const reps = rows.filter(r => !seen.has(r.k) && seen.add(r.k));
// ✅ 正例:score-based
const rep = group.reduce((best, r) => score(r) > score(best) ? r : best);
检查清单¶
写 / 改任何 useMemo / Map.set 聚合时:
[ ] 我是不是在做 group by key 取一行代表?
[ ] 「取第一个」会不会因为输入顺序(order by id desc)造成"代表 = 最新但最空"?
[ ] 我能不能写出 score 函数?score 设计要让用户"看到这行=最有信息"
[ ] score 函数本身要有注释说明"为什么字段 A 权重 2、字段 B 权重 1"
当前合规处(已审)¶
| 文件 | 实践 | 备注 |
|---|---|---|
data-view.tsx websites useMemo |
✅ score = emails(2) + status=2(1) | v0.10.114 修 |
data-view.tsx emails / phones useMemo |
✅ 跨行去重各值,不是行级 group | 无此问题 |
engine-manager.ts batchDedupeByUrl |
✅ source row 按 emails 非空优先 | v0.10.114 加 |
教训¶
- 「先到先得」是 group by 的隐性 bug — 输入顺序由外部决定,代表的好坏不该由外部顺序决定
- score 函数比注释好维护 — 写法直接说明了"什么是好代表"
- ISSUE-0075 4 次复发的 50k 窗口属同类家族 —「先到先得」+ "id desc 输入" → 总是最新但最空的行被选中