微信支付
1 选择支付类型
选择扫码支付模式二
2 选择统一订单管理
3 下载WeChatPay中的java SDK
4 编写WXPayConfig和IWXPayDomain实现类
WXPayConfig实现类
package com.github.wxpay.sdk;
import lombok.Data;
import java.io.InputStream;
@Data
public class WXPayConfigImpl extends WXPayConfig {
/**
* 公众账号ID
*/
private String appID;
/**
* 商户号
*/
private String mchID;
/**
* 生成签名的密钥
*/
private String key;
/**
* 支付回调地址
*/
private String notifyUrl;
/**
* 支付方式
*/
private String payType;
public InputStream getCertStream(){
return null;
}
public IWXPayDomain getWXPayDomain(){
return WXPayDomainSimpleImpl.instance();
}
}
IWXPayDomain实现类
package com.github.wxpay.sdk;
import org.apache.http.conn.ConnectTimeoutException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
/**
* Created by blaketang on 2017/6/16.
*/
public class WXPayDomainSimpleImpl implements IWXPayDomain {
private WXPayDomainSimpleImpl(){}
private static class WxpayDomainHolder{
private static IWXPayDomain holder = new WXPayDomainSimpleImpl();
}
public static IWXPayDomain instance(){
return WxpayDomainHolder.holder;
}
public synchronized void report(final String domain, long elapsedTimeMillis, final Exception ex) {
DomainStatics info = domainData.get(domain);
if(info == null){
info = new DomainStatics(domain);
domainData.put(domain, info);
}
if(ex == null){ //success
if(info.succCount >= 2){ //continue succ, clear error count
info.connectTimeoutCount = info.dnsErrorCount = info.otherErrorCount = 0;
}else{
++info.succCount;
}
}else if(ex instanceof ConnectTimeoutException){
info.succCount = info.dnsErrorCount = 0;
++info.connectTimeoutCount;
}else if(ex instanceof UnknownHostException){
info.succCount = 0;
++info.dnsErrorCount;
}else{
info.succCount = 0;
++info.otherErrorCount;
}
}
public synchronized DomainInfo getDomain(final WXPayConfig config) {
DomainStatics primaryDomain = domainData.get(WXPayConstants.DOMAIN_API);
if(primaryDomain == null ||
primaryDomain.isGood()) {
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
long now = System.currentTimeMillis();
if(switchToAlternateDomainTime == 0){ //first switch
switchToAlternateDomainTime = now;
return new DomainInfo(WXPayConstants.DOMAIN_API2, false);
}else if(now - switchToAlternateDomainTime < MIN_SWITCH_PRIMARY_MSEC){
DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2);
if(alternateDomain == null ||
alternateDomain.isGood() ||
alternateDomain.badCount() < primaryDomain.badCount()){
return new DomainInfo(WXPayConstants.DOMAIN_API2, false);
}else{
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
}else{ //force switch back
switchToAlternateDomainTime = 0;
primaryDomain.resetCount();
DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2);
if(alternateDomain != null)
alternateDomain.resetCount();
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
}
static class DomainStatics {
final String domain;
int succCount = 0;
int connectTimeoutCount = 0;
int dnsErrorCount =0;
int otherErrorCount = 0;
DomainStatics(String domain) {
this.domain = domain;
}
void resetCount(){
succCount = connectTimeoutCount = dnsErrorCount = otherErrorCount = 0;
}
boolean isGood(){ return connectTimeoutCount <= 2 && dnsErrorCount <= 2; }
int badCount(){
return connectTimeoutCount + dnsErrorCount * 5 + otherErrorCount / 4;
}
}
private final int MIN_SWITCH_PRIMARY_MSEC = 3 * 60 * 1000; //3 minutes
private long switchToAlternateDomainTime = 0;
private Map<String, DomainStatics> domainData = new HashMap<String, DomainStatics>();
}
5 将SDK install 到本地仓库
在用DOSS命令进行install
mvn source:jar install -Dmaven.test.skip=true
用这个命令主要是为了跳过测试,用maven中的install也可以
6 在工程里引入wxpay坐标
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>3.0.9</version>
</dependency>
选择WXPay中的方法
在这使用统一下单方法
/**
* 作用:统一下单<br>
* 场景:公共号支付、扫码支付、APP支付
* @param reqData 向wxpay post的请求数据
* @return API返回数据
* @throws Exception
*/
public Map<String, String> unifiedOrder(Map<String, String> reqData) throws Exception {
return this.unifiedOrder(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
}
请求参数在微信提供的文档里有;但是我们只需要APPID 商家Id 秘钥 回调URL 支付类型 即可,这些都写进yml文件里了,
7 在yml文件里配置APPId 商家Id 秘钥 回调URL 支付类型
ly:
pay:
wx:
appID: *************
mchID: ***********
key: *****************
payType: NATIVE
8 编写配置类,将属性配置到配置类中
把yml文件中的参数读取到WXPayConfigImpl中,再由此构建WXPay
package com.leyou.order.config;
import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayConfigImpl;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class PayConfiguration {
@Bean
@ConfigurationProperties(prefix = "ly.pay.wx")
public WXPayConfigImpl payConfig(){
return new WXPayConfigImpl();
}
/**
* 注册WXPay对象
* @param payConfig 支付相关配置
* @return WXPay对象
* @throws Exception 连结WX失败时用到
*/
@Bean
public WXPay wxPay(WXPayConfigImpl payConfig) throws Exception {
return new WXPay(payConfig);
}
}
9 发送带有订单数据的请求给微信支付,返回值为支付的URL
我们先初始化WXPay对象,并注入到Spring容器中:
controller
@GetMapping("/url/{id}")
public ResponseEntity<String> getUrl(@PathVariable("id") Long orderId){
return ResponseEntity.ok(orderService.getUrl(orderId));
}
service
@Autowired
private TbOrderService orderService;
@Autowired
private PayHelper payHelper;
@Autowired
private StringRedisTemplate redisTemplate;
private String payUrlKey = "ly:pay:orderid:";
public String getUrl(Long orderId) {
//先去redis中查询url有没有,有直接返回,没有取微信获得
String url = redisTemplate.opsForValue().get(payUrlKey + orderId);
if (!StringUtils.isEmpty(url)){
return url;
}
TbOrder order = orderService.getById(orderId);
if (order==null){
throw new LyException(ExceptionEnum.ORDER_NOT_FOUND);
}
//订单状态不为1 不能去支付
if (order.getStatus().intValue() != OrderStatusEnum.INIT.value().intValue()){
throw new LyException(ExceptionEnum.INVALID_ORDER_STATUS);
}
Long actualFee = 1L;//order.getActualFee();
String payUrl = payHelper.createOrder(orderId, actualFee, "乐优商城");
//得到url后 存入到redis中,设置时间为2小时,key为当前用户的userId
redisTemplate.opsForValue().set(payUrlKey+orderId,payUrl,2, TimeUnit.HOURS);
return payUrl;
}
在这用到了PayHelper工具类,
1 准备微信支付要是的数据,用Map来传输
2 调用unifiedOrder方法获得支付的连接
package com.leyou.order.utils;
import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayConfigImpl;
import com.github.wxpay.sdk.WXPayConstants;
import com.github.wxpay.sdk.WXPayUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class PayHelper {
@Autowired
private WXPay wxPay;
@Autowired
private WXPayConfigImpl payConfig;
public String createOrder(Long orderId, Long totalPay, String desc){
Map<String, String> data = new HashMap<>();
// 商品描述
data.put("body", desc);
// 订单号
data.put("out_trade_no", orderId.toString());
//金额,单位是分
data.put("total_fee", totalPay.toString());
//调用微信支付的终端IP
data.put("spbill_create_ip", "127.0.0.1");
//回调地址
data.put("notify_url", payConfig.getNotifyUrl());
// 交易类型为扫码支付
data.put("trade_type", payConfig.getPayType());
// 利用wxPay工具,完成下单
Map<String, String> result = null;
try {
result = wxPay.unifiedOrder(data);
} catch (Exception e) {
log.error("【微信下单】创建预交易订单异常失败", e);
throw new RuntimeException("微信下单失败", e);
}
// 校验业务状态
checkResultCode(result);
// 下单成功,获取支付链接
String url = result.get("code_url");
if (StringUtils.isBlank(url)) {
throw new RuntimeException("微信下单失败,支付链接为空");
}
return url;
}
public void checkResultCode(Map<String, String> result) {
// 检查业务状态
String resultCode = result.get("result_code");
if ("FAIL".equals(resultCode)) {
log.error("【微信支付】微信支付业务失败,错误码:{},原因:{}", result.get("err_code"), result.get("err_code_des"));
throw new RuntimeException("【微信支付】微信支付业务失败");
}
}
/**
* 验证签名
* @param result 微信回调返回的结果
* @throws Exception
*/
public void isValidSign(Map<String,String> result) throws Exception {
boolean bool1 = WXPayUtil.isSignatureValid(result, payConfig.getKey(), WXPayConstants.SignType.MD5);
boolean bool2 = WXPayUtil.isSignatureValid(result, payConfig.getKey(), WXPayConstants.SignType.HMACSHA256);
if (!bool1 && !bool2){
throw new RuntimeException("【微信支付回调】签名有误");
}
}
}
10 前端根据URL生成二维码
生成二维码用的是二维码生成插件qrious
将qrcode.js引入到前端工程中
在支付页面引入js
写一个div用于展示二维码
用返回的url生成二维码
// 生成付款链接
ly.http.get("/order/url/" + id).then(resp => {
new QRCode(document.getElementById("qrImage"), {
text: resp.data,
width: 250,
height: 250,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
11 编写用户支付成功后,接收微信返回支付结果的方法
controller
package com.leyou.order.controller;
import com.leyou.order.service.impl.PayService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* - 微信给这个发送请求,返回支付结果
* - 请求方式:官方文档虽然没有明说,但是测试得出是POST请求
* - 请求路径:我们之前指定的notify_url的路径是:/pay/wx/notify
* - 请求参数:是xml格式数据,包括支付的结果和状态
* - 返回结果:也是xml,表明是否成功
*/
@Slf4j
@RestController
@RequestMapping("/pay")
public class PayController {
@Autowired
private PayService payService;
@PostMapping(value = "/wx/notify",produces = {"application/xml"})
public Map<String,String> hello(@RequestBody Map<String,String> result){
//拿到结果判断金额,修改状态
log.info("[支付回调] 接收微信回调,结果:{}",result);
payService.handleNotify(result);
//返回信息
Map<String,String> msg = new HashMap<>();
msg.put("return_code","SUCCESS");
msg.put("return_msg","OK");
return msg;
}
}
service
package com.leyou.order.service.impl;
import com.github.wxpay.sdk.WXPay;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.order.entity.TbOrder;
import com.leyou.order.enums.OrderStatusEnum;
import com.leyou.order.service.TbOrderService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
@Service
public class PayService {
@Autowired
private WXPay wxPay;
@Autowired
private TbOrderService orderService;
@Transactional
public void handleNotify(Map<String, String> result) {
//判断的result_code是否为success
if (result.get("result_code") == null || result.get("result_code").equals("SUCCESS")){
throw new LyException(ExceptionEnum.INVALID_NOTIFY_PARAM);
}
//判断支付结果通知中的sign是否有效
try {
boolean flag = wxPay.isPayResultNotifySignatureValid(result);
} catch (Exception e) {
throw new LyException(ExceptionEnum.INVALID_NOTIFY_PARAM);
}
//1 获取返回的订单id
String tradeNo = result.get("out_trade_no");
if (StringUtils.isEmpty(tradeNo)){
throw new LyException(ExceptionEnum.INVALID_NOTIFY_PARAM);
}
//2 获取返回的总金额
String totalFee = result.get("total_fee");
if (StringUtils.isEmpty(totalFee)){
throw new LyException(ExceptionEnum.INVALID_NOTIFY_PARAM);
}
Long total = Long.valueOf(totalFee);
//3 获取数据库订单金额
TbOrder tbOrder = orderService.getById(Long.valueOf(tradeNo));
Long actualFee = tbOrder.getActualFee();
//4 返回金额和数据库金额做比对
//现在是测试,所以不能使用下面的判断
/* if (!total.equals(actualFee)){
throw new LyException(ExceptionEnum.INVALID_NOTIFY_PARAM);
}*/
//5 保证订单的状态为未付款
if (tbOrder.getStatus().longValue() != OrderStatusEnum.INIT.value()){
throw new LyException(ExceptionEnum.INVALID_ORDER_STATUS);
}
//6 修改订单状态
TbOrder tbOrder1 = new TbOrder();
tbOrder1.setOrderId(Long.valueOf(tradeNo).longValue());
tbOrder1.setStatus(OrderStatusEnum.PAY_UP.value());
boolean b = orderService.updateById(tbOrder1);
if (!b){
throw new LyException(ExceptionEnum.UPDATE_OPERATION_FAIL);
}
}
}
12 定时查询支付状态
后端:
controller
/**
* - 请求方式:Get
* - 请求路径 :/state/{id}
* - 请求参数:订单id
* - 返回结果:1或者其它,1代表未支付,其它是已经支付
*/
@GetMapping("/state/{id}")
public ResponseEntity<Integer> queryState(@PathVariable("id") Long id){
return ResponseEntity.ok(orderService.queryState(id));
}
service
public Integer queryState(long id) {
TbOrder tbOrder = orderService.getById(id);
if (tbOrder == null){
throw new LyException(ExceptionEnum.ORDER_NOT_FOUND);
}
return tbOrder.getStatus();
}