手把手教你springboot企业微信开发(二)

  • 1、企业微信开发第一步
  • 2、weixin-java-cp-demo-master
  • 1)、引入thymeleaf改造项目
  • 2)增加Configuration
  • 3)回到企业号
  • 4)穿透内外网
  • 5)完善



从这一篇开始,开始从实际项目探索企业微信开发。

1、企业微信开发第一步

看一下企业微信的开发者文档: 企业微信开发者文档.

在开发文档的首页中,我们看到:企业&定制化服务商开发流程

1. 获取企业号CorpID&Secret: 企业管理员建立管理组,获取CorpID&Secret
2. 开发对接相关接口: 开发测试应用,对接企业号接口,接口文档qydev.weixin.qq.com

企业号的CorpID和Secret,在上一篇已经讲解了,这里不赘述。对接接口的内容,在后面也会使用,暂时按下不表。

看【主动调用】,文档中有个醒目的获取AccessToken而且备注是:你应该审慎配置管理组的权限,够用即好,权限过大会增加误操作可能性及信息安全隐患。对于我们开发者而言:做好缓存、注意有时间限制7200s。下面还说频率的问题等等,这个也做到心中有数吧。

我们先把关注点集中在获取access_token上。在项目中,使用get请求获取access_token,请求地址为:https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=id&corpsecret=secrect

【回调模式】在开发中用的也很多,使用的时候按照文档来吧。

我在一个项目中,写了一个微信开发的WeixinUtil,java写的未必那么完美。它的作用就是基于主动调用的,来看一下,这里一个推送的实现:

/**
	 * 企业号推送文章
	 */
sentPushMsg(List<String> userIds , PushMsgEntity pushMsg) {
		String appid = sysConfigService.getWeixinAppid();
    	String secret = sysConfigService.getWeixinSecret();
		String agentId = sysConfigService.getParamValue("agentId");
		
		String qytoken = apiRedis.getWeAccessToken(appid); //从缓存中获取token
		if(qytoken == null) {
			TokenResponse tokenResponse = WeixinUtil.token(appid,secret);
			try {
				qytoken = tokenResponse.getAccessToken();
				apiRedis.setWeAccessToken(appid , qytoken);
			}catch(Exception e) {
				return R.error(ErrorCode.WEIXIN_API_GET_TOKEN_ERROR, "微信获取AccessToken失败,请重新获取!");
			}
		}
		MessageCustomArticle article = new MessageCustomArticle();
		
		article.setTitle(pushMsg.getTitle());
		article.setDescription(pushMsg.getMessage());
		article.setUrl(pushMsg.getUrl());
		article.setPicurl(pushMsg.getPicurl());
		
		MessageCustomSendRequest  messageCustomSendRequest  = WeixinUtil.getMessageCustomSendRequest(userIds , article);
		if(!StringUtil.isBlank(agentId)) {
			messageCustomSendRequest.setAgentid(new Integer(agentId));
		}
		
		Response res = WeixinUtil.messageCustomSend(qytoken , messageCustomSendRequest);
		if(res == null) {
			//....略去了业务代码
			return R.error( ErrorCode.SENT_TEMPLATE_MSG_ERROR, "消息发送失败");
		}else {
			//....略去了业务代码
			return R.ok(res.getErrMsg()).put("code", res.getErrCode());
		}
		
	}

大家在自己的项目中,也一定会有自己封装的方式。

接着上一篇,这里用的是什么呢?我在上一篇里说到,引入weixin-java-cp的Jar包了,这个jar包有非常强悍的作用。

2、weixin-java-cp-demo-master

1)、引入thymeleaf改造项目

在pom.xml中引入spring-boot-starter-thymeleaf

<dependency>
		    <groupId>org.springframework.boot</groupId>
		    <artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

引入之后,application.yml中配置解析html:

spring:
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    mode: HTML5
    encoding: UTF-8
    servlet:
        content-type: text/html
    cache: false

稍微解释一下,前缀prefix是地址,工程的相对路径,这边是404错误的根据地了吧,哈哈哈,得注意编译和请求得路径啊;后缀suffix需要spring解析的模板后缀,这里是html。mode、encoding、wevlet、cache就不说了哦。

在thymeleaf目录下,新建一个文件夹,thymeleaf,在文件夹中,新建一个first.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>first</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<p th:text="'Url:' + ${Url}"/>
<p th:text="'Error:' + ${Error}"/>
<p th:text="'Message:' + ${Message}"/>
<p th:text="'Exception:' + ${Exception}"/>
<p th:text="'Path:' + ${Path}"/>
<p th:text="'Status:' + ${Status}"/>
<p th:text="'Timestamp:' + ${Timestamp}"/>
</body>
</html>

看一下文件目录结构:

java企业微信开发 微信企业版开发_spring boot


新建一个ThemleafController.java,内容是:

package com.github.binarywang.demo.wx.cp.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/thymeleaf")
public class ThemleafController {

	  @GetMapping(value = "/first")
	  public String first(Model model) {
		model.addAttribute("Url", "thymeleaf/first");
		model.addAttribute("Error", "0");
		model.addAttribute("Path", "");
		model.addAttribute("Message", "");
		model.addAttribute("Exception", "");
		model.addAttribute("Status", "");
		model.addAttribute("Timestamp", System.currentTimeMillis());
	    return "/thymeleaf/first";
	  }

}

改一手application.yml

server:
  port: 8080

debug模式启动WxCpDemoApplication.java 在浏览器中输入http://localhost:8080/thymeleaf/first

java企业微信开发 微信企业版开发_java企业微信开发_02


出来它就可以了。

2)增加Configuration

springboot在很多配置项简洁得让人摸不着头脑,这里是基于springboot不得不说的就是这个,先上代码,然后解释:

package com.github.binarywang.demo.wx.cp.config;

import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

public class WebMvcConfig implements WebMvcConfigurer{
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }
}

这个Java文件的意思是以静态资源的形式访问/static/文件夹下的所有文件。

/static/**使用了**作为通配符。但是,并没有static文件夹,对吗?建立一个static文件夹,看一下项目结构:

java企业微信开发 微信企业版开发_web app_03

在里面放个txt文件文件名叫who_love_who.txt,文件里面写上自己喜欢的人的名字,文件名的话用我们的代码来试试看TA对你是否有意,哈哈哈。我的文件里面写的是:天赐我爱-love-天赐我爱。写好了测试文件,现在的项目结构是:

java企业微信开发 微信企业版开发_spring_04

重新debug

WxCpDemoApplication.java 启动好了之后,在浏览器输入:

http://localhost:8080/who_love_who.txt 我这边的是:

java企业微信开发 微信企业版开发_java_05


看到是乱码,我就放心了,她是爱谁谁,与我这单身技术狗无关!差不多了哦!配置起作用了。

3)回到企业号

回到企业号。找到上篇手把手教你springboot企业微信开发(一)中的应用配置页:

java企业微信开发 微信企业版开发_web app_06


拉到下面:

【开发者接口】–》点开【网页授权及JS-SDK】

java企业微信开发 微信企业版开发_spring_07


点,【 下载文件】的链接,下载了一个txt文件吧?好!这个文件,放到我之前说的那个static文件夹!现在的static文件夹:

java企业微信开发 微信企业版开发_java企业微信开发_08


是否疑问,我在大费周章在搞thymeleaf、txt静态资源是为了什么呢?下面细细说这个。

4)穿透内外网

访问链接: https://natapp.cn/login.

注册~~~购买,不不不申请免费隧道:

java企业微信开发 微信企业版开发_java_09


天底下最好的东西,一定的免费的,阳光是免费的、空气是免费的,爱也是免费的。点开刚刚申请的隧道:

java企业微信开发 微信企业版开发_web app_10


端口用个8080;authtoken复制备用。下载客户端:

java企业微信开发 微信企业版开发_java_11


自己选择吧。

下载完了,自己创建一个文件夹,就叫natapp得了,在里面放个文件,文件名config.ini在里面的配置:

#将本文件放置于natapp同级目录 程序将读取 [default] 段
#在命令行参数模式如 natapp -authtoken=xxx 等相同参数将会覆盖掉此配置
#命令行参数 -config= 可以指定任意config.ini文件
[default]
authtoken=myauthtoken                     #对应一条隧道的authtoken
clienttoken=                    #对应客户端的clienttoken,将会忽略authtoken,若无请留空,
log=none                        #log 日志文件,可指定本地文件, none=不做记录,stdout=直接屏幕输出 ,默认为none
loglevel=ERROR                  #日志等级 DEBUG, INFO, WARNING, ERROR 默认为 DEBUG
http_proxy=                     #代理设置 如 http://10.123.10.10:3128 非代理上网用户请务必留空

刚才我说复制的authtoken,粘贴在这里。打开natapp.exe有木有什么?

java企业微信开发 微信企业版开发_java_12


有类似这个吗?如果是那行的意思是:现在这个域名,已经穿透到了你本地的8080端口。

好,把我画线的部分复制到浏览器:

是不是有这样的画面:

java企业微信开发 微信企业版开发_spring_13


有的话,说明穿透成功了。把http://域名/thymeleaf/first复制下来,编码:

链接: url编码.把编码后的这段复制备用。把域名(不带http://的)复制下来备用。

5)完善

在pom.xml中引入jar包:

<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.45</version>
        </dependency>

引入这个fastjson是为了解析返回的字符串。

自动注入:

@Autowired
WxCpProperties wxCpProperties;

修改first方法:

@GetMapping(value = "/first")
	  public String first(ServletRequest request, ServletResponse response, Model model) {
		  
		  HttpServletRequest servletRequest = ((HttpServletRequest) request);
		  String userAgent = servletRequest.getHeader("user-agent");
		  
		  final WxCpService wxCpService = WxCpConfiguration
					.getCpService(wxCpProperties.getAppConfigs().get(0).getAgentId());

			
			String queryString = servletRequest.getQueryString();
			String backString = "";
			try {
				backString = HttpRequest.sendGet("https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token="
						+ wxCpService.getAccessToken() + "&" + queryString, null);
			} catch (WxErrorException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			JSONObject json_test = JSONObject.parseObject(backString);  
		  
		model.addAttribute("Url", "thymeleaf/first");
		model.addAttribute("Error", "0");
		model.addAttribute("Path", "");
		model.addAttribute("Message", "");
		model.addAttribute("Exception", "");
		model.addAttribute("Status", "");
		model.addAttribute("Timestamp", System.currentTimeMillis());
	    return "/thymeleaf/first";
	  }

这里用的HttpRequest.java是我自己写的:

package com.github.binarywang.demo.wx.cp.utils;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HttpRequest {
	
	public static Logger logger = LoggerFactory.getLogger(HttpRequest.class);
	
	/**
	 * 向指定URL发送GET方法的请求
	 * 
	 * @param url
	 *            发送请求的URL
	 * @param param
	 *            请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
	 * @return URL 所代表远程资源的响应结果
	 */
	public static String sendGet(String url, Map params) {
		return sendGet(url,"", params);
	}
	/**
	 * 向指定URL发送GET方法的请求
	 * 
	 * @param url
	 *            发送请求的URL
	 * @param param
	 *            请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
	 * @return URL 所代表远程资源的响应结果
	 */
	public static String sendGet(String url,String interfaceName, Map params) {
		String result = "";
		BufferedReader in = null;
		StringBuffer strBufParamsSet = new StringBuffer();
		
		if(params!=null) {
			Set<String> paramsSet = params.keySet();
			
			for(String parma:paramsSet) {
				if(strBufParamsSet.length()>0) {
					strBufParamsSet.append("&").append(parma).append("=").append(params.get(parma));
				}else {
					strBufParamsSet.append("?").append(parma).append("=").append(params.get(parma));
				}
			}
		}else {
			strBufParamsSet.append("");
		}
		
		
		try {
			String urlNameString = "";
			if(interfaceName==null || "".contains(interfaceName.trim())) {
				urlNameString = url + strBufParamsSet.toString();
			}else {
				urlNameString = url +"/"+ interfaceName + strBufParamsSet.toString();
			}
			urlNameString = urlNameString.replaceAll("\r\n", "");
			trustAllHttpsCertificates();
			logger.info("请求路径:" + urlNameString);
			URL realUrl = new URL(urlNameString);
			// 打开和URL之间的连接
			URLConnection connection = realUrl.openConnection();
			// 设置通用的请求属性
			connection.setRequestProperty("accept", "*/*");
			connection.setRequestProperty("connection", "Keep-Alive");
			connection.setRequestProperty("Accept-Charset", "UTF-8");
			connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
			// 建立实际的连接
			connection.connect();
			// 获取所有响应头字段
			Map<String, List<String>> map = connection.getHeaderFields();
//			// 遍历所有的响应头字段
//			for (String key : map.keySet()) {
//				System.out.println(key + "--->" + map.get(key));
//			}
			// 定义 BufferedReader输入流来读取URL的响应
			in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
			String line;
			while ((line = in.readLine()) != null) {
				result += line;
			}
		} catch (Exception e) {
			logger.debug("发送 get请求出现异常!获取文件流失败:" + e);
			
		}
		// 使用finally块来关闭输入流
		finally {
			try {
				if (in != null) {
					in.close();
				}
			} catch (Exception e2) {
				logger.debug("发送 get请求出现异常!获取文件流失败:" + e2);
			}
		}
		return result;
	}

	

	private static void trustAllHttpsCertificates() throws Exception {  
        javax.net.ssl.TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[1];  
        javax.net.ssl.TrustManager tm = new miTM();  
        trustAllCerts[0] = tm;  
        javax.net.ssl.SSLContext sc = javax.net.ssl.SSLContext  
                .getInstance("SSL");  
        sc.init(null, trustAllCerts, null);  
        javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(sc  
				                .getSocketFactory());

		HostnameVerifier hv = new HostnameVerifier() {
			public boolean verify(String urlHostName, SSLSession session) {
				System.out.println("Warning: URL Host: " + urlHostName + " vs. "
								   + session.getPeerHost());
				return true;
			}
    	};
		HttpsURLConnection.setDefaultHostnameVerifier(hv);
    }  
			  
    static class miTM implements javax.net.ssl.TrustManager,
			            javax.net.ssl.X509TrustManager {  
        public java.security.cert.X509Certificate[] getAcceptedIssuers() {  
            return null;  
        }  
  
        public boolean isServerTrusted(java.security.cert.X509Certificate[] certs) {
            return true;  
        }  
  
        public boolean isClientTrusted(java.security.cert.X509Certificate[] certs) {
            return true;  
        }  
  
        public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType)
                throws java.security.cert.CertificateException {  
            return;  
        }  
  
        public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType)
                throws java.security.cert.CertificateException {  
            return;  
        }  
    }  
}

到了这一步,把
再回企业微信应用配置的页面,复制这个:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=appIdf&redirect_uri=编码后的域名&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

复制到:【工作台应用主页】里面去。

复制域名到【网页授权及JS-SDK】:

java企业微信开发 微信企业版开发_spring boot_14


勾选【已上传域名归属校验文件】,【确定】!

点:【企业微信授权登录】已启用–》Web网页–》

java企业微信开发 微信企业版开发_web app_15


编辑填入域名

好了之后,debug重启:WxCpDemoApplication.java并且在这行打断点:

java企业微信开发 微信企业版开发_spring boot_16

在企业微信中打开:

java企业微信开发 微信企业版开发_java企业微信开发_17


点进去自己的应用,如果进断点,看backString,是不是与我类似:

java企业微信开发 微信企业版开发_spring boot_18


如果看到返回值与我类似的,恭喜,已经完成了!!!如果有疑问或者需要讨论,或者报错的不好解决的话,给我留言即可。

附上我这边最终的画面:

java企业微信开发 微信企业版开发_java企业微信开发_19


另外,稍微看一下natapp的情况,那里有请求的情况:

java企业微信开发 微信企业版开发_spring boot_20


好了,这一篇也结束了。