wecom-approval-notify-design.md 13 KB

企业微信应用消息推送「审核通知」设计方案

版本: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:

    {
     "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

    wecom:
    app:
    enabled: true
    corp-id: wx1234567890abcdef
    agent-id: 1000002
    corp-secret: YOUR_CORP_SECRET
    # userid 前缀/映射可选,若系统用户表存的是手机号而非 userid
    user-id-fallback: mobile
    

5.2 新增 QyWeixinClient

@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)」的方法内,审批结果确定后追加:

// 伪代码
notifyMessageService.createBpm(...);   // 既有站内通知,保留不动

if (wecomProperties.getApp().isEnabled()) {
    Set<String> userIds = resolveReceiverUserIds(instance, task);
    if (!userIds.isEmpty()) {
        wecomClient.sendMarkdown(userIds, buildApprovalMd(instance, task, result));
    }
}

5.5 消息模板(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 地址或本地路径,以便直接实现。