微信扫码支付

学习目标:

  • 能够根据微信支付的开发文档调用微信支付的api
  • 完成统一下单生成微信支付二维码功能
  • 完成支付回调的逻辑处理,掌握EchoSite的使用
  • 完成推送支付通知功能

1. 微信支付快速入门

10-3.png

1.1 微信支付申请(了解)

第一步:注册公众号(类型须为:服务号)

请根据营业执照类型选择以下主体注册:个体工商户| 企业/公司| 政府| 媒体| 其他类型

第二步:认证公众号

公众号认证后才可申请微信支付,认证费:300元/次。

第三步:提交资料申请微信支付

登录公众平台,点击左侧菜单【微信支付】,开始填写资料等待审核,审核时间为1-5个工作日内。

第四步:开户成功,登录商户平台进行验证

资料审核通过后,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资金数额,完成账户验证。

第五步:在线签署协议

本协议为线上电子协议,签署后方可进行交易及资金结算,签署完立即生效。

本课程已经提供好“传智播客”的微信支付账号,学员无需申请。

完成上述步骤,你可以得到调用API用到的账号和密钥

appid:微信公众账号或开放平台APP的唯一标识 wx8397f8696b538317

mch_id:商户号 1473426802

key:商户密钥 T6m9iK73b0kn9g5v426MKfHQH7X8rKwb

1.2 微信支付开发文档与SDK

在线微信支付开发文档:

https://pay.weixin.qq.com/wiki/doc/api/index.html

微信支付接口调用的整体思路:

按API要求组装参数,以XML方式发送(POST)给微信支付接口(URL),微信支付接口也是以XML方式给予响应。程序根据返回的结果(其中包括支付URL)生成二维码或判断订单状态。

我们解压从官网下载的sdk ,安装到本地仓库

com.github.wxpay.sdk.WXPay类下提供了对应的方法:

方法名 说明
microPay 刷卡支付
unifiedOrder 统一下单
orderQuery 查询订单
reverse 撤销订单
closeOrder 关闭订单
refund 申请退款
refundQuery 查询退款
downloadBill 下载对账单
report 交易保障
shortUrl 转换短链接
authCodeToOpenid 授权码查询openid

1.3 统一下单API

(1)新建工程,引入微信支付Api

  1. <dependency>
  2. <groupId>com.github.wxpay</groupId>
  3. <artifactId>wxpay-sdk</artifactId>
  4. <version>3.0.9</version>
  5. </dependency>

(2)创建com.github.wxpay.sdk包,包下创建MyConfig类 ,继承自抽象类WXPayConfig

  1. public class MyConfig extends WXPayConfig {
  2. String getAppID() {
  3. return "wx8397f8696b538317";
  4. }
  5. String getMchID() {
  6. return "1473426802";
  7. }
  8. String getKey() {
  9. return "T6m9iK73b0kn9g5v426MKfHQH7X8rKwb";
  10. }
  11. InputStream getCertStream() {
  12. return null;
  13. }
  14. IWXPayDomain getWXPayDomain() {
  15. return new IWXPayDomain() {
  16. public void report(String domain, long elapsedTimeMillis, Exception ex) {
  17. }
  18. public DomainInfo getDomain(WXPayConfig config) {
  19. return new DomainInfo("api.mch.weixin.qq.com",true);
  20. }
  21. };
  22. }
  23. }

(3)创建测试类,编写代码

  1. MyConfig config=new MyConfig();
  2. WXPay wxPay=new WXPay( config );
  3. Map<String,String> map=new HashMap();
  4. map.put("body","畅购");//商品描述
  5. map.put("out_trade_no","55555211");//订单号
  6. map.put("total_fee","1");//金额
  7. map.put("spbill_create_ip","127.0.0.1");//终端IP
  8. map.put("notify_url","http://www.baidu.com");//回调地址
  9. map.put("trade_type","NATIVE");//交易类型
  10. Map<String, String> result = wxPay.unifiedOrder( map );
  11. System.out.println(result);

执行后返回结果

  1. {nonce_str=fvMGIlLauUPNCtws, code_url=weixin://wxpay/bizpayurl?pr=I5sd2rc, appid=wx8397f8696b538317, sign=48B2938F70EDADC9CC235249BC085FD1D83456F67C46601FFD23B5AFBDA502D0, trade_type=NATIVE, return_msg=OK, result_code=SUCCESS, mch_id=1473426802, return_code=SUCCESS, prepay_id=wx17193859685440d561c4cef01259098400}

其中的code_url就是我们的支付URl ,我们可以根据这个URl 生成支付二维码

1.4 二维码JS插件- QRcode.js

QRCode.js 是一个用于生成二维码的 JavaScript 库。主要是通过获取 DOM 的标签,再通过 HTML5 Canvas 绘制而成,不依赖任何库。支持该库的浏览器有:IE6~10, Chrome, Firefox, Safari, Opera, Mobile Safari, Android, Windows Mobile, 等

我们看一下静态原型wxpay.html中的代码,显示二维码的地方放置<div id='qrcode'></div> ,然后编写脚本

  1. <script src="js/plugins/qrcode.min.js" ></script>
  2. <script type="text/javascript">
  3. let qrcode = new QRCode(document.getElementById("qrcode"), {
  4. width : 200,
  5. height : 200
  6. });
  7. qrcode.makeCode("weixin://wxpay/bizpayurl?pr=Y3hDTZy");
  8. </script>

2. 微信支付二维码

2.1 需求分析

用户在提交订单后,如果是选择支付方式为微信支付,那应该跳转到微信支付二维码页面,用户扫描二维码可以进行支付,金额与订单金额相同。
10-1.png

2.2 实现思路

前端页面向后端传递订单号,后端根据订单号查询订单,检查是否为当前用户的未支付订单,如果是则根据订单号和金额生成支付url返给前端,前端得到支付url生成支付二维码。

2.3 代码实现

2.3.1 提交订单跳转支付页

1)更新changgou_web_order的application.yml,添加读取超时设置

  1. #请求处理的超时时间
  2. ribbon:
  3. ReadTimeout: 4000
  4. #请求连接的超时时间
  5. ConnectTimeout: 3000

2)在changgou_order_web工程resources/templates下添加 ‘资源/pay.html,fail.html,wxpay.html’。

1564127922343.png

3)更新changgou_service_order中add(). 去除订单id的NO. ,设置返回值为订单Id

  1. #orderServiceImpl
  2. /**
  3. * 增加
  4. * @param order
  5. */
  6. @Override
  7. public String add(Order order){
  8. List<OrderItem> cartList = cartService.findCartList(order.getUsername());
  9. int totalMoney=0;
  10. int totalNum=0;
  11. int totalPayMoney=0;
  12. for (OrderItem orderItem : cartList) {
  13. totalMoney+=orderItem.getMoney();
  14. totalNum+=orderItem.getNum();
  15. totalPayMoney+=orderItem.getPayMoney();
  16. }
  17. order.setTotalNum(totalNum);
  18. order.setTotalMoney(totalMoney);
  19. order.setPayMoney(totalPayMoney);
  20. order.setPreMoney(totalMoney-totalPayMoney);
  21. //其他数据完善
  22. order.setCreateTime(new Date());
  23. order.setUpdateTime(order.getCreateTime());
  24. order.setBuyerRate("0"); //0:未评价,1:已评价
  25. order.setSourceType("1"); //来源,1:WEB
  26. order.setOrderStatus("0"); //0:未完成,1:已完成,2:已退货
  27. order.setPayStatus("0"); //0:未支付,1:已支付,2:支付失败
  28. order.setConsignStatus("0"); //0:未发货,1:已发货,2:已收货
  29. String orderId = idWorker.nextId() + "";
  30. order.setId(orderId);
  31. int result = orderMapper.insertSelective(order);
  32. for (OrderItem orderItem : cartList) {
  33. orderItem.setId(idWorker.nextId()+"");
  34. orderItem.setIsReturn("0");
  35. orderItem.setOrderId(order.getId());
  36. orderItemMapper.insertSelective(orderItem);
  37. }
  38. //扣减库存
  39. skuFeign.decrCount(order.getUsername());
  40. //清除缓存中的数据
  41. redisTemplate.delete("Cart_"+order.getUsername());
  42. return orderId;
  43. }
  44. #orderController
  45. @PostMapping
  46. public Result<String> add(@RequestBody Order order){
  47. String username = tokenDecode.getUserInfo().get("username");
  48. order.setUsername(username);
  49. String orderId = orderService.add(order);
  50. return new Result(true,StatusCode.OK,"添加成功",orderId);
  51. }
  1. 更新order.html中添加订单js
  1. add:function () {
  2. axios.post('/api/worder/add',this.order).then(function (response) {
  3. if (response.data.flag){
  4. alert("添加订单成功");
  5. var orderId = response.data.data;
  6. location.href="/api/worder/toPayPage?orderId="+orderId;
  7. }else{
  8. alert("添加失败");
  9. }
  10. })
  11. }

5)在orderController中新增方法,用于跳转支付页

5.1)changgou_service_order_api的OrderFeign新增接口定义

  1. /***
  2. * 根据ID查询数据
  3. * @param id
  4. * @return
  5. */
  6. @GetMapping("/order/{id}")
  7. public Result findById(@PathVariable("id") String id);

5.2) changgou_order_web中的orderController新增方法,跳转支付页

  1. @GetMapping("/toPayPage")
  2. public String toPayPage(String orderId, Model model){
  3. Result<Order> orderResult = orderFeign.findById(orderId);
  4. Order order = orderResult.getData();
  5. model.addAttribute("orderId",orderId);
  6. model.addAttribute("payMoney",order.getPayMoney());
  7. return "pay";
  8. }

2.3.2 支付微服务-下单

(1)创建changgou_service_pay (支付微服务), pom.xml添加依赖

  1. <dependency>
  2. <groupId>com.changgou</groupId>
  3. <artifactId>changgou_common</artifactId>
  4. <version>1.0-SNAPSHOT</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>com.github.wxpay</groupId>
  8. <artifactId>wxpay-sdk</artifactId>
  9. <version>3.0.9</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.springframework.boot</groupId>
  13. <artifactId>spring-boot-starter</artifactId>
  14. <exclusions>
  15. <exclusion>
  16. <groupId>org.springframework.boot</groupId>
  17. <artifactId>spring-boot-starter-logging</artifactId>
  18. </exclusion>
  19. </exclusions>
  20. </dependency>
  21. <dependency>
  22. <groupId>org.springframework.boot</groupId>
  23. <artifactId>spring-boot-starter-amqp</artifactId>
  24. </dependency>

排除log包,否则会因为包冲突无法正常启动

(2)创建配置文件application.yml

  1. server:
  2. port: 9010
  3. spring:
  4. application:
  5. name: pay
  6. rabbitmq:
  7. host: 192.168.200.128
  8. main:
  9. allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
  10. eureka:
  11. client:
  12. service-url:
  13. defaultZone: http://127.0.0.1:6868/eureka
  14. instance:
  15. prefer-ip-address: true

(3)创建com.github.wxpay.sdk包,包下创建Config类

  1. public class MyConfig extends WXPayConfig {
  2. String getAppID() {
  3. return "wx8397f8696b538317";
  4. }
  5. String getMchID() {
  6. return "1473426802";
  7. }
  8. String getKey() {
  9. return "T6m9iK73b0kn9g5v426MKfHQH7X8rKwb";
  10. }
  11. InputStream getCertStream() {
  12. return null;
  13. }
  14. IWXPayDomain getWXPayDomain() {
  15. return new IWXPayDomain() {
  16. public void report(String domain, long elapsedTimeMillis, Exception ex) {
  17. }
  18. public DomainInfo getDomain(WXPayConfig config) {
  19. return new DomainInfo("api.mch.weixin.qq.com",true);
  20. }
  21. };
  22. }
  23. }

(4)创建com.changgou包,包下创建类PayApplication

  1. @SpringBootApplication
  2. @EnableEurekaClient
  3. public class PayApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(PayApplication.class);
  6. }
  7. @Bean
  8. public WXPay wxPay(){
  9. try {
  10. return new WXPay( new MyConfig() );
  11. } catch (Exception e) {
  12. e.printStackTrace();
  13. return null;
  14. }
  15. }
  16. }

(3)创建com.changgou.pay.controller包 ,新增WxPayController

  1. @RestController
  2. @RequestMapping("/wxpay")
  3. public class WxPayController {
  4. @Autowired
  5. private WxPayService wxPayService;
  6. /**
  7. * 下单
  8. * @param orderId
  9. * @param money
  10. * @return
  11. */
  12. @GetMapping("/nativePay")
  13. public Result nativePay(@RequestParam("orderId")String orderId,@RequestParam("money") Integer money){
  14. Map map = wxPayService.nativePay( orderId, money );
  15. return new Result( true, StatusCode.OK,"",map );
  16. }
  17. }

(4)创建com.changgou.pay.service包,包下创建接口WxPayService

  1. public interface WxPayService {
  2. /**
  3. * 生成微信支付二维码
  4. * @param orderId
  5. * @param money
  6. * @return
  7. */
  8. Map nativePay(String orderId, Integer money);
  9. }

(5)创建com.changgou.pay.service.impl 包 ,新增服务类WxPayServiceImpl

  1. @Service
  2. public class WxPayServiceImpl implements WxPayService {
  3. @Autowired
  4. private WXPay wxPay;
  5. @Override
  6. public Map nativePay(String orderId, Integer money) {
  7. try {
  8. //1.封装请求参数
  9. Map<String,String> map=new HashMap();
  10. map.put("body","畅购商城");//商品描述
  11. map.put("out_trade_no",orderId);//订单号
  12. //map.put("total_fee",String.valueOf(money*100));//金额,以分为单位
  13. BigDecimal payMoney = new BigDecimal("0.01");
  14. BigDecimal fen = payMoney.multiply(new BigDecimal("100")); //1.00
  15. fen = fen.setScale(0,BigDecimal.ROUND_UP); // 1
  16. map.put("total_fee",String.valueOf(fen));
  17. map.put("spbill_create_ip","127.0.0.1");//终端IP
  18. map.put("notify_url","http://www.itcast.cn");//回调地址,先随便填一个
  19. map.put("trade_type","NATIVE");//交易类型
  20. Map<String, String> mapResult = wxPay.unifiedOrder( map ); //调用统一下单
  21. return mapResult;
  22. } catch (Exception e) {
  23. e.printStackTrace();
  24. return null;
  25. }
  26. }
  27. }

测试:地址栏输入http://localhost:9010/wxpay/nativePay?orderId=990099&money=1

2.3.3 购物车对接支付微服务

(2)新增changgou_service_pay_api模块 ,并添加common工程依赖,新增com.changgou.pay.feign包,包下创建接口

  1. @FeignClient("pay")
  2. public interface WxPayFeign {
  3. /**
  4. * 下单
  5. * @param orderId
  6. * @param money
  7. * @return
  8. */
  9. @GetMapping("/wxpay/nativePay")
  10. public Result nativePay(@RequestParam("orderId") String orderId,
  11. @RequestParam("money") Integer money );
  12. }

(3)changgou_web_order新增PayController

  1. @Controller
  2. @RequestMapping("/wxpay")
  3. public class PayController {
  4. @Autowired
  5. private OrderFeign orderFeign;
  6. @Autowired
  7. private WxPayFeign wxPayFeign;
  8. /**
  9. * 微信支付二维码
  10. * @param orderId
  11. * @return
  12. */
  13. @GetMapping
  14. public String wxPay(String orderId, Model model){
  15. //根据orderId查询订单
  16. Result orderResult = orderFeign.findById( orderId );
  17. if(orderResult.getData()==null){ //如果订单不存在
  18. return "fail";//出错页
  19. }
  20. Order order = orderResult.getData();
  21. //判断支付状态
  22. if( !"0".equals( order.getPayStatus() )){// 如果不是未支付订单
  23. return "fail";//出错页
  24. }
  25. Result payResult = wxPayFeign.nativePay( orderId, order.getPayMoney() );
  26. if(payResult.getData()==null){
  27. return "fail";//出错页
  28. }
  29. Map payMap= (Map)payResult.getData();
  30. payMap.put( "payMoney", order.getPayMoney() );
  31. payMap.put( "orderId" ,orderId );
  32. model.addAllAttributes( payMap );
  33. return "wxpay";
  34. }
  35. }

(4)将静态原型中wxpay.html拷贝到templates文件夹下作为模板,修改模板,部分代码如下:

二维码地址渲染

  1. <script type="text/javascript" th:inline="javascript">
  2. let qrcode = new QRCode(document.getElementById("qrcode"), {
  3. width : 200,
  4. height : 200
  5. });
  6. qrcode.makeCode([[${code_url}]]);
  7. </script>

显示订单号与金额

  1. <h4 class="fl tit-txt"><span class="success-icon"></span><span class="success-info" th:text="|订单提交成功,请您及时付款!订单号:${orderId}|"></span></h4>
  2. <span class="fr"><em class="sui-lead">应付金额:</em><em class="orange money" th:text="${payMoney}/100"></em></span>

设置支付跳转

  1. <li><a th:href="|/api/wxpay?orderId=${orderId}|"><img src="/img/_/pay3.jpg"></a></li>

(5) 更新网关地址过滤器。添加wxpay路径的拦截

1564132479486.png

同时更新网关服务的application.yml。添加地址路由标识
1564132093511.png

3. 支付回调逻辑处理

3.1 需求分析

在完成支付后,修改订单状态为已支付,并记录订单日志。

3.2 实现思路

(1)接受微信支付平台的回调信息(xml)

  1. <xml><appid><![CDATA[wx8397f8696b538317]]></appid>
  2. <bank_type><![CDATA[CFT]]></bank_type>
  3. <cash_fee><![CDATA[1]]></cash_fee>
  4. <fee_type><![CDATA[CNY]]></fee_type>
  5. <is_subscribe><![CDATA[N]]></is_subscribe>
  6. <mch_id><![CDATA[1473426802]]></mch_id>
  7. <nonce_str><![CDATA[c6bea293399a40e0a873df51e667f45a]]></nonce_str>
  8. <openid><![CDATA[oNpSGwbtNBQROpN_dL8WUZG3wRkM]]></openid>
  9. <out_trade_no><![CDATA[1553063775279]]></out_trade_no>
  10. <result_code><![CDATA[SUCCESS]]></result_code>
  11. <return_code><![CDATA[SUCCESS]]></return_code>
  12. <sign><![CDATA[DD4E5DF5AF8D8D8061B0B8BF210127DE]]></sign>
  13. <time_end><![CDATA[20190320143646]]></time_end>
  14. <total_fee>1</total_fee>
  15. <trade_type><![CDATA[NATIVE]]></trade_type>
  16. <transaction_id><![CDATA[4200000248201903206581106357]]></transaction_id>
  17. </xml>

(2)收到通知后,调用查询接口查询订单。

(3)如果支付结果为成功,则调用修改订单状态和记录订单日志的方法。

3.3 代码实现

3.3.1 内网映射工具EchoSite

在请求统一下单接口时,有个参数notify_url ,这个是回调地址,也就是在支付成功后微信支付会自动访问这个地址,通知业务系统支付完成。但这个地址必须是互联网可以访问的(也就是有域名的)。

那么如何测试呢?我们可以借助一个工具 EchoSite 内网映射工具

(1)打开网址: https://www.echosite.cn/ 注册用户,登录到控制台下载客户端。

(2)支付3元买一个域名(可以用1个月),点击域名端口—-抢注域名

10-4.png

(3)使用课程提供的软件echosite ,添加config.yml

  1. # 这是你的 EchoSite 购买域名的服务器标志
  2. server_addr: cross.echosite.cn:4443
  3. trust_host_root_certs: false
  4. echosite_id: 17799998888
  5. echosite_token: $2y$10$bY081mt/2KAFJLJXrsSNHe3f.6SM.SGfXgu1gG7aJvmPMPN9BTqrS
  6. # 以下是你需要开启的通道,只能开启属于你的域名通道
  7. # 以下分别是 http 和 https 以及 tcp 协议的示例
  8. tunnels:
  9. name1:
  10. subdomain: "changgou"
  11. proto:
  12. http: 127.0.0.1:9010

然后在echosite目录中输入以下命令

  1. echosite -config=config.yml start-all

运行效果如下:

13-4.png

这样你购买的域名就映射到127.0.0.1:9011上了。 ctrl+c 结束程序

怎么才能验证域名是否映射到你的计算机上了呢?

WxPayController新增notifyLogic方法

  1. /**
  2. * 回调
  3. */
  4. @RequestMapping("/notify")
  5. public void notifyLogic(){
  6. System.out.println("支付成功回调。。。。");
  7. }

测试:

1)通过本地方式访问该接口

2)通过域名形式访问该接口

3.3.2 接收回调信息

(1)修改支付微服务配置文件

  1. wxpay:
  2. notify_url: http://weizhaohui.cross.echosite.cn/wxpay/notify #回调地址

(2)修改WxPayServiceImpl ,引入

  1. @Value( "${wxpay.notify_url}" )
  2. private String notifyUrl;

(3)修改WxPayServiceImpl 的nativePay方法

  1. map.put("notify_url",notifyUrl);//回调地址

测试: 重新生成订单并支付。可以发现控制台在不断的触发支付回调通知。这是为什么呢?

注意:

1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。

2、后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起10次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功。

3.3.3 回调消息接收并转换

微信支付平台发送给回调地址的数据是二进制流,我们需要提取二进制流转换为字符串,这个字符串就是xml格式。

(1)在changgou_common工程添加工具类ConvertUtils。(资源提供:资源\类文件\工具类)

  1. /**
  2. * 转换工具类
  3. */
  4. public class ConvertUtils {
  5. /**
  6. * 输入流转换为xml字符串
  7. * @param inputStream
  8. * @return
  9. */
  10. public static String convertToString(InputStream inputStream) throws IOException {
  11. ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
  12. byte[] buffer = new byte[1024];
  13. int len = 0;
  14. while ((len = inputStream.read(buffer)) != -1) {
  15. outSteam.write(buffer, 0, len);
  16. }
  17. outSteam.close();
  18. inputStream.close();
  19. String result = new String(outSteam.toByteArray(), "utf-8");
  20. return result;
  21. }
  22. }

(2)修改notifyLogic方法

  1. /**
  2. * 回调
  3. */
  4. @RequestMapping("/notify")
  5. public void notifyLogic(HttpServletRequest request, HttpServletResponse response) throws IOException {
  6. System.out.println("支付成功回调。。。。");
  7. try {
  8. //输入流转换为xml字符串
  9. String xml = ConvertUtils.convertToString( request.getInputStream() );
  10. System.out.println(xml);
  11. //给微信支付一个成功的响应
  12. response.setContentType("text/xml");
  13. String data = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
  14. response.getWriter().write(data);
  15. } catch (IOException e) {
  16. e.printStackTrace();
  17. } catch (Exception e) {
  18. e.printStackTrace();
  19. }
  20. }

测试后,在控制台看到输出的消息

  1. <xml><appid><![CDATA[wx8397f8696b538317]]></appid>
  2. <bank_type><![CDATA[CFT]]></bank_type>
  3. <cash_fee><![CDATA[1]]></cash_fee>
  4. <fee_type><![CDATA[CNY]]></fee_type>
  5. <is_subscribe><![CDATA[N]]></is_subscribe>
  6. <mch_id><![CDATA[1473426802]]></mch_id>
  7. <nonce_str><![CDATA[c6bea293399a40e0a873df51e667f45a]]></nonce_str>
  8. <openid><![CDATA[oNpSGwbtNBQROpN_dL8WUZG3wRkM]]></openid>
  9. <out_trade_no><![CDATA[1553063775279]]></out_trade_no>
  10. <result_code><![CDATA[SUCCESS]]></result_code>
  11. <return_code><![CDATA[SUCCESS]]></return_code>
  12. <sign><![CDATA[DD4E5DF5AF8D8D8061B0B8BF210127DE]]></sign>
  13. <time_end><![CDATA[20190320143646]]></time_end>
  14. <total_fee>1</total_fee>
  15. <trade_type><![CDATA[NATIVE]]></trade_type>
  16. <transaction_id><![CDATA[4200000248201903206581106357]]></transaction_id>
  17. </xml>

我们可以将此xml字符串,转换为map,提取其中的out_trade_no(订单号),根据订单号修改订单状态。

3.3.4 查询订单验证通知

(1)WxPayService新增方法定义

  1. /**
  2. * 查询订单
  3. * @param orderId
  4. * @return
  5. */
  6. Map queryOrder(String orderId);

(2)WxPayServiceImpl实现方法

  1. @Override
  2. public Map queryOrder(String orderId) {
  3. Map map=new HashMap( );
  4. map.put( "out_trade_no", orderId );
  5. try {
  6. return wxPay.orderQuery( map );
  7. } catch (Exception e) {
  8. e.printStackTrace();
  9. return null;
  10. }
  11. }

(3)修改notifyLogic方法

  1. @Autowired
  2. private RabbitTemplate rabbitTemplate;
  3. /**
  4. * 回调
  5. */
  6. @RequestMapping("/notify")
  7. public void notifyLogic(HttpServletRequest request, HttpServletResponse response) throws IOException {
  8. System.out.println("支付成功回调。。。。");
  9. try {
  10. //输入流转换为xml字符串
  11. String xml = ConvertUtils.convertToString( request.getInputStream() );
  12. System.out.println(xml);
  13. //解析
  14. Map<String, String> map = WXPayUtil.xmlToMap( xml );
  15. //查询订单
  16. if("SUCCESS".equals(map.get( "result_code" ) )){ //如果返回的结果是成功
  17. Map result = wxPayService.queryOrder( map.get( "out_trade_no" ) );
  18. System.out.println("查询订单返回结果:"+result);
  19. //如果查询结果是成功发送到mq
  20. if("SUCCESS".equals( result.get( "result_code" ) )){
  21. Map m=new HashMap();
  22. m.put( "orderId",result.get( "out_trade_no" ) );
  23. m.put( "transactionId",result.get( "transaction_id" ));
  24. rabbitTemplate.convertAndSend( "","order_pay", JSON.toJSONString(m) );
  25. //如果成功,给微信支付一个成功的响应
  26. response.setContentType("text/xml");
  27. String data = "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
  28. response.getWriter().write(data);
  29. }
  30. }else {
  31. System.out.println(map.get( "err_code_des" ));//错误信息描述
  32. }
  33. } catch (IOException e) {
  34. e.printStackTrace();
  35. } catch (Exception e) {
  36. e.printStackTrace();
  37. }
  38. }

(4)rabbitmq中添加order_pay队列

3.3.5 修改订单状态

(1)com.changgou.order.listener包下创建OrderPayListener

  1. @Component
  2. @RabbitListener(queues = "order_pay")
  3. public class OrderPayListener {
  4. @Autowired
  5. private OrderService orderService;
  6. /**
  7. * 更新支付状态
  8. * @param message
  9. */
  10. @RabbitHandler
  11. public void updatePayStatus(String message){
  12. System.out.println("接收到消息:"+message);
  13. Map map = JSON.parseObject( message, Map.class );
  14. orderService.updatePayStatus( (String)map.get("orderId"), (String)map.get("transactionId") );
  15. }
  16. }

(2)OrderService接口新增方法定义

  1. /**
  2. * 修改订单状态为已支付
  3. * @param orderId
  4. * @param transactionId
  5. */
  6. void updatePayStatus(String orderId,String transactionId);

(3)OrderServiceImpl新增方法实现

  1. @Autowired
  2. private OrderLogMapper orderLogMapper;
  3. @Override
  4. public void updatePayStatus(String orderId, String transactionId) {
  5. Order order = orderMapper.selectByPrimaryKey(orderId);
  6. if(order!=null && "0".equals(order.getPayStatus())){ //存在订单且状态为0
  7. order.setPayStatus("1");
  8. order.setOrderStatus("1");
  9. order.setUpdateTime(new Date());
  10. order.setPayTime(new Date());
  11. order.setTransactionId(transactionId);//微信返回的交易流水号
  12. orderMapper.updateByPrimaryKeySelective(order);
  13. //记录订单变动日志
  14. OrderLog orderLog=new OrderLog();
  15. orderLog.setId( idWorker.nextId()+"" );
  16. orderLog.setOperater("system");// 系统
  17. orderLog.setOperateTime(new Date());//当前日期
  18. orderLog.setOrderStatus("1");
  19. orderLog.setPayStatus("1");
  20. orderLog.setRemarks("支付流水号"+transactionId);
  21. orderLog.setOrderId(order.getId());
  22. orderLogMapper.insert(orderLog);
  23. }
  24. }

4. 推送支付通知

4.1 需求分析

当用户完成扫码支付后,跳转到支付成功页面10-5.png
10-2.png

4.2 服务端推送方案

我们需要将支付的结果通知前端页面,其实就是我们通过所说的服务器端推送,主要有三种实现方案

(1)Ajax 短轮询
Ajax 轮询主要通过页面端的 JS 定时异步刷新任务来实现数据的加载

如果我们使用ajax短轮询方式,需要后端提供方法,通过调用微信支付接口实现根据订单号查询支付状态的方法(参见查询订单API) 。 前端每间隔三秒查询一次,如果后端返回支付成功则执行页面跳转。

缺点:这种方式实时效果较差,而且对服务端的压力也较大。不建议使用

(2)长轮询

长轮询主要也是通过 Ajax 机制,但区别于传统的 Ajax 应用,长轮询的服务器端会在没有数据时阻塞请求直到有新的数据产生或者请求超时才返回,之后客户端再重新建立连接获取数据。

如果使用长轮询,也同样需要后端提供方法,通过调用微信支付接口实现根据订单号查询支付状态的方法,只不过循环是写在后端的。

缺点:长轮询服务端会长时间地占用资源,如果消息频繁发送的话会给服务端带来较大的压力。不建议使用

(3)WebSocket 双向通信
WebSocket 是 HTML5 中一种新的通信协议,能够实现浏览器与服务器之间全双工通信。如果浏览器和服务端都支持 WebSocket 协议的话,该方式实现的消息推送无疑是最高效、简洁的。并且最新版本的 IE、Firefox、Chrome 等浏览器都已经支持 WebSocket 协议,Apache Tomcat 7.0.27 以后的版本也开始支持 WebSocket。

4.3 RabbitMQ Web STOMP 插件

借助于 RabbitMQ 的 Web STOMP 插件,实现浏览器与服务端的全双工通信。从本质上说,RabbitMQ 的 Web STOMP 插件也是利用 WebSocket 对 STOMP 协议进行了一次桥接,从而实现浏览器与服务端的双向通信。

10-5.png

4.3.1 STOMP协议

STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议。前身是TTMP协议(一个简单的基于文本的协议),专为消息中间件设计。它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。

4.3.2 插件安装

我们进入rabbitmq容器,执行下面的命令开启stomp插件

  1. rabbitmq-plugins enable rabbitmq_web_stomp rabbitmq_web_stomp_examples

将当前的容器提交为新的镜像

  1. docker commit 3989ec68bf3c rabbitmq:stomp

停止当前的容器

  1. docker stop 3989ec68bf3c

根据新的镜像创建同期

  1. docker run -di --name=changgou_rabbitmq -p 5671:5617 -p 5672:5672 -p 4369:4369 -p 15671:15671 -p 15672:15672 -p 25672:25672 -p 15670:15670 -p 15674:15674 rabbitmq:stomp

4.3.3 消息推送测试

我们在浏览器访问http://192.168.200.128:15670 可以看到stomp的例子代码 。

我们根据stomp的例子代码创建一个页面,内容如下:

  1. <html>
  2. <head>
  3. <title>RabbitMQ Web STOMP Examples : Echo Server</title>
  4. <meta charset="UTF-8">
  5. <script src="js/stomp.min.js"></script>
  6. </head>
  7. <script>
  8. var client = Stomp.client('ws://localhost:15674/ws');
  9. var on_connect = function(x) {
  10. id = client.subscribe("/exchange/paynotify", function(d) {
  11. alert(d.body);
  12. });
  13. };
  14. var on_error = function() {
  15. console.log('error');
  16. };
  17. client.connect('guest', 'guest', on_connect, on_error, '/');
  18. </script>
  19. </body>
  20. </html>

destination 在 RabbitMQ Web STOM 中进行了相关的定义,根据使用场景的不同,主要有以下 4 种:

  • 1./exchange/

对于 SUBCRIBE frame,destination 一般为/exchange//[/pattern] 的形式。该 destination 会创建一个唯一的、自动删除的、名为的 queue,并根据 pattern 将该 queue 绑定到所给的 exchange,实现对该队列的消息订阅。 对于 SEND frame,destination 一般为/exchange//[/routingKey] 的形式。这种情况下消息就会被发送到定义的 exchange 中,并且指定了 routingKey。

  • 2./queue/
    对于 SUBCRIBE frame,destination 会定义的共享 queue,并且实现对该队列的消息订阅。
    对于 SEND frame,destination 只会在第一次发送消息的时候会定义的共享 queue。该消息会被发送到默认的 exchange 中,routingKey 即为。
  • 3./amq/queue/
    这种情况下无论是 SUBCRIBE frame 还是 SEND frame 都不会产生 queue。但如果该 queue 不存在,SUBCRIBE frame 会报错。
    对于 SUBCRIBE frame,destination 会实现对队列的消息订阅。
    对于 SEND frame,消息会通过默认的 exhcange 直接被发送到队列中。
  • 4./topic/
    对于 SUBCRIBE frame,destination 创建出自动删除的、非持久的 queue 并根据 routingkey 为绑定到 amq.topic exchange 上,同时实现对该 queue 的订阅。
    对于 SEND frame,消息会被发送到 amq.topic exchange 中,routingKey 为。

我们在rabbitmq中创建一个叫paynotify的交换机(fanout类型)

测试,我们在rabbitmq管理界面中向paynotify交换机发送消息,页面就会接收这个消息。

为了安全,我们在页面上不能用我们的rabbitmq的超级管理员用户guest,所以我们需要在rabbitmq中新建一个普通用户webguest(普通用户无法登录管理后台)
13-2.png
设置虚拟目录权限

13-3.png

4.4 代码实现

实现思路:后端在收到回调通知后发送订单号给mq(paynotify交换器),前端通过stomp连接到mq订阅paynotify交换器的消息,判断接收的订单号是不是当前页面的订单号,如果是则进行页面的跳转。

(1)修改notifyLogic方法,在"SUCCESS".equals(map.get("result_code")) 后添加

  1. rabbitTemplate.convertAndSend("paynotify","",map.get("out_trade_no"));

(2)修改wxpay.html ,渲染js代码订单号和支付金额部分

  1. let client = Stomp.client('ws://192.168.200.128:15674/ws');
  2. let on_connect = function(x) {
  3. id = client.subscribe("/exchange/paynotify", function(d) {
  4. let orderId=[[${orderId}]];
  5. if(d.body==orderId){
  6. location.href='/api/wxpay/topaysuccess?payMoney='+[[${payMoney}]]; //参数支付金额
  7. }
  8. });
  9. };
  10. let on_error = function() {
  11. console.log('error');
  12. };
  13. client.connect('webguest', 'itcast', on_connect, on_error, '/');

(3)将paysuccess.html拷贝到static文件夹 。

(4)changgou_web_order的PayController中新增跳转支付成功接口

  1. @GetMapping("/topaysuccess")
  2. public String topaysuccess(String payMoney,Model model){
  3. model.addAttribute("payMoney",payMoney);
  4. return "paysuccess";
  5. }