概念边界与分层审计
状态:现行裁决(2026-10-07,依据外部架构评审逐条核处)。本文回答一个问题:受众层的每个词管什么、谁不能入侵谁。术语的含义定义在总纲 §2(唯一口径);本文是边界裁决——重叠排查的结论、分层地图、以及后续批次落地前必须对照的红线。两文冲突时以总纲 §2 为准,本文跟着改。
1. 定位口径
Herald 是轻量的通知编排与投递基础设施(lightweight notification orchestration and delivery infrastructure)。
「编排」二字是本次评审钉进定位的:Herald 从来不只是 send()——它决定谁、什么时候、通过什么渠道、以什么强度、是否聚合、是否过滤、是否升级、是否去重。投递管道(Phase 0-9 已落地)是地基;受众层(统一订阅与投递中枢)把「被通知者做主」立起来;编排是这两者之上的行为描述。README、总纲 §1、架构概览三处定位句已同步。
2. 五层地图
系统的分层边界,后续批次的功能必须先回答「落在哪一层」再动手:
┌────────────────────────────────────────────────────────────────┐
│ 集成层 API(/notify /audience /feeds)· Go SDK · webhook 回调 │
│ 来源适配器(bot / 公众号 / 应用内勾选) │
├────────────────────────────────────────────────────────────────┤
│ 受众与偏好层 Registry · SurfaceRegistry · PreferenceRegistry │
│ Relation(订阅/指派)· Filter/矩阵 · 审计流水 │
├────────────────────────────────────────────────────────────────┤
│ 编排层 规则引擎 · 路由展开 · 去重/频控 · Digest 聚合 │
│ 紧急度×强度匹配 · 投递模式执行(升级链/单渠道/并行) │
├────────────────────────────────────────────────────────────────┤
│ 投递运行时层 Planner→Task · Queue · Worker · Retry · ack · 状态机│
├────────────────────────────────────────────────────────────────┤
│ Provider 层 telegram / email / webhook / wechatmp / …(可换实现)│
└────────────────────────────────────────────────────────────────┘代码落位(双栖件注明主层):
| 层 | 包/模块 |
|---|---|
| 集成层 | api/(HTTP 面)、worker-sdk/go/、来源适配器与 webhook 回调(批次 8/11) |
| 受众与偏好层 | core/audience/(注册表/联系面/偏好/过滤)、core/audit/ |
| 编排层 | core/rules/、core/route/(路由表)、core/dedup/、core/digest/、core/escalation/;强度×紧急度匹配与三模式执行器(批次 9) |
| 投递运行时层 | core/service/(planner/pool——编排决策在此落成任务)、core/queue/、worker、core/retry/、core/ack/ |
| Provider 层 | providers/builtin/、core/errclass/(错误词汇属 provider 契约) |
3. 八词裁决:无重叠
评审点名排查的概念——受众 / 渠道 / 联系面 / provider / 偏好 / 投递模式 / 紧急度 / 侵扰度——逐词核处如下,结论:各自回答不同的问题,不合并、不换名。
| 词 | 归属侧 | 回答的问题 | 边界红线 |
|---|---|---|---|
受众 audience | 身份 | 「通知谁」 | 全系统唯一 ID;不存渠道凭据;不再派生同位身份实体(§5) |
渠道 channel | 触达手段 | 「走哪类通道」 | 带侵扰度、实时性、投递方向(§4)三个属性;渠道≠provider |
联系面 ContactSurface | 凭据实例 | 「这个受众在这条渠道上的具体地址」 | 渠道的运行时绑定态;静态配置里的 recipients/endpoints 只是它的种子 |
| provider | 投递实现 | 「用哪个实现投出去」 | 渠道经渠道块展开为 provider 实例;同一渠道可换实现(email→smtp 或 resend);强度阶梯按渠道类标定,provider 继承所在级 |
偏好 Preference | 用户选择 | 「受众自己要什么」 | 只能收窄不能放宽(静默底线、频控同理);存在订阅关系上 |
紧急度 Urgency | 消息侧 | 「这事有多紧急」 | 决定允许的强度区间(详设 §6.2);品类默认映射,单事件可覆盖 |
侵扰度 Intensity | 渠道侧 | 「这条渠道有多打扰」 | L0–L5 阶梯;与紧急度被 §6.2 矩阵连接——一个在消息上、一个在渠道上,永远不碰头 |
| 投递模式 | 编排侧 | 「怎么投:升级链/固定单渠道/并行」 | 策略件同一入口不同执行器(批次 9);与规则引擎升级正交(规则升级=叫谁,模式升级=怎么叫得更响) |
评审担心的「Intensity=high / Urgency=high 分不清」在本仓不成立,因为两者不共侧:紧急度是消息的属性,侵扰度是渠道的属性,二者只在匹配矩阵(详设 §6.2)相遇。评审建议的「Intensity=投递策略多强」这一轴已存在,名字叫投递模式(escalation/fixed/parallel)——不再造第三个「强度」词。
4. 投递方向:Push / Pull
评审指出 RSS 改变了投递模型(推 vs 拉),正确。裁决:
- 新增投递方向(push / pull)作为渠道属性:推送渠道由 Herald 主动投(telegram/email/短信/电话/站内信),拉式渠道由阅读者轮询(RSS)。
- RSS 是拉式渠道,不是一种 provider:RSS feed 是投递记录的拉式投影(详设 §9),走同一套「关系允许×联系面绑定」校验。
- 命名避让:不采用「Push/Pull Delivery Mode」的叫法——「投递模式」一词已被升级链/固定单渠道/并行三选占用(总纲 §2 术语表),一个词不能有两个含义。方向轴落名「投递方向」,写进渠道属性。
批次 7(RSS)已按此口径落地:core/feeds 存储拉式投影,/feeds/** 属集成层的拉式出口,rss 类渠道在投递管道就地投影、不产生 provider 任务,可见性校验复用受众层(token 归属经 SurfaceRegistry、品类准入经关系注册表+矩阵复核),不新增投递状态机分支;多地址容灾归阅读器侧,不代码化。
5. 幂等 / 去重 / 频控 / 聚合:四件事四道闸
评审的四分法与本仓现状对齐(详设 §10/§11),措辞收紧如下,四个概念永不混叫:
| 概念 | 解决什么 | 闸门位置 | 现状 |
|---|---|---|---|
幂等 idempotency_key/event_id | 同一事件的重复提交(重试风暴、上游重发) | 管道最前 | 已落地(Phase 0) |
| 折叠/状态机去重 | 等价内容的重复投递(×N 计数、状态翻转才发) | 编排层,投递之前、升级链之前 | 折叠已落地;状态机批次 10 |
| 频控 once/throttle/always | 时间窗内的频率上限 | 去重之后 | 批次 10 |
| 聚合 Digest | 多条不同消息合一条摘要(受众×品类×时间窗) | 独立旁路,摘要走全投递链 | 已落地(批次 6) |
先后关系:幂等 → 折叠/状态 → 频控 → (旁路)聚合 → 投递。审计对每道闸都有独立事件词汇(delivery.deduped 等),回答「为什么这条没投」。
6. 评审七点逐条回执
| # | 评审意见 | 处置 |
|---|---|---|
| ① | 定位升级为「通知编排与投递」 | 已改:总纲 §1、README、架构概览三处定位句加「编排」(§1) |
| ② | Surface 立为核心概念 | 已是现状:联系面链 受众→接收人→联系面→渠道→provider(详设 §2 关键约束);本次补显式句「渠道≠provider,可换实现」进总纲 §2 |
| ③ | Push/Pull 立为投递模型轴 | 采纳并改名:落为渠道属性「投递方向」,避让「投递模式」一词(§4) |
| ④ | Urgency/Intensity 重新定义或合并 | 核实为已分层:消息侧 vs 渠道侧,§6.2 矩阵连接;「策略强度」轴已由投递模式承担(§3) |
| ⑤ | 幂等/去重/频控/聚合四分 | 已四分,措辞收紧(§5) |
| ⑥ | Redis 不做 Digest 硬依赖 | 批次 6 落地即合规:digest.LeaderLock 构造注入,配置无 redis_addr 时为 nil、单机进程内定时器直跑;多实例才配租约。锁实现可换(接口只依赖 SET NX/续约/释放语义) |
| ⑦ | 停止新增受众子概念 | 立为红线(§7) |
7. 红线:受众层实体封顶
受众层实体清单封顶为七个:受众、接收人、端点、联系面、关系(订阅/指派)、偏好、聚合规则。评审点名的 Subscriber / Member / Contact / Identity / Profile / Target / Destination 等同位实体不再新增——受众是身份,订阅者/接收者是关系角色,这个分层(详设 §1)已经覆盖上述全部词义。新需求一律落为既有实体的属性或关系;确需第八个实体时,先改总纲 §2 术语契约并过一遍本文 §3 的八词排查,再动代码。
8. 批次回执与后续纪律
评审对批次 1-11 的分级判断与落地状态对照(落地详情见总纲 §13):
| 批次 | 评审判断 | 本仓处置 |
|---|---|---|
| 1 关系模型 | 非常合理 | ✅ 已落地 |
| 2 联系面与绑定 | 合理,核心能力 | ✅ 已落地 |
| 3 偏好中心 | 非常合理 | ✅ 已落地 |
| 4 关系过滤 | 非常重要 | ✅ 已落地 |
| 5 审计 | 应保留 | ✅ 已落地 |
| 6 Digest | 合理但后置 | ✅ 已落地(redis 可选合规);评审的「后置」指向当时未落地,不追溯 |
| 7 RSS | 应定义为拉式渠道 | ✅ 已按 §4 口径落地(core/feeds + /feeds/**,拉式投影不产生 provider 任务) |
| 8 来源适配器 | 合理 | ✅ 已按「订阅入口适配器」口径落地(SourceAdapter 动作归一化 + bot/公众号/应用内三端点 + Reconciler 定期对账;详设 §8 落地段) |
| 9 强度与模式 | 有价值,需钉边界 | ◑ 按 §3 八词裁决落地——Urgency/Intensity/投递模式三轴不合并 |
| 10 去重频控 | 必须做 | ◑ 按 §5 四分口径落地 |
| 11 集成者 API/SDK | 非常合理 | ◑ token 权限按评审意见最小化起步:notify/query 先行,config 类权限收紧到管理面评审后再开 |
后续纪律:批次 7-11 每批动手前,先对照 §2 五层地图回答「落在哪层」、对照 §3/§5 回答「用哪个既有词」;与本文冲突的实现不进门禁。