Files
eryao/docs/plans/static-notification-sync-plan.md
T

485 lines
11 KiB
Markdown
Raw Normal View History

2026-04-10 18:50:08 +08:00
# 静态通知配置同步计划
> 更新时间:2026-04-10
> 状态:最终执行版
## 1. 目标
为通知系统增加一条独立的“静态配置 -> 数据库同步”链路,使服务端可以从仓库内的通知配置文件读取通知定义,并将其注册、更新或撤销到数据库。
本计划解决的问题:
- 通过静态文件维护系统通知内容
- 手动触发后端读取并同步通知到数据库
- 支持已有通知的修改
- 支持已有通知的撤销
- 保持用户侧已读状态不因通知内容更新而丢失
本计划不替代主通知系统计划,而是在其基础上增加“静态通知同步”能力。
关联文档:
- `docs/plans/notification-system-plan.md`
---
## 2. 范围
### 2.1 In Scope
- 新增静态通知配置目录
- 定义静态通知 YAML 协议
- 定义对应的 Pydantic schema
- 实现后端扫描、校验、upsert 同步逻辑
- 实现对主通知的修改和撤销
- 新增手动触发同步脚本
### 2.2 Out of Scope
- 系统级离线推送
- 自动监听文件变化并实时同步
- 通过文件删除自动删库
- 复杂运营后台
- 严格对齐目标用户集合并自动删除既有投递记录
---
## 3. 现有代码基线
当前仓库已经有可直接复用的“静态配置 -> 数据库初始化”模式:
- 静态配置目录:`backend/src/core/config/static/database/`
- 现有 YAML
- `llm_catalog.yaml`
- `system_agents.yaml`
- 现有加载与校验:`backend/src/core/config/initial/init_data.py`
- 现有 CLI`backend/src/core/runtime/cli.py`
- 现有脚本:`infra/scripts/dev-migrate.sh`
通知同步应复用这套模式的核心思路:
- YAML 文件作为配置源
- Pydantic schema 做强校验
- 后端显式执行同步
- 数据库使用 upsert 语义更新
但通知同步不应直接并入 `init-data/bootstrap` 默认流程,因为通知内容属于持续变更的数据,不是纯启动种子数据。
---
## 4. 目录设计
建议新增静态通知目录:
```text
backend/src/core/config/static/notification/
└── notifications/
├── welcome_bonus.yaml
├── maintenance_2026_04.yaml
└── ...
```
第一阶段不增加总索引文件,直接扫描 `notifications/*.yaml`
原因:
- 少一层维护成本
- 避免“文件内容”和“索引文件”双源不一致
- 更适合增量增加通知文件
---
## 5. 数据模型变更
要支持“静态文件和数据库中的同一条通知”建立稳定映射,`notifications` 表需要增加来源标识字段。
建议新增字段:
- `source`
- `source_key`
- `source_version`
- `content_hash`
建议约束:
- `UNIQUE(source, source_key)`
### 5.1 字段职责
- `source`
- 通知来源
- 当前静态通知固定为 `static`
- `source_key`
- 静态通知唯一键
- 例如 `welcome_bonus`
- 用于可靠 upsert
- `source_version`
- 配置版本号
- 用于审计和变更追踪
- `content_hash`
- 标准化内容摘要
- 用于判断文件内容是否发生变化
### 5.2 推荐表结构补充
`notifications` 表基础上补充:
```sql
ALTER TABLE notifications
ADD COLUMN source VARCHAR(32) NOT NULL DEFAULT 'manual',
ADD COLUMN source_key VARCHAR(128),
ADD COLUMN source_version INTEGER,
ADD COLUMN content_hash VARCHAR(64);
CREATE UNIQUE INDEX uq_notifications_source_source_key
ON notifications(source, source_key)
WHERE source_key IS NOT NULL;
```
说明:
- `manual` 可作为非静态创建通知的默认来源
- 静态同步通知统一使用 `source='static'`
---
## 6. 静态通知 YAML 协议
每个 YAML 文件描述一条主通知及其投递目标。
推荐结构:
```yaml
notification:
source_key: welcome_bonus
version: 1
type: system
status: published
published_at: 2026-04-10T08:00:00Z
title: 新用户欢迎通知
body: 你已获得注册奖励,可前往积分中心查看。
payload:
action: open_route
route: /points
entity_id: null
tab: balance
targets:
mode: all_users
```
指定用户示例:
```yaml
notification:
source_key: maintenance_2026_04
version: 3
type: system
status: published
title: 系统维护通知
body: 今晚 23:00 到 23:30 进行维护。
payload:
action: none
targets:
mode: user_ids
user_ids:
- 11111111-1111-1111-1111-111111111111
- 22222222-2222-2222-2222-222222222222
```
---
## 7. Pydantic Schema 设计
静态通知文件必须先经过强校验,不能直接把 YAML 转 dict 入库。
建议新增模块:
- `backend/src/core/config/notification/static_schema.py`
建议 schema
- `StaticNotificationDefinition`
- `StaticNotificationTargets`
- `StaticNotificationFile`
`payload` 不重新定义,直接复用现有通知协议里的 schema:
- `NotificationPayloadNone`
- `NotificationPayloadRoute`
- `NotificationPayloadUrl`
### 7.1 `StaticNotificationDefinition` 职责
- `source_key`
- 静态通知唯一键
- `version`
- 配置版本号
- `type`
- 通知类型,当前默认 `system`
- `status`
- `draft/published/revoked`
- `published_at`
- 发布时间
- `title/body/payload`
- 通知内容
### 7.2 `StaticNotificationTargets` 职责
- `mode`
- `all_users``user_ids`
- `user_ids`
- 仅当 `mode='user_ids'` 时允许
### 7.3 校验约束
- `source_key` 必填且全局唯一
- `version >= 1`
- `status` 只允许 `draft/published/revoked`
- `payload` 必须符合现有通知 payload schema
- `targets.mode='all_users'` 时不允许传 `user_ids`
- `targets.mode='user_ids'``user_ids` 必填且不能为空
---
## 8. 同步语义
### 8.1 新建
当数据库中不存在 `(source='static', source_key=...)` 时:
1. 创建 `notifications`
2. 按目标规则写入 `user_notifications`
### 8.2 修改
当数据库中已存在同一 `source_key` 时:
1. 更新 `notifications.title/body/payload/status/published_at/source_version/content_hash`
2. 保留已有 `user_notifications`
3. 不重置 `is_read/read_at`
这是强规则:
- 修改主通知内容,不影响用户已读状态
### 8.3 撤销
当 YAML 中:
- `notification.status = revoked`
则同步时:
1. 更新 `notifications.status='revoked'`
2. 写入 `revoked_at`
3. 不删除 `user_notifications`
### 8.4 统一删除
本阶段不使用“文件消失自动删库”语义。
原因:
- 文件误删风险高
- 容易把版本控制操作误解释为业务删除
如果需要下线,显式通过配置状态控制:
- `status: revoked`
如果未来确实需要静态配置触发软删除,再单独增加明确字段,不在本阶段默认启用。
### 8.5 目标用户变更
第一阶段采用保守策略:
- 新增目标用户时,补插入 `user_notifications`
- 被移出目标集合的用户,不自动删除既有 `user_notifications`
原因:
- 防止误操作删除已投递历史
- 与“通知一旦发出就保留用户侧记录”的语义更一致
如果未来需要严格对齐文件目标集合,再单独增加显式 `--reconcile-targets` 行为。
---
## 9. 后端实现方案
### 9.1 模块位置
建议新增:
```text
backend/src/core/config/notification/
├── static_schema.py
└── static_sync.py
```
不建议把通知同步继续堆进 `core/config/initial/init_data.py`
原因:
- `init_data.py` 当前更适合 bootstrap seed
- 通知同步是持续执行的配置同步任务
- 语义上应独立
### 9.2 组件职责
- `static_schema.py`
- 定义 YAML 文件的 Pydantic schema
- `static_sync.py`
- 扫描目录
- 读取 YAML
- 校验 schema
- 计算差异
- 执行 upsert
现有通知模块中建议补充内部同步能力:
- `v1/notifications/repository.py`
- 补充按 `source/source_key` 查询与 upsert
- `v1/notifications/service.py`
- 补充内部同步逻辑与事务边界
### 9.3 日志与错误
遵循现有后端规则:
- 使用 `core.logging`
- 不使用 `print`
- YAML 校验失败要明确报错并中止
- 数据库 upsert 失败要中止,不吞错
---
## 10. CLI 与脚本方案
### 10.1 后端 CLI
`backend/src/core/runtime/cli.py` 中新增命令:
- `sync-notifications`
建议调用方式:
```bash
PYTHONPATH=backend/src uv run python -m core.runtime.cli sync-notifications
```
建议参数:
- `--path`
- `--source-key`
- `--dry-run`
第一阶段不默认提供危险的全量清理参数。
### 10.2 infra 脚本
新增:
```text
infra/scripts/register-notifications.sh
```
脚本风格复用 `infra/scripts/dev-migrate.sh`
- 读取 `.env`
- 通过 `uv run python -m core.runtime.cli sync-notifications` 调用后端 CLI
建议用法:
```bash
./infra/scripts/register-notifications.sh
./infra/scripts/register-notifications.sh --dry-run
./infra/scripts/register-notifications.sh --source-key welcome_bonus
```
---
## 11. 与现有通知系统的关系
这条静态同步链路只负责:
- 把 YAML 中的通知定义注册到数据库
- 更新通知主记录
- 撤销通知主记录
- 为目标用户补齐接收关系
它不替代现有通知 API
- 用户列表、未读数、已读接口仍走现有通知系统
- Flutter 端仍然从现有通知 API 和 Realtime 获取数据
如果通知内容被静态同步更新,而前台需要即时看到变更,建议在 Realtime 中补充:
- `notification_updated`
否则前台只能在下次 HTTP 拉取时看到更新后的内容。
---
## 12. 实施清单
1.`notifications` 表增加 `source/source_key/source_version/content_hash`
2. 增加 `(source, source_key)` 唯一约束
3. 新增 `backend/src/core/config/static/notification/notifications/` 目录
4. 定义静态通知 YAML 的 Pydantic schema
5. 实现 YAML 扫描、加载、校验与 upsert 同步逻辑
6. 为通知模块补充按 `source/source_key` 查询与更新能力
7.`core.runtime.cli` 中新增 `sync-notifications` 命令
8. 新增 `infra/scripts/register-notifications.sh`
9. 视需要补充 `notification_updated` Realtime 事件
10. 编写最小测试和 dry-run 校验
---
## 13. 验收标准
- [ ] 新增一个 YAML 文件后,可成功同步出对应主通知记录
- [ ] 相同 `source_key` 的 YAML 再次同步时,会更新主通知而不是插入重复记录
- [ ] 修改 `title/body/payload` 后,再同步可反映到数据库
- [ ] 用户侧已读状态在主通知内容更新后保持不变
- [ ]`status` 改为 `revoked` 后,再同步可使通知在用户列表中失效
- [ ] `--dry-run` 可输出计划变更而不写库
- [ ] YAML 结构不合法时同步失败,并给出明确错误
- [ ] 脚本可按全量或按 `source_key` 手动触发同步
---
## 14. 测试要求
后端至少覆盖:
- YAML schema 校验
- 新建通知同步
- 已有通知更新同步
- 撤销同步
- 相同 `source_key` 幂等 upsert
- 更新主通知时不重置 `user_notifications.is_read/read_at`
- 新增目标用户时补插入接收关系
- 被移出目标集合时不删除既有接收关系
脚本至少验证:
- 正常执行 CLI
- `--dry-run` 不写库
- `--source-key` 只同步指定通知
---
## 15. 后续扩展条件
只有在真实需求出现时,再考虑:
- 用删除文件触发软删除
- 严格对齐目标用户集合并清理历史接收关系
- 通过后台页面管理静态通知
- 将静态通知同步纳入更完整的发布工作流