畅购商城_第13章_微信扫码支付
畅购商城_第13章_微信扫码支付
第13章 微信扫码支付
课程内容:
请根据营业执照类型选择以下主体注册:| | | | 。
第四步:开户成功,登录商户平台进行验证
资料审核通过后,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资金数额,完成账户验证。
第五步:在线签署协议
本协议为线上电子协议,签署后方可进行交易及资金结算,签署完立即生效。
完成上述步骤,你可以得到调用API用到的账号和密钥
mch_id:商户号 1473426802
key:商户密钥 T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
我们解压从官网下载的sdk ,安装到本地仓库
com.github.wxpay.sdk.WXPay类下提供了对应的方法:
1.3 统一下单API
<dependency> <groupId>com.github.wxpay</groupId> <artifactId>wxpay-sdk</artifactId> <version>3.0.9</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.3</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.24</version> </dependency>
(2)创建com.github.wxpay.sdk包,包下创建MyConfig类 ,继承自抽象类WXPayConfig
(3)创建测试类,编写代码
MyConfig config=new MyConfig(); WXPay wxPay=new WXPay( config ); Map<String,String> map=new HashMap(); map.put("body","畅购");//商品描述 map.put("out_trade_no","55555211");//订单号 map.put("total_fee","1");//金额 map.put("spbill_create_ip","127.0.0.1");//终端IP map.put("notify_url","http://www.baidu.com");//回调地址 map.put("trade_type","NATIVE");//交易类型 Map<String, String> result = wxPay.unifiedOrder( map ); System.out.println(result);
执行后返回结果
{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> ,然后编写脚本
<script src="js/plugins/qrcode.min.js" ></script> <script type="text/javascript"> let qrcode = new QRCode(document.getElementById("qrcode"), { width : 200, height : 200 }); qrcode.makeCode("weixin://wxpay/bizpayurl?pr=Y3hDTZy"); </script>
2.1 需求分析
2.2 实现思路
前端页面向后端传递订单号,后端根据订单号查询订单,检查是否为当前用户的未支付订单,如果是则根据订单号和金额生成支付url返给前端,前端得到支付url生成支付二维码。
2.3 代码实现
2.3.1 提交订单跳转支付页
1)更新changgou_web_order的application.yml,添加读取超时设置
#请求处理的超时时间 ribbon: ReadTimeout: 4000 #请求连接的超时时间 ConnectTimeout: 3000
2)在changgou_order_web工程resources/templates下添加 ‘资源/pay.html,fail.html,wxpay.html’。
3)更新changgou_service_order中add() ,设置返回值为订单Id
- 更新order.html中添加订单js
add:function () { axios.post(/api/worder/add,this.order).then(function (response) { if (response.data.flag){ alert("添加订单成功"); var orderId = response.data.data; location.href="/api/worder/toPayPage?orderId="+orderId; }else{ alert("添加失败"); } }) }
5)在orderController中新增方法,用于跳转支付页
5.1)changgou_service_order_api的OrderFeign新增接口定义
/*** * 根据ID查询数据 * @param id * @return */ @GetMapping("/order/{id}") public Result findById(@PathVariable("id") String id);
5.2) changgou_order_web中的orderController新增方法,跳转支付页
@GetMapping("/toPayPage") public String toPayPage(String orderId, Model model){ Order order= orderFeign.findById(orderId).getData(); model.addAttribute("orderId",orderId); model.addAttribute("payMoney",order.getPayMoney()); return "pay"; }
2.3.2 支付微服务-下单
(1)创建changgou_service_pay (支付微服务), pom.xml添加依赖
<dependency> <groupId>com.changgou</groupId> <artifactId>changgou_common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.github.wxpay</groupId> <artifactId>wxpay-sdk</artifactId> <version>3.0.9</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
排除log包,否则会因为包冲突无法正常启动
(2)创建配置文件application.yml
server: port: 9010 spring: application: name: pay rabbitmq: host: 192.168.200.128 main: allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册 eureka: client: service-url: defaultZone: http://127.0.0.1:6868/eureka instance: prefer-ip-address: true
(3)创建com.github.wxpay.sdk包,包下创建Config类
(4)创建com.changgou包,包下创建类PayApplication
@SpringBootApplication @EnableEurekaClient public class PayApplication { public static void main(String[] args) { SpringApplication.run(PayApplication.class); } @Bean public WXPay wxPay(){ try { return new WXPay( new MyConfig() ); } catch (Exception e) { e.printStackTrace(); return null; } } }
(3)创建com.changgou.pay.controller包 ,新增WxPayController
@RestController @RequestMapping("/wxpay") public class WxPayController { @Autowired private WxPayService wxPayService; /** * 下单 * @param orderId * @param money * @return */ @GetMapping("/nativePay") public Result nativePay(@RequestParam("orderId")String orderId,@RequestParam("money") Integer money){ Map map = wxPayService.nativePay( orderId, money ); return new Result( true, StatusCode.OK,"",map ); } }
(4)创建com.changgou.pay.service包,包下创建接口WxPayService
(5)创建com.changgou.pay.service.impl 包 ,新增服务类WxPayServiceImpl
@Service public class WxPayServiceImpl implements WxPayService { @Autowired private WXPay wxPay; @Override public Map nativePay(String orderId, Integer money) { try { //1.封装请求参数 Map<String,String> map=new HashMap(); map.put("body","畅购商城");//商品描述 map.put("out_trade_no",orderId);//订单号 //map.put("total_fee",String.valueOf(money*100));//金额,以分为单位 BigDecimal payMoney = new BigDecimal("0.01"); BigDecimal fen = payMoney.multiply(new BigDecimal("100")); //1.00 fen = fen.setScale(0,BigDecimal.ROUND_UP); // 1 map.put("total_fee",String.valueOf(fen)); map.put("spbill_create_ip","127.0.0.1");//终端IP map.put("notify_url","http://www.itcast.cn");//回调地址,先随便填一个 map.put("trade_type","NATIVE");//交易类型 Map<String, String> mapResult = wxPay.unifiedOrder( map ); //调用统一下单 return mapResult; } catch (Exception e) { e.printStackTrace(); return null; } } }
测试:地址栏输入http://localhost:9010/wxpay/nativePay?orderId=990099&money=1
2.3.3 页面对接支付微服务
(1)新增changgou_service_pay_api模块 ,并添加common工程依赖,新增com.changgou.pay.feign包,包下创建接口
@FeignClient("pay") public interface WxPayFeign { /** * 下单 * @param orderId * @param money * @return */ @GetMapping("/wxpay/nativePay") public Result nativePay(@RequestParam("orderId") String orderId, @RequestParam("money") Integer money ); }
(2)changgou_web_order新增PayController
(3)将静态原型中wxpay.html拷贝到templates文件夹下作为模板,修改模板,部分代码如下:
二维码地址渲染
<script type="text/javascript" th:inline="javascript"> let qrcode = new QRCode(document.getElementById("qrcode"), { width : 200, height : 200 }); qrcode.makeCode([[${ code_url}]]); </script>
显示订单号与金额
<h4 class="fl tit-txt"><span class="success-icon"></span><span class="success-info" th:text="|订单提交成功,请您及时付款!订单号:${orderId}|"></span></h4> <span class="fr"><em class="sui-lead">应付金额:</em><em class="orange money" th:text="${payMoney}"></em>元</span>
设置支付跳转
<li><a th:href="|/api/wxpay?orderId=${orderId}|"><img src="/img/_/pay3.jpg"></a></li>
(4) 更新网关地址过滤器。添加wxpay路径的拦截
同时更新网关服务的application.yml。添加地址路由标识
3. 支付回调逻辑处理
3.1 需求分析
在完成支付后,修改订单状态为已支付,并记录订单日志。
3.2 实现思路
<xml><appid><![CDATA[wx8397f8696b538317]]></appid> <bank_type><![CDATA[CFT]]></bank_type> <cash_fee><![CDATA[1]]></cash_fee> <fee_type><![CDATA[CNY]]></fee_type> <is_subscribe><![CDATA[N]]></is_subscribe> <mch_id><![CDATA[1473426802]]></mch_id> <nonce_str><![CDATA[c6bea293399a40e0a873df51e667f45a]]></nonce_str> <openid><![CDATA[oNpSGwbtNBQROpN_dL8WUZG3wRkM]]></openid> <out_trade_no><![CDATA[1553063775279]]></out_trade_no> <result_code><![CDATA[SUCCESS]]></result_code> <return_code><![CDATA[SUCCESS]]></return_code> <sign><![CDATA[DD4E5DF5AF8D8D8061B0B8BF210127DE]]></sign> <time_end><![CDATA[20190320143646]]></time_end> <total_fee>1</total_fee> <trade_type><![CDATA[NATIVE]]></trade_type> <transaction_id><![CDATA[4200000248201903206581106357]]></transaction_id> </xml>
(2)收到通知后,调用查询接口查询订单。
(3)如果支付结果为成功,则调用修改订单状态和记录订单日志的方法。
3.3 代码实现
3.3.1 内网映射工具EchoSite
那么如何测试呢?我们可以借助一个工具 EchoSite 内网映射工具
(1)打开网址: https://www.echosite.cn/ 注册用户,登录到控制台下载客户端。
(2)支付3元买一个域名(可以用1个月),点击域名端口—抢注域名
(3)使用课程提供的软件echosite ,添加config.yml
# 这是你的 EchoSite 购买域名的服务器标志 server_addr: cross.echosite.cn:4443 trust_host_root_certs: false echosite_id: 17799998888 echosite_token: $2y$10$bY081mt/2KAFJLJXrsSNHe3f.6SM.SGfXgu1gG7aJvmPMPN9BTqrS # 以下是你需要开启的通道,只能开启属于你的域名通道 # 以下分别是 http 和 https 以及 tcp 协议的示例 tunnels: name1: subdomain: "changgou" proto: http: 127.0.0.1:9010
然后在echosite目录中输入以下命令
echosite -config=config.yml start-all
运行效果如下:
这样你购买的域名就映射到127.0.0.1:9011上了。 ctrl+c 结束程序
怎么才能验证域名是否映射到你的计算机上了呢?
WxPayController新增notifyLogic方法
/** * 回调 */ @RequestMapping("/notify") public void notifyLogic(){ System.out.println("支付成功回调。。。。"); }
测试:
1)通过本地方式访问该接口
2)通过域名形式访问该接口
3.3.2 接收回调信息
(1)修改支付微服务配置文件
wxpay: notify_url: http://weizhaohui.cross.echosite.cn/wxpay/notify #回调地址
(2)修改WxPayServiceImpl ,引入
@Value( "${wxpay.notify_url}" ) private String notifyUrl;
(3)修改WxPayServiceImpl 的nativePay方法
map.put("notify_url",notifyUrl);//回调地址
测试: 重新生成订单并支付。可以发现控制台在不断的触发支付回调通知。这是为什么呢?
注意:
1、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
3.3.3 回调消息接收并转换
(1)在changgou_common工程添加工具类ConvertUtils。(资源提供:资源类文件工具类)
/** * 转换工具类 */ public class ConvertUtils { /** * 输入流转换为xml字符串 * @param inputStream * @return */ public static String convertToString(InputStream inputStream) throws IOException { ByteArrayOutputStream outSteam = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = 0; while ((len = inputStream.read(buffer)) != -1) { outSteam.write(buffer, 0, len); } outSteam.close(); inputStream.close(); String result = new String(outSteam.toByteArray(), "utf-8"); return result; } }
(2)修改notifyLogic方法
测试后,在控制台看到输出的消息
<xml><appid><![CDATA[wx8397f8696b538317]]></appid> <bank_type><![CDATA[CFT]]></bank_type> <cash_fee><![CDATA[1]]></cash_fee> <fee_type><![CDATA[CNY]]></fee_type> <is_subscribe><![CDATA[N]]></is_subscribe> <mch_id><![CDATA[1473426802]]></mch_id> <nonce_str><![CDATA[c6bea293399a40e0a873df51e667f45a]]></nonce_str> <openid><![CDATA[oNpSGwbtNBQROpN_dL8WUZG3wRkM]]></openid> <out_trade_no><![CDATA[1553063775279]]></out_trade_no> <result_code><![CDATA[SUCCESS]]></result_code> <return_code><![CDATA[SUCCESS]]></return_code> <sign><![CDATA[DD4E5DF5AF8D8D8061B0B8BF210127DE]]></sign> <time_end><![CDATA[20190320143646]]></time_end> <total_fee>1</total_fee> <trade_type><![CDATA[NATIVE]]></trade_type> <transaction_id><![CDATA[4200000248201903206581106357]]></transaction_id> </xml>
我们可以将此xml字符串,转换为map,提取其中的out_trade_no(订单号),根据订单号修改订单状态。
3.3.4 查询订单验证通知
(1)WxPayService新增方法定义
/** * 查询订单 * @param orderId * @return */ Map queryOrder(String orderId);
(2)WxPayServiceImpl实现方法
@Override public Map queryOrder(String orderId) { Map map=new HashMap( ); map.put( "out_trade_no", orderId ); try { return wxPay.orderQuery( map ); } catch (Exception e) { e.printStackTrace(); return null; } }
(3)修改notifyLogic方法
(4)rabbitmq中添加order_pay队列
3.3.5 修改订单状态
(1)com.changgou.order.listener包下创建OrderPayListener
@Component @RabbitListener(queues = "order_pay") public class OrderPayListener { @Autowired private OrderService orderService; /** * 更新支付状态 * @param message */ @RabbitHandler public void updatePayStatus(String message){ System.out.println("接收到消息:"+message); Map map = JSON.parseObject( message, Map.class ); orderService.updatePayStatus( (String)map.get("orderId"), (String)map.get("transactionId") ); } }
(2)OrderService接口新增方法定义
/** * 修改订单状态为已支付 * @param orderId * @param transactionId */ void updatePayStatus(String orderId,String transactionId);
(3)OrderServiceImpl新增方法实现
4. 推送支付通知
4.1 需求分析
当用户完成扫码支付后,跳转到支付成功页面
4.2 服务端推送方案
我们需要将支付的结果通知前端页面,其实就是我们通过所说的服务器端推送,主要有三种实现方案
(1)Ajax 短轮询 Ajax 轮询主要通过页面端的 JS 定时异步刷新任务来实现数据的加载
缺点:这种方式实时效果较差,而且对服务端的压力也较大。不建议使用
(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 协议进行了一次桥接,从而实现浏览器与服务端的双向通信。
4.3.1 STOMP协议
STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议。前身是TTMP协议(一个简单的基于文本的协议),专为消息中间件设计。它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。
4.3.2 插件安装
我们进入rabbitmq容器,执行下面的命令开启stomp插件
rabbitmq-plugins enable rabbitmq_web_stomp rabbitmq_web_stomp_examples
将当前的容器提交为新的镜像
docker commit 3989ec68bf3c rabbitmq:stomp
停止当前的容器
docker stop 3989ec68bf3c
根据新的镜像创建容器
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的例子代码创建一个页面,内容如下:
<html> <head> <title>RabbitMQ Web STOMP Examples : Echo Server</title> <meta charset="UTF-8"> <script src="js/stomp.min.js"></script> </head> <script> var client = Stomp.client(ws://localhost:15674/ws); var on_connect = function(x) { id = client.subscribe("/exchange/paynotify", function(d) { alert(d.body); }); }; var on_error = function() { console.log(error); }; client.connect(guest, guest, on_connect, on_error, /); </script> </body> </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(普通用户无法登录管理后台)
设置虚拟目录权限
4.4 代码实现
实现思路:后端在收到回调通知后发送订单号给mq(paynotify交换器),前端通过stomp连接到mq订阅paynotify交换器的消息,判断接收的订单号是不是当前页面的订单号,如果是则进行页面的跳转。
(1)修改notifyLogic方法,在"SUCCESS".equals(map.get("result_code")) 后添加
rabbitTemplate.convertAndSend("paynotify","",map.get("out_trade_no"));
(2)修改wxpay.html ,渲染js代码订单号和支付金额部分
let client = Stomp.client(ws://192.168.200.128:15674/ws); let on_connect = function(x) { id = client.subscribe("/exchange/paynotify", function(d) { let orderId=[[${ orderId}]]; if(d.body==orderId){ location.href=/api/wxpay/topaysuccess?payMoney=+[[${ payMoney}]]; //参数支付金额 } }); }; let on_error = function() { console.log(error); }; client.connect(webguest, itcast, on_connect, on_error, /);
(3)将paysuccess.html拷贝到static文件夹 。
(4)changgou_web_order的PayController中新增跳转支付成功接口
@GetMapping("/topaysuccess") public String topaysuccess(String payMoney,Model model){ model.addAttribute("payMoney",payMoney); return "paysuccess"; }