1. 工程搭建与准备工作
1)建立支付服务接口模块pinyougou-pay-interface
2)建立支付服务实现模块pinyougou-pay-service (war) 依赖pinyougou-pay-interface 和pinyougou-common 、 spring dubbox 相关依赖 、微信SDK
3)添加微信SDJ依赖
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
4)在pinyougou-common工程中添加工具类HttpClient.java ,并添加依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
工具类
package util;
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 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.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.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
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;
}
}
添加配置文件weixinpay.properties,文件内容为
appid: 微信公众账号或开放平台APP的唯一标识
partner:财付通平台的商户账号
partnerkey:财付通平台的商户密钥
notifyurl: 回调地址
5)pinyougou-cart-web依赖工程pinyougou-pay-service
6)将二维码插件QRious 拷贝到pinyougou-cart-web 的plugins目录中
2. 微信支付二维码生成
我们通过HttpClient工具类实现对远程支付接口的调用。
接口链接:https://api.mch.weixin.qq.com/pay/unifiedorder
具体参数参见“统一下单”API, 构建参数发送给统一下单的url ,返回的信息中有支付url,根据url生成二维码,显示的订单号和金额也在返回的信息中。
2.1 后端代码实现
2.1.1 服务层
@Service
public class WeixinPayServiceImpl implements WeixinPayService {
@Value("${appid}")
private String appid;
@Value("${partner}")
private String partner;
@Value("${partnerkey}")
private String partnerkey;
/**
* 生成二维码
* @return
*/
public Map createNative(String out_trade_no,String total_fee){
//1.创建参数
Map<String,String> param=new HashMap();//创建参数
param.put("appid", appid);//公众号
param.put("mch_id", partner);//商户号
param.put("nonce_str", WXPayUtil.generateNonceStr());//随机字符串
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", "http://test.itcast.cn");//回调地址(随便写)
param.put("trade_type", "NATIVE");//交易类型
try {
//2.生成要发送的xml
String xmlParam = WXPayUtil.generateSignedXml(param, partnerkey);
System.out.println(xmlParam);
HttpClient client=new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
client.setHttps(true);
client.setXmlParam(xmlParam);
client.post();
//3.获得结果
String result = client.getContent();
System.out.println(result);
Map<String, String> resultMap = WXPayUtil.xmlToMap(result);
Map<String, String> map=new HashMap<>();
map.put("code_url", resultMap.get("code_url"));//支付地址
map.put("total_fee", total_fee);//总金额
map.put("out_trade_no",out_trade_no);//订单号
return map;
} catch (Exception e) {
e.printStackTrace();
return new HashMap<>();
}
}
}
2.1.2 控制层
/**
* 支付控制层
* @author Administrator
*
*/
@RestController
@RequestMapping("/pay")
public class PayController {
@Reference
private WeixinPayService weixinPayService;
/**
* 生成二维码
* @return
*/
@RequestMapping("/createNative")
public Map createNative(){
IdWorker idworker=new IdWorker();
return weixinPayService.createNative(idworker.nextId()+"","1");
}
}
注意:这里订单号通过分布式ID生成器生成,金额暂时写死,后续开发我们再对接业务系统得到订单号和金额
2.2 前端实现
2.2.1 服务层
在pinyougou-cart-web创建 payService.js
app.service('payService',function($http){
//本地支付
this.createNative=function(){
return $http.get('pay/createNative.do');
}
});
2.2.2 控制层
在pinyougou-cart-web 创建payController.js
app.controller('payController' ,function($scope ,payService){
//本地生成二维码
$scope.createNative=function(){
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,
level:'H',
value:response.code_url
});
}
);
}
});
2.2.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()">
设置二维码图片
<p class="red"></p>
<div class="fl code">
<img id="qrious">
<div class="saosao">
<p>请使用微信扫一扫</p>
<p>扫描二维码支付</p>
</div>
</div>
显示订单号
订单号:{{out_trade_no}}
显示金额
<em class="orange money">¥{{money}}</em>元
3. 检测支付状态
我们通过HttpClient工具类实现对远程支付接口的调用。
接口链接:https://api.mch.weixin.qq.com/pay/orderquery
具体参数参见“查询订单”API, 我们在controller方法中轮询调用查询订单(间隔3秒),当返回状态为success时,我们会在controller方法返回结果。前端代码收到结果后跳转到成功页面。
3.1 后端代码
3.1.1 服务层
在pinyougou-pay-service的WeixinPayServiceImpl.java中实现方法
@Override
public Map queryPayStatus(String out_trade_no) {
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());//随机字符串
String url="https://api.mch.weixin.qq.com/pay/orderquery";
try {
String xmlParam = WXPayUtil.generateSignedXml(param, partnerkey);
HttpClient client=new HttpClient(url);
client.setHttps(true);
client.setXmlParam(xmlParam);
client.post();
String result = client.getContent();
Map<String, String> map = WXPayUtil.xmlToMap(result);
System.out.println(map);
return map;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
3.1.2 控制层
在pinyougou-cart-web的PayController.java新增方法
/**
* 查询支付状态
* @param out_trade_no
* @return
*/
@RequestMapping("/queryPayStatus")
public Result queryPayStatus(String out_trade_no){
Result result=null;
while(true){
//调用查询接口
Map<String,String> map = weixinPayService.queryPayStatus(out_trade_no);
if(map==null){//出错
result=new Result(false, "支付出错");
break;
}
if(map.get("trade_state").equals("SUCCESS")){//如果成功
result=new Result(true, "支付成功");
break;
}
try {
Thread.sleep(3000);//间隔三秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return result;
}
3.2 前端
3.2.1 服务层
在payService.js新增方法
//查询支付状态
this.queryPayStatus=function(out_trade_no){
return $http.get('pay/queryPayStatus.do?out_trade_no='+out_trade_no);
}
3.2.2 控制层
在payController.js中新增方法
//查询支付状态
queryPayStatus=function(out_trade_no){
payService.queryPayStatus(out_trade_no).success(
function(response){
if(response.success){
location.href="paysuccess.html";
}else{
location.href="payfail.html";
}
}
);
}
在createNative方法的回调方法中调用此查询方法
//本地生成二维码
$scope.createNative=function(){
payService.createNative().success(
function(response){
..........
queryPayStatus(response.out_trade_no);//查询支付状态
}
);
}
4. 代码完善
4.1 查询时间限制
如果用户到了二维码页面一直未支付,或是关掉了支付页面,我们的代码会一直循环调用微信接口,这样会对程序造成很大的压力。所以我们要加一个时间限制或是循环次数限制,当超过时间或次数时,跳出循环
1)修改pinyougou-cart-web工程PayController.java的queryPayStatus方法
@RequestMapping("/queryPayStatus")
public Result queryPayStatus(String out_trade_no){
Result result=null;
int x=0;
while(true){
//调用查询接口
.......
try {
Thread.sleep(3000);//间隔三秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//为了不让循环无休止地运行,我们定义一个循环变量,如果这个变量超过了这个值则退出循环,设置时间为5分钟
x++;
if(x>=100){
result=new Result(false, "二维码超时");
break;
}
}
return result;
}
2)修改payController.js
//查询支付状态
queryPayStatus=function(out_trade_no){
payService.queryPayStatus(out_trade_no).success(
function(response){
if(response.success){
location.href="paysuccess.html";
}else{
if(response.message=='二维码超时'){
$scope.createNative();//重新生成二维码
}else{
location.href="payfail.html";
}
}
}
);
}
4.2 支付成功页面显示金额
1)修改payController.js 跳转页面传参
//查询支付状态
queryPayStatus=function(out_trade_no){
payService.queryPayStatus(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";
}
}
}
);
}
2)在payController.js中引入$location服务 ,新增方法
//获取金额
$scope.getMoney=function(){
return $location.search()['money'];
}
3)修改页面paysuccess.html ,引入JS (与pay.html相同) ,body添加指令
ng-app="pinyougou" ng-controller="payController"
4)用表达式显示金额
<p>支付金额:¥{{getMoney()}}元</p>
5. 支付日志
实现思路:
(1)在用户下订单时,判断如果为微信支付,就想支付日志表添加一条记录,信息包括支付总金额、订单ID(多个)、用户ID 、下单时间等信息,支付状态为0(未支付)
(2)生成的支付日志对象放入redis中,以用户ID作为key,这样在生成支付二维码时就可以从redis中提取支付日志对象中的金额和订单号。
(3)当用户支付成功后,修改支付日志的支付状态为1(已支付),并记录微信传递给我们的交易流水号。根据订单ID(多个)修改订单的状态为2(已付款)。
5.1 表结构
tb_paylog 支付日志表
字段 | 类型 | 长度 | 含义 |
out_trade_no | varchar | 30 | 支付订单号 |
create_time | datatime |
| 创建时间 |
pay_time | datatime |
| 支付完成时间 |
total_fee | bigint |
| 支付金额(分) |
transaction_id | varchar | 30 | 交易流水号 |
trade_state | varchar | 1 | 交易状态 |
pay_type | varchar | 1 | 支付类型: 1:微信 2:支付宝 3:网银 |
order_list | varchar | 200 | 订单表ID串,用逗号分隔 |
5.2 插入日志记录
修改pinyougou-order-service工程OrderServiceImpl.java 的add方法。
内容:判断如果支付方式为微信支付,向数据库插入支付日志记录,并放入redis存储
@Autowired
private TbPayLogMapper payLogMapper;
/**
* 增加
*/
public void add(TbOrder order) {
List<Cart> cartList = (List<Cart>)
redisTemplate.boundHashOps("cartList").get( order.getUserId() );
List<String> orderIdList=new ArrayList();//订单ID列表
double total_money=0;//总金额 (元)
for(Cart cart:cartList){
long orderId = idWorker.nextId();
......
orderIdList.add(orderId+"");//添加到订单列表
total_money+=money;//累加到总金额
}
if("1".equals(order.getPaymentType())){//如果是微信支付
TbPayLog payLog=new TbPayLog();
String outTradeNo= idWorker.nextId()+"";//支付订单号
payLog.setOutTradeNo(outTradeNo);//支付订单号
payLog.setCreateTime(new Date());//创建时间
//订单号列表,逗号分隔
String ids=orderIdList.toString().replace("[", "").replace("]", "").replace(" ", "");
payLog.setOrderList(ids);//订单号列表,逗号分隔
payLog.setPayType("1");//支付类型
payLog.setTotalFee( (long)(total_money*100 ) );//总金额(分)
payLog.setTradeState("0");//支付状态
payLog.setUserId(order.getUserId());//用户ID
payLogMapper.insert(payLog);//插入到支付日志表
redisTemplate.boundHashOps("payLog").put(order.getUserId(), payLog);//放入缓存
}
redisTemplate.boundHashOps("cartList").delete(order.getUserId());
}
5.2 读取支付日志
5.2.1 服务层
pinyougou-order-service的OrderServiceImpl.java实现方法
@Override
public TbPayLog searchPayLogFromRedis(String userId) {
return (TbPayLog) redisTemplate.boundHashOps("payLog").get(userId);
}
5.2.2 控制层
修改pinyougou-cart-web工程PayController.java的createNative方法
实现思路:调用获取支付日志对象的方法,得到订单号和金额
@Reference
private OrderService orderService;
/**
* 生成二维码
* @return
*/
@RequestMapping("/createNative")
public Map createNative(){
//获取当前用户
String userId=SecurityContextHolder.getContext().getAuthentication().getName();
//到redis查询支付日志
TbPayLog payLog = orderService.searchPayLogFromRedis(userId);
//判断支付日志存在
if(payLog!=null){
return weixinPayService.createNative(payLog.getOutTradeNo(),payLog.getTotalFee()+"");
}else{
return new HashMap();
}
}
5.3 修改订单状态
5.3.1 服务层
在pinyougou-order-service工程OrderServiceImpl.java实现该方法.
这个方法主要做三件事:
1. 修改支付日志状态
2. 修改关联的订单的状态
3. 清除缓存中的支付日志对象
@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();//获取订单号列表
String[] orderIds = orderList.split(",");//获取订单号数组
for(String orderId:orderIds){
TbOrder order = orderMapper.selectByPrimaryKey( Long.parseLong(orderId) );
if(order!=null){
order.setStatus("2");//已付款
orderMapper.updateByPrimaryKey(order);
}
}
//清除redis缓存数据
redisTemplate.boundHashOps("payLog").delete(payLog.getUserId());
}
5.3.2 控制层
修改pinyougou-cart-web的PayController.java。在微信支付接口有成功返回状态时,调用修改状态的方法
/**
* 查询支付状态
* @param out_trade_no
* @return
*/
@RequestMapping("/queryPayStatus")
public Result queryPayStatus(String out_trade_no){
Result result=null;
int x=0;
while(true){
//调用查询接口
Map<String,String> map = weixinPayService.queryPayStatus(out_trade_no);
if(map==null){//出错
result=new Result(false, "支付出错");
break;
}
if(map.get("trade_state").equals("SUCCESS")){//如果成功
result=new Result(true, "支付成功");
//修改订单状态
orderService.updateOrderStatus(out_trade_no, map.get("transaction_id"));
break;
}
try {
Thread.sleep(3000);//间隔三秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//为了不让循环无休止地运行,我们定义一个循环变量,如果这个变量超过了这个值则退出循环,设置时间为5分钟
......
}
return result;
}