简介本资源是一套面向Java开发者的企业微信审批系统集成方案聚焦解决审批表单中动态加载外部数据的典型需求如通过API对接公司内部小区信息库实现下拉选项自动填充。适用于具备Spring Boot与企业微信JS-SDK开发经验的中高级工程师尤其适合正在落地审批流程数字化改造的ToB应用团队。压缩包共20个文件含10个核心Java业务类覆盖Controller、Service及回调处理、6个MyBatis XML映射文件、1个application.yml配置文件、1个HTML前端页面及必要工程配置文件.gitignore等整体仅20KB轻量易集成。目前已有694人学习下载提供开箱即用的完整调用链路从前端JS签名注入、后端API鉴权与外部数据透传到审批控件渲染与回调事件处理目录结构清晰模块职责分明可直接嵌入现有企业微信应用项目快速验证与二次开发。1. 企业微信审批流里嵌外部选项不是加个 URL 就能用得过 JS SDK、后端鉴权、字段映射三道关某公司做 OA 升级时想在企业微信「审批模板」里把“供应商选择”这个环节从静态下拉改成调用自有主数据系统接口动态加载——结果发现后台配置页里那个灰掉的「关联外部选项」开关根本点不动。翻遍官方文档才明白这功能压根不是前端配个链接就能跑通的黑匣子而是需要 JS SDK 初始化、后端生成有效 signature、审批控件字段名与返回 JSON 结构严格对齐的三段式链路。它解决的不是“能不能显示”而是“如何让企业微信信任你的外部服务、并把用户选中的值原样回传给审批流”。适合正在对接企业微信审批 API 的 Java 后端 前端联调工程师尤其当你已经卡在「点击加载无反应」「选中后提交报错字段不存在」「回调地址 403」这类典型现场时这篇笔记就是你打开控制台前该先看的 checklist。2. 外部选项功能原理与企业微信侧约束为什么必须用 JS SDK 回调地址 审批模板字段绑定2.1 企业微信审批控件的「外部选项」本质是受控 iframe 加载模式企业微信不直接渲染你的页面而是以 iframe 方式加载你提供的 URL并通过window.postMessage与你的页面通信。整个流程分三步用户进入审批单 → 企业微信向你配置的「外部选项 URL」发起 GET 请求带code、state参数你的页面初始化 JS SDKwx.config调用wx.invoke(openSelectExternalOption, {...})触发弹窗用户选择后企业微信通过wx.onMenuShareAppMessage类似机制将选中项value和label回传到审批表单字段。关键约束有三点URL 必须是 HTTPS且域名需提前在「企业微信管理后台 → 应用管理 → 自建应用 → 可信域名」中备案JS SDK 签名必须由后端生成前端不能硬编码jsapi_ticket或nonceStr返回的 JSON 数据结构必须符合企业微信强 schema必须是{items: [{value: xxx, label: yyy}]}多一层包装或字段名错一个字母都会导致空白页。2.2 为什么 Java 后端必须参与签名生成JS SDK 初始化失败的根源在这里企业微信 JS SDK 要求每次调用前执行wx.config其参数signature是对以下字符串 SHA1 加密jsapi_ticketxxxnoncestryyytimestampzzzurlhttps://your-domain.com/external-option其中jsapi_ticket是企业微信颁发的临时票据2小时过期需用corpIdcorpSecret调用/cgi-bin/get_jsapi_ticket接口获取。前端无法安全存储corpSecret所以签名逻辑必须放在 Java 后端。常见错误是前端试图用固定时间戳拼接结果timestamp与企业微信服务器时间偏差超 7200 秒直接拒绝。提示Java 后端建议封装WxJsSdkSignService缓存jsapi_ticket并自动刷新避免每次请求都调用接口。我一般会用Caffeine做本地缓存过期时间设为 110 分钟留 10 分钟缓冲。2.3 审批模板字段与外部选项的映射关系字段名大小写、类型、必填性全要对齐在企业微信管理后台创建审批模板时添加「外部选项」控件后必须手动指定「字段名」如supplier_id。这个字段名会作为value的 key 回传到审批数据中。而你的外部页面返回的items数组里每个对象的value字段必须是字符串类型即使后端是 Long也要String.valueOf(id)且长度不能超过 128 字符。若你在模板里把该字段设为「必填」但用户未选择就点提交企业微信会拦截并提示「请选择供应商」不会走到你后端。3. Java 后端签名服务实现Spring Boot 下生成有效 wx.config 参数3.1 企业微信凭证配置与票据缓存在application.yml中配置基础参数wechat: corp-id: wwxxxxxxxxxxxxxx corp-secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx agent-id: 1000001定义票据响应实体Data public class JsApiTicketResponse { private int errcode; private String errmsg; private String ticket; private int expires_in; // 单位秒 }使用RestTemplate获取票据并缓存关键加锁防并发重复刷新Service public class WxJsSdkSignService { private final RestTemplate restTemplate new RestTemplate(); private final CaffeineCache jsapiTicketCache; public WxJsSdkSignService() { this.jsapiTicketCache Caffeine.newBuilder() .maximumSize(1) .expireAfterWrite(110, TimeUnit.MINUTES) .build(); } public String getJsApiTicket(String corpId, String corpSecret) { String cacheKey corpId; return jsapiTicketCache.get(cacheKey, key - { String url https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token getAccessToken(corpId, corpSecret); JsApiTicketResponse resp restTemplate.getForObject(url, JsApiTicketResponse.class); if (resp null || resp.getErrcode() ! 0) { throw new RuntimeException(获取 jsapi_ticket 失败: resp.getErrmsg()); } return resp.getTicket(); }); } private String getAccessToken(String corpId, String corpSecret) { String url https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid corpId corpsecret corpSecret; // 此处省略 AccessToken 响应解析同理用 Caffeine 缓存有效期 2h return xxx_access_token_xxx; } }3.2 签名生成 Controller接收前端传来的 URL返回完整 config 参数前端页面加载时会先请求/api/wx/sign?urlhttps%3A%2F%2Fxxx.com%2Foption获取签名RestController RequestMapping(/api/wx) public class WxSignController { Autowired private WxJsSdkSignService signService; GetMapping(/sign) public ResponseEntityMapString, String sign(RequestParam String url) { String nonceStr UUID.randomUUID().toString().replace(-, ); long timestamp System.currentTimeMillis() / 1000; String jsapiTicket signService.getJsApiTicket( wwxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ); // 拼接签名原文 String plain jsapi_ticket jsapiTicket noncestr nonceStr timestamp timestamp url url; String signature DigestUtils.sha1Hex(plain); MapString, String result new HashMap(); result.put(appId, wwxxxxxxxxxxxxxx); result.put(timestamp, String.valueOf(timestamp)); result.put(nonceStr, nonceStr); result.put(signature, signature); return ResponseEntity.ok(result); } }注意url参数必须是当前页面的完整 URL含协议、域名、路径、查询参数不能是相对路径或漏掉?codexxx。企业微信校验时会严格比对差一个字符就config:invalid url。3.3 外部选项数据接口返回符合 schema 的 JSON 列表提供/external-option接口接收企业微信带的code参数用于换取用户身份返回供应商列表GetMapping(/external-option) public ResponseEntityMapString, Object externalOption(RequestParam String code) { // 1. 用 code 换取 user_id可选若需权限控制 // 2. 查询主数据系统供应商列表 ListSupplier suppliers supplierService.listActive(); ListMapString, String items suppliers.stream() .map(s - { MapString, String item new HashMap(); item.put(value, String.valueOf(s.getId())); // 必须是字符串 item.put(label, s.getName()); return item; }) .collect(Collectors.toList()); MapString, Object result new HashMap(); result.put(items, items); // 顶层必须是 items 字段不能是 data/items 或 list return ResponseEntity.ok(result); }关键点返回体必须是{items: [...]}不能包在{ code: 0, data: { items: [...] } }里。企业微信解析失败时静默失败只留白屏。4. 前端页面 JS SDK 集成与外部选项调用Vue/React 通用写法4.1 页面加载后立即请求签名并初始化 SDK以 Vue 3 Composition API 为例在onMounted中执行import { onMounted, ref } from vue import axios from axios export default { setup() { const loading ref(true) onMounted(async () { try { // 1. 获取当前页面完整 URL注意必须 encodeURI const currentUrl encodeURI(window.location.href) // 2. 请求后端签名接口 const { data } await axios.get(/api/wx/sign?url${currentUrl}) // 3. 初始化 SDK wx.config({ debug: false, // 生产环境务必关闭 appId: data.appId, timestamp: data.timestamp, nonceStr: data.nonceStr, signature: data.signature, jsApiList: [openSelectExternalOption] // 必须显式声明 }) wx.ready(() { console.log(wx sdk ready) loading.value false }) wx.error((res) { console.error(wx config error:, res) // 常见错误invalid signature / invalid url / permission denied }) } catch (e) { console.error(init wx sdk failed, e) } }) return { loading } } }提示wx.config必须在页面 DOM 加载完成后调用且jsApiList里必须包含openSelectExternalOption否则后续调用直接报not in jsApiList。4.2 触发外部选项弹窗并处理选中结果在用户点击「选择供应商」按钮时调用const openExternalOption () { wx.invoke(openSelectExternalOption, { url: https://your-domain.com/external-option, // 必须与后台配置一致 title: 请选择供应商, placeholder: 搜索供应商名称, multiple: false, // true 支持多选但审批字段只存单值 search: true }, (res) { if (res.err_msg openSelectExternalOption:ok) { // res.data 格式{ value: 1001, label: XX科技有限公司 } console.log(选中:, res.data) // 将 value 写入表单隐藏域供审批提交时读取 document.getElementById(supplier_id).value res.data.value } else { console.warn(外部选项取消或失败:, res.err_msg) } }) }注意res.data是对象不是数组。即使配置了multiple: true企业微信在审批控件中也只支持单选回传历史兼容限制所以后端字段设计仍按字符串处理。4.3 审批提交时字段值传递隐藏域 表单序列化在审批单 HTML 中确保存在与模板字段名一致的input!-- 企业微信模板中字段名为 supplier_id -- input typehidden idsupplier_id namesupplier_id value / !-- 提交按钮触发企业微信默认提交 -- button onclickwx.invoke(submitApproval)提交审批/button企业微信 SDK 会自动扫描表单内name属性匹配的字段并将value作为该字段的值提交。无需额外 JS 绑定。5. 避坑指南90% 的「外部选项不显示/选不了/提交失败」都出在这五处5.1 现象页面白屏控制台无报错network 里看不到external-option请求原因企业微信 iframe 加载你的页面时若Content-Security-Policy头禁止了frame-ancestors self或页面返回了X-Frame-Options: DENY则 iframe 被浏览器拦截。解决后端响应头必须包含X-Frame-Options: ALLOW-FROM https://work.weixin.qq.com注意是ALLOW-FROM不是SAMEORIGIN且 Spring Boot 中需禁用默认 HSTS 头。Configuration public class WebSecurityConfig { Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { return http .headers(headers - headers .frameOptions(frameOptions - frameOptions .disable())) // 关闭默认 X-Frame-Options .build(); } }再手动在 Controller 返回头中添加response.setHeader(X-Frame-Options, ALLOW-FROM https://work.weixin.qq.com);5.2 现象wx.config:invalid signature但后端日志显示签名生成无异常原因前端传给/api/wx/sign?url的url参数未做encodeURI导致特殊字符如?codeabcstatedef中的被截断后端拼接的url字符串与实际页面 URL 不一致。解决前端必须严格encodeURI(window.location.href)后端接收后不要二次 decode直接用于签名拼接。企业微信校验时用的是原始 URL 字符串。5.3 现象点击「选择供应商」无反应控制台报not in jsApiList原因wx.config的jsApiList数组里漏写了openSelectExternalOption或wx.config调用时机过早DOM 未加载完、过晚用户已点击按钮。解决把wx.config放在onMounted或document.addEventListener(DOMContentLoaded)之后检查jsApiList是否精确匹配大小写敏感无空格。5.4 现象外部选项弹窗能打开列表也显示但选中后审批单里字段为空原因企业微信要求value字段必须是字符串而你的后端返回了数字如value: 1001JSON 解析后变成 number 类型企业微信拒绝识别。解决Java 返回时强制String.valueOf(id)JavaScript 返回时写value: id确保 JSON 中value是字符串类型。可用curl -v抓包验证响应体。5.5 现象审批提交后后台收到的数据里supplier_id字段是空字符串或undefined原因HTML 表单中input的name属性与企业微信审批模板中配置的字段名不一致如模板写supplierId页面写supplier_id或该input被 JS 动态移除/重命名。解决打开企业微信审批模板编辑页复制「字段名」精确粘贴到 HTML 的name属性用浏览器开发者工具检查该input是否始终存在于 DOM 中且value有值。6. 审批数据回传验证与调试技巧用企业微信「测试账号」 日志埋点定位真实链路6.1 用企业微信「测试账号」绕过审核快速验证全流程生产环境需提交审批模板审核但开发阶段可启用「测试账号」功能进入「企业微信管理后台 → 应用管理 → 自建应用 → 测试账号」添加你自己的微信账号需是企业成员在测试账号下创建审批单此时所有外部选项配置实时生效无需等待审核。这是血泪经验曾因等审核 2 天反复改代码却不知是签名问题还是字段名问题。开启测试账号后从改代码到看到弹窗10 分钟闭环。6.2 在关键节点埋点日志区分「前端失败」还是「后端失败」在 Java 后端external-option接口开头加日志log.info(【外部选项】收到请求code{}, userAgent{}, code, request.getHeader(User-Agent));在前端wx.invoke回调里打印wx.invoke(openSelectExternalOption, {...}, (res) { log.info(【外部选项】invoke callback, { err_msg: res.err_msg, data: res.data }) })然后对比若后端日志没打印 → 企业微信没发起请求 → 检查 iframe 加载、CSP 头若后端有日志但前端err_msg是config:fail→ SDK 初始化失败 → 查签名若前端err_msg是ok但data为空 → 后端返回 JSON 结构错误 → 抓包看响应体。6.3 用 Postman 模拟企业微信回调验证签名服务健壮性企业微信在用户提交审批后会向你配置的「审批回调 URL」发送 POST 请求。虽然本篇聚焦外部选项但回调验证是联调必过环节。用 Postman 构造请求Method: POSTURL:https://your-domain.com/api/wx/callbackBody (raw JSON):{ ToUserName: wwxxxxxxxxxxxxxx, FromUserName: USERID, CreateTime: 1712345678, MsgType: event, Event: approval_approval, ApprovalInfo: { sp_no: APPROVAL-123456789, status: approved, approver_list: [{userid: USERID, status: approved}] } }Headers:Content-Type: application/json后端解析时必须校验msg_signature企业微信用tokentimestampnoncebody生成否则拒收。这部分逻辑建议抽成WxCallbackValidator工具类避免每次手写。从那以后我每次对接企业微信新能力都强制走一遍「测试账号创建审批单 → 打开开发者工具看 network → 抓包确认每一步响应体 → 对照日志定位断点」四步闭环。哪怕只是改了个字段名也重新走一遍——因为企业微信的报错永远藏在最深的那层 silent fail 里不亲眼看见 response你永远不知道是哪行代码在假装工作。希望帮到你。本文还有配套的精品资源点击获取