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

12 KiB
Raw Blame History

静态通知配置同步计划

更新时间: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
  • 现有 CLIbackend/src/core/runtime/cli.py
  • 现有脚本:infra/scripts/dev-migrate.sh

通知同步应复用这套模式的核心思路:

  • YAML 文件作为配置源
  • Pydantic schema 做强校验
  • 后端显式执行同步
  • 数据库使用 upsert 语义更新

但通知同步不应直接并入 init-data/bootstrap 默认流程,因为通知内容属于持续变更的数据,不是纯启动种子数据。


4. 目录设计

建议新增静态通知目录:

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 表基础上补充:

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 文件描述一条主通知及其投递目标。

推荐结构:

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

指定用户示例:

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_usersuser_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 模块位置

建议新增:

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

建议调用方式:

PYTHONPATH=backend/src uv run python -m core.runtime.cli sync-notifications

建议参数:

  • --path
  • --source-key
  • --dry-run
  • --prune
  • --reconcile-targets

危险行为必须显式开启,不默认启用。

10.2 infra 脚本

新增:

infra/scripts/register-notifications.sh

脚本风格复用 infra/scripts/dev-migrate.sh

  • 读取 .env
  • 通过 uv run python -m core.runtime.cli sync-notifications 调用后端 CLI

建议用法:

./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. 后续扩展条件

只有在真实需求出现时,再考虑:

  • 用删除文件触发软删除
  • 通过后台页面管理静态通知
  • 将静态通知同步纳入更完整的发布工作流