fix(权限): 决策人��空时不再静默跳过已读记账 —— 那让权限邮件永远显示未读
## 形状
`DecidePermission` 的函数头承诺「权限邮件对他(决策人)也应当变成已读」,
但那两段记账都被 `if decider != ""` 包着。而**未读判据是 mail_reads**
(`unreadFor` / `readStateFor`),不是行级那列。⇒ decider 为空时:
mails.status = 'read' (全局冗余列改了)
mail_reads —— 没有这一行 (事实表没记)
⇒ 收件人在收件箱里**永远看到这封未读**,尽管它早已决策完。
## 线上实证
13 封这样的历史邮件,收件人均为 jianf,2026-09-08 / 09-13,主题全是
「权限请求: 请求执行 bash」—— 每一次都已被人类决策过(同意/拒绝)。
## 为什么今天 HTTP 路径触发不了
`permission.go` 会沿会话树上溯找人类(`NearestHumanInThread`),
HTTP 层传的也是 `user.Username`。但 repo 层是**所有调用方的门**,
`MarkMailRead` 早就为同一件事挡了(reader 是必填语义),只挡它一个等于漏网。
静默跳过正是 `if decider != ""` 的写法问题:它把「不知道谁读的」记成「不用记」。
## 同族核对(不是只看这一处)
全 internal/ 树扫 `UPDATE mails SET status ... 'read'`,共 4 处,
逐处核对其前置 45 行是否有 mail_reads 记账:
MarkMailRead ✓ / MarkMailsReadFor ✓ / MarkAllInboxReadForSession ✓ / DecidePermission ✗
⇒ 唯一缺口就是这一处。事实表本身干净:无空读者、无孤儿行。
## 判据
新增 2 格:空 decider 报错 / 决策后 mail_reads 必有记录且派生读数为 read。
两个变异都经得起(去掉报错守卫、去掉 mail_reads 写入,各红一格)。
## 顺带记一个判据的坑
`mail_status_readers_test.go` 的 readRe 是**纯文本**匹配 `mails.status`,
会把我注释里提到该列名也算成「新增一处读取」,导致那条清册判据变红。
已在注释里避开那个写法并写明原因 —— 改这块代码时若它突然变红,先看是不是注释。
This commit is contained in:
@ -1116,6 +1116,33 @@ func AttachPermissionDeadlines(mails []models.Mail) {
|
||||
// decider 是**做出决策的人**(用户名):权限邮件对他也应当变成已读 —— 但只对他一个人,
|
||||
// 而不是像原先那样把邮件行的全局 status 一改(抄送的其他 Agent 会因此看不到它)。
|
||||
func DecidePermission(ctx context.Context, mailID uuid.UUID, decider, decision string) (*models.PermissionRequest, error) {
|
||||
// ★ 2026-10-01:空 decider **直接报错**,不静默跳。
|
||||
//
|
||||
// 函数头那句「权限邮件对他也应当变成已读」在 decider 为空时**静默失效**:
|
||||
// 下面那两段都是 `if decider != ""` 才做,而**未读判据是 mail_reads**
|
||||
// (见 unreadFor / readStateFor),不是行级那列。
|
||||
// ⇒ 结果是那封邮件被判为 read,可收件人在收件箱里**永远看到它未读**。
|
||||
//
|
||||
// 线上实测 13 封这样的历史邮件(收件人均为 jianf,2026-09-08 / 09-13)。
|
||||
//
|
||||
// 为什么当时不报而现在报:那 13 封的成因已被后来的 decider 兜底堵住
|
||||
// (permission.go 会沿会话树上溯找人类),HTTP 路径也传 user.Username。
|
||||
// 但 repo 层是**所有调用方的门**——MarkMailRead 早就为同一件事挡了
|
||||
// (「已读按读者记录,reader 是必填语义」),只挡它一个等于**漏网**。
|
||||
// 静默跳过正是当初 `if decider != ""` 的写法问题:它把「不知道谁读的」
|
||||
// 记成「不用记」。
|
||||
//
|
||||
// 方向上这也是保守的:报错会让调用方看到「决策没落库」,
|
||||
// 而静默放行会让收件人反复看到一封已经决策完的权限询问(真会发生的形状)。
|
||||
//
|
||||
// ⚠ 注释里刻意不写那个列名的点号形式:mail_status_readers_test.go 的
|
||||
// readRe 是**纯文本**匹配,会把注释里提到它也算成「新增一处读取」。
|
||||
// 改代码时若那条判据突然变红,先看是不是这里写了列名。
|
||||
if strings.TrimSpace(decider) == "" {
|
||||
return nil, fmt.Errorf(
|
||||
"DecidePermission: decider 不能为空(未读判据是 mail_reads," +
|
||||
"空读者会让这封权限邮件在收件人收件箱里永远显示未读)")
|
||||
}
|
||||
var pr models.PermissionRequest
|
||||
var optsJSON []byte
|
||||
|
||||
@ -1143,14 +1170,11 @@ func DecidePermission(ctx context.Context, mailID uuid.UUID, decider, decision s
|
||||
`UPDATE mails SET permission_result = $1, status = 'read'
|
||||
WHERE mail_id = $2 AND status <> 'archived'`,
|
||||
decision, mailID)
|
||||
if decider != "" {
|
||||
// 记到决策人名下(按读者记,见 repo.markReadFor)
|
||||
_, _ = db.DB.ExecContext(context.Background(),
|
||||
`INSERT INTO mail_reads (mail_id, reader_name)
|
||||
SELECT $1, $2
|
||||
WHERE NOT EXISTS (SELECT 1 FROM mail_reads WHERE mail_id = $1 AND reader_name = $2)`,
|
||||
mailID, decider)
|
||||
}
|
||||
_, _ = db.DB.ExecContext(context.Background(),
|
||||
`INSERT INTO mail_reads (mail_id, reader_name)
|
||||
SELECT $1, $2
|
||||
WHERE NOT EXISTS (SELECT 1 FROM mail_reads WHERE mail_id = $1 AND reader_name = $2)`,
|
||||
mailID, decider)
|
||||
|
||||
return &pr, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user