前言
因公司需要开发一款手机打卡程序,本人没有安卓APP开发经验,所以决定将写个服务号的公众号,集外出打卡,打卡查询等功能;
一,开发前测试帐号申请
以下是官方给出的建议,大家可以多参考参考
1)如果想简单的发送消息,达到宣传效果,建议可选择订阅号;
2)如果想用公众号获得更多的功能,例如开通微信支付,建议可以选择服务号;
3)如果想用来管理内部企业员工、团队,对内使用,可申请企业号;
4)订阅号可通过微信认证资质审核通过后有一次升级为服务号的入口,升级成功后类型不可再变;
5)服务号不可变更成订阅号。
申请地址:
http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
二,选择natapp进行内网外穿,为什么?这是因为自制程序可以连外网,但怎样让微信服务器与你的自制程序通信?那只能把自己的程序也设置为外网,但用传统的打包布署发布到LINUX?那将会对开发极为不便。natapp就是解决我们可以直接将tomcat设置为内网外穿,这样每次开发完,直接运行,外网就可以找到你这个内网外穿的外网。我个人喜好natapp,当然也有其它优秀的内网外穿工具,可自行选择。
(1). 下载
在以下地址:https://natapp.cn/,进行下载,打开链接之后,选择客户端下,如下图:
图1:客户端下载
我在这里下载的是windows64位。
(2). 注册并登录
同时还需要进行注册,(注册过程不详述)并进行登陆。登陆后的页面如下图:
图2:登录成功后的页面
(3). 购买隧道
此时我们需要进行购买隧道,如下图。
图3:购买隧道
如果是自己的项目可以使用免费隧道。不过我推荐大家使用VIP_2型,根据自己的需要进行选择。我使用的是付费隧道。(如下图),我购买是一起花了13元,建议购买前去网上找优惠码,可便宜2元。然后建议不要买太便宜的,哈哈。
图4:隧道购买成功
(4). 配置已购买隧道
下面是购买成功后,我的隧道,我之前申请了一个免费隧道,一个付费隧道,所以我的里面有两个隧道。如下图
选择第二个隧道中的配置按钮,进行隧道配置。打开如图所示:
图5:配置已购买隧道
此时为止,natapp的配置已完成。
(5). 启动natapp
新建config.ini文件,存储在下载的客户端同一个目录下。该文件中需要添加内容如下
[default]
authtoken= 6fdb788dce71d7e2
clienttoken= #对应客户端的clienttoken,将会忽略authtoken,若无请留空,
log=none #log 日志文件,可指定本地文件, none=不做记录,stdout=直接屏幕输出 ,默认为none
loglevel=ERROR #日志等级 DEBUG, INFO, WARNING, ERROR 默认为 DEBUG
http_proxy= #代理设置 如 http://10.123.10.10:3128 非代理上网用户请务必留空
双击natapp程序启动成功如下图所示:
至此,大功告成.
三.在微信平台进行配置.
完成上面第一步的注册后,即下面这个网址.
http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
说明一下.
appid:是公众号开发识别码,配合开发者密码可调用公众号的接口能力。
appsecret:是校验公众号开发者身份的密码,具有极高的安全性。
再往下看,我们会看到URL和Token这两个属性,和上面appid/appsecret不同的是,上面的是微信分配给我们的,但是下面这两个是需要我们填进去的。
我们先来了解一下,这两个属性有什么作用。
URL:就是指我们自己的服务器地址
该URL是开发者用来接收和响应微信消息和事件的接口URL
(必须以http://或https://开头,分别支持80端口和443端口,这就是为什么我刚刚第二步说要进行内网外穿,哈哈)
Token:可任意填写,用作生成签名(必须为英文或数字,长度为3-32字符)
该签名在后边会用到,这里暂时随便填个内容也可以
接下来我们需要了解的是微信与我们的服务器交互的过程:
当我们在微信app上,给公众号发送一条内容的时候,实际会发送到微信的服务器上,此时微信的服务器就会对内容进行封装成某种格式的数据比如xml格式,再转发到我们配置好的URL上,所以该URL实际就是我们处理数据的一个请求路径。所以该URL必须是能暴露给外界访问的一个公网地址,不能使用内网地址,生产环境可以申请腾讯云,阿里云服务器等,但是在开发环境中可以暂时利用一些软件来完成内网穿透,便于修改和测试,如NATAPP,花生壳等软件,使用起来也很方便,在本地安装对应的软件,配置运行后,直接使用软件分配的临时域名来访问本地应用即可,只是偶尔会存在网络不稳定的情况。这里不详细介绍如何使用了,具体教程可参考软件官网。
在开发的过程中,我们会经常使用到微信公众号提供给开发者的开发文档
具体地址:https://mp.weixin.qq.com/wiki
大家打开后可以选择”接入指南”,参考微信提供的一些帮助信息。
四.正式开发
(1),URL接入
我们需要先来了解一下接入的过程是怎么样的。下图是微信官方对接入过程的介绍。
由以上介绍可知,当我们填入url与token的值,并提交后,微信会发送一个get请求到我们填写的url上,并且携带4个参数,而signature参数结合了开发者填写的token参数和请求中的timestamp参数、nonce参数来做的加密签名,我们在后台需要对该签名进行校验,看是否合法。实际上,我们发现微信带过来的4个参数中并没有带token参数,仅有signature是和token有关的,所以我们应该在本地应用中也准备一个和填入的token相同的参数,再通过微信传入的timestamp与nonce做相同算法的加密操作,若结果与微信传入的signature相同,即为合法,则原样返回echostr参数,代表接入成功,否则不做处理,则接入失败。
我这里是使用的springboot+springmvc+mybaties
1.首先新建一个servlet,为什么是servlet而不是controller?因为servlet支持一个入口对应两个方法,即get post.开发到后面你就会明白这个意义.
package com.cosun.cosunp.weixin;
import com.cosun.cosunp.service.IPersonServ;
import com.cosun.cosunp.tool.Constants;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* @author:homey Wong
* @Date: 2019/9/11 上午 9:03
* @Description:
* @Modified By:
* @Modified-date:
*/
@WebServlet(urlPatterns = "/weixin/hello")
public class WeiXinServlet extends HttpServlet {
private static Logger logger = LogManager.getLogger(WeiXinServlet.class);
@Autowired
IPersonServ personServ;
public static final String tooken = "homeyhomeyhomey";
private static JedisPool pool;
private static Jedis jedis;
public WeiXinServlet() {
//空构造函数
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
PrintWriter outPW = null;
try {
outPW = response.getWriter();
if (CheckUtil.checkSignature(signature, timestamp, nonce, tooken)) {
System.out.println("签名成功!");
outPW.write(echostr);
} else {
System.out.println("签名失败");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
outPW.close();
}
}
}
工具类CheckUtil
package com.cosun.cosunp.weixin;
import java.security.MessageDigest;
import java.util.Arrays;
/**
* @author:homey Wong
* @Date: 2019/9/11 0011 上午 9:10
* @Description:
* @Modified By:
* @Modified-date:
*/
public class CheckUtil {
public static boolean checkSignature(String singnature, String timestamp, String nonce, String tooken) {
String[] arr = {tooken, timestamp, nonce};
Arrays.sort(arr);
StringBuilder sb = new StringBuilder();
for (String s : arr) {
sb.append(s);
}
String temp = getSha1(sb.toString());
return temp.equals(singnature);
}
public static String getSha1(String str) {
if (str == null || str.length() == 0) {
return null;
}
char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f'};
try {
MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
mdTemp.update(str.getBytes("UTF-8"));
byte[] md = mdTemp.digest();
int j = md.length;
char buf[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
buf[k++] = hexDigits[byte0 >>> 4 & 0xf];
buf[k++] = hexDigits[byte0 & 0xf];
}
return new String(buf);
} catch (Exception e) {
return null;
}
}
}
可以拿你的手机测试一下.OK后,如下,可以互动了
@WebServlet(urlPatterns = "/weixin/hello")
public class WeiXinServlet extends HttpServlet {
private static Logger logger = LogManager.getLogger(WeiXinServlet.class);
@Autowired
IPersonServ personServ;
public static final String tooken = "homeyhomeyhomey";
private static JedisPool pool;
private static Jedis jedis;
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
pool = new JedisPool(new JedisPoolConfig(), "127.0.0.1");
jedis = pool.getResource();
System.out.println(jedis.get(Constants.accessToken));
System.out.println(jedis.get(Constants.jsapi_ticket));
try {
// https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxd5109277d8902606&redirect_uri=http://homey.nat100.top/weixin/getMobileLocate&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect
PrintWriter out = response.getWriter();
Map<String, String> map = WeiXinUtil.xmlToMap(request);
String fromUserName = map.get("FromUserName");
String toUserName = map.get("ToUserName");
String msgType = map.get("MsgType");
String content = map.get("Content");
String key = map.get("EventKey");
String message = null;
StringBuilder returnMes = new StringBuilder();
if ("event".equals(msgType)) {
if (clickquery.equals(key)) {
List<OutClockIn> allOutClockIn = personServ.findAllOutClockInByOpenId(fromUserName);
if (allOutClockIn.size() > 0) {
for (OutClockIn on : allOutClockIn) {
returnMes.append(on.getClockInDateStr() + ":");
returnMes.append(on.getClockInDateAMOnStr() + ",");
returnMes.append(on.getClockInAddrAMOn() + ",");
if (on.getAmOnUrl() != null && on.getAmOnUrl().trim().length() > 0) {
returnMes.append("上午已摄像.");
} else {
returnMes.append("上午还未摄像.");
}
returnMes.append(on.getClockInDatePMOnStr() + ",");
returnMes.append(on.getClockInAddrPMOn() + ".");
if (on.getPmOnUrl() != null && on.getPmOnUrl().trim().length() > 0) {
returnMes.append("下午已摄像.");
} else {
returnMes.append("下午还未摄像.");
}
returnMes.append(on.getClockInDateNMOnStr() + ",");
returnMes.append(on.getClockInAddNMOn() + ".");
if (on.getNmOnUrl() != null && on.getNmOnUrl().trim().length() > 0) {
returnMes.append("晚上已摄像.");
} else {
returnMes.append("晚上还未摄像.");
}
}
} else {
returnMes.append("暂无考勤信息");
}
InMsgEntity text = new InMsgEntity();
text.setFromUserName(toUserName); //原来的信息发送者,将变成信息接受者
text.setToUserName(fromUserName); //原理的接受者,变成发送者
text.setMsgType("text"); //表示消息的类型是text类型
text.setCreateTime(new Date().getTime());
text.setContent("您的考勤信息是:" + returnMes);
message = WeiXinUtil.textMessageToXml(text); //装换成 xml 格式发送给微信解析
}
} else if ("text".equals(msgType)) {
InMsgEntity text = new InMsgEntity();
text.setFromUserName(toUserName); //原来的信息发送者,将变成信息接受者
text.setToUserName(fromUserName); //原理的接受者,变成发送者
text.setMsgType("text"); //表示消息的类型是text类型
text.setCreateTime(new Date().getTime());
text.setContent("您发送的信息是:" + content);
message = WeiXinUtil.textMessageToXml(text); //装换成 xml 格式发送给微信解析
}
out.print(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void setRedisValue(AccessToken accessToken) {
// 初始化Redis连接池
pool = new JedisPool(new JedisPoolConfig(), "127.0.0.1");
jedis = pool.getResource();
jedis.set(Constants.accessToken, accessToken.getAccessToken());
jedis.set(Constants.expiresin, accessToken.getExpiresin() + "");
//jedis.set(Constants.jsapi_ticket, accessToken.getJsapi_ticket());
}
至此微信公众号的接入就已经完成了,后继可以进行功能开发啦.