跳转至

分组聚合取代表 — 「按 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 输入" → 总是最新但最空的行被选中