乐优商城:笔记(十六):实现微信支付功能
文章目录
1 介绍
微信支付官方文档:https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F
我们选择的是扫码支付:
扫码支付有两种模式,我们选择流程二
2 开发流程
流程图:
这里我们把商户要做的事情总结一下:
- 1、商户生成订单
- 2、商户调用微信下单接口,获取预交易的链接
- 3、商户将链接生成二维码图片,展示给用户;
- 4、用户支付并确认
- 5、支付结果通知: 微信异步通知商户支付结果,商户告知微信支付接收情况
- 商户如果没有收到通知,可以调用接口,查询支付状态
在前面的业务中,我们已经完成了:
- 1、生成订单
接下来,我们需要做的是:
- 2、调用微信接口,生成链接。
- 3、并且根据链接生成二维码图片
- 4、支付成功后修改订单状态
3 下单并生成支付链接
3.1 API说明
在微信支付文档中,可以查询得到以下信息:
支付路径
URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder
请求参数
注:
- 通知地址
- 在后面的代码中,我们将请求参数分成两部分填充:
第一部分如上图所示,第二部分封装到config中,这个config是我们自己写的,实现了官方的接口WXPayConfig:
PayConfig:
官方的接口WXPayConfig:
然后在WXPay中的unifiedOrder方法中填充:
请求方式
post,源码如下:
返回结果
不一定执行了就一定成功,要做 通信和业务是否成功的校验 和 签名是否有效的校验,所以返回结果要做一个判断:
3.2 统一下单工具类
将属性定义到yml文件中
yml:
编写自定义配置,读取属性
payconfig:
@Data public class PayConfig implements WXPayConfig { private String appID; // 公众账号ID private String mchID;// 商户号 private String key;// 生成签名的密钥 private int httpConnectTimeoutMs; // 连接超时时间 private int httpReadTimeoutMs;// 读取超时时间 private String notifyUrl; @Override public InputStream getCertStream() { return null; } }
将PayConfig注册到Spring容器中
WXPayConfiguraturation:
@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); } }
现在工具类都准备好了,接下来我们生成预交易链接:
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.WX_PAY_ORDER_FAIL); } } catch (Exception e) { log.error("[微信支付] 校验签名失败,数据:{}", result); throw new LyException(ExceptionEnum.WX_PAY_ORDER_FAIL); } } }
3.3 生成预交易链接
3.3.1 controller
@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_ERROE); } Long actualPay = 1L/*order.getActualPay()*/; OrderDetail detail = order.getOrderDetails().get(0);//订单中可能有多件商品,获取第一件商品的标题作为订单的描述 String desc = detail.getTitle(); return payHelper.createOrder(orderId, actualPay, desc); }
此时刷新页面,可以看到已经成功产生了URL:
3.4 生成二维码
这里我们使用一个生成二维码的JS插件:qrcode,官网:https://github.com/davidshimjs/qrcodejs
我们把这个js脚本引入到项目中:
然后在页面引用:
页面定义一个div,用于展示二维码:
然后获取到付款链接后,根据链接生成二维码:
刷新页面查看效果:
此时,客户用手机扫描二维码,可以看到付款页面
4 付款状态
当用户扫描上述二维码完成付款,微信通知服务端用户支付成功,此时我们来修改订单状态,我们需要做的也就是以下几步:
但是有一个问题,微信什么时候会通知服务端用户支付状态呢?我们注意到,我们统一下单接口有一个参数:
到时微信会自动调用notify_url的链接来查询用户的支付结果,一个URL对应一个controller,用来接收异步通知,我们完成这个功能就可以,但是有一个问题我们需要注意到,那就是通知URL必须为外网可以访问的URL,而我们的项目没有发布,只提供局域网访问,如何获得一个能够供外网访问的域名呢?不要紧,我们接下来学习一个小工具
4.1 内网穿透
名词解释:
简单来说内网穿透的目的就是:让外网能访问你本地的应用,例如在外网打开你本地http://127.0.0.1指向的web站点
我们使用的工具为:NatApp,网址:https://natapp.cn/ ,使用起来非常简单
简单使用见官方文档:
4.2 接收回调
官方文档:
通知参数(xml格式):
返回参数(也是xml格式):
4.2.1 引入依赖
在pom文件中引入了xml解析器:
4.2.1 controller
@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; } }
4.2.2 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 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); }
以上工作我们完成以后去页面重新购物并付款,此时发现我们虽然付款成功,查看数据库,数据库中的订单状态也已经改变,但是页面怎么不跳转呢?原因是我们只在后端改了,但是前端并不知道,所以就算付款完成我们前端也依旧不跳转
前端应该是出现了二维码界面之后一直发起查询订单状态的请求,否则查询一次失败了不再进行查询,避免出现我们订单状态有延迟而订单状态也确实发生了改变的情况下,接下来我们来实现查询订单状态的接口
4.3 付款状态
我们在前端代码中已经定义了查询的js代码:
4.3.1 工具类
我们查询完成后,把支付交易状态分为三种情况:
- 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; } }
4.3.2 查询订单
查询订单状态有两种可能,一种是已支付,一种是未支付,如果是已支付那就说明真的已经支付过了;如果是未支付,那有可能是异步通知有延时,必须去微信支付查询支付状态,我们在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;// 并不知道是否支付,再去发起请求申请一次 } }
4.3.3 controller
@GetMapping("state/{id}") public ResponseEntity<Integer> queryOrderState(@PathVariable("id")Long orederId){ return ResponseEntity.ok(orderService.queryOrderState(orederId).getValue()); }
4.3.4 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); }
我们再次来刷新前台页面测试一次:
点击提交订单跳转到订单支付页面,并生成支付二维码:
支付成功后跳转到支付成功页面:
用户的移动支付端可以收到支付详情:
到此为止,乐优商城的主线业务就全部完成了~~~
- 网上图书商城项目学习笔记-007登录功能实现
- Cordova in action 学习笔记三:实现网页实现不了的功能
- ITCAST视频-Spring学习笔记(使用CGLIB实现AOP功能与AOP概念解释)
- 用js+cookie实现商城的购物车功能
- HoloLens开发笔记-瞬移功能实现
- 电子商务系统的设计与实现(八):前端商城系统功能细化
- 电子商务系统的设计与实现(八):前端商城系统功能细化
- 【unity学习笔记】unity实现钩子功能
- 商城项目实战24:实现添加商品功能
- 从零开始写javaweb框架笔记15-搭建轻量级JAVAWEB框架-实现依赖注入功能
- Easyui笔记1:实现combobox下拉框检索匹配功能
- 转:Stm32学习笔记:按键单击、双击、长按功能实现
- Java菜鸟学习笔记--语法篇(一):用Math.random()实现验证码功能
- 【Android游戏开发十六】Android Gesture之【触摸屏手势识别】操作!利用触摸屏手势实现一个简单切换图片的功能!
- react简书项目学习笔记33react中实现路由功能
- javascript实现 京东淘宝等商城的商品图片大图预览功能(图片放大器)
- #Android笔记#基于popupwindow的底部菜单栏设计与功能实现
- Unity3d笔记:炉石传说中的功能实现解析
- Extjs4开发笔记(四)&#8212;&#8212;实现登录功能
- ORACLE 全文索引功能实现学习笔记