# 静态通知配置同步计划 > 更新时间: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. 后续扩展条件 只有在真实需求出现时,再考虑: - 用删除文件触发软删除 - 通过后台页面管理静态通知 - 将静态通知同步纳入更完整的发布工作流