【企业微信】审批中的“关联外部选项” Java实现!!!

【企业微信】审批中的“关联外部选项” Java实现!!!

作者环境

  • java 1.8.281
  • 后端普通的Springboot工程,能连数据库,Redis均可
  • 前端普通的Vite + Vue3工程。
  • Vue3 + Vant

实现思路

  • 前端引入,企业微信前端jsapi
  • 后端使用 WxJava - 微信开发 Java SDK ,微信开源的javaSDK工具包,进行与企业微信的交互

实现过程

  • 通过阅读企业微信开发文档-审批控件中的外部选项 得知,要想实现前端与企业微信通信,需要先配置 wx.agentConfig
  • 配置 wx.agentConfig ,需要企业 的corpid,应用的agentid,以及 signature 签名
  • 要配置 signature 签名,需要 企业 corpid,应用agentid,应用的 Secret
  • 要签名需要 JsapiTicket 以及 AgentJsapiTicket
获取企业 corpid,应用agentid,应用的 Secret
  • 获取方法是企业管理员在后台查看,教程
踩坑!!! 获取企业 corpid,应用agentid,应用的 Secret
  • 注意在之前 企业微信的系统级应用比如打卡,审批等,可以查看agentid,Secret。在2023年12月1日起逐步回收系统级应用的agentid,Secret。 但是可以使用,自建应用的agentid,Secret代为获取JsapiTicket 以及 AgentJsapiTicket,具体参见

引入依赖

xml
复制代码
<!--wei xin about--> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-cp</artifactId> <version>4.6.0</version> </dependency>

后端 编写获取 JsapiTicket 以及 AgentJsapiTicket ,以及签名 signature

  • 在application.yaml中新增自定义配置
yaml
复制代码
wx-mu: cp: corp-id: 你的企业corpId corp-secret: 你的企业应用的corpSecret agent-id-a: 你的企业应用agentId
  • 编写配置类
java
复制代码
package com.ruoyi.project.weixinabout.bean; import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Data @Component public class WeiXinConfig { @Value("${wx-mu.cp.corp-id}") String cpCorpId; @Value("${wx-mu.cp.corp-secret}") String cpCorpSecret; @Value("${wx-mu.cp.agent-id-a}") String cpAgentIdA; }
  • 编写Bean实例类
java
复制代码
package com.ruoyi.project.weixinabout.bean; import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; import me.chanjar.weixin.cp.config.WxCpConfigStorage; import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class WeiXinCpConfig { // 注入配置类 @Autowired WeiXinConfig weiXinConfig; @Bean WxCpConfigStorage createWxCpConfigStorage() { // 构建微信Cp 即企业微信配置实例 WxCpDefaultConfigImpl wxCpDefaultConfig = new WxCpDefaultConfigImpl(); wxCpDefaultConfig.setCorpId(weiXinConfig.cpCorpId); wxCpDefaultConfig.setCorpSecret(weiXinConfig.cpCorpSecret); return wxCpDefaultConfig; } @Bean WxCpServiceImpl createWxCpService(WxCpConfigStorage wxCpConfigStorage) { // 构建微信Cp 服务实例 WxCpServiceImpl wxCpService = new WxCpServiceImpl(); wxCpService.setWxCpConfigStorage(wxCpConfigStorage); return wxCpService; } }
  • 编写api 控制器
java
复制代码
package com.ruoyi.project.weixinabout.cp.controller; import com.ruoyi.framework.web.domain.AjaxResult; import com.ruoyi.project.weixinabout.cp.service.WeiXinCpService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @CrossOrigin @Controller @RequestMapping("/wx") @RestController @Slf4j public class WeiXinCpController { // 引入服务层 @Autowired WeiXinCpService weiXinCpService; // 对链接进行签名生成 signature @GetMapping("/qm") public AjaxResult test1(String url) { Object qm = weiXinCpService.getJsapiTicketUrlQm(url); return AjaxResult.success(qm); } // 后期获取项目列表,或者其他信息,最主要的是上面的签名接口 @GetMapping("/proList") public AjaxResult projectList() { Object list = weiXinCpService.projectList(); return AjaxResult.success(list); } }
  • 服务层
  • JS-SDK使用权限签名算法参见
java
复制代码
package com.ruoyi.project.weixinabout.cp.service; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestAlgorithm; import cn.hutool.crypto.digest.Digester; import com.ruoyi.project.weixinabout.bean.WeiXinConfig; import com.ruoyi.project.weixinabout.cp.entity.ProjectInfo; import com.ruoyi.project.weixinabout.cp.mapper.WeiXinCpMapper; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.List; @Service @Slf4j public class WeiXinCpService { // 引入配置 @Autowired WeiXinConfig weiXinConfig; // 引入企业微信服务 @Autowired WxCpServiceImpl wxCpService; // 数据库操作,自定义 @Autowired WeiXinCpMapper weiXinCpMapper; // sha1 签名算法实例 Digester digester = new Digester(DigestAlgorithm.SHA1); public Object getJsapiTicketUrlQm(String url) { try { // 拿到 CorpId String corpid = weiXinConfig.getCpCorpId(); // 拿到 AgentId String agentid = weiXinConfig.getCpAgentIdA(); // 获取 JsapiTicket String jsapiTicket = wxCpService.getJsapiTicket(); // 签名需要的时间戳 long timestamp = System.currentTimeMillis(); // 签名需要的随机字符串 String nonceStr = IdUtil.fastUUID(); // 拼接字符串 String string1 = StrUtil.format("jsapi_ticket={}&noncestr={}&timestamp={}&url={}", jsapiTicket, nonceStr, timestamp, url); // 计算sha1签名 String signature = digester.digestHex(string1); // 获取AgentJsapiTicket String agentJsapiTicket = wxCpService.getAgentJsapiTicket(); long agentTimestamp = System.currentTimeMillis(); String agentNonceStr = IdUtil.fastUUID(); String string2 = StrUtil.format("jsapi_ticket={}&noncestr={}&timestamp={}&url={}", agentJsapiTicket, agentNonceStr, agentTimestamp, url); // 计算sha1签名 String agentSignature = digester.digestHex(string2); // 构建返回数据集 return new HashMap<String, Object>() {{ put("corpid", corpid); put("agentid", agentid); put("timestamp", timestamp); put("nonceStr", nonceStr); put("signature", signature); put("agentSignature", agentSignature); put("agentTimestamp", agentTimestamp); put("agentNonceStr", agentNonceStr); }}; } catch (Exception e) { throw new RuntimeException(e); } } // 其他自己的信息,看具体需求获取 public Object projectList() { return weiXinCpMapper.getProjectInfo(); } }

踩坑!!! 注意 !!!

  • JsapiTicket和AgentJsapiTicket不同都需要获取,并生成签名signature,和agentSignature,前端会用到

前端引入依赖

  • <script src="https://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
  • <script src="https://open.work.weixin.qq.com/wwopen/js/jwxwork-1.0.0.js"></script>

踩坑!!! 注意 !!!

  • 两个依赖都需要获取
  • 前端需要配置两个微信的配置,wx.configwx.agentConfig
  • 具体参见wx.config
  • 具体参见wx.agentConfig
  • 注意wx.config中的参数要用,JsapiTicket生成时使用的参数 !!!!
  • 注意wx.agentConfig中的参数要用,AgentJsapiTicket生成时使用的参数 !!!!

前端

踩坑!!! 注意 !!!

  • 注意必须先调用 wx.configwx.ready 之后在调用 wx.agentConfig 否则会出错!!! 具体参考
  • 在调用 wx.invoke('saveApprovalSelectedItems' 时候注意参数 keyselectedData 都要是字符串类型具体参考,第四点
  • 在回调的时候注意key 是企业微信访问时携带的params,我们需要获取到并原样传回,否则会失败!!!具体参考,第二点

新建组件

vue
复制代码
<script setup> import {onMounted, ref} from "vue"; import axios from "axios"; import {baseUrl} from "../config/base.js"; // 业务数据 const columns = ref([]); // 企业微信访问时的url信息 const hrefSelf = ref(''); // 企业微信访问时的url的query参数信息 const queryInfos = ref({}); // 业务数据 const currentId = ref(); // 业务数据 const searchValue = ref(); // 业务数据 const activeId = ref(0); // 业务数据 const SQ = ref([]); // 业务数据 const SH = ref([]); // 业务数据 const logs = ref([]); // 当点击,向企业微信保存数据 const onChange = (event) => { console.log(event); currentId.value = event.id wx.invoke('saveApprovalSelectedItems', { "key": queryInfos.value.key, // 字符串,从 URL 中获取到的 key "selectedData": JSON.stringify([ { key: `${event.id}`, value: event.text } ])// 字符串,选中的选项格式化为 JSON 字符串,格式见下文 }, (res) => { logs.value.push(JSON.stringify(res)) if (res.err_msg === 'saveApprovalSelectedItems:ok') { // 保存成功 console.log("保存成功"); } }); } // 业务函数 假搜索 const onSearch = () => { console.log(searchValue.value); if (!searchValue.value) { SQ.value.forEach(item => item.isShow = true) SH.value.forEach(item => item.isShow = true) return; } const regex = new RegExp(searchValue.value); SQ.value.forEach(item => { if (!regex.test(item.text)) { item.isShow = false } }) SH.value.forEach(item => { if (!regex.test(item.text)) { item.isShow = false } }) } // 签名,配置wx插件 const getAgent = async () => { let href = location.href let res1 = await axios.get(`${baseUrl}/wx/qm`, { params: { url: href } }) let res = res1.data console.log(res); wx.config({ beta: true,// 必须这么写,否则wx.invoke调用形式的jsapi会有问题 debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: res.data.corpid, // 必填,企业微信的corpID,必须是本企业的corpID,不允许跨企业使用 timestamp: res.data.timestamp, // 必填,生成签名的时间戳 nonceStr: res.data.nonceStr, // 必填,生成签名的随机串 signature: res.data.signature,// 必填,签名,见 附录-JS-SDK使用权限签名算法 jsApiList: ['saveApprovalSelectedItems', 'getApprovalSelectedItems'], // 必填,需要使用的JS接口列表,凡是要调用的接口都需要传进来 success: function (res) { // 回调 console.log("config 成功!", res); }, fail: function (res) { if (res.errMsg.indexOf('function not exist') > -1) { alert('版本过低请升级') } } }); wx.ready(() => { console.log("ready"); wx.agentConfig({ corpid: res.data.corpid, // 必填,企业微信的corpid,必须与当前登录的企业一致 agentid: res.data.agentid, // 必填,企业微信的应用id (e.g. 1000247) timestamp: res.data.agentTimestamp, // 必填,生成签名的时间戳 nonceStr: res.data.agentNonceStr, // 必填,生成签名的随机串 signature: res.data.agentSignature,// 必填,签名,见附录-JS-SDK使用权限签名算法 jsApiList: ['saveApprovalSelectedItems', 'getApprovalSelectedItems'], //必填,传入需要使用的接口名称 success: function (res) { // 回调 console.log("agentConfig 成功!", res); }, fail: function (res) { if (res.errMsg.indexOf('function not exist') > -1) { alert('版本过低请升级') } } }); }) wx.error(() => { console.log("error"); }) } // 解析 企业微信的params信息,主要是获取key const buildQueryInfo = () => { let endUrl = hrefSelf.value.split('?')[1] let end; if (!endUrl) { return; } end = endUrl.split('&') end.forEach(item => { let it = item.split('=') queryInfos.value[it[0]] = it[1] }) logs.value.push(JSON.stringify(queryInfos.value)); } // 业务函数 const getProjectInfo = async () => { let res = await axios.get(`${baseUrl}/wx/proList`) console.log(res); res.data.data.forEach((item, index) => { if (item.projectSort === 'SQ') { SQ.value.push({text: item.projectName, id: item.id, isShow: true}) } if (item.projectSort === 'SH') { SH.value.push({text: item.projectName, id: item.id, isShow: true}) } }) } onMounted(async () => { // 配置wx插件 await getAgent() // 保存query参数信息 hrefSelf.value = location.href // 解析query参数信息 buildQueryInfo() // 业务逻辑 await getProjectInfo() }) </script> <template> <div class="main-box"> <van-search v-model="searchValue" show-action @update:model-value="onSearch" placeholder="请输入搜索关键词"> <template #action> <div @click="onSearch">搜索</div> </template> </van-search> <van-tabs v-model:active="activeId"> <van-tab title="售前"> <div class="pro-list"> <van-list :finished="true" finished-text="没有更多了" > <van-cell :style="{'font-weight': item.id===currentId?'bolder':''}" v-show="item.isShow" v-for="item in SQ" :key="item.id" :title="item.text" @click="()=>onChange(item)" /> </van-list> </div> </van-tab> <van-tab title="售后"> <div class="pro-list"> <van-list :finished="true" finished-text="没有更多了" > <van-cell :style="{'font-weight': item.id===currentId?'bolder':''}" v-show="item.isShow" v-for="item in SH" :key="item.id" :title="item.text" @click="()=>onChange(item)" /> </van-list> </div> </van-tab> </van-tabs> </div> </template> <style scoped> .main-box { .pro-list { height: 550px; overflow: scroll; } .pro-list::-webkit-scrollbar { display: none; } } </style>

配置页面

  • 打包并发布页面
  • 在企业微信后台配置审批模板的时候,选择外联选项

配置页面 踩坑!!! 注意 !!!

  • 一定要把页面配置到公网,且有域名 ,而且要是 https的
  • 一定要在企业微信管理后台包所在的企业的服务器地址添加到白名单里

效果展示

wx-cp3.png

结语

  • 在开发的过程中坑巨多,祝鱼友们好运!!!
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
李若欣
下载 APP