From ed024cd0b23faefed71848952301fa80f942c11d Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 21 Sep 2026 07:31:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9:=20=E6=88=91=E4=B8=80=E7=9B=B4?= =?UTF-8?q?=E9=87=8F=E7=9A=84=E6=98=AF**=E4=BB=A3=E7=90=86=E9=87=8F**=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E7=9C=9F=E5=AE=9E=E9=87=8D=E6=8A=95?= =?UTF-8?q?=E9=9D=A2=E6=98=AF=20`unreadFor`=EF=BC=88=E5=8F=AA=E8=AF=BB=20m?= =?UTF-8?q?ail=5Freads=EF=BC=89=EF=BC=8C=E4=B8=8D=E6=98=AF=20`mails.status?= =?UTF-8?q?=3D'unread'`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 发现路径:向 pi 解释 `4→8→2` 时去核 `?status=unread` 到底挂在哪一列,读到: repo.go:611 if status == "unread" { q += ` AND ` + unreadFor("$1") } // :612 repo.go:488 func unreadFor(arg) = (m.status <> 'archived' AND NOT EXISTS( SELECT 1 FROM mail_reads r WHERE r.mail_id=m.mail_id AND r.reader_name=arg)) ⇒ **真实判据只看 `mail_reads`,`mails.status` 完全没参与。** ⇒ "`m.status='read'` 但 `mail_reads` 无行"**同样在重投面内**(那正是 40 行 gap 的成因) ⇒ 而我上面所有"会 `catchUp` 重投"的数都拿 `m.status='unread'` 筛的 ⇒ **系统性少算**。 一次计算内实测(join 非 archived ∧ 有真孩子): 真实(unreadFor 口径) = 92 我的代理(status='unread') = 77 代理漏掉(status='read' 无行) = 15 加法自洽 77 + 15 = 92 = 直接算 ✓ ★ 这又是本仓那条 **"判据必须读决定行为的那个列"**,方向相反而已: 上次是**测试**读错列(守冗余列 ⇒ 守不住 bug), 这次是**我自己**读错列(拿冗余列当筛选 ⇒ 量小重投面)。 **两处错的是同一个东西:把 `mails.status` 当权威。** --- docs/API.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/API.md b/docs/API.md index 2c5b002..7dc5f6f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -658,6 +658,38 @@ curl -X POST {host}/api/v1/mail/read -H "Authorization: Bearer $AGENT_KEY" "收件人回过"只是**充分**证据,真实漏记量 **≥** 该判据命中数)。 **"补完 40 行"与"历史账平了"是两件事** —— 这正是本节开头那条"A 对不代表 B 对"的同一个形状。 + ★★★ **"重投面"我一直量的是代理量,不是真实量(2026-09-21 发现并改正)**: + 上面所有"会出现在 `?status=unread`、会被 `catchUp` 选中"的数, + 我都用了 `mails.status='unread'` 作筛选 —— **但线上判据根本不看那一列**: + + ```go + // server/internal/repo/repo.go:611 + if status == "unread" { q += ` AND ` + unreadFor("$1") } // :612 + // :488 + func unreadFor(arg string) string { + return `(m.status <> 'archived' AND NOT EXISTS ( + SELECT 1 FROM mail_reads r WHERE r.mail_id = m.mail_id AND r.reader_name = ` + arg + `))` + } + ``` + + ⇒ **真实判据 = `mail_reads` 里有没有"我这个读者"的行**(外加"非 archived"), + **`mails.status` 完全没参与。** + ⇒ 于是"`m.status='read'` 但 `mail_reads` 无行"**同样在重投面内** —— 那正是那 40 行 gap 的成因。 + ⇒ 我一直报的那一档**只是它的真子集**。一次计算内实测(`join` 非 archived ∧ 有真孩子): + + ``` + 真实集合(unreadFor 口径) = 92 + 我的代理(m.status='unread' 那一档) = 77 + 代理漏掉的一档(m.status='read' 但权威列无行) = 15 + 加法自洽:77 + 15 = 92 = 直接算的 92 ✓ + ``` + + ⇒ **凡我说"重投面有 N 封",都系统性少算了"`status='read'` 而权威列无行"那一档。** + ⇒ 这又是本节那条 **"判据必须读决定行为的那个列"** —— 只不过方向相反: + 上次是**测试**读错了列(读冗余列,守不住 bug), + 这次是**我自己**读错了列(用冗余列当筛选,量小了重投面)。 + **两处错的是同一个东西:把 `mails.status` 当成了权威。** + ★ **而且这不是"历史账"问题,是活的重投源**:严格口径下那批里有相当一部分 (宽松口径时量到 **85 封**,同样会漂)在 `sessions.status <> 'archived'` 的会话里 ⇒ **会出现在 `?status=unread` 里、会被 `catchUp` 选中重投**