目标
- 掌握二维码生成插件qrious的使用
- 能够说出微信支付开发的整体思路
- 能够调用微信支付接口(统一下单)生成支付二维码
- 能够调用微信支付接口(查询订单)查询支付状态
- 实现支付日志的生成与订单状态的修改
1. 二维码
1. 什么是二维码
二维码,又称QR Code,QR全称为Quick Response,是一个近几年来移动设备上超流行的一种编码方式,比传统的Bar Code条形码能存更多信息,也能表示更多的数据类型
二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的。它具有条码技术的一些共性:每种码制有其特定的字符集;每个字符占有一定的宽度;具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化点。
2. 二维码的优势
- 信息容量大,可以容纳多达1850个大写字母或2710个数字或500多个汉字
- 应用范围广,支持文字、声音、图片、指纹等
- 容错能力强,即使图片出现部分破损也能使用
- 成本低,容易制作
3. 二维码容错级别
L级(低) 7%的码字可以被恢复
M级(中) 15%可以被恢复
Q级(四分)25%可以被恢复
H级(高) 30%可以被恢复
4. 二维码生成插件qrious
qrious是一款基于HTML5 Canvas的纯js二维码生成插件。
参数如下
配置:
<html>
<head>
<title>demo</title>
<meta charset="utf-8">
<script src="qrious.min.js"></script>
</head>
<body>
<img id="qrious">
<script>
var qr = new QRious(
{
element:document.getElementById('qrious'),
size:250,
value:'http://www.baidu.com',
level:'H'
}
);
</script>
</body>
</html>
2. 微信扫码支付简介
1. 微信扫码支付申请
微信扫码支付是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站支付、实体店单品或订单支付、媒体广告支付等场景。
申请步骤:(了解)
第一步:注册公众号(类型须为:服务号)
请根据营业执照类型选择以下主体注册:个体工商户| 企业/公司| 政府| 媒体| 其他类型。
第二步:认证公众号
公众号认证后才可申请微信支付,认证费:300元/次。
第三步:提交资料申请微信支付
登录公众平台,点击左侧菜单【微信支付】,开始填写资料等待审核,审核时间为1-5个工作日内。
第四步:开户成功,登录商户平台进行验证
资料审核通过后,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资金数额,完成账户验证。
第五步:在线签署协议
本协议为线上电子协议,签署后方可进行交易及资金结算,签署完立即生效。
2. 开发文档
微信支付接口调用的整体思路:
按API要求组装参数,以XML方式发送(POST)给微信支付接口(URL),微信支付接口也是以XML方式给予响应。程序根据返回的结果(其中包括支付URL)生成二维码或判断订单状态。
在线微信支付开发文档:
https://pay.weixin.qq.com/wiki/doc/api/index.html
在本章课程中会用到”统一下单”和”查询订单”两组API
-
appid:微信公众账号或开放平台APP的唯一标识
-
mch_id:商户号 (配置文件中的partner)
-
partnerkey:商户密钥
-
sign:数字签名, 根据微信官方提供的密钥和一套算法生成的一个加密信息, 就是为了保证交易的安全性
3. 微信支付SDK
下载源码后install到本地仓库
mvn install:install-file -DgroupId=com.github.wxpay -DartifactId=wxpay-sdk -Dvers ion=0.0.3 -Dpackaging=jar -Dfile=e:\wxpay-sdk-0.0.3.jar
使用微信支付SDK,在maven工程引入依赖
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
主要会用到微信支付SDK的以下功能
- 获取随机字符串
WXPayUtil.generateNonceStr()
- MAP转换xml字符串(自动添加签名)
WXPayUtil.gennerateSignedXml(param,partnerkey)
- XML字符串转换map
WXPayUtil.xmlToMap(result)
4. HttpClient工具类
HttpClient是Apache Jakarta Common下的子项目,提供支持http协议的客户端编程工具包,支持http协议最新的版本和建议。应用于很多项目,如Cactus和HTMLUtil都使用HttpClient
通俗说就是模拟浏览器的行为
为了简化HttpClient的使用,提供工具类HttpClient(对原生HttpClient进行封装)
该工具类的使用步骤
HttpClient client=new HttpClient(请求的url地址);
client.setHttps(true);//是否是HTTPS协议
client.setXmlParam(xmlParam);//发送的xml数据
client.post();//执行post请求
String result=client.getContent();//获取结果
5. 工程搭建和准备工作
- 建立支付服务接口模块pay-interface
- 建立支付服务实现模块pay-service(war),依赖pay-interface和其他,参见content-service,不用依赖dao。微信SDK也要引入,同之前。添加tomcat插件,端口为9000,添加spring配置文件
- 在common工程添加工具类HttpClient.java,并添加依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
添加配置文件weixinpay.properties
appid=wx8397f8696b538317
partner=1473426802
partnerkey=xxxxxx
notifyurl=http://a31ef7db.ngrok.io/WeChatPay/WeChatPayNotify
appid:微信公众账号或开放平台APP的唯一标识
partner:财付通平台的商户账号
partnerkey:财付通平台的商户密钥
notifyurl:回调地址
- cart-web依赖工程pay-service
- 将二维码插件QRious,拷贝到cart-web的plugins目录
3. 品优购-微信支付二维码生成
1. 需求分析和实现思路
1. 需求分析
在支付页面生成支付二维码,并显示订单号和金额
用户拿出手机,打开微信扫描页面上的二维码,然后在微信中完成支付
2. 实现思路
通过HttpClient工具类实现对远程支付接口的调用
接口链接:https://api.mch.weixin.qq.com/pay/unifiedorder
具体参数见“统一下单”API,构建参数发给统一下单的url,返回的信息中有支付url,根据url生成二维码,显示的订单号和金额也在返回的信息中
2. 后端代码实现
1. 服务接口层
- 在pay-interface创建包com.pinyougou.pay.service,包下建立接口
public interface WeixinPayService {
/**
* 生成二维码
* @param out_trade_no 商户订单号
* @param total_fee 标价金额
* @return
*/
public Map createNative(String out_trade_no,String total_fee);
2. 服务实现层
pay-service创建com.pinyougou.pay.service.impl包,新建类
@Service
public class WeixinPayServiceImpl implements WeixinPayService {
@Value("${appid}")
private String appid;
@Value("${partner}")
private String partner;
@Value("${partnerkey}")
private String partnerkey;
@Value("${notifyurl}")
private String notifyurl;
@Override
public Map createNative(String out_trade_no, String total_fee) {
// 1. 参数封装
Map param = new HashMap();
param.put("appid",appid);//公众账号ID
param.put("mch_id",partner);// 商户号
param.put("nonce_str", WXPayUtil.generateNonceStr());//随机字符串
// param.put("sign","");// 签名 ,之后生成
param.put("body","品优购");// 商品描述
param.put("out_trade_no",out_trade_no);//商户订单号
param.put("total_fee",total_fee);//交易金额,分
param.put("spbill_create_ip","127.0.0.1");//终端IP
param.put("notify_url",notifyurl);// 通知地址
param.put("trade_type","NATIVE ");// 交易类型
try {
String paramXml = WXPayUtil.generateSignedXml(param, partnerkey);
System.out.println("请求的参数:"+paramXml);
// 2. 发送请求
HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
client.setHttps(true);
client.setXmlParam(paramXml);
client.post();
// 3. 获取结果
String xmlResult = client.getContent();
Map<String, String> mapResult = WXPayUtil.xmlToMap(xmlResult);
Map map = new HashMap();
map.put("code_url",mapResult.get("code_url"));
map.put("out_trade_no",out_trade_no);
map.put("total_fee",total_fee);
return map;
} catch (Exception e) {
e.printStackTrace();
return new HashMap();
}
}
}
3. 控制层
cart-web创建PayController.java
@RestController
@RequestMapping("/pay")
public class PayController {
@Reference
private WeixinPayService weixinPayService;
@RequestMapping("/createNative")
public Map createNative(){
IdWorker idWorker = new IdWorker();
return weixinPayService.createNative(idWorker.nextId()+"","1");
}
}
这里订单号通过分布式id生成器生成,金额暂时写死,后续开发再对接业务系统得到订单号和金额。
4. 测试
启动user-service、cart-service、order-service、pay-service
为了测试方便,将cart-web的security放行pay部分的后端代码,启动cart-web
发现签名错误
3. 前端代码实现
1. 服务层
在cart-web创建payService.js
app.service('payService',function ($http) {
// 本地支付
this.createNative=function () {
return $http.get('pay/createNative.do');
}
});
2. 控制层
在cart-web创建payController.js
app.controller('payController',function ($scope, payService) {
payService.createNative().success(
function (response) {
// 显示订单号和金额
$scope.money = (response.total_fee/100).toFixed(2);
$scope.out_trade_no = response.out_trade_no;
// 生成二维码
var qr = new QRious({
element:document.getElementById('qrious'),
size:250,
value:response.code_url,
level:'H'
});
}
);
});
3. 页面
修改pay.html,引入js
<script type="text/javascript" src="plugins/angularjs/angular.min.js"> </script>
<script type="text/javascript" src="js/base.js"> </script>
<script type="text/javascript" src="js/service/payService.js"> </script>
<script type="text/javascript" src="js/controller/payController.js"> </script>
<script type="text/javascript" src="plugins/qrious.min.js"></script>
指令
<body ng-app="pinyougou" ng-controller="payController" ng-init="createNative()">
设置二维码图片的id
<img id="qrious">
显示订单号和金额
<h4 class="fl tit-txt"><span class="success-icon"></span><span class="success-info">订单提交成功,请您及时付款!订单号:{{out_trade_no}}</span></h4>
<span class="fr"><em class="sui-lead">应付金额:</em><em class="orange money">¥{{money}}</em>元</span>
4. 品优购-检测支付状态
1. 需求分析及实现思路
1. 需求分析
当用户支付成功后跳转到成功页面
当返回异常时跳转到错误页面
2. 实现思路
通过HttpClient工具类实现对远程支付接口的调用
接口链接:https://api.mch.weixin.qq.com/pay/orderquery
具体参数参见“查询订单”api,在controller方法中轮询调用查询订单(间隔3秒),当返回状态为success,在controller方法返回结果。前端代码收到结果跳转到成功页面
怎么能让支付完成马上就知道呢?
设置循环调用
一种是前端循环调用后端
一种是后端循环调用微信支付查询结果,前端调用后端
2. 检测支付状态-后端代码
1. 服务接口层
WeixinPayService.java
// 查询订单支付状态
public Map queryPayStatus(String out_trade_no);
2. 服务实现层
@Override
public Map queryPayStatus(String out_trade_no) {
// 1. 封装参数
Map param = new HashMap();
param.put("appid",appid);//公众账号ID
param.put("mch_id",partner);//商户号
param.put("out_trade_no",out_trade_no);//商户订单号
param.put("nonce_str",WXPayUtil.generateNonceStr());//随机字符串
try {
String xmlParam = WXPayUtil.generateSignedXml(param, partnerkey);
// 2. 发送请求
HttpClient client = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
client.setHttps(true);
client.setXmlParam(xmlParam);
client.post();
// 3. 获取结果
String xmlResult = client.getContent();
Map<String, String> mapResult = WXPayUtil.xmlToMap(xmlResult);
System.out.println("调用查询API返回结果:"+mapResult);
return mapResult;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
3. 控制层
cart-web的PayController.java
@RequestMapping("/queryPayStatus")
public Result queryPayStatus(String out_trade_no){
Result result = null;
while(true){
Map map = weixinPayService.queryPayStatus(out_trade_no);
if(map==null){
result = new Result(false,"支付发生错误");
break;
}
if("SUCCESS".equals(map.get("trade_state"))){
result = new Result(true,"支付成功");
break;
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return result;
}
3. 检测支付状态-前端代码
生成二维码之后就开始调用方法查询状态
1. 服务层
// 查询支付状态
this.queryPayStatus = function (out_trade_no) {
return $http.get('pay/queryPayStatus.do?out_trade_no='+out_trade_no);
}
2. 控制层
payController.js
// 调用查询
queryPayStatus = function () {
payService.queryPayStatus($scope.out_trade_no).success(
function (response) {
if(response.success){
location.href = "paysuccess.html";
}else{
location.href = "payfail.html";
}
}
);
}
payService.createNative().success(
function (response) {
// 显示订单号和金额
$scope.money = (response.total_fee/100).toFixed(2);
$scope.out_trade_no = response.out_trade_no;
// 生成二维码
var qr = new QRious({
element:document.getElementById('qrious'),
size:250,
value:response.code_url,
level:'H'
});
queryPayStatus();//调用查询
}
);
4. 查询时间限制
1. 问题分析
如果用户到了二维码页面一直未支付,或是关掉了支付页面,代码会一直循环调用微信接口,这样会对程序造成很大的压力,所以需要加一个时间限制或者循环次数限制,当超过该阈值,跳出循环。
2. 代码完善
- 修改cart-web工程PayController.java的queryPayStatus方法
@RequestMapping("/queryPayStatus")
public Result queryPayStatus(String out_trade_no){
Result result = null;
int x = 0;
while(true){
Map map = weixinPayService.queryPayStatus(out_trade_no);
if(map==null){
result = new Result(false,"支付发生错误");
break;
}
if("SUCCESS".equals(map.get("trade_state"))){
result = new Result(true,"支付成功");
break;
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 为了不让循环无休止的运行,定义一个循环变量,如果这个变量超过这个值退出循环,设置时间为5min
x++;
if(x>=100){
result = new Result(false,"二维码超时");
break;
}
}
return result;
}
- 修改payController.js
// 查询支付状态
queryPayStatus = function () {
payService.queryPayStatus($scope.out_trade_no).success(
function (response) {
if(response.success){
location.href = "paysuccess.html";
}else{
if(response.message==="二维码超时"){
$scope.createNative();//重新生成二维码
}else{
location.href = "payfail.html";
}
}
}
);
}
- 测试
5. 支付成功页面显示金额
1. 问题分析
支付成功页面需要显示真正支付金额
2. 代码完善
- 修改payController.js,跳转页面传参
// 查询支付状态
queryPayStatus = function () {
payService.queryPayStatus($scope.out_trade_no).success(
function (response) {
if(response.success){
location.href = "paysuccess.html#?money="+$scope.money;
}else{
if(response.message==="二维码超时"){
$scope.createNative();//重新生成二维码
}else{
location.href = "payfail.html";
}
}
}
);
}
- 在payController.js中引入
$location
服务,新增方法
// 获取金额
$scope.getMoney = function () {
return $location.search()['money'];
}
- 修改页面paysuccess.html,引入js和body指令,用表达式显示金额
<p>支付金额:¥{{getMoney()}}元</p>
5. 品优购-支付日志
1. 需求分析
现在系统还有两个问题
- 系统中无法查询到支付记录
- 支付后订单状态没有改变
现在来解决这两个问题
实现思路
- 在用户下订单时,判断如果为微信支付,就向支付日志表添加一条记录,信息包括支付总金额、订单id(多个)、用户id、下单时间等信息,支付状态为0(未支付)
- 生成的支付日志对象放入redis中,以用户id作为key,在生成支付二维码时就可以从redis中提取支付日志对象的金额和订单号
- 当用户支付成功,修改支付日志的支付状态为1(已支付),并记录微信传递给我们的交易流水号,根据订单id(多个)修订订单的状态为2(已付款)
2. 表结构分析
tb_paylog 支付日志表
3. 插入日志记录
修改order-service工程OrderServiceImpl.java的add方法
内容:判断如果支付方式为微信支付,向数据库插入支付日志记录,并放入redis存储。
@Override
public void add(TbOrder order) {
// 1. 从redis中提取购物车列表
List<Cart> cartList = (List<Cart>) redisTemplate.boundHashOps("cartList").get(order.getUserId());
// 2. 循环购物车列表添加订单
List<String> orderIdList = new ArrayList<>();//订单id列表
double total_money = 0;//总金额(元)
for (Cart cart : cartList) {
TbOrder tbOrder = new TbOrder();
long orderId = idWorker.nextId();
tbOrder.setOrderId(orderId);
tbOrder.setPaymentType(order.getPaymentType());// 付款方式
tbOrder.setStatus("1");//未付款
tbOrder.setCreateTime(new Date());
tbOrder.setUpdateTime(new Date());
tbOrder.setUserId(order.getUserId());
tbOrder.setReceiverAreaName(order.getReceiverAreaName());//收货人地址
tbOrder.setReceiverMobile(order.getReceiverMobile());//收货人电话
tbOrder.setReceiver(order.getReceiver());// 收货人
tbOrder.setSourceType(order.getSourceType());//订单来源
tbOrder.setSellerId(cart.getSellerId());//商家id
double money = 0;//合计数
// 循环购物车中每条明细记录
for (TbOrderItem orderItem : cart.getOrderItemList()) {
orderItem.setId(idWorker.nextId());
orderItem.setOrderId(orderId);//订单编号
orderItem.setSellerId(order.getSellerId());//商家id
orderItemMapper.insert(orderItem);
money += orderItem.getTotalFee().doubleValue();
}
tbOrder.setPayment(new BigDecimal(money));//合计
orderMapper.insert(tbOrder);
orderIdList.add(orderId+"");//添加到订单列表
total_money +=money;//累加到总金额
}
// 添加支付日志
if("1".equals(order.getPaymentType())){
TbPayLog payLog = new TbPayLog();
payLog.setOutTradeNo(idWorker.nextId()+"");//支付单号
payLog.setCreateTime(new Date());
payLog.setUserId(order.getUserId());
payLog.setOrderList(orderIdList.toString().replace("[","").replace("]",""));
payLog.setTotalFee((long) (total_money*100));//金额(分)
payLog.setTradeState("0");//交易状态
payLog.setPayType("1");//微信
payLogMapper.insert(payLog);
redisTemplate.boundHashOps("payLog").put(order.getUserId(),payLog);//放入缓存
}
// 3. 清除redis中的购物车
redisTemplate.boundHashOps("cartList").delete(order.getUserId());
}
4. 读取支付日志
1. 服务接口层
order-interface工程的OrderService.java新增方法
// 根据用户id获取支付日志
public TbPayLog searchPayLogFromRedis(String userId);
2. 服务实现层
@Override
public TbPayLog searchPayLogFromRedis(String userId) {
return (TbPayLog) redisTemplate.boundHashOps("payLog").get(userId);
}
3. 控制层
修改cart-web工程的PayController.java的createNative方法
实现思路:调用获取支付日志对象的方法,得到订单号和金额
@RequestMapping("/createNative")
public Map createNative(){
// 1. 获取当前登录用户名
String username = SecurityContextHolder.getContext().getAuthentication().getName();
// 2. 提取支付日志(缓存)
TbPayLog payLog = orderService.searchPayLogFromRedis(username);
// 3. 调用微信支付接口
if(payLog!=null){
return weixinPayService.createNative(payLog.getOutTradeNo(),payLog.getTotalFee()+"");
}else{
return new HashMap();
}
}
5. 修改订单状态
1. 服务接口层
在order-interface的OrderService.java新增方法
// 支付成功修改状态
public void updateOrderStatus(String out_trade_no,String transaction_id);
2. 服务实现层
在order-service工程OrderServiceImpl.java实现方法
做三件事:
- 修改支付日志的状态
- 修改关联的订单状态
- 清除缓存的支付日志对象
@Override
public void updateOrderStatus(String out_trade_no, String transaction_id) {
// 1. 修改支付日志的状态和相关字段
TbPayLog payLog = payLogMapper.selectByPrimaryKey(out_trade_no);
payLog.setPayTime(new Date());//支付时间
payLog.setTradeState("1");// 交易成功
payLog.setTransactionId(transaction_id);//微信交易流水号
payLogMapper.updateByPrimaryKey(payLog);//修改
// 2. 修改订单表的状态
String orderList = payLog.getOrderList();// 订单id 串
String[] orderIds = orderList.split(",");
for (String orderId : orderIds) {
TbOrder order = orderMapper.selectByPrimaryKey(Long.valueOf(orderId));
order.setStatus("2");//已付款状态
order.setPaymentTime(new Date());//支付时间
orderMapper.updateByPrimaryKey(order);
}
// 3. 清除缓存的payLog
redisTemplate.boundHashOps("payLog").delete(payLog.getUserId());
}
3. 控制层
修改cart-web的PayController.java,在微信支付接口有成功返回状态时,调用修改状态的方法
if("SUCCESS".equals(map.get("trade_state"))){
result = new Result(true,"支付成功");
// 修改订单状态
orderService.updateOrderStatus(out_trade_no, (String) map.get("transaction_id"));
break;
}
6. 支付日志显示
需求:在运营商后台,显示支付日志列表,实现按日期、状态、用户进行查询
TODO