1. 二维码
1.1 什么是二维码
- 二维码又称 QR Code,QR 全称 Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的 BarCode 条形码能存更多的信息,也能表示更多的数据类型。
- 二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的;在代码编制上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设备自动识读以实现信息自动处理:它具有条码技术的一些共性:每种码制有其特定的字符集;每个字符占有一定的宽度;具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化点。
1.2 二维码优势
- 信息容量大, 可以容纳多达 1850 个大写字母或 2710 个数字或 500 多个汉字
- 应用范围广, 支持文字,声音,图片,指纹等等…
- 容错能力强, 即使图片出现部分破损也能使用
- 成本低, 容易制作
1.3 二维码容错级别
- L 级(低) 7%的码字可以被恢复。
- M 级(中) 的码字的 15%可以被恢复。
- Q 级(四分)的码字的 25%可以被恢复。
- H 级(高) 的码字的 30%可以被恢复。
1.4 二维码生成插件 qrious
- qrious 是一款基于 HTML5 Canvas 的纯 JS 二维码生成插件。通过 qrious.js 可以快速生成各种二维码,你可以控制二维码的尺寸颜色,还可以将生成的二维码进行 Base64 编码。
qrious.js 二维码插件的可用配置参数如下:
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
background | String | “white” | 二维码的背景颜色。 |
foreground | String | “black” | 二维码的前景颜色。 |
level | String | “L” | 二维码的误差校正级别(L, M, Q, H)。 |
mime | String | “image/png” | 二维码输出为图片时的 MIME 类型。 |
size | Number | 100 | 二维码的尺寸,单位像素。 |
value | String | “” | 需要编码为二维码的值 |
下面的代码即可生成一张二维码
<html>
<head>
<title>二维码入门小 demo</title>
</head>
<body>
<img id="qrious">
<script src="qrious.min.js"></script>
<script>
var qr = new QRious({
element: document.getElementById('qrious'),
size: 250,
value: 'http://www.baidu.com'
});
</script>
</body>
</html>
运行效果:
2. 微信扫码支付
2.1 开发文档
- 按 API 要求组装参数,以 XML 方式发送(POST)给微信支付接口(URL),微信支付接口也是以 XML 方式给予响应。程序根据返回的结果(其中包括支付 URL)生成二维码或判断订单状态。
- 在线微信支付开发文档:https://pay.weixin.qq.com/wiki/doc/api/index.html
- 我们在本章中会用到”统一下单”和”查询订单”两组 API
- appid:微信公众账号或开放平台 APP 的唯一标识
- mch_id:商户号 (配置文件中的 partner)
- partnerkey:商户密钥
- sign: 数字签名, 根据微信官方提供的密钥和一套算法生成的一个加密信息, 就是为了保证交易的安全性
2.2 微信支付 SDK
微信支付提供了 SDK, 大家下载后打开源码,install 到本地仓库
也可以使用 maven 工程中引入微信支付 SDK 的依赖坐标
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
- 我们主要会用到微信支付 SDK 的以下功能:
// 获取随机字符串
String str = WXPayUtil.generateNonceStr()
// MAP 转换为 XML 字符串(自动添加签名)
// 参数: maps: 需要传递的参数 Map 集合, partnerkey : 商户密钥
String xmlParam = WXPayUtil.generateSignedXml(maps, partnerkey);
//XML字符串转换为 Map 对象
Map<String, String> stringStringMap = WXPayUtil.xmlToMap(xmlMap);
2.3 HttpClient 工具类
- HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。HttpClient 已经应用在很多的项目中,比如 Apache Jakarta上很著名的另外两个开源项目 Cactus 和 HTMLUnit 都使用了 HttpClient。
- HttpClient 通俗的讲就是模拟了浏览器的行为,如果我们需要在后端向某一地址提交数据获取结果,就可以使用HttpClient.
关于 HttpClient(原生)具体的使用不属于我们本章的学习内容,我们这里这里为了简化 HttpClient 的使用,提供了工具类 HttpClient(对原生 HttpClient 进行了封装)
- HttpClient 工具类使用的步骤
// 创建客户端
HttpClient client = new HttpClient(请求的 url 地址);
//是否是 https 协议
client.setHttps(true);
//发送的 xml 数据(String类型)
client.setXmlParam(xmlParam);
//执行 post 请求
client.post();
//获取结果
String result = client.getContent();
- 使用时需导入依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
2.4 工程搭建与准备工作
- 建立支付服务接口模块 pay-interface (jar)
- 建立支付服务实现模块 pay-service(war)依赖 pay-interface 和 pay-common 、 spring 、 dubbo 相关依赖 、 微信 SDK (因为不需要连接数据库所以不用引用 dao 工程)
<!-- 微信SDK -->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
<!-- spring,dubbo 等其他依赖自行添加 -->
- 在pay-common 工程中添加工具类 HttpClient.java ,并添加依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
- 添加配置文件 weixinpay.properties
# appid:微信公众账号或开放平台 APP 的唯一标识
appid=wx8397f8696b5
# partner:财付通平台的商户账号 (mch_id:商户号)
partner=1473426802
# partnerkey:财付通平台的商户密钥
partnerkey=T6m9iK73b0kn9g5v426MKfHQ
# notifyurl: 回调地址, 支付成功后微信服务器向回调地址发送信息
notifyurl=http://********/WeChatPay/WeChatP
# payUrl : 远程支付接口, 用于生成支付的 url
payUrl=https://api.mch.weixin.qq.com/pay/unifiedorder
- pinyougou-cart-web 依赖工程 pinyougou-pay-service
- 将二维码插件 QRious 拷贝到 pinyougou-cart-web 的 plugins 目录中
2.5 代码实现
2.5.1 前端
2.5.1.1 页面
- 修改 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="plugins/qrious.min.js"></script>
<!-- controller层要放在元素 <img id="qrious"> 之后,否则js代码无法获取到 #qrious 元素-->
<script type="text/javascript" src="js/controller/payController.js"> </script>
- 指令
<body ng-app="pyg" ng-controller="payController" ng-init="createQrCode()">
- 设置二维码图片的 ID
<div class="fl code">
<img id="qrious" >
<div class="saosao">
<p>请使用微信扫一扫</p>
<p>扫描二维码支付</p>
</div>
</div>
- 显示订单号
<span class="success-info">
订单提交成功,请您及时付款!订单号:{{out_trade_no}}
</span>
- 显示金额
<span class="fr">
<em class="sui-lead">应付金额:</em>
<em class="orange money">¥{{total_fee}}</em>元
</span>
2.5.1.2 控制层 payController.js
//控制层
app.controller('payController', function ($scope, $location,payService) {
//生成二维码
//1,向支付系统发送生成二维码请求
//2,支付系统调用微信支付品台支付下单接口
//3,微信支付品台返回支付地址
//4,根据此地址生成二维码
$scope.createQrCode = function () {
//调用服务层方法,向微信支付品台下单,获取支付地址
payService.createQrCode().success(function (data) {
//获取支付地址
var code_url = data.code_url;
//支付金额
$scope.total_fee = data.total_fee;
//订单号
$scope.out_trade_no = data.out_trade_no;
//使用qrious插件生成二维码
var qr = new QRious({
element: document.getElementById('qrious'),
size: 300,
background: 'white',
foreground: 'black',
level: 'H',
value: code_url
});
//调用方法
queryStatus($scope.out_trade_no);
})
};
//实时监控二维码支付状态
//每3秒查询一次二维码支付状态,如果支付成功,跳转到支付成功页面,否则跳转到支付失败页面
queryStatus = function (out_trade_no) {
//调用服务层方法,查询订单状态
payService.queryStatus(out_trade_no).success(function (data) {
//判断
if (data.success) {
//跳转到支付成功页面
location.href = "paysuccess.html#?money="+$scope.total_fee;
} else if (data.message == "timeout") {
//重新生成二维码
$scope.createQrCode();
}
else {
location.href = "payfail.html";
}
})
};
//获取支付成功后金额
$scope.loadTotalFee = function () {
$scope.money = $location.search()["money"];
}
});
2.5.1.3 服务层
//服务层
app.service('payService', function ($http) {
//生成二维码
this.createQrCode = function () {
return $http.get('../pay/createQrCode');
};
//监控二维码支付状态
this.queryStatus = function (out_trade_no) {
return $http.get('../pay/queryStatus/'+out_trade_no);
}
});
2.5.2 后端
2.5.2.1 控制层
import com.alibaba.dubbo.config.annotation.Reference;
import com.pyg.pay.service.PayService;
import com.pyg.utils.PygResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@RestController
@RequestMapping("/pay")
public class PayController {
//注入远程支付对象
@Reference(timeout = 10000000)
private PayService payService;
//生成二维码
//1,向支付系统发送生成二维码请求
//2,支付系统调用微信支付品台支付下单接口
//3,微信支付品台返回支付地址
//4,根据此地址生成二维码
/**
* 生成二维码
*
* @return
*/
@RequestMapping("createQrCode")
public Map createQrCode(HttpServletRequest request) {
//获取用户名
String userId = request.getRemoteUser();
//调用服务层方法,向微信支付品台下单
Map maps = payService.createQrCode(userId);
return maps;
}
/**
* 需求:实时监控二维码状态,及时发现二维码是否支付成功
*/
@RequestMapping("queryStatus/{out_trade_no}")
public PygResult queryStatus(@PathVariable String out_trade_no) {
PygResult result = null;
int i = 0;
while (true) {
//调用查询接口
Map<String, String> map = payService.queryStatus(out_trade_no);
if (map == null) {//出错
result = new PygResult(false, "支付出错");
break;
}
if (map.get("trade_state").equals("SUCCESS")) {//如果成功
result = new PygResult(true, "支付成功");
break;
}
try {
Thread.sleep(3000);//间隔三秒
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
if (i >= 100) {
result = new PygResult(false, "timeout");
break;
}
}
return result;
}
}
2.5.2.2 服务层接口
import java.util.Map;
public interface PayService {
/**
* 需求:生成二维码
* @param userId
* @return Map
*/
public Map createQrCode(String userId);
/**
* 需求:查询支付二维码状态,查询是否支付成功
* @param out_trade_no
* @return
*/
public Map queryStatus(String out_trade_no);
}
2.5.2.3 服务层实现类
import com.alibaba.dubbo.config.annotation.Service;
import com.alibaba.fastjson.JSON;
import com.github.wxpay.sdk.WXPayUtil;
import com.pyg.mapper.TbOrderMapper;
import com.pyg.pay.service.PayService;
import com.pyg.pojo.TbOrder;
import com.pyg.pojo.TbOrderExample;
import com.pyg.utils.HttpClient;
import com.pyg.utils.IdWorker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class PayServiceImpl implements PayService {
//注入商户appid
@Value("${appid}")
private String appid;
//注入商户号
@Value("${partner}")
private String partner;
//注入商户秘钥
@Value("${partnerkey}")
private String partnerkey;
//注入微信支付接口
@Value("${payUrl}")
private String payUrl;
// 回调地址
@Value("${notifyurl}")
private String notifyurl;
//注入订单mapper接口代理对象
@Autowired
private TbOrderMapper orderMapper;
/**
* 需求:生成二维码
*
* @param userId 用户名
* @return Map 封装的数据
* 商户订单号和保存的订单订单号有关系没有? 支付订单
*/
public Map createQrCode(String userId) {
try {
//查询支付金额
//创建订单example对象
TbOrderExample example = new TbOrderExample();
//创建criteria对象
TbOrderExample.Criteria criteria = example.createCriteria();
//设置参数
criteria.andUserIdEqualTo(userId);
//执行查询
List<TbOrder> orderList = orderMapper.selectByExample(example);
//定义记录总金额变量
Double totalFee = 0d;
//循环多个订单,计算总金额
for (TbOrder tbOrder : orderList) {
totalFee += tbOrder.getPayment().doubleValue();
}
//创建一个map对象,封装支付下单参数
Map<String, String> maps = new HashMap<>();
//公众账号ID
maps.put("appid", appid);
//商户号
maps.put("mch_id", partner);
//随机字符串
maps.put("nonce_str", WXPayUtil.generateNonceStr());
//商品描述
maps.put("body", "十里堡");
//创建idworker对象,生成支付订单号
IdWorker idWorker = new IdWorker();
long payId = idWorker.nextId();
//out_trade_no
maps.put("out_trade_no", payId + "");
//设置支付金额
maps.put("total_fee", total_fee * 100);
maps.put("spbill_create_ip", "127.0.0.1");
maps.put("notify_url", notify_url);
maps.put("trade_type", "NATIVE");
//使用微信支付工具类对象,生成一个具有签名 sign 的xml格式参数(String类型)
String xmlparam = WXPayUtil.generateSignedXml(maps, partnerkey);
//创建httpClient对象,向微信支付品台发送请求,获取支付地址
HttpClient httpClient = new HttpClient(payUrl);
//设置请求方式
httpClient.setHttps(true);
//设置请求参数
httpClient.setXmlParam(xmlparam);
//设置请求方式
httpClient.post();
//获取回调结果
String content = httpClient.getContent();
//把xml格式转换成对象
//返回支付地址
Map<String, String> stringStringMap = WXPayUtil.xmlToMap(content);
//金额
stringStringMap.put("total_fee", totalFee + "");
//支付订单号
stringStringMap.put("out_trade_no", payId + "");
return stringStringMap;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 需求:查询支付二维码状态,查询是否支付成功
*
* @param out_trade_no 支付订单号
* @return 向微信支付品台发送请求,查询订单状态
* 1,封装向微信传递参数
* 2,把map参数转换为xml格式(此格式必须带签名)
* 3,使用httpClient发送请求(向微信支付品台)
* 4,获取微信支付返回结果
* 5,把结果返回给表现层
* 6,判断是否支付成功
*/
public Map queryStatus(String out_trade_no) {
try {
//创建map对象,封装查询微信支付状态参数
Map<String, String> maps = new HashMap<>();
//商户appid 唯一标识
maps.put("appid", appid);
//商户号 唯一标识
maps.put("mch_id", partner);
//设置订单号
maps.put("out_trade_no", out_trade_no);
//随机字符串
maps.put("nonce_str", WXPayUtil.generateNonceStr());
//具有签名的xml格式参数
String xmlParam = WXPayUtil.generateSignedXml(maps, partnerkey);
//使用httpClient向微信支付品台发送查询订单请求
HttpClient httpClient = new HttpClient("https://api.mch.weixin.qq.com/pay/orderquery");
//设置请求方式
httpClient.setHttps(true);
//设置参数
httpClient.setXmlParam(xmlParam);
//请求方式
httpClient.post();
//获取查询结果
String xmlMap = httpClient.getContent();
//把返回结果xml转换成map对象
Map<String, String> stringStringMap = WXPayUtil.xmlToMap(xmlMap);
//返回支付状态
return stringStringMap;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
工具类
1.1 HttpClient 工具类
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
/**
* http请求客户端
*
* @author Administrator
*
*/
public class HttpClient {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;
public boolean isHttps() {
return isHttps;
}
public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}
public String getXmlParam() {
return xmlParam;
}
public void setXmlParam(String xmlParam) {
this.xmlParam = xmlParam;
}
public HttpClient(String url, Map<String, String> param) {
this.url = url;
this.param = param;
}
public HttpClient(String url) {
this.url = url;
}
public void setParameter(Map<String, String> map) {
param = map;
}
public void addParameter(String key, String value) {
if (param == null)
param = new HashMap<String, String>();
param.put(key, value);
}
public void post() throws ClientProtocolException, IOException {
HttpPost http = new HttpPost(url);
setEntity(http);
execute(http);
}
public void put() throws ClientProtocolException, IOException {
HttpPut http = new HttpPut(url);
setEntity(http);
execute(http);
}
public void get() throws ClientProtocolException, IOException {
if (param != null) {
StringBuilder url = new StringBuilder(this.url);
boolean isFirst = true;
for (String key : param.keySet()) {
if (isFirst)
url.append("?");
else
url.append("&");
url.append(key).append("=").append(param.get(key));
}
this.url = url.toString();
}
HttpGet http = new HttpGet(url);
execute(http);
}
/**
* set http post,put param
*/
private void setEntity(HttpEntityEnclosingRequestBase http) {
if (param != null) {
List<NameValuePair> nvps = new LinkedList<NameValuePair>();
for (String key : param.keySet())
nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
}
if (xmlParam != null) {
http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
}
}
private void execute(HttpUriRequest http) throws ClientProtocolException,
IOException {
CloseableHttpClient httpClient = null;
try {
if (isHttps) {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
public boolean isTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.build();
} else {
httpClient = HttpClients.createDefault();
}
CloseableHttpResponse response = httpClient.execute(http);
try {
if (response != null) {
if (response.getStatusLine() != null)
statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
// 响应内容
content = EntityUtils.toString(entity, Consts.UTF_8);
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
httpClient.close();
}
}
public int getStatusCode() {
return statusCode;
}
public String getContent() throws ParseException, IOException {
return content;
}
}