目录
说明
本例基于wx-tools.jar
微信公众号文档
微信公众号文档告诉我们,是有两种扫描二维码事件的,如果是已经关注过公众号的,则是event=scan,如果没有关注过的,则是event=subscribe,他们的event是不同的.
我们在设计详细需求的时候一般是event=scan这种比较多的,比如扫描二维码后可以做什么.但也有event=subscribe这种需求情况,比如拉新用户扫描关注获得多少积分.
表结构
基础表有两张,其他随着需求的增加而看.
基础表wx_qr
CREATE TABLE `wx_qr` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
`ticket` varchar(300) DEFAULT NULL COMMENT '二维码票据',
`type` int(11) DEFAULT NULL COMMENT '二维码类型/枚举索引',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`body` varchar(100) DEFAULT NULL COMMENT '携带的数据',
`over` tinyint(1) DEFAULT NULL COMMENT '二维码是否关闭?当关闭时,就没有用了.',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=74 DEFAULT CHARSET=utf8
基础表wx_qr_user
CREATE TABLE `wx_qr_user` (
`quid` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
`qrid` int(11) DEFAULT NULL COMMENT '二维码id',
`userid` int(11) DEFAULT NULL COMMENT '扫描的用户',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`is_effective` tinyint(1) DEFAULT NULL COMMENT '扫描是否有效',
PRIMARY KEY (`quid`)
) ENGINE=InnoDB AUTO_INCREMENT=54 DEFAULT CHARSET=utf8
扫码这个动作在后台是需要三步走的:1.生产二维码 2.接收扫描信息 3处理事件.
那么wx_qr表对应的就是生成二维码,wx_qr_user代表的是扫描表,至于处理事件建议是每个事件一张表.
一个应用,扫描二维码的场景本身也不到10个,每个场景一张表也没什么.
思路
由于一个应用或许不止一个扫描二维码的场景,而微信只有一个通知口,那么就需要我们在接收到扫描信息里去判断这到底是个什么事件,怎么去判断呢?幸好微信的通知里面有一个EventKey这个关键字.
我们可以通过它来判断:
比如在扫描二维码进行登录的场景里,把该事件定义为EventKey=1,在扫描二维码进行报名时,把事件定义为EventKey=2
那么在接收微信通知时我们就可以这样做:
int eventKey;
if (eventKey == 1){
//去做登录的逻辑
} else if (eventKey == 2){
//去做报名的逻辑
}else if (eventKey == 3){
//去做修改信息的
}else....
但这样做时不符合程序的开闭原则的.如果有新的或者改动,就得修改这一页粘稠的逻辑代码.
遇到这种情况,一般会使用策略模式,但是就目前微信公众号的通知来说,使用枚举更好.
策略者模式一般会使用接口,枚举一样也可以和接口关联起来.先说一下接口
/**
* 扫描事件的接口
* @author
*
* 2019年1月11日
*/
public interface Scaner {
public WxXmlOutMessage
handleScan(UserSubScribeDto dto, String ticket, WxQrUtil wxQrUtil, ScanSource common);
}
解释下这handleScan方法:
UserSubScribeDto 用户信息类
public class UserSubScribeDto {
//userid
private Integer userid;
//openid
private String openid;
//是否取消关注
private Boolean outfollow;
//是否被拉黑
private Boolean closure;
}
String ticket 二维码的长长一串的票据,大概长这样:
gQEW7zwAAAAAAAAAAS5odHRwOi8vd2VpeGdasdasdNvbS9xLzAyaFlwMlpVTnhkffasa2wxNXc1WU5zYzQAAgRQNzxcAwQQDgAA
WxQrUtil 这个就是一个枚举类,我们接下来会讲
ScanSource 也是一个枚举类,代表着当前扫描事情的性质来源
/**
* 扫描事件来源
* @author
*
* 2019年2月13日
*/
public enum ScanSource {
COMMON,//关注过的扫描事件,是公共的
REGEIST//未关注的扫描事件,带有注册关注性质的
}
WxXmlOutMessage 返回值,返回信息,这是wx-tools.jar里的一个类,使用它可以回复给扫描者信息或者回复给微信服务器.
枚举类的详解:我们需要构造一个完美的枚举,如下所示:
public enum WxQrUtil {
/**
* 后台登录
*/
LOGIN_BACK(LoginBackScan.class,true,60 * 5),
/**
* 活动报名
*/
ACTIVITY_FACT_JOIN(ActivityScan.class, false , 60 * 60 ),
/**
* 商家引导用户注册获得积分
*/
BUSINESS_MAKE_FIRST_BUSINESS(BusinessMakeFirstScan.class,false,null);
private Class<? extends Scaner> scannerClass;//处理器
private boolean oneAble;//一次性
private Integer expireSeconds;//二维码时效,单位秒,为null代表没有时效
}
它有三个属性,分别是处理器,是否一次性,以及有效时间.
这三个属性很重要.处理器实现自Scaner.class,是真正处理该二维码事件的处理类,而有的二维码是一次性的,比如登录二维码,交易二维码.有的二维码不是一次性的.有的二维码有有效时间,必须在多少秒内扫描才有效,而有的没有.
好了,我们已经写了三个枚举:LOGIN_BACK,ACTIVITY_FACT_JOIN,BUSINESS_MAKE_FIRST_BUSINESS分别是后台登录,活动报名,以及商家引导用户获得积分.
后台登录是一次性二维码,且必须5分钟之内扫描,否则无效.
活动报名不是一次性二维码,可以多人扫描,但必须一个小时之内扫描,否则无效.
商家引导用户注册获得积分,不是一次性二维码,可以多人扫描,且没有时间限制.
它们的处理类先不看,我们再先给该枚举多个创建二维码的方法:(使用wx-tools创建二维码)
public String createQr(IService iService) throws WxErrorException{
WxQrcode qrcode = new WxQrcode();
WxScene scene = new WxScene(this.ordinal());//这步很重要,把当前枚举的索引变成了eventKey
WxQrActionInfo action_info = new WxQrActionInfo(scene);
qrcode.setAction_info(action_info );
if (this.expireSeconds == null){//如果为时间限制为null,就设置为永久二维码
qrcode.setAction_name(QR_LIMIT_SCENE);
} else {
qrcode.setAction_name(QR_SCENE);//否则附带上时间
qrcode.setExpire_seconds(this.expireSeconds);
}
QrCodeResult result = iService.createQrCode(qrcode);
return result.getTicket();
}
我们在生成二维码的方法里把时间限制传递过去,而且收到了微信传来的ticket票据.
我们平时就可以这样生成二维码了:
String ticket = WxQrUtil.ACTIVITY_FACT_JOIN.createQr(iService);//生成活动报名二维码
生成完之后,你需要把这个二维码存到数据库wx_qr里面,把当前枚举的索引也给存起来,如果有需要附加信息,请存到wx_qr的body里面去.
在生成二维码的时候我们做了一个巧妙的东西,就是把当前枚举的索引传递给了微信服务器,如果有人扫描,微信通知里会把这个索引作为eventKey又传递回来.
那么到时候,我们接到通知,取出eventKey就可以这样做:
int eventKey;//索引
WxQrUtil[] values = WxQrUtil.values();//枚举类的所有内容
WxQrUtil util = values[eventKey];//根据索引取出枚举
Class<? extends Scaner> scanerClass = util.getScannerClass();//根据枚举取出处理类的class
Scaner scaner = scanerClass.newInstance();//实例化
scaner.handleScan(dto, ticket, wxQrUtil, common);//详细处理
大概像这样:
生成二维码
扫描二维码:
思路很不错,只是关于Scaner处理器还需要唠叨两句.
处理器
在写过很多二维码之后,发现但凡二维码,总要有以下一些步骤要去做到的.所以需要抽取出来.
写个BaseScaner implements Scaner,然后其他的细节处理器继承自BaseScaner即可.
在说BaseScaner之前,得先把ScanHandler亮出来,这个类继承自wx-tools的WxMessagehandler
public class WxScanHandler implements WxMessageHandler {
private static Logger log = LoggerFactory.getLogger(WxScanHandler.class);
@Override
public WxXmlOutMessage handle(WxXmlMessage wxMessage, Map<String, Object> context, IService iService)
throws WxErrorException {
log.info(wxMessage.toString());
/**
* 获取二维码票据,扫描人,以及附加数据
*/
String ticket = wxMessage.getTicket();
String openid = wxMessage.getFromUserName();
int ordinal = Integer.parseInt(wxMessage.getEventKey());
WxQrUtil wxQrUtil = WxQrUtil.values()[ordinal];
Class<? extends Scaner> scannerClass = wxQrUtil.getScannerClass();
try {
Scaner scanner = scannerClass.newInstance();
UserSubScribeDto dto = SpringUtil.getBean(UserMapper.class).findUserSubScribeDtoByOpenid(openid);//通过openid查询数据库获取扫描者信息
return scanner.handleScan(dto, ticket, wxQrUtil, ScanSource.COMMON);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return WxXmlOutMessage.TEXT().content(WxOutMessageConsts.ERROR).build();
}
}
如上述代码所示,获得了处理器的实例,直接调用处理器的handleScan方法.
我们接下来看BaseScaner
/**
* 基础处理器
*
*
* 2019年2月13日
*/
public abstract class BaseScaner implements Scaner {
protected WxQr wxQr;
protected UserSubScribeDto dto;
protected boolean effective;
protected WxQrUser qrUser;
protected WxQrUtil wxQrUtil;
@Override
public WxXmlOutMessage handleScan(UserSubScribeDto dto, String ticket, WxQrUtil wxQrUtil, ScanSource common) {
this.dto = dto;
this.wxQrUtil = wxQrUtil;
WxXmlOutMessageResult res = null;
if ((res = checkWxQr(ticket)).isOk()) {
if ((res = checkSource(common)).isOk()) {
if ((res = checkUser(dto)).isOk()) {
if ((res = checkQrExpireTime(wxQrUtil)).isOk()) {
res = checkEffective();
if(res != null && res.isOk()){
effective = true;
}
}
}
}
insertWxQrUser();
if (effective){
res = handleScanWithUserAndWxQr();
}
after();
}
return res == null ?defaultWxXmlOutMessage() :res.getMsg();
}
protected WxXmlOutMessage defaultWxXmlOutMessage() {
return null;
}
private void insertWxQrUser() {
WxQrUser qrUser = new WxQrUser();
qrUser.setQrid(wxQr.getId());
qrUser.setUserid(dto.getUserid());
qrUser.setEffective(effective);
qrUser.setCreateTime(new Date());
SpringUtil.getBean(WxQrUserMapper.class).insert(qrUser);
this.qrUser = qrUser;
}
private WxXmlOutMessageResult checkQrExpireTime(WxQrUtil wxQrUtil) {
/**
* 3.时间是否过期
*/
Integer expireSeconds = wxQrUtil.getExpireSeconds();
if (expireSeconds != null
&& wxQr.getCreateTime().getTime() + expireSeconds * 1000 < System.currentTimeMillis()) {
return WxXmlOutMessageResult.error(WxXmlOutMessage.TEXT().content(WxOutMessageConsts.TIME_OUT).build());
}
return WxXmlOutMessageResult.ok(null);
}
/**
* 1.查二维码记录,看是否存在或者被关闭
*/
private WxXmlOutMessageResult checkWxQr(String ticket) {
wxQr = SpringUtil.getBean(WxQrMapper.class).findByTicket(ticket);
if (wxQr == null || wxQr.getOver()) {
return WxXmlOutMessageResult.error(WxXmlOutMessage.TEXT().content(WxOutMessageConsts.QR_ERROR).build());
}
return WxXmlOutMessageResult.ok(null);
}
protected WxXmlOutMessageResult checkUser(UserSubScribeDto dto) {
if (dto == null || dto.getClosure() || dto.getOutfollow()) {
return WxXmlOutMessageResult.error(WxXmlOutMessage.TEXT().content(WxOutMessageConsts.ERROR).build());
}
return WxXmlOutMessageResult.ok(null);
}
protected WxXmlOutMessageResult checkSource(ScanSource common) {
if (common != ScanSource.COMMON){
return WxXmlOutMessageResult.error(WxXmlOutMessage.TEXT().content(WxOutMessageConsts.NO_SOURCE).build());
}
return WxXmlOutMessageResult.ok(null);
}
/**
* 判断扫描是否有效
*
* @param dto
* @param wxQr
* @return
*/
protected abstract WxXmlOutMessageResult checkEffective();
/**
* 交由子类去处理
*
* @param dto
* @param wxQr
* @param qrUser
* @return
*/
protected abstract WxXmlOutMessageResult handleScanWithUserAndWxQr();
protected WxXmlOutMessage notEffective(UserSubScribeDto dto, WxQr wxQr, WxQrUser qrUser) {
return WxXmlOutMessage.TEXT().content(WxOutMessageConsts.NO_EFFECTIVE).build();
}
protected void after() {
if (wxQrUtil.isOneAble()) {
// 让二维码关闭
SpringUtil.getBean(WxQrMapper.class).over(wxQr.getId());
}
}
}
WxQr:和wx_qr的表对应的类
effective:扫描是否有效
WxqrUser:和wx_qr_user表对应的类
这里有一个回复信息的封装类:
public class WxXmlOutMessageResult {
private boolean ok;
private WxXmlOutMessage msg;
public static WxXmlOutMessageResult error(WxXmlOutMessage msg){
WxXmlOutMessageResult result = new WxXmlOutMessageResult();
result.setMsg(msg);
return result;
}
public static WxXmlOutMessageResult ok(WxXmlOutMessage msg){
WxXmlOutMessageResult result = new WxXmlOutMessageResult();
result.setMsg(msg);
result.setOk(true);
return result;
}
在BaseScaner处理过程中,我们会发现有很多校验,这时候这个封装类就起作用了,如果返回true,代表着可以接着往下走,如果返回false代表着校验不通过.并把信息返回过去.
我们接着仔细看BaseScaner:
1.
this.dto = dto;
this.wxQrUtil = wxQrUtil;
WxXmlOutMessageResult res = null;
先把扫描者及枚举赋值到变量里.然后置WxXmlOutMessageResult 为null.这个没什么说的.
2.
if ((res = checkWxQr(ticket)).isOk()) {
if ((res = checkSource(common)).isOk()) {
if ((res = checkUser(dto)).isOk()) {
if ((res = checkQrExpireTime(wxQrUtil)).isOk()) {
res = checkEffective();
if(res != null && res.isOk()){
effective = true;
}
}
}
}
insertWxQrUser();
if (effective){
res = handleScanWithUserAndWxQr();
}
after();
}
这里做了很多判断:
checkWxQr校验二维码是否存在,
checkSource校验来源事件是否正确
checkUser校验扫描者是否存在
checkQrExpireTime校验二维码的有效时间.
checkEffective校验逻辑是否有效,需要子类重写
handleScanWithUserAndWxQr(),处理方法需要子类重写
我们会发现,只要二维码存在,不管后面是否校验成功,都会去走insertWxQrUser()这个方法,这个方法就是插入到Wx_qr_user表里.
也会去走after这个方法,after方法里面只是做了关闭一次性二维码的操作.
但若是想走handleScanWithUserAndWxQr处理方法,必须被完全校验正确.
我们写一个伪的后台登录的Scaner子类的例子,继承自BaseScaner
public class LoginBackScan extends BaseScaner{
@Override
protected WxXmlOutMessageResult checkEffective() {
if (userid 有权限登录后台){
return WxXmlOutMessageResult.ok(null);
}
return WxXmlOutMessageResult.error(null);
}
@Override
protected WxXmlOutMessageResult handleScanWithUserAndWxQr() {
把登录信息插入到wx_qr_login表里
return WxXmlOutMessageResult.ok(WxXmlOutMessage.TEXT().content(WxOutMessageConsts.OK).build());
}
}
在这个例子里面:需要判断用户是否有权限,以及最后处理的时候需要我们把扫描记录插入新表,这时候需要新建其他表了,上面我们说过,一个事件一张表,我们写个wx_qr_login登录记录表,由于后台登录是一次性二维码,我直接将该主键设置为qrid,而不是一个新的id
CREATE TABLE `wx_login_back` (
`qrid` int(11) NOT NULL COMMENT '二维码表',
`quid` int(11) DEFAULT NULL COMMENT '扫描表',
`userid` int(11) DEFAULT NULL COMMENT '用户表',
`is_use` tinyint(1) DEFAULT NULL COMMENT '是否使用过表',
`create_time` datetime DEFAULT NULL,
`use_time` datetime DEFAULT NULL,
PRIMARY KEY (`qrid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
那么我会插入这条记录.
在用户扫码完成后,前台会访问后台,查找ticket的那条二维码是否被扫描过,如果被合法的扫描过,wx_login_back是有记录的,还要看是否被使用过,只有没有被使用过的记录 才能用作登录的证据,通过userid查询用户信息保存成登录状态,然后把is_use置为已经使用,不能再使用了.
3.
return res == null ?defaultWxXmlOutMessage() :res.getMsg();
处理代码的最后一句,如果为null,则返回一个defaultWxXmlOutMessage()这个方法的返回值.该方法是protected方法,子类可以自由重写.BaseScaner里什么都没有做,也是返回Null
在BaseScaner里,有一个关键字需要注意,那就是protected,有一些方法是protected的,那些都是你可以用来重写的东西.
比方说
checkUser校验用户:默认的是如果用户不存在或者被封禁或者已取消关注则是不通过校验的,而有的时候,比如event=subscribe,附带关注二维码事件时,在关注逻辑里,则可能需要重写为如果用户不存在才能通过校验.
checkSource校验来源,则扫描关注二维码事件里应该重写为
!supper.checkSource()
等等
此类大可细看,在此不讲.
ok!!!一口气写完,酣畅淋漓~~~ 诸君再会~