# 企业微信应用消息推送「审核通知」设计方案 版本:v0.1(评审稿,已按官方文档核验) 日期:2026-09-11 范围:后端自动推送 · 企微自建应用消息 · **集团单一企微(单 corpId)** --- ## 1. 背景与目标 项目(养老院管理系统,若依 RuoYi 体系:前端 `kyj-yanglao-web-new` + Java 服务端)当前的审核/审批: - 流程由 BPM 工作流引擎驱动,前端通过 `/bpm/task/approve`、`/bpm/task/reject`、`/bpm/task/return` 触发「通过/拒绝/退回」。 - 已有**站内通知中心**:前端读取未读数 `/system/notify-message/getBpmListCount`、标记已读 `/system/notify-message/updateReadById`。 **问题**:站内通知仅在 Web 端可见,审核通过/拒绝/待办变更无法及时触达相关人员(经办人、发起人、审批人)。 **目标**:在审核结果产生时(后端),通过**企业微信自建应用消息**,将审核通知实时推送到关联人员的企业微信;采用后端自动推送,不依赖前端唤起页面。 **关键前提**:集团为**单一企业微信(单 corpId)**,`userid` 全集团唯一,因此可按 userid **精准推送给对应人员**,一套 `corpId/secret/agentId` 全局复用,无需按租户/机构拆分多套企微配置。 --- ## 2. 现状梳理 | 能力 | 现状 | 位置(前端) | | --- | --- | --- | | 审核动作入口 | 通过/拒绝/退回 | `src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue`(`handleAudit`) | | 审核接口 | `/bpm/task/approve`、`/bpm/task/reject`、`/bpm/task/return` | `src/api/bpm/task/index.ts` | | 待办/已办列表 | 是 | `src/views/bpm/task/todo/index.vue` | | 站内通知 | 未读数 + 已读 | `/system/notify-message/*` | | 后端技术栈 | 若依 RuoYi(Spring Boot) | 不在本仓库 | 前端审核动作只是把结果提交给后端,**最终的审批状态流转和通知都在服务端**。因此企微推送必须落在后端。 --- ## 3. 总体架构 ``` 申请人 审批人 企微 App(单 corpId) │ │ │ │ 提交单据(BPM启动) │ │ │ ───────────────────► │ │ │ │ │ │ 审批通过/拒绝 │ │ │ │ ─────────────────► │ │ 审核结果完成 │ │ └── 后端流程引擎触发通知 │ │ │ ▼ │ ┌─────────────────────────┐ │ │ NotifyMessage 记录站内通知│ │ └─────────────────────────┘ │ │ │ ▼ │ ┌─────────────────────────┐ │ │ QyWeixinClient │ │ │ - 取 access_token(缓存) │ │ │ - 按 userid 精准发应用消息 │ │ └─────────────────────────┘ │ │ │ ▼ │ 企微 App 推给对应人员 ``` ### 最优挂接点(推荐) 不直接改 `approve/reject` 各自的方法,而是在**审核完成时统一产生的通知出口**处理,做到一次接入、覆盖所有审核场景: - 方案 A(推荐):在服务端「审核通知落地」的公共位置挂接发送逻辑 —— 即当前 BPM 审批完成后写站内通知(`notify-message`)的地方,追加企微推送。所有通过/拒绝/退回都经过此处。 - 方案 B:在 BPM 流程事件监听器上挂接(例如审批任务完成 / 流程实例结束事件),事件驱动,与通知中心解耦。 两种方案都满足「后端自动推送、前端零改动」。方案 A 复用既有通知点位更简单;方案 B 更符合流程事件化、可扩展(未来其它消息也可复用)。 --- ## 4. 企业微信应用消息机制(前置说明) ### 4.1 三要素(一套,全局复用) | 配置项 | 含义 | 获取位置 | | --- | --- | --- | | `corpId` | 企业 ID(全集团唯一) | 企微管理后台 → 我的企业 → 企业信息 | | `corpSecret` | 自建应用的密钥 | 应用管理 → 目标应用 → Secret | | `agentId` | 应用 AgentId | 应用管理 → 目标应用 → AgentId | ### 4.2 调用链路(两步) 1. 获取 access_token(有效期 7200s,需缓存复用,勿每次请求): `GET https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corpid}&corpsecret={secret}` 2. 发送应用消息(给指定 userid,可多选): `POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}` body: ```json { "touser": "zhangsan|lisi", "msgtype": "markdown", "agentid": 1000002, "markdown": { "content": "..." }, "safe": 0 } ``` ### 4.3 前置条件(必须满足,否则发不出去) - 目标用户必须是企微通讯录成员,且 `touser` 用其 **userid**(非手机号/姓名)。 - 服务端出口 IP 需加入该应用「企业可信 IP」白名单,否则 gettoken 失败。 - 自建应用需勾选「消息发送」权限;接收人需在该应用可见范围内。 - 应用可见范围至少要覆盖需要被通知的人员(经办人/审批人)。 - **集团为单一企微(单 corpId)**:`userid` 全集团唯一,可直接按 userid 精准推送给对应人员,一套配置全局有效,无需按租户/机构拆分。 --- ## 5. 后端改动点(若依 RuoYi / Spring Boot) > 后端不在本仓库,此处给出落地清单;实际编码在拿到后端仓库后进行。 ### 5.1 配置 `application.yml` ```yaml wecom: app: enabled: true corp-id: wx1234567890abcdef agent-id: 1000002 corp-secret: YOUR_CORP_SECRET # userid 前缀/映射可选,若系统用户表存的是手机号而非 userid user-id-fallback: mobile ``` ### 5.2 新增 `QyWeixinClient` ```java @Component public class QyWeixinClient { // 1. getAccessToken():按 agentId 维度 Redis 缓存 7200s,失效再拉取(单 corpId 下基本一份 token) // 2. sendMarkdown(targetUseridSet, mdContent):非空 userid 才发,多个用 "|" 拼接 // 3. 记录返回的 invaliduser/unlicenseduser 日志,便于排查 userid 映射 // 异常处理:RestClientException 记日志,不向上抛,保证不影响主审批流程 } ``` ### 5.3 通知条件判断与接收人计算 - 仅当审核**结果产生**(通过/拒绝/退回)且配置 `enabled=true`、存在**接收人 userid** 时才发送。 - 接收人=申请人(单据发起人)+ 上一审批人。userid 来源(单 corpId 下 userid 全集团唯一,直接可用于 `touser`): - 用户表若有 userid 字段直接取; - 否则按配置 `user-id-fallback: mobile`,用手机号在企微通讯录换取 userid(`/cgi-bin/user/getuserid`)。手机号必须开通并唯一。 - 由于单 corpId,**无需**按租户/机构维护多套企微参数与 user↔corpId 映射。 ### 5.4 挂接点(方案 A:通知出口处) 在服务端「BPM 审批写站内通知(notify-message)」的方法内,审批结果确定后追加: ```java // 伪代码 notifyMessageService.createBpm(...); // 既有站内通知,保留不动 if (wecomProperties.getApp().isEnabled()) { Set userIds = resolveReceiverUserIds(instance, task); if (!userIds.isEmpty()) { wecomClient.sendMarkdown(userIds, buildApprovalMd(instance, task, result)); } } ``` ### 5.5 消息模板(Markdown,示例) ```markdown **养老院审批通知** > 单据:退住申请 > 编号:DS-2026-0001 > 发起人:张三 > 审批人:李四 > 结果:✅ 已通过 / ❌ 已拒绝 / ↩️ 已退回 审批意见:…… 时间:2026-09-11 14:30 ``` ### 5.6 幂等与失败处理 - token 按 agentId 缓存且处理并发,避免重复获取。 - 发送失败(网络/接口返回 errcode)仅记录日志并告警,**不影响主审批事务**(异步或非事务内同步发送,推荐用 `@Async`)。 - 可选开启 `enable_duplicate_check=1` 防重复推送(默认 1800s 内同内容去重)。 - 支持开关 `enabled:false` 一键关闭,灰度可配置按 tenant 或单据类型白名单。 --- ## 6. 配置清单(供实施联调用) | 步骤 | 内容 | 负责人 | | --- | --- | --- | | 1 | 企微后台创建自建应用,取得 corpId / agentId / secret | 企微管理员 | | 2 | 应用可见范围勾选需被通知人员 | 企微管理员 | | 3 | 服务器出口 IP 加入「企业可信 IP」 | 运维 + 企微管理员 | | 4 | 确认用户表具备 userid(或手机号可映射) | 后端开发 | | 5 | 后端接配置 + QyWeixinClient + 通知出口挂接 | 后端开发 | | 6 | 联调:发起一单审批,验证审批人/申请人企微能收消息 | 后端 + 测试 | | 7 | 灰度与开关 | 运维 | --- ## 7. 边界与风险 - **userid 准确性**是本方案最大风险:userid 与实际用户不匹配、接收人不在应用可见范围,会导致个别用户收不到(响应 `invaliduser`)。需在联调阶段核对映射。单 corpId 下全集团 userid 唯一,映射相对清晰。 - token 需按应用缓存且处理并发;Redis 或本地内存皆可,过期自动刷新。 - 大量审批并发时,企微频率限制(每应用对同一成员 ≤30 次/分、≤1000 次/时)需留意,必要时异步排队;建议避开整点 0/30 分调用。 - markdown 消息需企微客户端 2.4.5+;若应用开启「在微工作台中始终进入主页」,非文本消息会被转成文本并截断(≤20 字节)。 - 后端自动推送意味着**后端仓库需要新代码**;本前端仓库无需改动(除非未来要提供"审核通知开关/接收人配置"页面)。 - 敏感信息:secret、access_token 不可落入前端仓库或日志(官方明确要求 token 仅存后端)。 --- ## 8. 官方文档核验结论(2025/09/24 版) 已根据企微官方开发文档逐项核对,**当前方案整体无误**,并补充以下细节: ### 8.1 已确认与方案一致 | 方案写法 | 官方结论 | 是否一致 | | --- | --- | --- | | `gettoken?corpid=&corpsecret=` 取 token | GET `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET`,需应用为启用状态 | ✅ | | access_token 有效期 7200s,需缓存复用 | `expires_in: 7200`(2 小时),须缓存、不能频繁调用否则被频率拦截 | ✅ | | `message/send?access_token=` 发消息 | POST `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN` | ✅ | | `touser` 用 userid,多个用 `\|` 分隔 | 最多 1000 个;也支持 `toparty`/`totag`/`@all` | ✅ | | `agentid` 为整型必填 | 企业内部应用在设置页查看 | ✅ | | `msgtype: markdown` | 官方支持 markdown 消息(有语法子集) | ✅ | | 后端自动推送 | 官方明示「**请勿将 access_token 返回给前端,全部由后台发起企微 API 请求**」——印证后端方案正确 | ✅ | ### 8.2 需补充的细节 1. **token 缓存**:单 corpId 下应用仅一个,`access_token` 全局一份即可(仍按 agentId 语义缓存,便于扩展)。多企业场景才需按企业隔离。 2. **接收人必须在应用可见范围**:否则该 userid 返回在 `invaliduser`;若全部无权限则 errcode=81013。发送方法需记录 `invaliduser/unlicenseduser` 便于排查。 3. **markdown 客户端要求**:需企微客户端 2.4.5+;若应用设为「在微工作台中始终进入主页」,会转纯文本(≤20 字节截断)。 4. **频率与错峰**:每应用对同一成员 ≤30 次/分、≤1000 次/时;建议避开每小时 0/30 分整点调用。 5. **防重复**:可开 `enable_duplicate_check=1`(默认 1800s 同内容去重),避免重复推送。 6. **返回大小写**:返回包 userid 统一转小写,接收人分派时按小写处理更稳妥。 7. **secret 需应用启用状态**:配置联调前确认自建应用为「已启用」。 --- ## 9. 待确认问题(评审后) 1. 接收人集合:只推给申请人?还是申请人 + 全部历史审批人?是否包含"当前待办审批人"? 2. 推送事件范围:仅「通过/拒绝/退回」这类结果,还是也要「新待办提醒」? 3. 是否需要在 Web 端提供一个"审核通知设置"(接收人、开关)页面? 4. 后端仓库位置:评审通过后请提供后端 Git 地址或本地路径,以便直接实现。