diff --git a/README.md b/README.md index 229e33f..292f547 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ 新项目不要从本 README 中零散复制接口代码,请按以下规范接入: +- [统一通知中心接入操作手册](docs/统一通知中心接入交接说明.md):可直接交给其他项目负责人的完整接入流程、代码入口与验收标准。 +- [通知中心新项目开通操作手册](docs/通知中心新项目开通操作手册.md):仅供通知中心维护人登记项目、生成密钥、安全交付和验收使用。 - [接入指南](docs/接入指南.md):项目登记、环境变量、签名、发送、多人通知和上线验证。 - [消息规范](docs/消息规范.md):标题、正文、状态和按钮的统一写法。 - [错误与状态规范](docs/错误与状态规范.md):HTTP 返回、投递状态、错误码和重试边界。 diff --git a/docs/统一通知中心接入交接说明.md b/docs/统一通知中心接入交接说明.md new file mode 100644 index 0000000..b62ff27 --- /dev/null +++ b/docs/统一通知中心接入交接说明.md @@ -0,0 +1,552 @@ +# 由壹点统一通知中心接入操作手册 + +> 交付对象:第一次接入“由壹点”统一钉钉通知、不了解通知中心内部实现的业务项目负责人、后端开发和测试人员。 +> +> 目标:按照本文从申请开通到生产验收逐步执行,不需要复制通知中心项目,也不需要持有钉钉应用凭证。 + +## 先看这里:你现在能不能开始接入 + +业务项目真正调用通知中心前,必须已经从通知中心维护人处取得以下三项配置: + +```dotenv +YYD_NOTIFICATION_CENTER_URL=https://sc.jiansun.vip/notification-center +YYD_NOTIFICATION_PROJECT_CODE=通知中心正式分配的项目代码 +YYD_NOTIFICATION_PROJECT_SECRET=通知中心正式生成的项目独立密钥 +``` + +按下面的情况继续: + +| 你现在的情况 | 下一步 | +| --- | --- | +| 三项配置都没有 | 先填写本文第五节的接入申请,不要自己猜项目代码或生成密钥 | +| 只有通知中心地址 | 仍未开通,等待通知中心维护人登记项目并发放代码和密钥 | +| 已有地址、项目代码和项目密钥 | 从本文第七节开始配置业务后端 | +| 还不知道通知发给谁 | 先按本文第六节确认钉钉 `userId` 的数据来源 | +| 只想给一个人发送联调消息 | 准备该员工的真实钉钉 `userId`,它只用于本次测试,不是接入密钥 | + +请勿把“海棠的钉钉 `userId`”之类的收件人信息,与上述三项接入配置混为一谈。前三项用于证明“哪个项目在调用”;`recipientUserId` 用于说明“这一次通知发给谁”。 + +## 一、通知中心是什么 + +统一通知中心是已经独立部署的后端服务。业务项目通过 HTTPS 接口提交通知,通知中心再使用统一的“由壹点”钉钉机器人向员工发送单聊。 + +```text +业务项目后端 + → 由壹点统一通知中心 + → 钉钉机器人 + → 收件人员工 +``` + +业务项目需要做的事情: + +- 申请自己的项目身份和项目密钥; +- 在自己的后端增加一个很小的通知客户端; +- 在业务事件发生时提供收件人、标题、正文和跳转地址; +- 保存通知中心返回的投递 ID,必要时查询投递状态。 + +业务项目不需要做的事情: + +- 不复制、合并或部署通知中心整个仓库; +- 不连接通知中心数据库; +- 不申请或保存钉钉 `AppKey`、`AppSecret`、`RobotCode`; +- 不直接调用钉钉机器人接口; +- 不把项目密钥放到前端。 + +## 二、双方分别负责什么 + +| 角色 | 必须完成的工作 | +| --- | --- | +| 业务项目负责人 | 说明通知场景、收件人规则、跳转域名、预计发送量,指定开发和验收人员 | +| 业务项目开发 | 在业务后端保存配置,接入客户端,提供正确的收件人 `userId`,实现发送和必要的状态查询 | +| 通知中心维护人 | 审核申请、登记项目、生成独立密钥、配置跳转域名白名单、安全交付三项配置 | +| 测试或验收人员 | 使用真实员工验证发送、防重、状态和按钮跳转,不接触项目密钥 | + +## 三、开始开发前先确认五件事 + +1. 哪个业务事件触发通知,例如“审批单创建”或“任务即将到期”。 +2. 通知发给谁,业务系统从哪里取得该员工的钉钉 `userId`。 +3. 用户点击通知后进入哪个生产 HTTPS 域名。 +4. 同一事件是否可能重复执行,以及如何使用稳定的幂等键防止重复通知。 +5. 点击通知后是否必须自动建立业务登录态。普通通知默认不启用免登。 + +如果第 2 项没有可靠答案,请先解决员工账号与钉钉 `userId` 的对应关系。姓名、手机号、邮箱和业务系统内部 ID 都不能直接作为通知收件人。 + +## 四、正确的接入顺序 + +必须按顺序执行,不要跳过项目开通直接编写生产调用: + +```text +第 1 步:业务项目填写接入申请 + ↓ +第 2 步:通知中心维护人审核名称、代码和跳转域名 + ↓ +第 3 步:通知中心维护人登记项目并生成独立密钥 + ↓ +第 4 步:维护人安全交付 URL、PROJECT_CODE、PROJECT_SECRET + ↓ +第 5 步:业务后端配置三项环境变量并接入客户端 + ↓ +第 6 步:使用一名真实员工完成首次发送和状态查询 + ↓ +第 7 步:生产环境完成两名真实员工验收 +``` + +业务项目不能自行宣布开通完成。没有收到正式项目代码和项目密钥时,说明通知中心还没有为该项目建立调用身份。 + +## 五、填写并提交接入申请 + +复制下面的模板,填写后交给通知中心维护人。业务项目不需要也不能自己提供项目密钥。 + +```text +【统一通知中心接入申请】 + +项目中文名称: +建议 projectCode: +业务负责人及联系方式: +技术负责人及联系方式: + +通知场景: +触发条件: +收件人规则: +钉钉 userId 的数据来源: +预计每日通知量: +预计峰值通知量: + +生产跳转域名(只写域名,不写协议和路径): +测试跳转域名(如有): +是否需要点击后免登:否 / 是 + +计划联调日期: +计划上线日期: +``` + +不会填写时可参考以下完整示例: + +```text +【统一通知中心接入申请】 + +项目中文名称:产品开发平台 +建议 projectCode:product_development +业务负责人及联系方式:张三 / 钉钉 +技术负责人及联系方式:李四 / 钉钉 + +通知场景:产品任务即将到期 +触发条件:任务距截止时间不足 24 小时且仍未完成 +收件人规则:任务负责人 +钉钉 userId 的数据来源:员工账号绑定表 dingtalkUserId 字段 +预计每日通知量:30 条 +预计峰值通知量:每分钟 5 条 + +生产跳转域名:product.example.com +测试跳转域名:product-test.example.com +是否需要点击后免登:否 + +计划联调日期:2026年9月1日 +计划上线日期:2026年9月5日 +``` + +登记字段要求: + +| 字段 | 要求 | 示例 | +| --- | --- | --- | +| `projectCode` | 2~40 位小写字母、数字、下划线或短横线;创建后不再修改 | `fixed_asset` | +| 项目名称 | 用户能看懂的简短名称,会显示在通知标题中 | `固定资产` | +| 跳转域名 | 标准 HTTPS 域名,不含协议、端口和路径;不使用通配符 | `asset.example.com` | +| 通知场景 | 写清触发条件、接收人和需要采取的动作 | `资产报废申请创建后通知审批人` | +| 预计发送量 | 至少说明日常量和峰值 | `日均 50 条,峰值每分钟 10 条` | + +通知中心维护人完成登记后,应通过安全渠道分别交付: + +```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` | +| `idempotencyKey` | 业务项目根据事件、记录、收件人和版本生成 | `task_due:53:user-id:v1` | +| `title` | 当前业务事件 | `产品任务即将到期` | +| `content` | 当前业务记录 | 截止时间、任务名称、负责人 | +| `actionUrl` | 当前业务记录的 HTTPS 页面 | `https://product.example.com/tasks/53` | + +### 6.3 钉钉 userId 到底是什么 + +钉钉 `userId` 是员工在当前企业通讯录中的稳定标识,不是姓名,也不是手机号、邮箱、工号或业务数据库主键。 + +业务项目按以下优先顺序取得: + +1. 已有员工账号绑定:读取当前收件人账号保存的 `dingtalkUserId` 或等价字段。 +2. 已有企业通讯录同步:从项目已经同步的钉钉员工记录中读取 `userId`。 +3. 仅做首次联调:由负责人确认一名真实员工,并通过公司现有的权威人员映射取得该员工 `userId`。 +4. 完全没有员工与钉钉身份对应关系:先补齐账号绑定或通讯录同步,不能通过姓名猜测。 + +如果正式业务永远只通知固定负责人,可以把该负责人的 `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, +}) +``` + +发送一条通知: + +```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) +``` + +查询投递状态: + +```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" +} +``` + +| 字段 | 必填 | 规则 | +| --- | --- | --- | +| `idempotencyKey` | 是 | 最长 160 字符;同一业务事件和同一收件人必须稳定不变 | +| `recipientUserId` | 是 | 企业通讯录中的钉钉 `userId`,不是姓名、手机号或业务账号 ID | +| `title` | 是 | 只写业务主题,最长 100 字符;不要自己添加项目名或“由壹点”前缀 | +| `content` | 是 | Markdown 业务正文;不要自己写首行标题;字段之间保留空行 | +| `actionTitle` | 否 | 最长 20 字符,建议写明确动作,例如“立即审批” | +| `actionUrl` | 是 | 标准 443 端口的完整 HTTPS 地址,域名必须已登记在项目白名单中 | + +通知中心会根据项目配置统一生成: + +```text +【项目名称】业务标题 +``` + +例如业务项目提交“资产报废待审批”,用户最终看到的是“【固定资产】资产报废待审批”。完整文案规则见[消息规范](./消息规范.md)。 + +## 十、幂等防重和多人通知 + +推荐的幂等键格式: + +```text +{事件类型}:{业务记录ID}:{recipientUserId}:{消息版本} +``` + +示例: + +```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`。 +- [ ] 能使用投递 `id` 查询状态。 +- [ ] 通知标题自动包含正确项目名称,没有重复前缀或双标题。 +- [ ] 按钮进入已登记的 HTTPS 域名和正确业务记录。 +- [ ] 重复执行同一业务事件不会产生第二条通知。 +- [ ] 无效钉钉 `userId` 最终进入明确失败状态。 + +### 14.2 生产验收 + +生产上线不能只验证健康接口,至少完成: + +- [ ] 项目负责人收到一条真实业务通知。 +- [ ] 另一名真实员工收到一条真实业务通知。 +- [ ] 两人看到的项目名称、业务内容和按钮都正确。 +- [ ] 重复提交同一请求没有重复消息。 +- [ ] 能从业务日志关联“业务事件 → 幂等键 → 投递 ID → 最终状态”。 +- [ ] 日志、错误页、浏览器请求和监控平台均未出现项目密钥。 +- [ ] 已明确通知失败由谁处理,以及如何联系通知中心维护人。 +- [ ] 如启用免登,已额外验证员工身份、权限、票据过期和一次性消费。 + +验收完成后,双方保存以下记录: + +```text +项目名称: +projectCode: +业务项目版本或 Git 提交: +通知中心版本或 Git 提交: +生产验证时间: +验证通知的业务事件 ID: +验证通知的投递 ID: +发送结果: +点击跳转结果: +业务负责人: +技术负责人: +通知中心维护人: +遗留问题: +``` + +## 十五、常见接入错误 + +| 错误做法 | 正确做法 | +| --- | --- | +| 前端直接调用通知中心 | 只允许业务后端调用 | +| 从其他项目复制项目密钥 | 每个项目由通知中心单独生成密钥 | +| 使用姓名、手机号或本地用户 ID 作为收件人 | 使用经过确认的钉钉 `userId` | +| 标题自己添加“【项目名】” | 只提交业务标题,由通知中心统一拼接 | +| 签名路径包含公网 `/notification-center` 前缀 | 签名内部路径 `/v1/notifications` | +| 先序列化签名,发送时再次序列化成不同正文 | 对实际发送的同一份 raw body 签名 | +| 每次重试随机生成新的幂等键 | 重试沿用原幂等键,只更换 nonce | +| 把 HTTP `202` 当成已送达 | 保存投递 ID 并查询最终状态 | +| 多人共用一个幂等键 | 每名收件人使用独立幂等键 | +| 收到超时就无限重发 | 有限重试;结果不确定时先人工核验 | +| 只测试自己,不测试第二名员工 | 生产至少完成双人真实验收 | + +## 十六、完成标准 + +只有同时满足以下条件,才算正式接入完成: + +1. 项目已经在通知中心登记,并使用独立项目密钥和域名白名单。 +2. 通知由业务后端发出,前端和 Git 仓库中没有任何密钥。 +3. 业务项目保存投递 ID,正确处理幂等、状态查询和有限重试。 +4. 通知内容符合[消息规范](./消息规范.md)。 +5. 已完成两名真实员工的生产通知、跳转和防重验收。 +6. 已记录双方负责人、上线版本、验证证据和故障处理方式。 + +更细的逐项门禁可继续使用[新项目接入清单](./新项目接入清单.md)。 diff --git a/docs/通知中心新项目开通操作手册.md b/docs/通知中心新项目开通操作手册.md new file mode 100644 index 0000000..1c34765 --- /dev/null +++ b/docs/通知中心新项目开通操作手册.md @@ -0,0 +1,207 @@ +# 通知中心新项目开通操作手册 + +> 使用对象:由壹点统一通知中心维护人。 +> +> 本文不交给业务项目作为开发教程。业务项目请阅读[统一通知中心接入操作手册](./统一通知中心接入交接说明.md)。 + +## 一、开通结果是什么 + +新项目开通完成后,应同时满足: + +1. 通知中心生产配置中存在唯一项目代码、项目名称、独立密钥和跳转域名白名单。 +2. 通知中心服务已重新加载配置并通过健康检查。 +3. 业务项目已通过安全渠道取得 URL、项目代码和项目密钥。 +4. 业务项目使用一名真实员工完成首次通知和状态查询。 +5. 项目密钥没有进入 Git、群聊、工单、共享文档、截图或普通日志。 + +仅把接入文档发给对方,不代表项目已经开通。 + +## 二、收到申请后先审核 + +业务项目应提交: + +- 项目中文名称; +- 建议 `projectCode`; +- 业务负责人和技术负责人; +- 通知场景和触发条件; +- 收件人规则以及钉钉 `userId` 的数据来源; +- 日常和峰值通知量; +- 生产及测试跳转域名; +- 是否需要钉钉 H5 免登; +- 计划联调和上线日期。 + +审核门槛: + +- `projectCode` 使用 2~40 位小写字母、数字、下划线或短横线,创建后不随中文名称改变。 +- 项目名称简短并适合出现在 `【项目名称】业务标题` 中。 +- 跳转域名为业务项目真实控制的 HTTPS 域名,不使用通配符,不包含协议、端口或路径。 +- 收件人规则明确,不能使用姓名猜测钉钉 `userId`。 +- 预计发送量在当前串行投递能力可承受范围内;峰值明显较大时先做容量评估。 +- 普通通知默认不启用免登;只有点击后必须自动建立登录态时才审核免登配置。 + +信息不完整时退回补充,不先生成密钥。 + +## 三、确认项目身份没有冲突 + +修改生产配置前只读核对: + +1. 当前 `YYD_NOTIFICATION_PROJECTS_JSON` 能正常解析为数组。 +2. 数组中不存在相同 `code`。 +3. 申请的域名没有误写为协议、路径或非标准端口。 +4. 项目名称不会与已有项目造成用户无法分辨的通知来源。 +5. 如启用免登,回调地址属于该项目的 `allowedHosts`,回跳路径前缀采用最小范围。 + +不要删除、重排或重新生成已有项目的密钥。新增项目时必须完整保留所有既有项目配置。 + +生产项目配置结构: + +```json +{ + "code": "product_development", + "name": "产品开发", + "secret": "由通知中心维护人生成的独立随机密钥", + "allowedHosts": ["product.example.com"] +} +``` + +启用免登时才增加: + +```json +{ + "login": { + "enabled": true, + "callbackUrl": "https://product.example.com/api/dingtalk/broker-login", + "allowedRedirectPathPrefixes": ["/tasks", "/dashboard"] + } +} +``` + +## 四、生成和保管项目密钥 + +每个项目必须使用独立的高强度随机密钥,长度至少 24 个字符。推荐在受控服务器或受控工作站生成 32 字节以上的随机值。 + +要求: + +- 不复用其他项目密钥; +- 不使用项目名、日期、手机号或人工编写的口令; +- 生成过程不进入共享终端日志; +- 只写入通知中心生产 `.env` 和业务项目后端的密钥配置; +- 临时交接文件必须放在已确认不受 Git 跟踪的位置,并限制读取权限; +- 业务项目确认保存后删除不再需要的临时明文副本。 + +业务项目不需要知道钉钉 `AppKey`、`AppSecret`、`RobotCode`,不得将这些凭证与项目密钥一起交付。 + +## 五、变更生产配置 + +这是生产写入操作,必须获得上线授权后执行。 + +执行顺序: + +1. 确认目标服务器和目录是通知中心,而不是其他业务项目。 +2. 备份通知中心 `.env`,备份文件包含密钥,权限必须限制为仅维护人可读。 +3. 备份通知中心 SQLite 数据目录,确保现有通知记录可恢复。 +4. 在 `YYD_NOTIFICATION_PROJECTS_JSON` 数组末尾新增项目,完整保留所有已有项目。 +5. 再次解析 JSON,确认项目代码唯一、密钥长度合格、域名格式正确。 +6. 使用当前正式发布方式重新加载通知中心配置。 +7. 验证本机 `/health` 与公网 `/notification-center/health`。 +8. 验证现有项目仍可鉴权,新项目能够通过签名请求进入通知队列。 + +重要限制: + +- 日常发布使用仓库当前的 `scripts/git-build-deploy.ps1` 流程。 +- 当前 `scripts/deploy-material-host.sh` 已具备“保留生产 `.env` 中未列出的已登记项目”逻辑;使用前仍需确认部署的是包含该保护逻辑的最新版本,并在发布后核对项目列表,不能使用更早的脚本副本。 +- 不能为了新增项目而更换其他项目密钥。 +- 不能把生产 `.env`、密钥或完整 `docker compose config` 输出保存到共享日志。 + +健康检查只证明服务和数据库可用,不证明新项目已经能够发送通知。仍需完成下一节的真实业务验收。 + +## 六、安全交付三项配置 + +向业务项目技术负责人交付: + +```dotenv +YYD_NOTIFICATION_CENTER_URL=https://sc.jiansun.vip/notification-center +YYD_NOTIFICATION_PROJECT_CODE=实际项目代码 +YYD_NOTIFICATION_PROJECT_SECRET=实际项目独立密钥 +``` + +交付方式: + +- URL 和项目代码可以写入接入记录; +- 项目密钥只通过双方认可的一对一安全渠道或密钥管理服务交付; +- 密钥与普通接入说明分开发送; +- 要求接收人在业务后端保存后确认,不在群内回复或引用密钥; +- 不替业务项目长期保存收件人名单。 + +可直接发送给业务项目负责人的非敏感说明: + +```text +你的项目已在由壹点统一通知中心完成登记。 + +公共地址:https://sc.jiansun.vip/notification-center +项目代码:<实际项目代码> +项目密钥:将通过单独的安全渠道发送 + +请把三项配置只放在业务后端。通知收件人的钉钉 userId 不是接入密钥, +应由你们的业务账号绑定或通讯录映射在每次发送时提供。 +接入步骤请按《由壹点统一通知中心接入操作手册》执行。 +``` + +## 七、首次联调怎么做 + +业务项目准备: + +- 一名真实测试员工的钉钉 `userId`; +- 一个只在该员工和业务负责人之间可见的低风险测试场景; +- 一个属于项目白名单域名的 HTTPS 跳转地址; +- 稳定且不会与生产业务事件冲突的测试幂等键。 + +维护人与业务项目共同确认: + +1. 创建接口返回 `202` 或同幂等请求返回 `200`。 +2. 返回体包含投递 `id`。 +3. 查询接口只能由当前项目查询该投递记录。 +4. 最终状态进入 `sent`、`unread`、`read` 或给出明确 `failed` 原因。 +5. 测试员工实际收到“由壹点”机器人单聊。 +6. 标题包含正确项目名称。 +7. 按钮只进入登记的项目域名和正确页面。 +8. 相同幂等键重复请求不产生第二条消息。 + +首次真实发送只做能证明链路的一次必要调用;未获得真实发送授权时使用客户端示例测试,不向员工发送消息。 + +## 八、生产验收和交接记录 + +正式上线至少由两名真实员工完成通知与跳转验证。记录中不保存密钥,只保存: + +```text +项目名称: +projectCode: +allowedHosts: +业务负责人: +技术负责人: +通知中心维护人: +业务项目版本或 Git 提交: +通知中心版本或 Git 提交: +生产验证时间: +测试业务事件 ID: +通知中心投递 ID: +最终投递状态: +按钮跳转结果: +遗留问题: +``` + +完成后把[统一通知中心接入操作手册](./统一通知中心接入交接说明.md)、[消息规范](./消息规范.md)和[错误与状态规范](./错误与状态规范.md)交给项目后续维护人。 + +## 九、密钥轮换或停用 + +密钥泄露、负责人变更或项目停用时,不要直接覆盖后结束: + +1. 确认受影响的唯一项目代码和环境。 +2. 说明轮换或停用窗口及对发送通知的影响。 +3. 备份当前配置并准备恢复原密钥的回滚路径。 +4. 新密钥只交付给该项目,不影响其他项目。 +5. 协调通知中心与业务后端在约定窗口切换。 +6. 使用一条有上限的真实通知验证新密钥。 +7. 确认旧密钥失效,并删除不再需要的明文副本。 + +项目停用前保留必要的历史投递记录,不因停用项目直接删除数据库数据。