腾讯企业微信API开发指南
提示:阅读完文章大概需要3分钟左右根据理解能力看不要求看完、文章中会设计大多的软件知识概念主要与技术是Java其他语言只要指导怎么操作过后这个流程细则知道过后也会得心应手,无外乎是换个写法和方式而已。主要有五个章节、从最基本的环境概念实操讲着走,深入浅出、大白话逐字逐句耐心的解释。
目录
- 腾讯企业微信API开发指南
- 前言
- 一、需求分析
- 二、环境的搭建
- (一)企业微信所需要的环境参数准备
- (二)IDEA与PostMan的安装
- 这里我默认您已经安装了IDEA专业版与PostMan,仅给出关键界面与配置 PostMan安装完进入:  IDEA安装完打开即可maven可与不配置使用默认的也行  JDK 1.8  到此Java环境已经配置好。
- 第三章 企业微信文档解读
- 第四章 关键代码示例
- 第五章 联调-客户端服务端调试
- 总结
前言
**企业微信是目前大多传统型企业管理企业组织架构业务沟通的不二选择,那么随着企业微信的功能升级随之付费也会产生,功能遍强大的同时,进一步接口的开放性和灵活性也在调整、那么对于传统业务型企业来讲与自家的ERP、HR等系统肯定是不互通的。因为如果要求乙方人员来开发费用增加、对方觉得不耐烦、不屑于这个接口对接、天马行空、不外乎也是照着文档指挥。此时就需要自家开发人员根据腾讯企业微信的官方文档进行二次对接,例如群机器人消息预警、通过自建应用提示员工的生日、通过企业微信的审批流做一个通用审批,通过接口触发来整合ERP当中不能移动端审批的问题、通过客户好友关系的变化,来记录员工与好友之前的关联关系等业务,那么这篇文章主要是讲整合业务中遇到的需求需要使用企业微信API接口进行协同处理时候遇到的一些常见操作例如怎么阅读企业微信的接口文档、怎么设置回调回调服务、怎么获取Token、怎么上传零时素材、提供关键的核心代码与概念解读、那么对于企业微信管理员以及开发人员来讲掌握这块在企业中的应用是很有必要的,掌握过后,那么不管是企业微信在出什么样的接口只要弄懂一个,其他的都可以掌握和知晓,也可以掌握他里面的一些设计理念、当然、这样就能够很好的利用企业微信接口的便利性把企业微信当作是一个介质与辅助工具来弥补一些其他系统功能的一个欠缺以及通过企业微信进行即时通讯。**
声明:此文档仅代表个人在使用中的一些经验总结和看法作为后期工作对接以及内外部交流的一个备忘文档,这样有新同事负责这块工作能够很快的上手,快速的对接业务以及维护、全部内容都为原创手敲整理、如有雷同请联系修改或者指导完善、因开源代码存在可复制性和传播性如有和你相同的代码纯属巧合师出同源望理解不存在抄袭,坚持原创整理,如需要将代码作为自家的业务代码,请具备基本的软件语言知识例如Java代码阅读的能力,这样才能更好的应用文章的代码、如果觉得可以请赞转发或者留言、在工作空闲时候为博友们解答。或者文章末尾打赏即可,多多支持。转载请注明来源谢谢!不得恶意解读或者用于商业传播(网课、短视频等)。
提示:以下进入正文
一、需求分析
需求1:通过自建应用发送员工生日提醒
需求2:群机器人发送服务器中断日志
需求3:通过企业微信通讯录回调完成会员开卡中获取员工与客户之间的关系
需求4:通过企业微信的审批流搭建一个通用的审批流
注:前面3个需求已经对接很久,已经平稳,此片文章只做需求4的开发和解读,前面三个也是一样的类似,后面第三章会带大家解读一遍对应的接口文章理清思路,代码逻辑都是通用的。
二、环境的搭建
(一)企业微信所需要的环境参数准备
1.申请企业微信管理员账户
注:可不申请管理员账户,如果不申请管理员账户,管理员懂怎么操作的话,可以直接将你需要的东西拿给你即可,我这边是领导需要的急,将三天出效果,又遇到周末,避免非技术问题来回沟通申请管理员账户,设置步骤如下:
①打开企业微信将对应的开发人员设置成超级管理员
步骤为:【我的企业】-----》【权限管理】—》【管理员】添加管理员 管理范围当前企业
2.获取关键信息
主要需要的信息归纳如下:
①企业微信的企业ID
② 审批模板建立获取模板ID 【模板ID在地址栏最后一串】
步骤:【应用管理】------》【审批】-----》【模板】-----》自己根据业务建立一个模板、我这里是需要一个通用的模板,通用什么意思,意思就是我指需要审核,不需要其他的,其他系统后期都可以使用这个流程模板,不局限的意思,至于节点的审核结果通过服务回调服务数据库直连到本地使用报表系统或者其他系统去处理达到联动的目的。
③ 自建应用 【通用审批】
主要的参数需要:AgentId、Secret
企业可信域名的配置这里可以是本企业任一一个绑定过主体的域名
企业应用可信IP配置:可信IP就是最终在那台电脑开发那台电脑部署时候的外网IP,外网IP可通过如下地址查询:https://ip.skk.moe/
接收回调服务地址配置:这里的配置的地址就是触发流程审批过后企业微信会把日志以xml或json的格式打到这台服务器此时会是加密的需要根据企业微信的解密接口解密即可获取到回调服务即审批节点抄送节点的信息、这里讲配置,具体代码在第四章实际操作进行讲解。注意回调服务所在的服务接口前缀也必须是与主体绑定过的云主机或者弹性服务器也就是虚拟机才行,这是他的规定遵守即可,可在企业已绑定的服务器中测试即可。
在这里插入图片描述
到此:企业微信的配置参数已经设置于获取到
(二)IDEA与PostMan的安装
这里我默认您已经安装了IDEA专业版与PostMan,仅给出关键界面与配置
PostMan安装完进入:

IDEA安装完打开即可maven可与不配置使用默认的也行

JDK 1.8

到此Java环境已经配置好。
第三章 企业微信文档解读
(一)企业微信接口流程与关键官方文档整理
那么由流程图可见,我们的思路就清晰多了,具体涉及对应的接口整理如下:
1、获取token:https://developer.work.weixin.qq.com/document/path/91039
2、企业微信回调服务解密:https://developer.work.weixin.qq.com/devtool/introduce?id=10128
3、提交审批:https://developer.work.weixin.qq.com/document/path/91853
提交审批注意控件的类型即如图:
也就是在提交申请的时候,模板里面的控件参数
到此涉及的文档已经整理好,注意的是加解密的时候,一定要使用腾讯官方的示例代码与环境去操作,不要自行搭配环境做无用工。我已经截图如下示意。
2.回调服务注意的点
服务器要开外网端口映射,否在Mysql,服务端口外网是访问不到的,云服务也就是需要在安全组和策略中配置自己的服务端口,以及将服务器绑定到外网或者主子域名上去。否着访问直接404,那么这种情况由两种,一个是域名与IP地址写错了,一个是参数填写错误。
第四章 关键代码示例
1、获取Token
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import java.util.HashMap;
/***
* *
* @param corpid 企业微信的企业ID
* @param corpsecret 企业微信应用或自建应用的密钥
* @return
*/
public static String Getaccess_token(String corpid, String corpsecret)
{
HashMap<String,Object> paramMap=new HashMap<>();
paramMap.put("corpid",corpid);
paramMap.put("corpsecret",corpsecret);
String thisresult= HttpUtil.get("https://qyapi.weixin.qq.com/cgi-bin/gettoken",paramMap);
JSONObject jsonobject= JSONUtil.parseObj(thisresult);
String access_token= (String) jsonobject.get("access_token");
return access_token;
}
2、上传临时素材
public static String upload(String accessToken, String type, File file) {
String media_id_str="";
JSONObject jsonObject = null;
String last_wechat_url = upload_wechat_url.replace("ACCESS_TOKEN", accessToken).replace("TYPE", type);
// 定义数据分割符
String boundary = "----------sunlight";
try {
URL uploadUrl = new URL(last_wechat_url);
HttpURLConnection uploadConn = (HttpURLConnection) uploadUrl.openConnection();
uploadConn.setDoOutput(true);
uploadConn.setDoInput(true);
uploadConn.setRequestMethod("POST");
// 设置请求头Content-Type
uploadConn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);
// 获取媒体文件上传的输出流(往微信服务器写数据)
OutputStream outputStream = uploadConn.getOutputStream();
// 从请求头中获取内容类型
String contentType = "Content-Type: " + getContentType();
// 请求体开始
outputStream.write(("--" + boundary + "\r\n").getBytes());
outputStream.write(String.format("Content-Disposition: form-data; name=\"media\"; filename=\"%s\"\r\n", file.getName()).getBytes());
outputStream.write(String.format("Content-Type: %s\r\n\r\n", contentType).getBytes());
// 获取媒体文件的输入流(读取文件)
DataInputStream in = new DataInputStream(new FileInputStream(file));
byte[] buf = new byte[1024 * 8];
int size = 0;
while ((size = in.read(buf)) != -1) {
// 将媒体文件写到输出流(往微信服务器写数据)
outputStream.write(buf, 0, size);
}
// 请求体结束
outputStream.write(("\r\n--" + boundary + "--\r\n").getBytes());
outputStream.close();
in.close();
// 获取媒体文件上传的输入流(从微信服务器读数据)
InputStream inputStream = uploadConn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
StringBuffer buffer = new StringBuffer();
String str = null;
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
bufferedReader.close();
inputStreamReader.close();
// 释放资源
inputStream.close();
inputStream = null;
uploadConn.disconnect();
// 使用json解析 parseObject
jsonObject = JSONObject.parseObject(buffer.toString());
System.out.println(jsonObject);
//JSONObject jsonObject = upload(token, fileType, new File(filePath));
Object media_id = jsonObject.get("media_id");
media_id_str=media_id.toString();
System.err.println("------上传临时素材media_id---------" + media_id_str);
} catch (Exception e) {
System.out.println("上传文件失败!");
e.printStackTrace();
}
return media_id_str;
}
部署到服务端外部使用的时候,不可能传磁盘的路径,那么传的肯定是File,所以前端控制器还得封装以下,用body上传File
@ResponseBody
@PostMapping("/getfilemediaid")
public String getfilemediaid(@RequestParam("file") MultipartFile multipartFile) {
String access_token = Getaccess_token("", "");
// 将 MultipartFile 转换为 File 类型
File file = convertMultiPartToFile(multipartFile);
// 处理上传的文件
String mediaid = upload(access_token, "file", file);
return mediaid;
}
private File convertMultiPartToFile(MultipartFile file) {
File convFile = new File(file.getOriginalFilename());
try (FileOutputStream fos = new FileOutputStream(convFile)) {
fos.write(file.getBytes());
} catch (IOException e) {
// 处理异常
}
return convFile;
}
3、回调服务验证服务器的代码
①将腾讯的验证demo里面的关键类拷贝进工程
②编写回调服务验证接口
// URL验证的接口 必须是GET请求
@GetMapping("/callback")
@ResponseBody
public void callback(@RequestParam(name = "msg_signature") String signature, String timestamp, String nonce,
String echostr, final HttpServletResponse response) throws Exception {
log.info("get验签请求参数 msg_signature = {}, timestamp = {}, nonce = {} , echostr = {}", signature, timestamp, nonce, echostr);
// 参数1 回调服务中配置 的token、参数2 回调服务中配置的 key 参数3 回调服务中 的企业ID
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt("", "", "");
String sEchoStr = wxcpt.VerifyURL(signature, timestamp, nonce, echostr);
log.info("weixin-callback:get请求回调签名校验通过, result = " + sEchoStr);
PrintWriter out = null;
try {
out = response.getWriter();
//必须要返回解密之后的明文
if (!sEchoStr.equals(null)) {
System.out.println(sEchoStr);
log.info("验证成功!");
} else {
log.error("URL验证失败");
}
} catch (Exception e) {
e.printStackTrace();
}
out.write(sEchoStr);
out.flush();
}
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Random;
/**
* 提供接收和推送给企业微信消息的加解密接口(UTF8编码的字符串).
* <ol>
* <li>第三方回复加密消息给企业微信</li>
* <li>第三方收到企业微信发送的消息,验证消息的安全性,并对消息进行解密。</li>
* </ol>
* 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
* <ol>
* <li>在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:
* http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html</li>
* <li>下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt</li>
* <li>如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件</li>
* <li>如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件</li>
* </ol>
*/
public class WXBizMsgCrypt {
static Charset CHARSET = Charset.forName("utf-8");
Base64 base64 = new Base64();
byte[] aesKey;
String token;
String receiveid;
/**
* 构造函数
* @param token 企业微信后台,开发者设置的token
* @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey
* @param receiveid, 不同场景含义不同,详见文档
*
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
public WXBizMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException {
if (encodingAesKey.length() != 43) {
throw new AesException(AesException.IllegalAesKey);
}
this.token = token;
this.receiveid = receiveid;
aesKey = Base64.decodeBase64(encodingAesKey + "=");
}
// 生成4个字节的网络字节序
byte[] getNetworkBytesOrder(int sourceNumber) {
byte[] orderBytes = new byte[4];
orderBytes[3] = (byte) (sourceNumber & 0xFF);
orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
return orderBytes;
}
// 还原4个字节的网络字节序
int recoverNetworkBytesOrder(byte[] orderBytes) {
int sourceNumber = 0;
for (int i = 0; i < 4; i++) {
sourceNumber <<= 8;
sourceNumber |= orderBytes[i] & 0xff;
}
return sourceNumber;
}
// 随机生成16位字符串
String getRandomStr() {
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 16; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
/**
* 对明文进行加密.
*
* @param text 需要加密的明文
* @return 加密后base64编码的字符串
* @throws AesException aes加密失败
*/
String encrypt(String randomStr, String text) throws AesException {
ByteGroup byteCollector = new ByteGroup();
byte[] randomStrBytes = randomStr.getBytes(CHARSET);
byte[] textBytes = text.getBytes(CHARSET);
byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
byte[] receiveidBytes = receiveid.getBytes(CHARSET);
// randomStr + networkBytesOrder + text + receiveid
byteCollector.addBytes(randomStrBytes);
byteCollector.addBytes(networkBytesOrder);
byteCollector.addBytes(textBytes);
byteCollector.addBytes(receiveidBytes);
// ... + pad: 使用自定义的填充方式对明文进行补位填充
byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
byteCollector.addBytes(padBytes);
// 获得最终的字节流, 未加密
byte[] unencrypted = byteCollector.toBytes();
try {
// 设置加密模式为AES的CBC模式
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
// 加密
byte[] encrypted = cipher.doFinal(unencrypted);
// 使用BASE64对加密后的字符串进行编码
String base64Encrypted = base64.encodeToString(encrypted);
return base64Encrypted;
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.EncryptAESError);
}
}
/**
* 对密文进行解密.
*
* @param text 需要解密的密文
* @return 解密得到的明文
* @throws AesException aes解密失败
*/
String decrypt(String text) throws AesException {
byte[] original;
try {
// 设置解密模式为AES的CBC模式
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
// 使用BASE64对密文进行解码
byte[] encrypted = Base64.decodeBase64(text);
// 解密
original = cipher.doFinal(encrypted);
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.DecryptAESError);
}
String xmlContent, from_receiveid;
try {
// 去除补位字符
byte[] bytes = PKCS7Encoder.decode(original);
// 分离16位随机字符串,网络字节序和receiveid
byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
int xmlLength = recoverNetworkBytesOrder(networkOrder);
xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
from_receiveid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),
CHARSET);
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.IllegalBuffer);
}
// receiveid不相同的情况
if (!from_receiveid.equals(receiveid)) {
throw new AesException(AesException.ValidateCorpidError);
}
return xmlContent;
}
/**
* 将企业微信回复用户的消息加密打包.
* <ol>
* <li>对要发送的消息进行AES-CBC加密</li>
* <li>生成安全签名</li>
* <li>将消息密文和安全签名打包成xml格式</li>
* </ol>
*
* @param replyMsg 企业微信待回复用户的消息,xml格式的字符串
* @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
* @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
*
* @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
public String EncryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException {
// 加密
String encrypt = encrypt(getRandomStr(), replyMsg);
// 生成安全签名
if (timeStamp == "") {
timeStamp = Long.toString(System.currentTimeMillis());
}
String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);
// System.out.println("发送给平台的签名是: " + signature[1].toString());
// 生成发送的xml
String result = XMLParse.generate(encrypt, signature, timeStamp, nonce);
return result;
}
/**
* 检验消息的真实性,并且获取解密后的明文.
* <ol>
* <li>利用收到的密文生成安全签名,进行签名验证</li>
* <li>若验证通过,则提取xml中的加密消息</li>
* <li>对消息进行解密</li>
* </ol>
*
* @param msgSignature 签名串,对应URL参数的msg_signature
* @param timeStamp 时间戳,对应URL参数的timestamp
* @param nonce 随机串,对应URL参数的nonce
* @param postData 密文,对应POST请求的数据
*
* @return 解密后的原文
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
public String DecryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
throws AesException {
// 密钥,公众账号的app secret
// 提取密文
Object[] encrypt = XMLParse.extract(postData);
// 验证安全签名
String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString());
// 和URL中的签名比较是否相等
// System.out.println("第三方收到URL中的签名:" + msg_sign);
// System.out.println("第三方校验签名:" + signature);
if (!signature.equals(msgSignature)) {
throw new AesException(AesException.ValidateSignatureError);
}
// 解密
String result = decrypt(encrypt[1].toString());
return result;
}
/**
* 验证URL
* @param msgSignature 签名串,对应URL参数的msg_signature
* @param timeStamp 时间戳,对应URL参数的timestamp
* @param nonce 随机串,对应URL参数的nonce
* @param echoStr 随机串,对应URL参数的echostr
*
* @return 解密之后的echostr
* @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
*/
public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr)
throws AesException {
String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);
if (!signature.equals(msgSignature)) {
throw new AesException(AesException.ValidateSignatureError);
}
String result = decrypt(echoStr);
return result;
}
}
③ 回调服务数据解析接口
/**
*
*
* @param request request
* @param sMsgSignature 签名
* @param sTimestamp 时间戳
* @param sNonce 随机值
* @return success
*/
@ResponseBody
@PostMapping(value = "/callback")
public String callback(final HttpServletRequest request,
@RequestParam(name = "msg_signature") final String sMsgSignature,
@RequestParam(name = "timestamp") final String sTimestamp,
@RequestParam(name = "nonce") final String sNonce) throws Exception {
log.info("post验签请求参数 msg_signature = {}, timestamp = {}, nonce {}", sMsgSignature, sTimestamp, sNonce);
postCallback(request, sMsgSignature, sTimestamp, sNonce);
return "success";
}
public void postCallback(HttpServletRequest request, String sMsgSignature, String sTimestamp, String sNonce)
throws Exception {
WXBizMsgCrypt wxcpt = null;
FlowSpInfo account8=new FlowSpInfo();
// try {
InputStream inputStream = request.getInputStream();
String postData = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
wxcpt = new WXBizMsgCrypt("", "", "");
// sMsg是回传回来的数据,xml格式的
String sMsg = wxcpt.DecryptMsg(sMsgSignature, sTimestamp, sNonce, postData);
// 将xml格式转换为json对象,从而来获取里面的字段
JSONObject jsonObject2 = XML.toJSONObject(sMsg);
String jsonstr = jsonObject2.get("xml").toString();
Gson gn = new Gson();
JsonObject jsonObject3 = gn.fromJson(jsonstr, JsonObject.class);
System.out.println(jsonObject3);
// 这里解析了入库即可
}
注意验证成功过后,注释调第一个验证接口,否则不进入第二个
④ 流程表单封装
@Data
@Accessors(chain = true)
public class CommitApprovalApplyParam {
/**
* 申请人userid
*/
private String creator_userid;
/**
* 模板id
*/
private String template_id;
/**
* 审批人模式:0-通过接口指定审批人、抄送人(此时approver、notifyer等参数可用); 1-使用此模板在管理后台设置的审批流程,支持条件审批。默认为0
*/
private int use_template_approver;
/**
* 抄送方式:1-提单时抄送(默认值); 2-单据通过后抄送;3-提单和单据通过后抄送。仅use_template_approver为0时生效。
*/
private int notify_type;
/**
* 抄送人节点userid列表,仅use_template_approver为0时生效。
*/
private String[] notifyer;
/**
* 审批申请数据,可定义审批申请中各个控件的值,其中必填项必须有值,选填项可为空,数据结构同“获取审批申请详情”接口返回值中同名参数“apply_data”
*/
private JSONObject apply_data;
/**
* 审批流程信息,用于指定审批申请的审批流程,支持单人审批、多人会签、多人或签,可能有多个审批节点,仅use_template_approver为0时生效。
*/
private JSONArray approver;
/**
* 摘要信息,用于显示在审批通知卡片、审批列表的摘要信息,最多3行
*/
private JSONArray summary_list;
public CommitApprovalApplyParam() {
this.use_template_approver = 0;
this.apply_data = new JSONObject();
this.approver = new JSONArray();
this.summary_list = new JSONArray();
}
public CommitApprovalApplyParam(String creator_userid, String template_id) {
this();
this.creator_userid = creator_userid;
this.template_id = template_id;
}
public void setNotifyer(String... notifyer){
this.notifyer = notifyer;
}
/**
* 添加审批人
*
* @param attr 节点审批方式:1-或签;2-会签,仅在节点为多人审批时有效
* 会签:指同一个审批节点设置多个人,如ABC三人,三人会同时收到审批,需全部同意之后,审批才可到下一审批节点;
* 或签:指同一个审批节点设置多个人,如ABC三人,三人会同时收到审批,只要其中任意一人审批即可到下一审批节点;
//* @param userId 审批节点审批人userid列表,若为多人会签、多人或签,需填写每个人的userid
*/
public void addApprover(int attr, String... userId) {
JSONObject json = new JSONObject();
json.put("attr", attr);
json.put("userid", userId);
this.approver.add(json);
}
/**
* 审批申请详情
*
* @param contents 审批申请详情,由多个表单控件及其内容组成,其中包含需要对控件赋值的信息
*/
public void setApplyDataContents(JSONObject... contents) {
// 审批申请详情,由多个表单控件及其内容组成,其中包含需要对控件赋值的信息
this.apply_data.put("contents", contents);
}
/**
* 摘要信息,用于显示在审批通知卡片、审批列表的摘要信息,最多3行
*
* @param text 摘要行显示文字,用于记录列表和消息通知的显示,不要超过20个字符
*/
public void setSummaryInfo(String... text) {
for (String s : text) {
JSONArray summaryInfoArr = new JSONArray();
JSONObject textObj = new JSONObject();
textObj.put("text",s);
textObj.put("lang","zh_CN");
summaryInfoArr.add(textObj);
JSONObject summaryInfo = new JSONObject();
summaryInfo.put("summary_info",summaryInfoArr);
// 摘要信息,用于显示在审批通知卡片、审批列表的摘要信息,最多3行
this.summary_list.add(summaryInfo);
}
}
/**
* 文本/多行文本控件(control参数为Text或Textarea)
*
* @param id 控件ID
* @param text 文本内容,在此填写文本/多行文本控件的输入值
*/
public JSONObject controlText(String id, String text) {
JSONObject value = new JSONObject();
value.put("text", text);
return createControl(id, ControlTypeConstant.text, value);
}
/**
* 文本/多行文本控件(control参数为Text或Textarea)
*
* @param id 控件ID
//* @param text 文本内容,在此填写文本/多行文本控件的输入值
*/
public JSONObject controlTextarea(String id, String textarea) {
JSONObject value = new JSONObject();
value.put("textarea", textarea);
return createControl(id, ControlTypeConstant.textarea, value);
}
/**
* 数字控件(control参数为Number)
*
* @param id 控件ID
* @param new_number 数字内容,在此填写数字控件的输入值
*/
public JSONObject controlNumber(String id, String new_number) {
JSONObject value = new JSONObject();
value.put("new_number", new_number);
return createControl(id, ControlTypeConstant.number, value);
}
/**
* 金额控件(control参数为Money)
*
* @param id 控件ID
* @param new_money 金额内容,在此填写金额控件的输入值
*/
public JSONObject controlMoney(String id, String new_money) {
JSONObject value = new JSONObject();
value.put("new_money", new_money);
return createControl(id, ControlTypeConstant.money, value);
}
/**
* 日期/日期+时间控件(control参数为Date)
*
* @param id 控件ID
* @param type 时间展示类型:day-日期;hour-日期+时间 ,和对应模板控件属性一致
* @param s_timestamp 时间戳-字符串类型,在此填写日期/日期+时间控件的选择值,以此为准
*/
public JSONObject controlDate(String id, String type, String s_timestamp) {
JSONObject value = new JSONObject();
JSONObject date = new JSONObject();
value.put("date", date);
date.put("type", type);
date.put("s_timestamp", s_timestamp);
return createControl(id, ControlTypeConstant.date, value);
}
/**
* 单选/多选控件(control参数为Selector)
*
* @param id 控件ID
* @param type 选择方式:single-单选;multi-多选
* @param key 选项key,可通过“获取审批模板详情”接口获得
*/
public JSONObject controlSelector(String id, String type, String... key) {
JSONObject value = new JSONObject();
JSONObject selector = new JSONObject();
JSONArray options = new JSONArray();
value.put("selector", selector);
selector.put("type", type);
selector.put("options", options);
for (String s : key) {
JSONObject option = new JSONObject();
option.put("key", s);
options.add(option);
}
return createControl(id, ControlTypeConstant.selector, value);
}
/**
* 成员控件(control参数为Contact,且value参数为members)
*
* @param id 控件ID
* @param userIds 所选成员的userid
* @param names 成员名
*/
public JSONObject controlMembers(String id, List<String> userIds, List<String> names) {
JSONObject value = new JSONObject();
JSONArray members = new JSONArray();
value.put("members", members);
for (int i = 0; i < userIds.size(); i++) {
JSONObject member = new JSONObject();
member.put("userid", userIds.get(i));
member.put("name", names.get(i));
members.add(member);
}
return createControl(id, ControlTypeConstant.contact, value);
}
/**
* 部门控件(control参数为Contact,且value参数为departments)
*
* @param id 控件ID
* @param openapiId 所选部门id
* @param names 所选部门名
*/
public JSONObject controlDepartments(String id, List<String> openapiId, List<String> names) {]
JSONObject value = new JSONObject();
JSONArray members = new JSONArray();
value.put("departments", members);
for (int i = 0; i < openapiId.size(); i++) {
JSONObject member = new JSONObject();
member.put("openapi_id", openapiId.get(i));
member.put("name", names.get(i));
members.add(member);
}
return createControl(id, ControlTypeConstant.contact, value);
}
/**
* 附件控件(control参数为File,且value参数为files)
*
* @param id 控件ID
* @param fileid 文件id,该id为临时素材上传接口返回的的media_id,注:提单后将作为单据内容转换为长期文件存储;目前一个审批申请单,全局仅支持上传6个附件,否则将失败。
*/
public JSONObject controlFile(String id, String... fileid) {
JSONObject value = new JSONObject();
JSONArray files = new JSONArray();
value.put("files", files);
for (String s : fileid) {
JSONObject file = new JSONObject();
file.put("file_id", s);
files.add(file);
}
return createControl(id, ControlTypeConstant.file, value);
}
/**
* 明细控件(control参数为Table)
*
* @param id 控件ID
* @param table children 明细内容,一个明细控件可能包含多个子明细
* list 子明细列表,在此填写子明细的所有子控件的值,子控件的数据结构同一般控件
*/
public JSONObject controlTable(String id, JSONObject[]... table) {
JSONObject value = new JSONObject();
JSONArray rows = new JSONArray();
value.put("children", rows);
for (JSONObject[] row : table) {
JSONObject rowValue = new JSONObject();
rowValue.put("list",row);
rows.add(rowValue);
}
return createControl(id, ControlTypeConstant.file, value);
}
/**
* table 明细
* @param cols 这里就是多个控件值调用createControl方法
* @return 控件数组
*/
public JSONObject[] tableRow(JSONObject... cols){
return cols;
}
public JSONObject createControl(String id, String type, JSONObject value) {
JSONObject control = new JSONObject();
control.put("id", id);
control.put("control", type);
control.put("value", value);
return control;
}
}
⑤ 发起流程接口封装
字段
具体的代码:
@Autowired
private RestTemplate restTemplate;
@ResponseBody
@PostMapping("/startles")
public String startles(String spuseridjson,String spuseridjsoncs, String creatoruserid, String flowtype, String flowfrom, String spnr,@RequestParam(required = false) String spbz,@RequestParam(required = false)String mediaid, String spnrzy, int notifytype) {
String result="";
// 附件和备注都不为null的情况下
if (spbz != null && !spbz.isEmpty()&&mediaid != null && !mediaid.isEmpty()){
CommitApprovalApplyParam param = new CommitApprovalApplyParam(creatoruserid, CommonConstant.TEMPLATEID);
ObjectMapper mapper = new ObjectMapper();
Map<Integer, String> spr = null;
try {
spr = mapper.readValue(spuseridjson, new TypeReference<Map<Integer, String>>() {
});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
for (Map.Entry<Integer, String> entry : spr.entrySet()) {
param.addApprover(entry.getKey(), entry.getValue());
}
long currentTimeMillis = System.currentTimeMillis() / 1000;
String currentTimeString1 = String.valueOf(currentTimeMillis);
param.setApplyDataContents(
// 流程类型
param.controlText("Text-1709360916989", flowtype),
// 流程来源
param.controlText("Text-1709361014320", flowfrom),
// 需审核内容
param.controlText("Text-1709540632195", spnr),
// 备注 非必填
param.controlText("Text-1709540972926", spbz),
//创建日期 取当前电脑日期即服务器时间戳
param.controlDate("Date-1709361002874", "day", currentTimeString1),
// 附件
param.controlFile("File-1709360996293", mediaid)
);
//String spuseridjsoncs = "{\"1\":\"20425\", \"2\":\"20425\"}";
//摘要信息
param.setSummaryInfo(spnrzy);
Map<Integer, String> spr2 = null;
try {
spr2 = mapper.readValue(spuseridjsoncs, new TypeReference<Map<Integer, String>>() {
});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
for (Map.Entry<Integer, String> entry : spr2.entrySet()) {
param.setNotifyer(entry.getValue());
}
//抄送方式:1-提单时抄送(默认值); 2-单据通过后抄送;3-提单和单据通过后抄送。仅use_template_approver为0时生效。
param.setNotify_type(notifytype);
//System.out.println(JSON.toJSONString(param));
result= commitApprovalApply(param);
}else if(!(spbz != null && !spbz.isEmpty())&&mediaid != null && !mediaid.isEmpty()){
CommitApprovalApplyParam param = new CommitApprovalApplyParam(creatoruserid, CommonConstant.TEMPLATEID);
ObjectMapper mapper = new ObjectMapper();
Map<Integer, String> spr = null;
try {
spr = mapper.readValue(spuseridjson, new TypeReference<Map<Integer, String>>() {
});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
for (Map.Entry<Integer, String> entry : spr.entrySet()) {
param.addApprover(entry.getKey(), entry.getValue());
}
long currentTimeMillis = System.currentTimeMillis() / 1000;
String currentTimeString1 = String.valueOf(currentTimeMillis);
param.setApplyDataContents(
// 流程类型
param.controlText("Text-1709360916989", flowtype),
// 流程来源
param.controlText("Text-1709361014320", flowfrom),
// 需审核内容
param.controlText("Text-1709540632195", spnr),
// 备注 非必填
//param.controlText("Text-1709540972926", spbz),
//创建日期 取当前电脑日期即服务器时间戳
param.controlDate("Date-1709361002874", "day", currentTimeString1),
// 附件
param.controlFile("File-1709360996293", mediaid)
);
//摘要信息
param.setSummaryInfo(spnrzy);
Map<Integer, String> spr2 = null;
try {
spr2 = mapper.readValue(spuseridjsoncs, new TypeReference<Map<Integer, String>>() {
});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
for (Map.Entry<Integer, String> entry : spr2.entrySet()) {
param.setNotifyer(entry.getValue());
}
//抄送方式:1-提单时抄送(默认值); 2-单据通过后抄送;3-提单和单据通过后抄送。仅use_template_approver为0时生效。
param.setNotify_type(notifytype);
//System.out.println(JSON.toJSONString(param));
result= commitApprovalApply(param);
}else if(!(mediaid != null && !mediaid.isEmpty())&&spbz != null && !spbz.isEmpty()){
CommitApprovalApplyParam param = new CommitApprovalApplyParam(creatoruserid, CommonConstant.TEMPLATEID);
ObjectMapper mapper = new ObjectMapper();
Map<Integer, String> spr = null;
try {
spr = mapper.readValue(spuseridjson, new TypeReference<Map<Integer, String>>() {
});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
for (Map.Entry<Integer, String> entry : spr.entrySet()) {
param.addApprover(entry.getKey(), entry.getValue());
}
long currentTimeMillis = System.currentTimeMillis() / 1000;
String currentTimeString1 = String.valueOf(currentTimeMillis);
param.setApplyDataContents(
// 流程类型
param.controlText("Text-1709360916989", flowtype),
// 流程来源
param.controlText("Text-1709361014320", flowfrom),
// 需审核内容
param.controlText("Text-1709540632195", spnr),
// 备注 非必填
param.controlText("Text-1709540972926", spbz),
//创建日期 取当前电脑日期即服务器时间戳
param.controlDate("Date-1709361002874", "day", currentTimeString1)
// 附件
//param.controlFile("File-1709360996293", mediaid)
);
//String spuseridjsoncs = "{\"1\":\"20425\", \"2\":\"20425\"}";
//摘要信息
param.setSummaryInfo(spnrzy);
Map<Integer, String> spr2 = null;
try {
spr2 = mapper.readValue(spuseridjsoncs, new TypeReference<Map<Integer, String>>() {
});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
for (Map.Entry<Integer, String> entry : spr2.entrySet()) {
param.setNotifyer(entry.getValue());
}
//抄送方式:1-提单时抄送(默认值); 2-单据通过后抄送;3-提单和单据通过后抄送。仅use_template_approver为0时生效。
param.setNotify_type(notifytype);
//System.out.println(JSON.toJSONString(param));
result= commitApprovalApply(param);
}
else {
CommitApprovalApplyParam param = new CommitApprovalApplyParam(creatoruserid, CommonConstant.TEMPLATEID);
ObjectMapper mapper = new ObjectMapper();
Map<Integer, String> spr = null;
try {
spr = mapper.readValue(spuseridjson, new TypeReference<Map<Integer, String>>() {
});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
for (Map.Entry<Integer, String> entry : spr.entrySet()) {
param.addApprover(entry.getKey(), entry.getValue());
}
long currentTimeMillis = System.currentTimeMillis() / 1000;
String currentTimeString1 = String.valueOf(currentTimeMillis);
param.setApplyDataContents(
// 流程类型
param.controlText("Text-1709360916989", flowtype),
// 流程来源
param.controlText("Text-1709361014320", flowfrom),
// 需审核内容
param.controlText("Text-1709540632195", spnr),
// 备注 非必填
//param.controlText("Text-1709540972926", spbz),
//创建日期 取当前电脑日期即服务器时间戳
param.controlDate("Date-1709361002874", "day", currentTimeString1)
// 附件
//param.controlFile("File-1709360996293", mediaid)
);
//摘要信息
param.setSummaryInfo(spnrzy);
Map<Integer, String> spr2 = null;
try {
spr2 = mapper.readValue(spuseridjsoncs, new TypeReference<Map<Integer, String>>() {
});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
for (Map.Entry<Integer, String> entry : spr2.entrySet()) {
param.setNotifyer(entry.getValue());
}
//抄送方式:1-提单时抄送(默认值); 2-单据通过后抄送;3-提单和单据通过后抄送。仅use_template_approver为0时生效。
param.setNotify_type(notifytype);
//System.out.println(JSON.toJSONString(param));
result= commitApprovalApply(param);
}
return result;
}
工具方法
public String commitApprovalApply(CommitApprovalApplyParam param) {
String access_token = Getaccess_token("", "");
String url = "https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=ACCESS_TOKEN"
.replace("ACCESS_TOKEN", access_token);
ResponseEntity<com.alibaba.fastjson.JSONObject> responseEntity = restTemplate.postForEntity(url, param, com.alibaba.fastjson.JSONObject.class);
//System.out.println("调用结果:" + responseEntity);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
com.alibaba.fastjson.JSONObject body = responseEntity.getBody();
if (body.getInteger("errcode") == 0) {
return body.getString("sp_no");
}
throw new RuntimeException("提交审批流程失败:" + body.getString("errmsg"));
}
throw new RuntimeException("提交审批流程请求访问失败");
}
其他代码,tomcat会拦截地址栏的json格式作为地址栏的参数,需要写一个配置类
@Configuration
public class TomcatConfig {
@Bean
public TomcatServletWebServerFactory webServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.addConnectorCustomizers((Connector connector) -> {
connector.setProperty("relaxedPathChars", "\"<>[\\]^`{|}");
connector.setProperty("relaxedQueryChars", "\"<>[\\]^`{|}");
});
return factory;
}
}
第五章 联调-客户端服务端调试
流程启动通过封装成接口哪里调用就哪里使用,同回调服务装在一起的,通过测试最终返回流程号则调试成功,最后服务器会解析对应的字段到数据库,数据库使用的是MySQL+MybatisFlex
表字段
CREATE TABLE `flowspinfo1` (
`sp_No` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '审批流程编号',
`create_Time` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建时间',
`event` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '事件名称',
`to_User_Name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程发起人',
`from_User_Name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '发生介质',
`msg_Type` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '消息类型',
`agent_i_d` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '发送消息应用',
`sp_Status` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程申请状态',
`apply_Time` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程申请时间戳',
`applyer` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '流程发起人信息 ',
`template_Id` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '申请人提交的审批模板',
`notifyer` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '抄送人',
`sp_Record` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '审批节点情况',
`sp_Name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '审批流程模板名称',
`statu_Change_Event` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '审批申请状态变化类型'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
Mybatis-flex的配置我使用的是土办法,方式调用即非Spring项目的方式,写类进行连接配置如下:
public class DatabaseConfig {
public static BasicDataSource getDataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("");
dataSource.setUsername("root");
String encodedPassword = "";
byte[] decodedBytes= Base64.getDecoder().decode(encodedPassword);
String password=new String(decodedBytes);
dataSource.setPassword(password);
return dataSource;
}
public static <T> T access(Class<T> mapperClass) {
BasicDataSource dataSource = DatabaseConfig.getDataSource();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()
.setDataSource(dataSource)
// 打印执行得SQL
.setLogImpl(CustomLogImpl.class)
.addMapper(mapperClass)
.start();
return mapperClass.cast(bootstrap.getMapper(mapperClass));
}
}
去掉日志中的连接打印,自定义日志
public class CustomLogImpl implements Log {
private final String jdbcConnectionMessage = "Closing JDBC Connection";
public CustomLogImpl(String clazz) {
// Do Nothing
}
@Override
public boolean isDebugEnabled() {
return true; // 根据实际需求返回相应的值
}
@Override
public boolean isTraceEnabled() {
return true; // 根据实际需求返回相应的值
}
@Override
public void error(String s, Throwable e) {
System.err.println(s);
e.printStackTrace(System.err);
}
@Override
public void error(String s) {
System.err.println(s);
}
@Override
public void debug(String s) {
if (!s.contains(jdbcConnectionMessage)) {
System.out.println(s);
}
}
@Override
public void trace(String s) {
if (!s.contains(jdbcConnectionMessage)) {
System.out.println(s);
}
}
@Override
public void warn(String s) {
if (!s.contains(jdbcConnectionMessage)) {
System.out.println(s);
}
}
}
实体类的编写
import com.mybatisflex.annotation.Table;
import lombok.Data;
@Table("flowspinfo")
@Data
public class FlowSpInfo {
// 创建时间 createTime
//private String createTime;
// 事件名称 Event
private String event;
// 流程发起人 ToUserName
private String toUserName;
// 发生介质 FromUserName
private String fromUserName;
// 消息类型 MsgType
private String msgType;
// 发送消息应用 AgentID
private String agentID;
// 流程申请状态 spStatus
private String spStatus;
// 流程申请时间戳 ApplyTime
private String applyTime;
// 审批流程编号 SpNo
private String spNo;
// 流程发起人信息 Applyer
private String applyer;
// 申请人提交的审批模板 TemplateId
private String templateId;
// 抄送人 Notifyer
private String notifyer;
// 审批节点情况 SpRecord
private String spRecord;
// 审批流程模板名称
private String SpName;
}
使用Mybatis-flex
// 数据库操作接口
public interface FlowSpInfoMapper extends BaseMapper<FlowSpInfo> {
}
具体使用方法示例
FlowSpInfoMapper Execute = DatabaseConfig.access(FlowSpInfoMapper.class);
@Test
void Test_db_mybatisflex(){
FlowSpInfo flowSpInfo =new FlowSpInfo();
flowSpInfo.setSpNo("202403050158");
FlowSpInfo newaccount= UpdateWrapper.of(account8)
.toEntity();
Execute.insert(newaccount);
}
注意:回调服务回来的json字符串的解析代码我没有贴太长了,可以自己转换为对象通过键值的方式去获取即可。
总结
提示:看到这里我想您多少有些收货:觉得可以可以收藏点赞或者打赏即可
以上就是企业微信调用的基本操作,觉得可以可以打赏请我喝一杯咖啡或者小额打赏即可。