# 发给自己日历：复用现有接口与后端能力补齐需求

日期：2026-09-18。面向后端同事的实现与联调说明。

## 1. 目标与现状

用户在桌面端“发给自己”切换历史月份时，需要快速看到每日数量，并点击日期定位到该范围内的记录。桌面端、Flutter 移动端应复用同一个后端统计规则，不由各客户端分别下载整月记录再计算。

当前已复用的接口：

| 接口 | 已有能力 | 当前缺口 |
| --- | --- | --- |
| `POST /api/v1/calendar/buckets/query` | 指定日期范围、时区和归属日策略，返回每日数量、定位锚点 | scope 仅支持个人总日历和单主题，不能表达发给自己、未分类、主题子树的完整筛选规则 |
| `POST /api/v1/calendar/records/query` | 指定日期查询记录，支持稳定游标、倒序/正序 | 需要与新增统计范围使用完全相同的过滤、权限和去重规则 |
| `POST /api/v1/topics/display/statistics` | 单主题摘要和日期统计 | 内部组合主题摘要与上述日历统计，不是独立的子树/未分类快捷接口；纯月份概览不应额外加载整个主题摘要 |

后端事实已对照 `jotmo-record` master `c6c5fbf563a113faa9d153a387b2ceb8355cbd87`（2026-09-18）核实：

- `gin/api/calendar_bucket.go`：请求体严格解析，当前没有下述新增筛选字段。
- `gin/compound/calendar_bucket.go`：月份统计读取日历索引候选，批量核对当前记录/主题/隐私事实后按日聚合，使用 `recordreadcache` 缓存。
- `internal/calendarbucket`：记录变更通过现有机制刷新日历读侧索引。
- `gin/compound/topic_display.go`：主题统计复用 `CalendarBucketCompound.QueryBuckets`。

这不是“后端完全没有缓存或索引”的问题。主要缺口是现有统计范围不能直接覆盖桌面端业务范围，导致插件取得月份统计后，又逐日、每页 50 条读取记录做二次过滤/去重。

## 2. P0：统一统计范围

建议扩展现有两项 calendar 接口，保持不传新参数时原行为不变；不另建桌面端专属统计系统。

| 业务范围 | 必须统计的内容 | 不应统计的内容 |
| --- | --- | --- |
| 发给自己－全部 | 与“发给自己”真实列表可见记录一致；当前列表基于 `/home/feed/query` 的 `source_kinds: [1, 2]`，再排除 DSH 和隐私锁 | 私聊/群聊专属条目、DSH 输入、隐私锁/无权限/已删除内容；不能直接采用个人总日历数量 |
| 发给自己－未分类 | 与 `/records/uncategorized/query` 同一 owner 判定，再应用个人内容过滤 | 已归类、DSH、隐私锁、无权限、已删除内容；不能简单用“没有返回 topic_core”推断未分类 |
| 发给自己－某主题 | 指定主题及可见后代主题的记录并集，按 `record_uid` 去重；支持只看本主题 | 不可访问主题、隐私主题、DSH 系统主题及 DSH 来源记录；父子/兄弟主题重复挂载不得重复计数 |
| 原个人总日历 | 保留当前旧客户端的范围和行为 | 不因本次“发给自己”需求把旧入口的聊天等合法内容静默移除 |

边界规则：

1. DSH 判定依据稳定字段：记录 `creation_source = 3`、系统主题 `kind = 3`，不能按标题包含“DSH”或中文名称过滤普通主题。
2. 首页展示开关 `show_in_home`、多主题挂载、普通记录与聊天挂载的关系，须由现有列表 owner 规则裁决；不能仅按 `origin_kind` 或主主题名字猜测。特别需要验证“日历显示有内容，但对应发给自己列表无法定位”的情况。
3. 隐私锁、隐私主题、删除状态及成员访问权限同时作用于数量、`has_records` 和定位锚点。新增个人视图不返回可推断隐藏内容存在的数量/锚点。
4. 父主题下子主题的展开/收起是客户端展示偏好，不影响“包含后代”的统计范围。
5. 后代层级变化、主题解散/移动、记录重新指定主题，必须刷新相关范围；循环或异常层级需有明确失败行为，不能超时无限遍历。

## 3. 请求与响应建议（新增字段尚未上线）

以下是语义草案，不是现有可调用合同；字段名及枚举可由后端按项目规范确定，需联调前提供最终版本。

在现有 `bucket_scope_kind`、`bucket_scope_uid`、日期、时区参数基础上，增加：

- `view_scope`：区分 `send_to_self`、`uncategorized`、`topic`；省略保持旧规则。
- `include_descendants`：仅主题范围有效；省略保持单主题旧规则。

例如月份请求（草案）：

```json
{
  "bucket_scope_kind": 2,
  "bucket_scope_uid": "<topic_uid>",
  "view_scope": "topic",
  "include_descendants": true,
  "start_date": "2026-08-01",
  "end_date": "2026-08-31",
  "timezone": "Asia/Shanghai"
}
```

`send_to_self` 和 `uncategorized` 使用个人 scope，不接受任意其他用户身份；身份仅来自鉴权上下文。非法 scope/参数组合必须明确报错。

每日响应至少保留：

- `bucket_date`、过滤并去重后的 `count`。
- 当前范围内合法的 `first_record_uid`、`first_send_at` 与访问状态，用于直接定位，减少点击日期后为找锚点多发一次请求。
- `timezone`，并明确实际采用的归属日策略。
- 建议增加可缓存的 `revision`/结果版本；若暂不提供，明确缓存失效与一致性约定。

**锚点排序需明确：**当前后端 `first_record_uid` 实际按 `send_at` 较大者优先、同时间按 UID 较大者优先（即倒序首条）。保留已发布语义，不能静默改为当日最早记录。若新增最早锚点，单独定义字段或显式排序。统计锚点、当天分页和列表定位必须一致。

日期接口需接收相同的 view/子树参数，在服务端过滤、合并、去重后再分页，而不是分页后由客户端丢弃记录。保留 `oldest_first`、`has_more` 和稳定游标；同一时间的多条记录不能漏项/重复。月份零数量应无有效锚点；整月无数据可以返回空 `daily_data`。

## 4. P0：缓存与更新

- 复用现有日历索引、批量权限检查和 `recordreadcache`；不要把整个重算问题换个位置后，仍每次扫描用户全部历史正文。
- 缓存键必须覆盖账户、业务视图、主题/子树范围、日期范围、时区、归属日策略、数据/权限版本。
- 新增/删除记录、发送时间调整、主题归属/层级变化、首页展示策略变化、隐私/权限变化，均需要使受影响统计失效。
- 时间由旧日移到新日时，两天都失效。隐私/权限收紧不能继续返回旧缓存中的数量或锚点。
- 优先按受影响日期/范围增量更新或失效；若 P0 先沿用用户级版本失效，可接受，但请注明，后续再细化，不能把用户级失效称为日期级增量更新。
- 权限和正确性优先；查询或投影失败要明确报错，不返回看似成功的部分月份或把失败当成零条。

## 5. 兼容与上线交接

1. 不传新字段的 Flutter/旧插件请求，保持原参数、统计含义和响应结构兼容。
2. 当前服务严格拒绝未知字段，插件在后端发布前不会发送上述草案参数。
3. 后端交付最终请求/响应样例、错误码、能力版本/检测方式、测试环境地址和部署版本；若采用上线顺序控制，应明确先后端、后插件。
4. 新能力未上线时保留现有精确过滤路径；已明确支持后不得因权限错误/超时偷偷退回全账户统计，避免泄露或计数错误。
5. Flutter 无须为兼容而立即改版；需要同业务视图时再使用同一新增能力。本需求不授权自动部署生产服务。

## 6. 验收用例

| 用例 | 预期 |
| --- | --- |
| 同日普通记录、普通主题记录、私聊、群聊、DSH 混合 | 发给自己、未分类、原总日历各自严格符合定义，且与对应列表一致 |
| 普通主题名称恰好包含 DSH | 不按名称误排除 |
| 父主题/子主题/兄弟主题挂载同一记录 | 子树并集只计 1 次；分页无重复 |
| 隐私记录、锁定主题、无权限后代 | 数量和锚点都不泄露；锁定后旧缓存失效 |
| 记录从 A 主题移动至 B 主题或取消归类 | A/B、祖先、未分类、全部按其实际受影响范围更新 |
| 首页隐藏主题、普通记录附加聊天关系 | 与真实列表规则一致，日期定位不落到列表外记录 |
| 23:59/00:00、跨月、夏令时、不同归属日策略 | 桶日期、统计和当天查询一致 |
| 同一时间大量记录、当天超过 50/100 条 | 统计完整，分页不漏不重，不要求客户端遍历来算数量 |
| 空月份或只有不可见内容的月份 | 无合法数量/锚点，不混淆“无数据”和错误 |
| 旧客户端省略新增参数 | 行为与升级前兼容 |
| 退出/切换账户、隐私变化期间旧请求晚到 | 不串账户、不用旧版本污染新缓存 |

性能验收：翻到一个月份，客户端只发一次月份统计业务请求（鉴权刷新等独立请求除外），不能再为取得数量逐日请求 `/calendar/records/query`。点击某日再读取记录，或直接使用合法锚点定位。用至少 30 个有数据日期、每日多页记录、多个后代主题的测试账户，分别报告冷/热缓存的 P50/P95、请求数、响应大小和后端候选扫描/批量回源次数；性能阈值由后端结合环境确认，不用未经测量的“秒开”作为已达成结论。

## 7. 插件当前先行优化及限制

仍使用现有 calendar 接口，不发送未发布字段，不修改后端：

- 保留现有按账户/范围/月份缓存与后台刷新。
- 增加最多 512 个已完成日期的内存统计缓存，60 秒有效，按日期版本和上游数量/锚点/主题成员集检查复用；缓存不含正文、媒体或记录 UID 列表。
- 已知某一天变化时，只重新扫描失效日期；用户中断翻月后保留已完成日期，不缓存未完成日期。
- 复用日缓存不延长其原有有效期；账户、主题、时区隔离，隐私变更按版本失效。

**边界：首次打开未缓存的历史月份，个人/未分类/主题子树目前仍需必要的逐日过滤。现阶段只能减少重复读取；后端补齐上述统一统计能力后，才能移除这条首次加载慢路径。**
