515 lines
12 KiB
Markdown
515 lines
12 KiB
Markdown
# 静态通知配置同步计划
|
||
|
||
> 更新时间: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:
|
||
deleted: false
|
||
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`
|
||
- `deleted`
|
||
- 显式软删除主通知
|
||
- `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`
|
||
- `deleted` 为可选布尔值
|
||
- `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 统一删除
|
||
|
||
本阶段支持两种明确的下线方式:
|
||
|
||
1. 在 YAML 中显式写 `deleted: true`
|
||
2. 执行同步时使用 `--prune`,将文件中已不存在的静态通知软删除
|
||
|
||
- `deleted: true` 语义:
|
||
|
||
- 设置 `notifications.deleted_at`
|
||
- 不删除既有 `user_notifications`
|
||
|
||
- `--prune` 语义:
|
||
|
||
- 扫描范围内缺失的静态通知会被软删除
|
||
- 不会删除非 `source='static'` 的通知
|
||
|
||
默认情况下,不因为文件消失自动删库。
|
||
|
||
原因:
|
||
|
||
- 文件误删风险高
|
||
- 容易把版本控制操作误解释为业务删除
|
||
|
||
如果只是想临时停止用户可见,优先用:
|
||
|
||
- `status: revoked`
|
||
|
||
如果想做统一下线并保留审计主记录,可用:
|
||
|
||
- `deleted: true`
|
||
|
||
### 8.5 目标用户变更
|
||
|
||
默认采用保守策略:
|
||
|
||
- 新增目标用户时,补插入 `user_notifications`
|
||
- 被移出目标集合的用户,不自动删除既有 `user_notifications`
|
||
|
||
原因:
|
||
|
||
- 防止误操作删除已投递历史
|
||
- 与“通知一旦发出就保留用户侧记录”的语义更一致
|
||
|
||
如果执行同步时显式加上 `--reconcile-targets`,则:
|
||
|
||
- 当前目标集合之外的既有 `user_notifications` 会被删除
|
||
|
||
---
|
||
|
||
## 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`
|
||
- `--prune`
|
||
- `--reconcile-targets`
|
||
|
||
危险行为必须显式开启,不默认启用。
|
||
|
||
### 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
|
||
./infra/scripts/register-notifications.sh --prune --reconcile-targets
|
||
```
|
||
|
||
---
|
||
|
||
## 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. 支持 `--prune` 和 `--reconcile-targets`
|
||
10. 视需要补充 `notification_updated` Realtime 事件
|
||
11. 编写最小测试和 dry-run 校验
|
||
|
||
---
|
||
|
||
## 13. 验收标准
|
||
|
||
- [ ] 新增一个 YAML 文件后,可成功同步出对应主通知记录
|
||
- [ ] 相同 `source_key` 的 YAML 再次同步时,会更新主通知而不是插入重复记录
|
||
- [ ] 修改 `title/body/payload` 后,再同步可反映到数据库
|
||
- [ ] 用户侧已读状态在主通知内容更新后保持不变
|
||
- [ ] 将 `status` 改为 `revoked` 后,再同步可使通知在用户列表中失效
|
||
- [ ] 将 `deleted` 改为 `true` 后,再同步可使通知从用户列表和未读数中消失
|
||
- [ ] `--dry-run` 可输出计划变更而不写库
|
||
- [ ] `--prune` 可将文件中已不存在的静态通知软删除
|
||
- [ ] `--reconcile-targets` 可严格对齐目标用户集合
|
||
- [ ] YAML 结构不合法时同步失败,并给出明确错误
|
||
- [ ] 脚本可按全量或按 `source_key` 手动触发同步
|
||
|
||
---
|
||
|
||
## 14. 测试要求
|
||
|
||
后端至少覆盖:
|
||
|
||
- YAML schema 校验
|
||
- 新建通知同步
|
||
- 已有通知更新同步
|
||
- 撤销同步
|
||
- 显式软删除同步
|
||
- 相同 `source_key` 幂等 upsert
|
||
- 更新主通知时不重置 `user_notifications.is_read/read_at`
|
||
- 新增目标用户时补插入接收关系
|
||
- 被移出目标集合时不删除既有接收关系
|
||
- `--reconcile-targets` 下删除多余接收关系
|
||
- `--prune` 下软删除缺失静态通知
|
||
|
||
脚本至少验证:
|
||
|
||
- 正常执行 CLI
|
||
- `--dry-run` 不写库
|
||
- `--source-key` 只同步指定通知
|
||
|
||
---
|
||
|
||
## 15. 后续扩展条件
|
||
|
||
只有在真实需求出现时,再考虑:
|
||
|
||
- 用删除文件触发软删除
|
||
- 通过后台页面管理静态通知
|
||
- 将静态通知同步纳入更完整的发布工作流
|