485 lines
11 KiB
Markdown
485 lines
11 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:
|
|||
|
|
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. 后续扩展条件
|
|||
|
|
|
|||
|
|
只有在真实需求出现时,再考虑:
|
|||
|
|
|
|||
|
|
- 用删除文件触发软删除
|
|||
|
|
- 严格对齐目标用户集合并清理历史接收关系
|
|||
|
|
- 通过后台页面管理静态通知
|
|||
|
|
- 将静态通知同步纳入更完整的发布工作流
|