- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩 4 条行尾注释的翻译,代码前缀全等) - 交叉引用(design/16 §4.6、D4/B3、0010 迁移文件名)逐字保留 - 保留未译技术词:DDL/rowid/LIKE/LEFT JOIN/DTO/rune/WS/actor 及全部标识符与表名 - 顺带副作用:store.go、session_persona_test.go 由注释文字改变而变得 gofmt 干净(未运行 gofmt -w)
49 lines
2.1 KiB
Go
49 lines
2.1 KiB
Go
package history
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
)
|
||
|
||
// 本文件负责每会话的读游标(design/16 §4.2,Android 协同批次 D4/B3)。本层
|
||
// 只有一个主人账号、没有按设备的身份,因此游标是会话作用域的:在任一端已读
|
||
// 即为该账号的每一端把该会话标为已读。游标存放在 chat_sessions.read_at,与
|
||
// chat_messages.created_at 同为毫秒单位,因此未读角标就是一个朴素的
|
||
// `created_at > read_at` 计数(见 listSessionsQuery)。
|
||
|
||
// MarkSessionRead 把会话的读游标推进到 before,并把结果游标作为 Unix 毫秒
|
||
// 时间戳返回。before 为零意味着「现在」—— 调用方「把我看到的全部标为已读」
|
||
// 的场景,无需自己读时钟。
|
||
//
|
||
// 游标只向前移动:该 UPDATE 使用 SQLite 的双参数标量 MAX,因此一个过期或
|
||
// 乱序的请求(两端竞争、请求重试)绝不能让消息重新变为未读。负的 before
|
||
// 因此不会挪动已存的值,而不是把它向后移。
|
||
//
|
||
// 未知的会话 id 返回 ErrSessionNotFound,与 RenameSession/SetPinned 一致
|
||
// —— 隔离(群聊/陌生人)会话没有 chat_sessions 行,在此不可寻址。
|
||
func (s *Store) MarkSessionRead(ctx context.Context, sessionID string, before time.Time) (int64, error) {
|
||
ms := before.UnixMilli()
|
||
if before.IsZero() {
|
||
ms = time.Now().UnixMilli()
|
||
}
|
||
res, err := s.db.ExecContext(ctx,
|
||
`UPDATE chat_sessions SET read_at = MAX(read_at, ?) WHERE id = ?`, ms, sessionID)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("history: mark session read %s: %w", sessionID, err)
|
||
}
|
||
n, err := res.RowsAffected()
|
||
if err != nil {
|
||
return 0, fmt.Errorf("history: mark session read %s rows: %w", sessionID, err)
|
||
}
|
||
if n == 0 {
|
||
return 0, fmt.Errorf("history: mark session read %s: %w", sessionID, ErrSessionNotFound)
|
||
}
|
||
var readAt int64
|
||
if err := s.db.QueryRowContext(ctx,
|
||
`SELECT read_at FROM chat_sessions WHERE id = ?`, sessionID).Scan(&readAt); err != nil {
|
||
return 0, fmt.Errorf("history: read session cursor %s: %w", sessionID, err)
|
||
}
|
||
return readAt, nil
|
||
}
|