YouYiDianNotificationCenter/docs/统一通知中心接入交接说明.md
2026-08-29 20:51:03 +08:00

536 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 由壹点统一通知中心接入操作手册
> 交付对象:第一次接入“由壹点”统一钉钉通知、不了解通知中心内部实现的业务项目负责人、后端开发和测试人员。
>
> 目标:按照本文从申请开通到生产验收逐步执行,不需要复制通知中心项目,也不需要持有钉钉应用凭证。
## 先看这里:你现在能不能开始接入
业务项目真正调用通知中心前,必须已经从通知中心维护人处取得以下三项配置:
```dotenv
YYD_NOTIFICATION_CENTER_URL=https://sc.jiansun.vip/notification-center
YYD_NOTIFICATION_PROJECT_CODE=通知中心正式分配的项目代码
YYD_NOTIFICATION_PROJECT_SECRET=通知中心正式生成的项目独立密钥
```
按下面的情况继续:
| 你现在的情况 | 下一步 |
| --- | --- |
| 三项配置都没有 | 只把项目名称告诉通知中心维护人,系统会自动生成项目代码和密钥 |
| 只有通知中心地址 | 仍未开通,等待通知中心维护人登记项目并发放代码和密钥 |
| 已有地址、项目代码和项目密钥 | 从本文第七节开始配置业务后端 |
| 业务系统只有员工花名 | 发送时直接提交准确花名,通知中心会查询 `gongxiangp2` 花名册 |
| 还不知道通知发给谁 | 发送前准备准确花名或钉钉 `userId` |
| 只想给一个人发送联调消息 | 发送时填写该员工的准确花名或钉钉 `userId`;这不是接入配置 |
请勿把“海棠”或“海棠的钉钉 `userId`”之类的收件人信息,与上述三项接入配置混为一谈。前三项用于证明“哪个项目在调用”;`recipientAlias` 或 `recipientUserId` 用于说明“这一次通知发给谁”。
## 一、通知中心是什么
统一通知中心是已经独立部署的后端服务。业务项目通过 HTTPS 接口提交通知,通知中心再使用统一的“由壹点”钉钉机器人向员工发送单聊。
```text
业务项目后端
→ 由壹点统一通知中心
→ 钉钉机器人
→ 收件人员工
```
业务项目需要做的事情:
- 申请自己的项目身份和项目密钥;
- 在自己的后端增加一个很小的通知客户端;
- 在业务事件发生时提供收件人、标题、正文和跳转地址;
- 保存通知中心返回的投递 ID,必要时查询投递状态。
业务项目不需要做的事情:
- 不复制、合并或部署通知中心整个仓库;
- 不连接通知中心数据库;
- 不申请或保存钉钉 `AppKey`、`AppSecret`、`RobotCode`;
- 不直接调用钉钉机器人接口;
- 不把项目密钥放到前端。
## 二、双方分别负责什么
| 角色 | 必须完成的工作 |
| --- | --- |
| 业务项目负责人 | 提供项目名称 |
| 业务项目开发 | 在业务后端保存三项配置,发送收件人、标题、正文和可选跳转信息 |
| 通知中心维护人 | 创建项目,安全交付通知中心地址、项目代码和项目密钥 |
| 测试或验收人员 | 使用真实员工验证发送、防重、状态和按钮跳转,不接触项目密钥 |
## 三、开始开发前只需确认发送内容
1. 哪个业务事件触发通知,例如“审批单创建”或“任务即将到期”。
2. 通知发给谁:准确花名或钉钉 `userId` 二选一。
3. 标题和正文写什么。
4. 是否需要按钮;需要时提交按钮文字和跳转地址。
5. 为同一业务事件生成稳定的幂等键,避免重复通知。
花名无需提前登记到项目。通知中心会在发送时查询 `gongxiangp2` 已有花名册;找不到对应钉钉 `userId` 时不会推送,并返回明确错误。
## 四、正确的接入顺序
必须按顺序执行,不要跳过项目开通直接编写生产调用:
```text
第 1 步:业务项目提供项目名称
↓
第 2 步:通知中心自动生成项目代码和项目密钥
↓
第 3 步:维护人安全交付 URL、PROJECT_CODE、PROJECT_SECRET
↓
第 4 步:业务后端配置三项环境变量并接入客户端
↓
第 5 步:使用一名真实员工完成首次发送和状态查询
```
业务项目不能自行宣布开通完成。没有收到正式项目代码和项目密钥时,说明通知中心还没有为该项目建立调用身份。
## 五、开通项目
业务项目只需发送一行信息:
```text
项目名称:产品开发平台
```
通知中心管理面板会自动生成项目代码和项目密钥。项目代码如有特殊需要,可以在高级设置中自定义;普通接入无需填写。
通知中心维护人完成登记后,应通过安全渠道分别交付:
```dotenv
YYD_NOTIFICATION_CENTER_URL=https://sc.jiansun.vip/notification-center
YYD_NOTIFICATION_PROJECT_CODE=项目代码
YYD_NOTIFICATION_PROJECT_SECRET=项目独有密钥
```
收到三项配置后的检查:
- URL 应为 `https://sc.jiansun.vip/notification-center`,末尾不要自行增加接口路径;
- 项目代码必须与通知中心维护人登记的值完全一致,区分下划线和短横线;
- 项目密钥应通过一对一安全渠道或密钥管理服务交付;
- 不要把密钥转发到群聊、工单、截图、共享文档或 Git;
- 没收到项目密钥时不要从其他项目复制,也不要使用文档示例值。
如果通知中心维护人还没有完成项目登记,应停在这里等待开通。业务项目可以先准备代码结构,但不能完成真实签名联调。
## 六、分清接入配置和每次发送的数据
### 6.1 三项静态接入配置
以下三项通常在一个环境中配置一次,由通知中心维护人交付:
| 配置 | 作用 | 能否多个项目共用 |
| --- | --- | --- |
| `YYD_NOTIFICATION_CENTER_URL` | 通知中心公共访问地址 | 地址可以共用 |
| `YYD_NOTIFICATION_PROJECT_CODE` | 标识当前调用来自哪个业务项目 | 不能冒用其他项目 |
| `YYD_NOTIFICATION_PROJECT_SECRET` | 为每次请求生成签名 | 绝对不能共用 |
### 6.2 每次发送通知都要提供的业务数据
以下内容不是接入密钥,而是每条通知的具体内容:
| 数据 | 从哪里取得 | 示例 |
| --- | --- | --- |
| `recipientUserId` | 业务项目的员工账号绑定或权威通讯录映射 | 海棠对应的钉钉 `userId` |
| `recipientAlias` | 通知中心会通过素材平台内部目录解析的准确花名 | `海棠` |
| `idempotencyKey` | 业务项目根据事件、记录、收件人和版本生成 | `task_due:53:user-id:v1` |
| `title` | 当前业务事件 | `产品任务即将到期` |
| `content` | 当前业务记录 | 截止时间、任务名称、负责人 |
| `actionUrl` | 当前业务记录页面;可为空,非空时原样透传 | `http://10.0.0.8:8080/tasks/53` |
### 6.3 两种收件人方式怎么选择
钉钉 `userId` 是员工在当前企业通讯录中的稳定标识,不是姓名,也不是手机号、邮箱、工号或业务数据库主键。
能够取得 `userId` 的项目,按以下优先顺序取得:
1. 已有员工账号绑定:读取当前收件人账号保存的 `dingtalkUserId` 或等价字段。
2. 已有企业通讯录同步:从项目已经同步的钉钉员工记录中读取 `userId`。
3. 仅做首次联调:由负责人确认一名真实员工,并通过公司现有的权威人员映射取得该员工 `userId`。
4. 完全没有员工与钉钉身份对应关系:改用准确花名,或者先补齐账号绑定或通讯录同步。
如果项目只能提供花名,使用 `recipientAlias`:
- 花名由通知中心调用素材平台内部目录精确解析到钉钉 `userId`;
- 不做模糊猜测,不按姓名、手机号、邮箱或业务 ID 猜人;
- 找不到对应 `userId` 时返回 `RECIPIENT_ALIAS_NOT_FOUND`,不会发送通知;
- 目录暂时不可用时返回 `503 RECIPIENT_ALIAS_DIRECTORY_UNAVAILABLE`,同样不会入队;
- 同一请求只能传 `recipientUserId` 或 `recipientAlias` 其中一个。
如果正式业务永远只通知固定负责人,可以把其花名或 `userId` 保存在业务项目后端;如果收件人随业务记录变化,就从业务记录读取对应花名或 `userId`,不能把联调人员写死在代码里。
通知中心只做精确花名解析,不会根据任意姓名模糊猜人,也不会把某个测试人员设置成所有项目的默认收件人。
## 七、在业务项目中保存配置
三项配置只能保存在业务后端的环境变量或正式密钥管理服务中:
```dotenv
YYD_NOTIFICATION_CENTER_URL=https://sc.jiansun.vip/notification-center
YYD_NOTIFICATION_PROJECT_CODE=your_project_code
YYD_NOTIFICATION_PROJECT_SECRET=通过安全渠道取得的项目密钥
```
必须同时满足:
- `.env` 已被 Git 忽略,仓库中只提交不含真实值的 `.env.example`。
- 配置只注入后端进程,不能发送给浏览器或移动端。
- 不使用 `NEXT_PUBLIC_`、`VITE_` 等会公开到前端的变量名。
- 日志、异常上报和调试页面不输出密钥、签名或完整请求头。
- 测试和生产建议使用不同的项目身份和密钥;禁止多个项目共用一个密钥。
配置完成后,后端启动时应检查三项值都存在;缺少任意一项就明确报错并停止通知调用,不要静默跳过或改用其他项目的配置。
## 八、业务后端接入
### 8.1 Node.js 项目推荐直接复用现有客户端
Node.js 18 及以上项目可以复制:
- [`examples/node/notification-client.mjs`](../examples/node/notification-client.mjs)
- [`examples/node/notification-client.test.mjs`](../examples/node/notification-client.test.mjs)
初始化客户端:
```js
import { YouYiDianNotificationClient } from './notification-client.mjs'
const notificationClient = new YouYiDianNotificationClient({
baseUrl: process.env.YYD_NOTIFICATION_CENTER_URL,
projectCode: process.env.YYD_NOTIFICATION_PROJECT_CODE,
projectSecret: process.env.YYD_NOTIFICATION_PROJECT_SECRET,
})
```
使用钉钉 `userId` 发送一条通知:
```js
const recipientUserId = employee.dingtalkUserId
if (!recipientUserId) throw new Error('该员工尚未绑定钉钉 userId,不能发送通知')
const delivery = await notificationClient.send({
idempotencyKey: `asset_disposal:${disposal.id}:${recipientUserId}:v1`,
recipientUserId,
title: '资产报废待审批',
content: [
`**申请时间:** ${appliedAt}`,
'',
`**资产:** ${assetName}`,
'',
`**申请人:** ${applicantName}`,
'',
'> 请核对资产及配件明细后处理。',
].join('\n'),
actionTitle: '立即审批',
actionUrl: `https://asset.example.com/workflows/disposals/${disposal.id}`,
})
// 保存到业务记录,后续可查询实际投递状态。
await saveNotificationDeliveryId(disposal.id, recipientUserId, delivery.id)
```
业务项目只能提供花名时,改传准确 `recipientAlias`,不要同时传 `recipientUserId`:
```js
const recipientAlias = task.ownerAlias
const delivery = await notificationClient.send({
idempotencyKey: `task_due:${task.id}:${recipientAlias}:v1`,
recipientAlias,
title: '产品任务即将到期',
content: `**截止时间:** ${deadline}\n\n**任务:** ${task.title}`,
actionTitle: '查看任务',
actionUrl: `https://product.example.com/tasks/${task.id}`,
})
```
查询投递状态:
```js
const latest = await notificationClient.getDelivery(deliveryId)
console.log(latest.status, latest.lastErrorCode)
```
在通知中心仓库内,可以运行以下命令验证示例客户端的签名和请求格式:
```bash
npm run test:example
```
### 8.2 其他开发语言按统一协议实现
发送接口:
```text
POST /v1/notifications
```
每次请求必须包含以下请求头:
```text
x-yyd-project-code
x-yyd-timestamp
x-yyd-nonce
x-yyd-signature
```
- `x-yyd-timestamp`:当前 Unix 秒,业务服务器与通知中心的时间差不能超过五分钟。
- `x-yyd-nonce`:每次 HTTP 请求重新生成的 16~100 位随机字符串。
- `x-yyd-signature`:使用项目密钥计算的 HMAC-SHA256 小写十六进制字符串。
签名原文:
```text
timestamp\nnonce\nHTTP_METHOD\nPATH\nSHA256(rawBody)
```
发送通知时的示意算法:
```text
rawBody = JSON 序列化后的实际请求正文
bodyHash = SHA256(rawBody)
canonical = timestamp + "\n" + nonce + "\nPOST\n/v1/notifications\n" + bodyHash
signature = HMAC_SHA256_HEX(projectSecret, canonical)
```
必须注意:
- 计算签名的 `rawBody` 必须与网络中真正发送的正文逐字节一致。
- `PATH` 使用内部接口路径 `/v1/notifications`,不包含域名、查询参数和公网反向代理前缀 `/notification-center`。
- GET 查询请求的正文是空字符串,仍需对空正文的 SHA-256 值签名。
- 每次重试使用新的 timestamp 和 nonce,但业务幂等键保持不变。
## 九、发送内容和字段规则
请求正文:
```json
{
"idempotencyKey": "asset_disposal:53:ding-user-id:v1",
"recipientUserId": "员工的钉钉userId",
"title": "资产报废待审批",
"content": "**申请时间:** 2026年8月5日 14:30\n\n**资产:** 摄影灯 A-102\n\n**申请人:** 张三",
"actionTitle": "立即审批",
"actionUrl": "https://asset.example.com/workflows/disposals/53"
}
```
花名方式只替换收件人字段:
```json
{
"idempotencyKey": "asset_disposal:53:海棠:v1",
"recipientAlias": "海棠",
"title": "资产报废待审批",
"content": "**申请时间:** 2026年8月5日 14:30\n\n**资产:** 摄影灯 A-102",
"actionTitle": "立即审批",
"actionUrl": "https://asset.example.com/workflows/disposals/53"
}
```
| 字段 | 必填 | 规则 |
| --- | --- | --- |
| `idempotencyKey` | 是 | 最长 160 字符;同一业务事件和同一收件人必须稳定不变 |
| `recipientUserId` | 二选一 | 企业通讯录中的钉钉 `userId`;与 `recipientAlias` 只能传一个 |
| `recipientAlias` | 二选一 | 通知中心会通过素材平台内部目录精确解析的准确花名,最长 50 字符;与 `recipientUserId` 只能传一个 |
| `title` | 是 | 只写业务主题,最长 100 字符;不要自己添加项目名或“由壹点”前缀 |
| `content` | 是 | Markdown 或纯文本业务正文;通知中心不会再自动插入标题或固定说明 |
| `actionTitle` | 否 | 最长 20 字符,建议写明确动作,例如“立即审批” |
| `actionUrl` | 否 | 可为空;非空时按原样透传,支持 HTTP、HTTPS、内网 IP/域名和端口 |
通知中心会根据项目配置统一生成:
```text
【项目名称】业务标题
```
例如业务项目提交“资产报废待审批”,用户最终看到的是“【固定资产】资产报废待审批”。完整文案规则见[消息规范](./消息规范.md)。
## 十、幂等防重和多人通知
推荐的幂等键格式:
```text
{事件类型}:{业务记录ID}:{recipientUserId或recipientAlias}:{消息版本}
```
示例:
```text
asset_disposal_created:53:16408293139985728:v1
```
规则:
- 同一业务事件重复执行时,必须复用相同幂等键。
- 相同 `projectCode + idempotencyKey` 且内容相同,只返回原投递记录,不会重复通知。
- 相同幂等键但内容不同会返回 `409 IDEMPOTENCY_CONFLICT`,必须修复业务逻辑,不能随机换键绕过。
- 确实需要发送一条内容更新后的新通知时,经过业务确认后提升消息版本,例如从 `v1` 改为 `v2`。
当前接口一次发送给一名员工。向多人通知时逐人调用,并把每个人的钉钉 `userId` 或准确花名放入幂等键:
```js
for (const recipient of recipients) {
await notificationClient.send({
...message,
recipientUserId: recipient.dingtalkUserId,
idempotencyKey: `survey_created:${survey.id}:${recipient.dingtalkUserId}:v1`,
})
}
```
批量发送时应设置合理的并发上限,记录每名收件人的结果;不能因为其中一人失败就反复重发已经受理的其他通知。
## 十一、正确理解返回值和投递状态
创建接口常见返回:
| HTTP 状态 | 含义 | 业务项目处理 |
| --- | --- | --- |
| `202` | 新通知已进入队列 | 保存返回的投递 `id`;不能当作已经送达 |
| `200` | 相同幂等请求已经存在 | 按成功受理处理,继续使用返回的原投递 `id` |
| `401` | 项目、时间或签名错误 | 停止自动重试,检查配置、服务器时间和签名实现 |
| `409` | nonce 重复或幂等内容冲突 | nonce 重复时换新 nonce;内容冲突必须修复业务逻辑 |
| `422` | 字段、长度或跳转地址不合法 | 修正请求内容后再提交 |
| `500` | 通知中心内部异常 | 保留原幂等键,短暂退避后有限重试并告警 |
查询接口:
```text
GET /v1/notifications/{deliveryId}
```
投递状态:
| 状态 | 含义 | 业务项目应该做什么 |
| --- | --- | --- |
| `pending` | 已受理,等待发送 | 等待,不要重新创建通知 |
| `sending` | 正在发送 | 等待,不要重新创建通知 |
| `retrying` | 通知中心正在有限重试 | 等待,由通知中心处理 |
| `sent` | 钉钉已接受发送请求 | 可继续查询已读状态 |
| `unread` | 已投递但尚未阅读 | 按业务需要展示或记录 |
| `read` | 收件人已经阅读 | 终态 |
| `failed` | 已确定失败,或结果不确定而停止自动重试 | 记录错误并通知负责人处理 |
完整错误码和重试边界见[错误与状态规范](./错误与状态规范.md)。
## 十二、业务端重试和日志要求
重试原则:
1. 网络连接失败或 HTTP `500` 时可以短暂退避并有限重试。
2. 每次 HTTP 重试生成新的 timestamp 和 nonce。
3. 所有重试必须复用原 `idempotencyKey`。
4. 收到 HTTP `200 / 202` 后不再创建新通知,改为查询原投递 ID。
5. HTTP `401 / 409 / 422` 默认不自动重试,先修正配置或业务数据。
6. `DELIVERY_OUTCOME_UNKNOWN` 必须先到钉钉侧人工核验,再决定是否补发,避免员工收到重复通知。
业务日志建议记录:
- 业务事件 ID;
- `idempotencyKey`;
- 通知中心返回的投递 `id`;
- 收件人钉钉 `userId`;
- HTTP 状态、投递状态和 `lastErrorCode`;
- 请求耗时和发生时间。
禁止记录:
- 项目密钥;
- `x-yyd-signature`;
- 钉钉应用密钥和机器人凭证;
- 免登码和一次性登录票据;
- 消息中不必要的个人敏感信息。
## 十三、钉钉 H5 免登是可选项
普通通知只需要“发送 + 查询”接口,不需要接入免登。
只有用户点击卡片后必须自动进入业务登录态时,项目负责人才能申请开启免登。开启后还需要:
1. 向通知中心登记精确的 HTTPS 回调地址和允许回跳的站内路径前缀。
2. 业务后端接收一次性登录票据。
3. 使用本项目的 HMAC 身份调用 `/v1/auth/tickets/redeem` 兑换钉钉 `userId`。
4. 在创建本地 Session 前,重新核验员工绑定、在职状态和业务权限。
5. 验证票据过期、重复使用和跨项目使用都会失败。
通知中心票据只能证明钉钉身份,不能替代业务项目自己的权限判断。免登的完整协议见[接入指南](./接入指南.md#8-钉钉-h5-免登是可选能力)。
## 十四、联调与上线验收
### 14.1 开发自测
- [ ] 三项配置只存在于后端,仓库和前端产物中没有真实密钥。
- [ ] 服务端时间已同步,请求签名路径不包含 `/notification-center` 反向代理前缀。
- [ ] 使用本人真实钉钉 `userId` 能创建通知并取得投递 `id`。
- [ ] 如使用花名,准确花名能解析并发送,找不到对应 `userId` 时会被明确拒绝。
- [ ] 能使用投递 `id` 查询状态。
- [ ] 通知标题自动包含正确项目名称,没有重复前缀或双标题。
- [ ] 按钮进入正确业务记录;如启用免登且目标满足现有 HTTPS 登录流程,会自动包装到统一入口。
- [ ] 重复执行同一业务事件不会产生第二条通知。
- [ ] 无效钉钉 `userId` 最终进入明确失败状态。
### 14.2 生产验收
生产上线不能只验证健康接口,至少完成:
- [ ] 项目负责人收到一条真实业务通知。
- [ ] 另一名真实员工收到一条真实业务通知。
- [ ] 两人看到的项目名称、业务内容和按钮都正确。
- [ ] 重复提交同一请求没有重复消息。
- [ ] 能从业务日志关联“业务事件 → 幂等键 → 投递 ID → 最终状态”。
- [ ] 日志、错误页、浏览器请求和监控平台均未出现项目密钥。
- [ ] 已明确通知失败由谁处理,以及如何联系通知中心维护人。
- [ ] 如启用免登,已额外验证员工身份、权限、票据过期和一次性消费。
验收完成后,双方保存以下记录:
```text
项目名称:
projectCode:
业务项目版本或 Git 提交:
通知中心版本或 Git 提交:
生产验证时间:
验证通知的业务事件 ID:
验证通知的投递 ID:
发送结果:
点击跳转结果:
通知中心维护人:
遗留问题:
```
## 十五、常见接入错误
| 错误做法 | 正确做法 |
| --- | --- |
| 前端直接调用通知中心 | 只允许业务后端调用 |
| 从其他项目复制项目密钥 | 每个项目由通知中心单独生成密钥 |
| 把任意姓名直接当花名发送 | 只使用素材平台内部目录能够精确解析的准确花名 |
| 同时发送 `recipientUserId` 和 `recipientAlias` | 两种收件人方式只能选择一种 |
| 标题自己添加“【项目名】” | 只提交业务标题,由通知中心统一拼接 |
| 签名路径包含公网 `/notification-center` 前缀 | 签名内部路径 `/v1/notifications` |
| 先序列化签名,发送时再次序列化成不同正文 | 对实际发送的同一份 raw body 签名 |
| 每次重试随机生成新的幂等键 | 重试沿用原幂等键,只更换 nonce |
| 把 HTTP `202` 当成已送达 | 保存投递 ID 并查询最终状态 |
| 多人共用一个幂等键 | 每名收件人使用独立幂等键 |
| 收到超时就无限重发 | 有限重试;结果不确定时先人工核验 |
| 只测试自己,不测试第二名员工 | 生产至少完成双人真实验收 |
## 十六、完成标准
只有同时满足以下条件,才算正式接入完成:
1. 项目已经在通知中心创建,并使用自己的项目代码和项目密钥。
2. 通知由业务后端发出,前端和 Git 仓库中没有任何密钥。
3. 业务项目保存投递 ID,正确处理幂等、状态查询和有限重试。
4. 通知内容符合[消息规范](./消息规范.md)。
5. 已完成一次真实生产通知、跳转和防重验收。
6. 已记录上线版本、验证证据和故障处理方式。
更细的逐项门禁可继续使用[新项目接入清单](./新项目接入清单.md)。