故事开头怎么写呢,大概2016年10月份吧,当时出差飞机刚落地,久未联系的同学打来电话,说在创业做共享按摩椅,需求说的很简单,扫码支付启动。当时市场上共享按摩椅才兴起,由于平时工作不涉及支付,相关硬件通信模块更是了解甚少,就给回绝了。然后在回去的路上在想,不可能一辈子coding吧,支付、物联网会越来越火,怎么就不能尝试一下呢。
俗话说隔行如隔山,外行看来很简单的需求,不就扫码支付成功后硬件启动。既然想尝试,就详细了解下需求,然后把功能分解一下
过程娓娓道来,先看系统吧。http://m.xyxspace.com 【测试系统地址 】
登录账号可关注上面微信公众号,回复“测试账号”获得。若登录失败,密码可能被其他人修改,可在公众号回复“重置密码”后,后台会自动初始化账号信息,此功能可在系统后台配置。系统demo发邮件至540769049@qq.com,标明来由。
系统架构图
系统业务逻辑流程图
系统功能
1)后台管理模块
a)硬件设备管理
- 设备的增删改查,导出
- 设备上所贴二维码的生成
- 设备的定价
- 设备在线状态、信号强度、grps坐标的更新
b)会员管理
- 会员的增删改查,导出
- 会员余额,积分的管理
c)代金券管理
- 关注公众号、绑定手机号码,自动赠送代金券
- 给指定合作商户批量生成代金券,给指定用户批量绑定代金券,代金券的批量导出
- 代金券的增删改查
- 代金券的过期失效
d)订单管理
- 订单的删查,导出
e)统计功能
- 收入明细
- 按所属网点、每一台设备从微信、支付宝、投币维度统计指定时间点的收入详情
- 统计指定时间范围内每天的收入趋势
- 统计指定时间范围内每小时的收入趋势
f)充值功能
2)支付模块
支持微信、支付宝扫码支付。
3)物联网模块
与硬件设备的通信,控制硬件设备。
一:从无到有,系统搭建
很多系统功能如用户注册、登录、权限,菜单管理等等,都是通用的,这里借鉴了一个成熟的通用框架(https://blog.youkuaiyun.com/wernisng090/article/details/50864520),写的挺好,简单了解一下实现过程,直接在此基础上修改了。
二:按照业务流程图一一道来
1.用户扫码
- 入口
@RequestMapping(value = "/{numericPart:[\\d]*}")
public ModelAndView scaning() {
String url = request.getRequestURL().toString();
String mcode = url.substring(url.lastIndexOf("/") + 1);
session.setAttribute("mcode", mcode);
String id = "";
if (Constants.WEIXIN.equals(appType)) {// 来自微信
logger.info("微信扫码...");
id = (String) session.getAttribute("openid");
} else if (Constants.ALIPAY.equals(appType)) {// 来自支付宝
logger.info("支付宝扫码...");
id = (String) session.getAttribute("user_id");
}
if (StringUtils.isBlank(id)) {// session中未存在用户信息时,授权重新获取用户信息
return indexService.createRedirectURL(appType,mcode);
} else {
return commonService.trunView(id, mcode, basePath);// 跳转到首页价格页面
}
}
public ModelAndView createRedirectURL(String appType,String mcode) {
if (Constants.WEIXIN.equals(appType)) {// 微信客户端
String redirectUrl = OAuthManager.generateRedirectURI(WXConstants.REDIRECT_URI, WXConstants.SCOPE, mcode);
return new ModelAndView("redirect:" + redirectUrl);
} else if (Constants.ALIPAY.equals(appType)) {// 支付宝客户端
String redirectUrl = OAuthALiService.generateRedirectURI(AlipayConfig.REDIRECTURI, AlipayConfig.ALIPAY_SCOPE, mcode);
return new ModelAndView("redirect:" + redirectUrl);
} else {
ModelAndView mv = new ModelAndView();
mv.setViewName("view/index/unAllow");
return mv;
}
}
- 判断客户端类型
String userAgent = request.getHeader("user-agent");
if (StringUtils.isNotBlank(userAgent)) {
userAgent = userAgent.toLowerCase();
if (userAgent.indexOf("micromessenger") > -1) {// 微信客户端
return Constants.WEIXIN;
} else if (userAgent.indexOf("alipayclient") > -1) {
return Constants.ALIPAY;
}
}
2.授权获取用户信息
- 微信
2018-08-10 14:29:29,927 INFO [com.xyx.controller.IndexController] - <微信扫码...>
2018-08-10 14:29:34,583 INFO [com.xyx.wx.controller.WXController] - <weixin回调方法...>
2018-08-10 14:29:34,852 INFO [com.xyx.wx.controller.WXController] -
<oauth2获取到的用户信息:[GetUserinfoResponse{openid='o6QuCwb26Pxx1wKkR8CfF9WXoRjU', nickname='她', sex='1', province='河南', city='郑州', country='中国', headimgurl='http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoSibuECOFWda0OseruXfvAWp2GLs0LiceQ87mImXkMRDpAwz9ddNxZatgzroqamaRvWd9hEHTs4xCA/132', privilege=[], unionid='null'}]>
- 支付宝
2018-08-10 14:31:55,456 INFO [com.xyx.controller.IndexController] - <支付宝扫码...>
2018-08-10 14:32:01,387 INFO [com.xyx.alipay.controller.AlipayController] - <ali授权回调方法>
2018-08-10 14:32:01,963 INFO [com.xyx.alipay.controller.AlipayController] -
<{"alipay_user_info_share_response":{"code":"10000","msg":"Success","avatar":"https:\/\/tfs.alipayobjects.com\/images\/partner\/T1fV0vXhpbXXXXXXXX","city":"北京市","gender":"m","is_certified":"T","is_student_certified":"T","nick_name":"neversayd","province":"北京","user_id":"2088012705591342","user_status":"T","user_type":"2"},"sign":"aONatLtOKIl0eWhapZ9VwA+2FupRWE5JM8c353ZrA+VEvmPSvY6E8RObNnXewTvKWL7Jom4WDAOBO8NqE7QfwKYU1WpBEPTe/Ysms+d3ROcvRbhNZiPAgTIpD1hwLR+49oyTtdKrZaUgE+u+T5LE/GL8mx0XHR2Msd9nfJ8O2LsoCtT2EeKSlBazRATcErAoGXciVik7DS9mj9CCXUyYR17kVZgYbROVbvKJNPpT+bPLRL3jH7ofPgh3VR4xmngT2cUHg/qyMaH6vWX4ZsYQgRdLldLga43HWLlU4Nya9haQZFIPzKZNZbhzOqyA7upUaFJFM9vHDkuWQHNM4/ojaA=="}>
- 用户列表
二:支付模块
一码多付,使用微信公众号支付,支付宝手机网站支付功能。
可参考之前写的一个blog,所示代码都是从本系统里摘抄的,https://mp.youkuaiyun.com/postedit/79003172
三:微信公众号
- 微信消息事件分发,event事件分发(接口中相应抽象方法已定义,子类继承实现即可)
/**
* 消息事件分发
*/
private void dispatchMessage(){
logger.info("distributeMessage start");
if(StringUtils.isBlank(wechatRequest.getMsgType())){
logger.info("msgType is null");
}
MsgType msgType = MsgType.valueOf(wechatRequest.getMsgType());
logger.info("msgType is " + msgType.name());
switch (msgType) {
case event:
dispatchEvent();
break;
case text:
onText();
break;
case image:
onImage();
break;
case voice:
onVoice();
break;
case video:
onVideo();
break;
case shortvideo:
onShortVideo();
break;
case location:
onLocation();
break;
case link:
onLink();
break;
default:
onUnknown();
break;
}
}
/**
* event事件分发
*/
private void dispatchEvent() {
EventType event = EventType.valueOf(wechatRequest.getEvent());
logger.info("dispatch event,event is " + event.name());
switch (event) {
case CLICK:
click();
break;
case subscribe:
subscribe();
break;
case unsubscribe:
unSubscribe();
break;
case SCAN:
scan();
break;
case LOCATION:
location();
break;
case VIEW:
view();
break;
case TEMPLATESENDJOBFINISH:
templateMsgCallback();
break;
case scancode_push:
scanCodePush();
break;
case scancode_waitmsg:
scanCodeWaitMsg();
break;
case pic_sysphoto:
picSysPhoto();
break;
case pic_photo_or_album:
picPhotoOrAlbum();
break;
case pic_weixin:
picWeixin();
break;
case location_select:
locationSelect();
break;
case kf_create_session:
kfCreateSession();
break;
case kf_close_session:
kfCloseSession();
break;
case kf_switch_session:
kfSwitchSession();
break;
default:
break;
}
}
/**
* 微信消息类型,大小写对应微信接口,msgType的枚举值
*/
public enum MsgType {
event, //事件
text, //文本消息
image,
location,
link,
voice,
video,
shortvideo, //小视频消息
music,
news,
transfer_customer_service;//客服系统
}
/**
* 微信事件类型
*/
public enum EventType {
subscribe, //关注
unsubscribe, //取消关注
/** 创建菜单使用 */
click,
CLICK, //点击
/** 创建菜单使用 */
view,
VIEW, //跳转链接
SCAN, //扫描
LOCATION, //上报地理位置
TEMPLATESENDJOBFINISH, //模板消息发送成功之后事件
scancode_push, //扫码推事件
scancode_waitmsg, //扫码推事件且弹出“消息接收中”提示框的事件
pic_sysphoto, //弹出系统拍照发图的事件
pic_photo_or_album, //弹出拍照或者相册发图的事件
pic_weixin, //弹出微信相册发图器的事件
location_select, //弹出地理位置选择器的事件
media_id, //下发消息(除文本消息)
view_limited, //跳转图文消息URL
kf_create_session, //接入会话
kf_close_session, //关闭会话
kf_switch_session, //转接会话
}
- 微信access_token、jsapi_ticket中控服务器
可参考之前写的另一篇blog,所示代码都是从本系统里摘抄的,https://blog.youkuaiyun.com/gotohomebye/article/details/78768112
- 微信模板消息(支付成功后的消息通知,账户信息通知)
- 微信菜单管理
/**
* 微信菜单
*/
public class Menu {
private List<MenuButton> button;
public List<MenuButton> getButton() {
return button;
}
public void setButton(List<MenuButton> button) {
this.button = button;
}
}
/**
* 菜单按钮
*/
public class MenuButton {
private EventType type;//菜单的响应动作类型
private String name;//菜单标题,不超过16个字节,子菜单不超过40个字节
private String key;//click等点击类型必须 菜单KEY值,用于消息接口推送,不超过128字节
private String url;//view类型必须 网页链接,用户点击菜单可打开链接,不超过256字节
private String mediaId;//media_id类型和view_limited类型必须 调用新增永久素材接口返回的合法media_id
private List<MenuButton> subButton;//子菜单,每个一级菜单最多包含5个二级菜单
}
/**
* 菜单按钮类型
*/
public enum MenuButtonType {
/** 点击 */
click,
/** 跳转URL */
view,
/** 扫码推事件 */
scancode_push,
/** 扫码推事件且弹出“消息接收中”提示框 */
scancode_waitmsg,
/** 弹出系统拍照发图 */
pic_sysphoto,
/** 弹出拍照或者相册发图 */
pic_photo_or_album,
/** 弹出微信相册发图器 */
pic_weixin,
/** 弹出地理位置选择器 */
location_select,
/** //下发消息(除文本消息) */
media_id,
/** 跳转图文消息URL */
view_limited;
}
/**
* 微信菜单操作
*/
public class MenuManager {
private static Logger logger = Logger.getLogger(MenuManager.class);
private static final String MENU_CREATE_POST_URL = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=";
private static final String MENU_GET_GET_URL = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=";
private static final String MENU_DEL_GET_URL = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=";
private String accessToken;
public MenuManager() {
this.accessToken = TokenProxy.accessToken();
}
/**
* 创建菜单
* @throws WeChatException
*/
public void create(Menu menu) throws WeChatException{
logger.info("创建菜单");
String resultStr = HttpUtils.post(MENU_CREATE_POST_URL+this.accessToken, JSON.toJSONString(menu));
WeChatUtil.isSuccess(resultStr);
}
/**
* 查询菜单
*/
public Menu getMenu() {
logger.info("查询菜单");
String resultStr = HttpUtils.get(MENU_GET_GET_URL+this.accessToken);
try {
WeChatUtil.isSuccess(resultStr);
} catch (WeChatException e) {
e.printStackTrace();
return null;
}
JSONObject menuObject = JSONObject.parseObject(resultStr);
Menu menu = menuObject.getObject("menu", Menu.class);
return menu;
}
/**
* 删除菜单
* @throws WeChatException
*/
public void delete() throws WeChatException{
logger.info("删除菜单");
String resultStr = HttpUtils.get(MENU_DEL_GET_URL+this.accessToken);
WeChatUtil.isSuccess(resultStr);
}
四:支付宝支付
五:系统技术
使用springmvc+mybatis3.2后台框架,mysql5.7数据库,HTML5+css3.0+bootstrap前端页面,redis、shiro 、ehcache 、druid等技术。
- mysql5.7 保存微信emoj表情
- 设备上所张贴二维码的自动生成
- redis数据库的使用(缓存订单,保存每台设备的定价,设备启动倒计时)
- 短信网关(绑定手机号码赠送代金券)
- 邮件系统(发送提醒邮件)
- 系统部署在阿里云上
- 域名的备案