微信支付:
1. 介绍
微信支付官方文档:https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F
选择扫码支付:
此处我们使用模式二来开发
2. 开发流程
模式二与模式一相比,流程更为简单,不依赖设置的回调支付URL。
商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数code_url;
商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。
注意:code_url有效期为2小时,过期后扫码不能再发起支付。
业务流程时序图:
这里我们把商户(我们)要做的事情总结一下:
- 1、商户生成订单
- 2、商户调用微信下单接口,获取预交易的链接
- 3、商户将链接生成二维码图片,展示给用户;
- 4、用户支付并确认
- 5、支付结果通知:
- 微信异步通知商户支付结果,商户告知微信支付接收情况
- 商户如果没有收到通知,可以调用接口,查询支付状态
- 6、如果支付成功,发货,修改订单状态
在前面的业务中,我们已经完成了:
- 1、生成订单
接下来,我们需要做的是:
- 2、调用微信接口,生成链接。
- 3、并且生成二维码图片
- 4、支付成功后修改发订单状态
3. 下单并生成支付链接
3.1 API说明
在微信支付文档中,可以查询得到以下信息:
支付路径:URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder
请求参数:
其中,appid、mch_id、nonce_str等都是可以提前配置,或者随机生成,可以统一配置
但是其他参数都需要我们自己组织了(着重第二部分,即上图划红线区域,第三部分可以写成固定的)
注:
- 通知地址:
- 在后面的代码中,我们将请求参数分成两部分填充(蓝色和绿色划分为一部分,红色为另一部分)
第一部分如上图所示,第二部分封装到config中,这个config是我们自己写的,实现了官方的接口 WXPayConfig
PayConfig:
注:notifyUrl为下单通知回调地址(我们的地址,即现在这台服务器的地址)
官方的接口WXPayConfig:
结构:
然后在WXPay中的unifiedOrder方法中填充:
请求方式: POST
返回结果:
不一定执行了就一定成功,要做 通信和业务是否成功的校验 和 签名是否有效的校验,所以返回结果要做一个判断:
3.2 统一配置
在微信支付参数中,appid、mch_id可以提前配置,sign签名需要商户密钥,我们需要提前配置,另外请求的超时时长等,所以微信SDK提供了统一配置类:
我们将这些属性定义到application.yml文件中:
编写自定义配置,读取属性:
@Data
public class PayConfig implements WXPayConfig {
private String appID;//= "wx8397f8696b538317"; // 公众账号ID
private String mchID;// = "1473426802"; // 商户号
private String key;// = "T6m9iK73b0kn9g5v426MKfHQH7X8rKwb"; // 生成签名的密钥
private int httpConnectTimeoutMs;// = 1000; // 连接超时时间
private int httpReadTimeoutMs;// = 5000;// 读取超时时间
private String notifyUrl;//异步通知商户(我们的地址,即现在这台服务器的地址),下单通知回调地址
@Override
public InputStream getCertStream() {
return null;
}
}
将PayConfig注册到Spring容器中:
@Configuration
public class WXPayConfiguration {
@Bean
@ConfigurationProperties(prefix = "ly.pay")
public PayConfig payConfig(){
return new PayConfig();
}
@Bean
public WXPay wxPay(PayConfig payConfig){
return new WXPay(payConfig, WXPayConstants.SignType.HMACSHA256);
}
}
现在工具类都准备好了,接下来我们先根据订单的编号,调用后台服务,生成预交易链接:
@Slf4j
@Component
public class PayHelper {
@Autowired
private WXPay wxPay;
@Autowired
private PayConfig payConfig;
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderStatusMapper orderStatusMapper;
public String createOrder(Long orderId, Long totalPay, String desc){
try {
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", "NATIVE");
// 利用wxPay工具,完成下单
Map<String, String> result = wxPay.unifiedOrder(data);
// 判断通信和业务标示
isSuccess(result);
// 校验签名
isValidSign(result);
// 下单成功,获取支付链接
String url = result.get("code_url");
return url;
} catch (Exception e) {
log.error("[微信下单] 创建预交易订单异常失败", e);
return null;
}
}
public void isSuccess(Map<String, String> result) {
// 判断通信标识
String returnCode = result.get("return_code");
if("FAIL".equals(returnCode)){
// 通信失败
log.error("[微信下单] 微信下单通信失败,失败原因:{}", result.get("return_msg"));
throw new LyException(ExceptionEnum.WX_PAY_ORDER_FAIL);
}
// 判断业务标示
String resultCode = result.get("result_code");
if("FAIL".equals(resultCode)){
// 通信失败
log.error("[微信下单] 微信下单业务失败,错误码:{}, 错误原因:{}",
result.get("err_code"), result.get("err_code_des"));
throw new LyException(ExceptionEnum.WX_PAY_ORDER_FAIL);
}
}
public void isValidSign(Map<String, String> result) {
// 校验签名: 重新生成签名,和传过来的签名进行比较
try {
String sign1 = WXPayUtil.generateSignature(result, payConfig.getKey(), WXPayConstants.SignType.HMACSHA256);
String sign2 = WXPayUtil.generateSignature(result, payConfig.getKey(), WXPayConstants.SignType.MD5);
String sign = result.get("sign");
if (!StringUtils.equals(sign, sign1) && !StringUtils.equals(sign, sign2)) {
//签名有误
throw new LyException(ExceptionEnum.INVALID_SIGN_ERROR);
}
} catch (Exception e) {
log.error("[微信支付] 校验签名失败,数据:{}", result);
throw new LyException(ExceptionEnum.INVALID_SIGN_ERROR);
}
}
public PayState queryPayState(Long orderId) {
try {
//组织请求参数
Map<String, String> data = new HashMap<>();
// 订单号
data.put("out_trade_no", orderId.toString());
// 查询状态
Map<String, String> result = wxPay.orderQuery(data);
// 校验通信状态
isSuccess(result);
// 校验签名
isValidSign(result);
// 校验金额
String totalFeeStr = result.get("total_fee");
String tradeNo = result.get("out_trade_no");
if(StringUtils.isEmpty(totalFeeStr) || StringUtils.isEmpty(tradeNo)){
throw new LyException(ExceptionEnum.CART_NOT_FOUND);
}
// 3.1 获取结果中的金额
Long totalFee = Long.valueOf(totalFeeStr);
// 3.2 获取订单金额
Order order = orderMapper.selectByPrimaryKey(orderId);
if(totalFee != /*order.getActualPay()*/ 1){
// 金额不符
throw new LyException(ExceptionEnum.CART_NOT_FOUND);
}
String state = result.get("trade_state"); //交易状态
if(SUCCESS.equals(state)){
// 支付成功
// 修改订单状态
OrderStatus status = new OrderStatus();
status.setStatus(OrderStatusEnum.PAYED.value());
status.setOrderId(orderId);
status.setPaymentTime(new Date());
int count = orderStatusMapper.updateByPrimaryKeySelective(status);
if(count != 1){
throw new LyException(ExceptionEnum.CART_NOT_FOUND);
}
// 返回成功
return PayState.SUCCESS;
}
if("NOTPAY".equals(state) || "USERPAYING".equals(state)){
return PayState.NOT_PAY;
}
return PayState.FAIL;
}catch (Exception e){
return PayState.NOT_PAY;// 并不知道是否支付,再发起请求查一次
}
}
}
3.3 创建订单支付链接
3.3.1 web
// 创建支付链接
@GetMapping("url/{id}")
public ResponseEntity<String> createPayUrl(@PathVariable("id") Long orderId){
return ResponseEntity.ok(orderService.createPayUrl(orderId));
}
3.3.2 service
// 创建支付链接
public String createPayUrl(Long orderId) {
// 查询订单
Order order = queryOrderById(orderId);
// 判断订单状态,如果订单已支付,下面的查询就很多余
OrderStatus orderStatus = order.getOrderStatus();
Integer status = orderStatus.getStatus();
if(status != OrderStatusEnum.UN_PAY.value()){
throw new LyException(ExceptionEnum.ORDER_STATUS_ERROR);
}
//支付金额
Long actualPay = 1L/*order.getActualPay()*/;
OrderDetail detail = order.getOrderDetails().get(0);//订单中可能有多件商品,获取第一件商品的标题作为订单的描述
String desc = detail.getTitle();
return payHelper.createOrder(orderId, actualPay, desc);
}
3.3.3 测试
刷新页面,可以看到发起请求成功,产生URL:
并且返回了一个支付链接:
4. 生成二维码
这里我们使用一个生成二维码的JS插件:qrcode,官网:https://github.com/davidshimjs/qrcodejs
我们把这个js脚本引入到项目中:
然后在页面引用:
页面定义一个div,用于展示二维码:
然后获取到付款链接后,根据链接生成二维码:
刷新页面,查看效果:
此时,客户用手机扫描二维码,可以看到付款页面。
5. 修改订单状态
当用户扫描上述二维码完成付款,微信通知服务端用户支付成功,此时我们来修改订单状态,我们需要做的也就是以下几步:
但是有一个问题,微信什么时候会通知服务端用户支付状态呢?我们注意到,我们统一下单接口有一个参数:
到时微信会自动调用notify_url的链接来查询用户的支付结果,一个URL对应一个controller,用来接收异步通知,我们完成这个功能就可以,但是有一个问题我们需要注意到,那就是通知URL必须为外网可以访问的URL,而我们的项目没有发布,只提供局域网访问,如何获得一个能够供外网访问的域名呢?不要紧,我们接下来学习一个小工具
5.1 内网穿透
名词解释:
简单来说内网穿透的目的就是:让外网能访问你本地的应用,例如在外网打开你本地http://127.0.0.1指向的web站点
我们使用的工具为:NatApp,网址:https://natapp.cn/ ,使用起来非常简单
简单使用见官方文档:
5.2 接收回调
官方文档:
通知参数(xml格式):
返回参数(也是xml格式):
注:xml与json都是一种数据封装的方式 ,都是<k,v>结构,xml的k是标签名
5.2.1 引入依赖
在pom文件中引入了xml解析器:
5.2.2 web
由于返回参数是xml形式,因此我们需要声明返回xml格式
@Slf4j
@RestController
@RequestMapping("notify")
public class NotifyController {
@Autowired
private OrderService orderService;
// 微信支付的成功回调
@PostMapping(value = "pay",produces = "application/xml") //声明返回xml格式
public Map<String,String> hello(@RequestBody Map<String,String> result){//消息转化器,在pom文件中引入了xml解析器
//处理回调
orderService.handleNotify(result);
log.info("[支付回调] 接收微信支付回调, 结果:{}", result);
//返回成功
Map<String,String> msg = new HashMap<>();
msg.put("return_code", "SUCCESS");
msg.put("return_msg", "OK");
return msg;
}
}
在这里,我们将MvcConfig拦截器的映射路径从之前的拦截所有(微信调用不了)改为只拦截/order下的请求,/notify这个接口将来就不会被拦截: ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190612213340978.png?)
5.2.3 service
// 处理回调
public void handleNotify(Map<String, String> result) {
// 1 数据校验
payHelper.isSuccess(result);
// 2 签名校验
payHelper.isValidSign(result);
// 3 金额校验
String totalFeeStr = result.get("total_fee");
String tradeNo = result.get("out_trade_no");//订单编号
if(StringUtils.isEmpty(totalFeeStr) || StringUtils.isEmpty(tradeNo)){
throw new LyException(ExceptionEnum.INVALID_ORDER_PARAM);
}
Long totalFee = Long.valueOf(totalFeeStr);//获取结果金额,把金额转成Long类型
Long orderId = Long.valueOf(tradeNo);
Order order = orderMapper.selectByPrimaryKey(orderId);//获取订单,校验金额
if(totalFee != /*order.getActualPay()*/ 1){
// 金额不符
throw new LyException(ExceptionEnum.INVALID_ORDER_PARAM);
}
// 4 修改订单状态
OrderStatus status = new OrderStatus();
status.setStatus(OrderStatusEnum.PAYED.value());
status.setOrderId(orderId);
status.setPaymentTime(new Date());
int count = orderStatusMapper.updateByPrimaryKeySelective(status);
if(count != 1){
throw new LyException(ExceptionEnum.UPDATE_ORDER_STATUS_ERROR);
}
log.info("[订单回调], 订单支付成功! 订单编号:{}", orderId);
}
以上工作我们完成以后去页面重新购物并付款,此时发现我们虽然付款成功,查看数据库,数据库中的订单状态也已经改变,但是页面怎么不跳转呢?原因是我们只在后端改了,但是前端并不知道,所以就算付款完成我们前端也依旧不跳转
前端应该是出现了二维码界面之后不断的发起查询订单状态的请求,否则查询一次失败了不再进行查询,避免出现我们订单状态有延迟而订单状态也确实发生了改变的情况下,接下来我们来实现查询订单状态的接口
6. 付款成功
跳转到支付页面后,我们等待用户付款,付款完成则跳转到付款成功页面。
6.1 页面循环查询支付状态
不过,因为不清楚用户何时会付款,因此这里采用循环的方式,不断请求判断是否支付成功。我们在前端代码中已经定义了查询的js代码:
6.2 工具类
我们查询完成后,把支付交易状态分为三种情况:
0,通信失败或者未支付,需要重新查询
1,支付成功
2,支付失败
我们定义一个枚举来表示:
public enum PayState {
NOT_PAY(0),
SUCCESS(1),
FAIL(2);
PayState(int value) {
this.value = value;
}
int value;
public int getValue() {
return value;
}
}
6.3 查询订单
查询订单状态有两种可能,一种是已支付,一种是未支付,如果是已支付那就说明真的已经支付过了;如果是未支付,那有可能是异步通知有延时,必须去微信支付查询支付状态,我们在payHelper中补充查询的API,该API具体应用场景及接口等详见官方文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_2
public PayState queryPayState(Long orderId) {
try {
// 组织请求参数
Map<String, String> data = new HashMap<>();
// 订单号
data.put("out_trade_no", orderId.toString());
// 查询状态
Map<String, String> result = wxPay.orderQuery(data);
// 校验状态
isSuccess(result);
// 校验签名
isValidSign(result);
// 校验金额
String totalFeeStr = result.get("total_fee");
String tradeNo = result.get("out_trade_no");
if(StringUtils.isEmpty(totalFeeStr) || StringUtils.isEmpty(tradeNo)){
throw new LyException(ExceptionEnum.INVALID_ORDER_PARAM);
}
// 3.1 获取结果中的金额
Long totalFee = Long.valueOf(totalFeeStr);
// 3.2 获取订单金额
Order order = orderMapper.selectByPrimaryKey(orderId);
if(totalFee != /*order.getActualPay()*/ 1){
// 金额不符
throw new LyException(ExceptionEnum.INVALID_ORDER_PARAM);
}
String state = result.get("trade_state");
if(SUCCESS.equals(state)){
// 支付成功
// 修改订单状态
OrderStatus status = new OrderStatus();
status.setStatus(OrderStatusEnum.PAYED.value());
status.setOrderId(orderId);
status.setPaymentTime(new Date());
int count = orderStatusMapper.updateByPrimaryKeySelective(status);
if(count != 1){
throw new LyException(ExceptionEnum.UPDATE_ORDER_STATUS_ERROR);
}
// 返回成功
return PayState.SUCCESS;
}
if("NOTPAY".equals(state) || "USERPAYING".equals(state)){
return PayState.NOT_PAY;
}
return PayState.FAIL;
}catch (Exception e){
return PayState.NOT_PAY;// 并不知道是否支付,再去发起请求申请一次
}
}
6.4 web
// 查询订单状态
@GetMapping("state/{id}")
public ResponseEntity<Integer> queryOrderState(@PathVariable("id")Long orederId){
return ResponseEntity.ok(orderService.queryOrderState(orederId).getValue());
}
6.5 service
// 查询订单状态
public PayState queryOrderState(Long orderId) {
// 查询订单状态
OrderStatus orderStatus = orderStatusMapper.selectByPrimaryKey(orderId);
Integer status = orderStatus.getStatus();
if(!status.equals(OrderStatusEnum.UN_PAY.value())){
return PayState.SUCCESS;// 如果是已支付,则是真的已支付
}
// 如果未支付,但其实不一定是未支付(未支付/已支付,但是微信通知没到),必须去微信查询支付状态
return payHelper.queryPayState(orderId);
}
我们再次来刷新前台页面测试一次:
点击提交订单跳转到订单支付页面,并生成支付二维码:
当付款成功后,自动跳转到付款成功页面:
用户的移动支付端可以收到支付详情:
到此为止,乐优商城的主线业务就全部完成了~~~