再说一次,企业微信开发文档很坑,特别坑,坑成鬼了。
有时候浪费很长时间改bug,查来查去,都没有发现到底哪里错了,查资料,搜资料,询问大神,都没有发现bug。改到最后都抑郁了,才发现用浏览器访问根本就不会报出具体的错误,pc端的企业微信客户端也不行,只有用手机上的企业微信客户端访问才会有具体的错误信息,在这方面浪费了很长时间,感觉自己有点傻,但是内心不承认。
总之,要想一帆风顺开发,听起来像是开玩笑,总会遇到让自己怀疑人生的bug。
文档对于很多细节描述不具体,而且也有很多东西没有demo可以参观,有demo的还是PHP,怎么说呢,有些东西想在网上查查,都查不到,所有,在开始之前,请让我先好好吐槽一下,以解我这几天的心头之火。
好吧,废话不多说,总不能转行吧!所以还是好好学习吧!!
企业微信自建应用审批接口开发:
企业微信自带有审批接口,最主要是,使用企业微信审批能力,在非审批应用内设置流程、发起审批。还能订阅通知消息,接收审批状态变化情况。
现在很多企业都使用企业微信审批接口,使用企业微信审批接口,审批发生变化,会自动发送消息给审批人以及抄送人,比较方便。微信自带的审批接口适用于小企业,简单的业务。一般大公司都会开发适用于自己企业的OA系统。
企业微信自建应用审批接口开发:
1.首先在企业微信后端,创建应用,进入审批接口,添加模板
2.自建应用发起审批
通过JS-SDK,可在自建应用中发起审批,需要设置可信域名,花生壳免费域名,我试了,不行!
3.设置工作台应用主页,在主页内发起审批
注意:
1.测试时一定要用手机企业微信客户端,会显示详细错误信息,用浏览器或者pc端, 比较坑爹。
2.善于利用签名工具,验证自己各数据是否正确生成
1.首先在企业微信后端,创建应用,进入审批接口,添加模板
2.自建应用发起审批
点击接入流程说明,进入开发API
开发逻辑为上图所示
2.1
通过JS-SDK,可在自建应用中发起审批。查看JS-SDK调用详细说明
具体步骤:
1.通过config接口注入权限验证配置
2.通过agentConfig注入应用的权限
3.调用审批流程引擎JS-API
1.通过config接口注入权限验证配置
wx.config({
beta: true,// 必须这么写,否则wx.invoke调用形式的jsapi会有问题
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: \'\', // 必填,企业微信的corpID
timestamp:\'\' , // 必填,生成签名的时间戳
nonceStr: \'\', // 必填,生成签名的随机串
signature: \'\',// 必填,签名,见 附录-JS-SDK使用权限签名算法
jsApiList: [\'agentConfig\',\'thirdPartyOpenPage\',\'selectExternalContact\'] // 必填,需要使用的JS接口列表,凡是要调用的接口都需要传进来
});
wx.ready(function(){
2.通过agentConfig注入应用的权限。
wx.agentConfig({
corpid: \'\', // 必填,企业微信的corpid,必须与当前登录的企业一致
agentid: \'\', // 必填,企业微信的应用id
timestamp: , // 必填,生成签名的时间戳
nonceStr: \'\', // 必填,生成签名的随机串
signature: \'\',// 必填,签名,见附录1
jsApiList: [\'thirdPartyOpenPage\',\'selectExternalContact\'], //必填
success: function(res) {
3.调用审批流程引擎JS-API
wx.invoke(\'thirdPartyOpenPage\', {
"oaType": "10001",// String
"templateId": "46af67a118a6ebf000002",// String
"thirdNo": "thirdNo",// String
"extData": {
\'fieldList\': [{
\'title\': \'采购类型\',
\'type\': \'text\',
\'value\': \'市场活动\',
},
{
\'title\': \'订单链接\',
\'type\': \'link\',
\'value\': \'https://work.weixin.qq.com\',
}],
}
},
function(res) {
// 输出接口的回调信息
console.log(res);
});
},
fail: function(res) {
if(res.errMsg.indexOf(\'function not exist\') > -1){
alert(\'版本过低请升级\')
}
}
});
});
各个接口都调用成功的话,那么离成功就不远了;
2.1.jsp页面主要代码
<script type="text/javascript">
function approval() {
//动态获取当前页面url
var link = window.location.href;
$.ajax({
type:"GET",
data:{"url":link},
url:"<%=request.getContextPath()%>/approval/send_approval.do",
dataType:"json",
success:function (data) {
console.log(data);
wx.config({
beta: true,
debug: true,
appId: data.appId,
timestamp: data.config_timestamp,
nonceStr: data.config_nonceStr,
signature: data.config_signature,
jsApiList: [\'agentConfig\',\'openUserProfile\',\'thirdPartyOpenPage\',\'selectExternalContact\']
});
wx.ready(function () {
alert("config");
wx.agentConfig({
corpid: data.appId,
agentid: data.agentid,
timestamp: data.agent_timestamp,
nonceStr: data.agent_nonceStr,
signature: data.agent_signature,
jsApiList: [\'thirdPartyOpenPage\',\'selectExternalContact\'],
success: function(res) {
//审批流程js调用
alert("agentConfig");
wx.invoke(\'thirdPartyOpenPage\', {
"oaType": data.oaType,
"templateId": data.templateId,
"thirdNo": data.thirdNo,
"extData": {
\'fieldList\': [{
\'title\': \'采购类型\',
\'type\': \'text\',
\'value\': \'市场活动\',
},
{
\'title\': \'订单链接\',
\'type\': \'link\',
\'value\': \'https://work.weixin.qq.com\',
}],
}
},
function(res) {
// 输出接口的回调信息
alert("thirdPartyOpenPage");
alert(res);
}
);
},
fail: function(res) {
alert("approval提交不通过");
alert("agentConfig:"+res.errMsg);
if(res.errMsg.indexOf(\'function not exist\') > -1){
alert(\'版本过低请升级\')
}
}
});
});
wx.error(function(res){
});
}
})
}
</script>
2.2后端主要代码
@RequestMapping(value = "send_approval.do")
public void sendApproval(HttpServletRequest request, HttpServletResponse response,String url)throws Exception{
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
JSONObject jsonObject = iApprovalService.approvalCreate(url).getData();
//返回数据到jsp页面
response.getWriter().print(jsonObject);
}
public JSONObject approvalCreate(String url){
JSONObject object = new JSONObject();
//agentConfig接口下主要数据
String approval_ticket = JS_Util.getApprovalTicket();
JSONObject appJSONObject = JS_Util.getSignature(url,approval_ticket);
object.put("agent_nonceStr",appJSONObject.get("nonceStr").toString());
object.put("agent_timestamp",appJSONObject.get("timestamp").toString());
object.put("agent_signature",appJSONObject.get("signature").toString());
System.out.println("===============================");
System.out.println(appJSONObject.toString());
//config接口下主要数据
String ticket = JS_Util.getJsApiTicket();
JSONObject jsonObject = JS_Util.getSignature(url,ticket);
object.put("config_nonceStr",jsonObject.get("nonceStr").toString());
object.put("config_timestamp",jsonObject.get("timestamp").toString());
object.put("config_signature",jsonObject.get("signature").toString());
System.out.println("===========================");
System.out.println(jsonObject.toString());
//invoke接口下主要数据
object.put("thirdNo",String.valueOf(generateOrderNo()));
object.put("appId",PropertiesUtil.getProperty("corpid"));
object.put("agentid",PropertiesUtil.getProperty("mycreateapp_agentid"));
object.put("templateId",PropertiesUtil.getProperty("approval_id"));
object.put("oaType","10001");
return object;
}
signatrue的生成
可以使用签名工具,查看生成的signatrue是否正确,可以快速的排除自己代码的错误
signature生成
签名算法
签名生成规则如下:
参与签名的参数有四个: noncestr(随机字符串), jsapi_ticket, timestamp(时间戳), url(当前网页的URL, 不包含#及其后面部分)
将这些参数使用URL键值对的格式 (即 key1=value1&key2=value2…)拼接成字符串string1。
有两个注意点:1. 字段值采用原始值,不要进行URL转义;2. 必须严格按照如下格式拼接,不可变动字段顺序。
jsapi_ticket=JSAPITICKET&noncestr=NONCESTR×tamp=TIMESTAMP&url=URL
然后对string1作sha1加密即可。
注意事项
签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同。
签名用的url必须是调用JS接口页面的完整URL。
出于安全考虑,开发者必须在服务器端实现签名的逻辑。
public class JS_Util {
private static Logger logger = LoggerFactory.getLogger(JS_Util.class);
//获取企业的jsapi_ticket
private final static String GET_WX_TICKET = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN";
//应用jsapi_tictet
private final static String GET_APP_TICKET = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=ACCESS_TOKEN&type=agent_config";
//获取企业的ticket
public static String getJsApiTicket(){
String access_token = WeiXinUtil.getAccessToken(PropertiesUtil.getProperty("corpid"),PropertiesUtil.getProperty("mycreateapp_secret")).getAccess_token();
String url = GET_WX_TICKET.replace("ACCESS_TOKEN",access_token);
JSONObject jsonObject = WeiXinUtil.httpRequest(url,"GET",null);
String ticket = jsonObject.get("ticket").toString();
return ticket;
}
//获取审批流程ticket
public static String getApprovalTicket(){
String access_token = WeiXinUtil.getAccessToken(PropertiesUtil.getProperty("corpid"),PropertiesUtil.getProperty("mycreateapp_secret")).getAccess_token();
String url = GET_APP_TICKET.replace("ACCESS_TOKEN",access_token);
JSONObject jsonObject = WeiXinUtil.httpRequest(url,"GET",null);
String ticket = jsonObject.get("ticket").toString();
return ticket;
}
/**
* 参与签名的参数有四个: noncestr(随机字符串), jsapi_ticket, timestamp(时间戳), url(当前网页的URL, 不包含#及其后面部分)
* 将这些参数使用URL键值对的格式 (即 key1=value1&key2=value2…)拼接成字符串string1。
* 然后对string1作sha1加密即可。
* @return
*/
public static JSONObject getSignature(String url,String ticket){
JSONObject jsonObject = new JSONObject();
String noncestr = getRandomString(16);
String timestamp = Long.toString(System.currentTimeMillis()).substring(0,10);
// jsapi_ticket=JSAPITICKET&noncestr=NONCESTR×tamp=TIMESTAMP&url=URL 顺序不能更改
String str = "jsapi_ticket="+ticket+
"&noncestr="+noncestr+
"×tamp="+timestamp+
"&url="+url;
String signature = SHA1(str);
jsonObject.put("nonceStr",noncestr);
jsonObject.put("timestamp",timestamp);
jsonObject.put("signature",signature);
jsonObject.put("ticket",ticket);
return jsonObject;
}
private static String SHA1(String decript){
try{
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(decript.getBytes());
byte[] digest = md.digest();
StringBuffer hexstr = new StringBuffer();
for (int i = 0; i < digest.length; i++) {
String shaHex = Integer.toHexString(digest[i] & 0xFF);
if (shaHex.length() < 2) {
hexstr.append(0);
}
hexstr.append(shaHex);
}
return hexstr.toString();
}catch (Exception e){
logger.error("sha1加密错误",e);
}
return "";
}
private static String getRandomString(int length){
String keyString = "ergrfewfwdgggcvv;uihefujsncjdvngrjegeuirgverggvbergbvuigverug";
int len = keyString.length();
StringBuffer str = new StringBuffer();
for(int i=0;i<length;i++){
str.append(keyString.charAt((int) Math.round(Math.random() * (len - 1))));
}
return str.toString();
}
}
获取企业的ticket和应用的ticket,请求的url不同,但是使用的accessToken是相同的
public class WeiXinUtil {
private static Logger log = LoggerFactory.getLogger(WeiXinUtil.class);
//微信的请求url
//获取access_token的接口地址(GET)
public final static String access_token_url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corpId}&corpsecret={corpsecret}";
/**
* 1.发起https请求并获取结果
*/
public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
StringBuffer buffer = new StringBuffer();
try {
// 创建SSLContext对象,并使用我们指定的信任管理器初始化
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();
httpUrlConn.setSSLSocketFactory(ssf);
httpUrlConn.setDoOutput(true);
httpUrlConn.setDoInput(true);
httpUrlConn.setUseCaches(false);
// 设置请求方式(GET/POST)
httpUrlConn.setRequestMethod(requestMethod);
if ("GET".equalsIgnoreCase(requestMethod))
httpUrlConn.connect();
// 当有数据需要提交时
if (null != outputStr) {
OutputStream outputStream = httpUrlConn.getOutputStream();
// 注意编码格式,防止中文乱码
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 将返回的输入流转换成字符串
InputStream inputStream = httpUrlConn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
bufferedReader.close();
inputStreamReader.close();
// 释放资源
inputStream.close();
inputStream = null;
httpUrlConn.disconnect();
jsonObject = JSONObject.fromObject(buffer.toString());
} catch (ConnectException ce) {
log.error("Weixin server connection timed out.");
} catch (Exception e) {
log.error("https request error:{}", e);
}
return jsonObject;
}
/**
* 获取应用的access_token
*/
public static AccessToken getAccessToken(String appid, String appsecret) {
AccessToken accessToken = null;
String requestUrl = access_token_url.replace("{corpId}", appid).replace("{corpsecret}", appsecret);
JSONObject jsonObject = httpRequest(requestUrl, "GET", null);
// 如果请求成功
if (null != jsonObject) {
try {
accessToken = new AccessToken();
accessToken.setAccess_token(jsonObject.getString("access_token"));
accessToken.setExpires_in(jsonObject.getInt("expires_in"));
} catch (JSONException e) {
accessToken = null;
// 获取token失败
log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
}
}
return accessToken;
}
}
主要的参数都被我统一放在了properties文件里,写了个工具类PropertiesUtil进行取出,方便管理。
3.设置工作台应用主页,在主页内发起审批,域名为设置的可信域名,刚开始用的花生壳,没有测试通过,域名所有权验证不通过
测试时一定要用手机企业微信客户端,会显示详细错误信息,用浏览器或者pc端, 比较坑爹。