【项目系列】乐优商城项目(十七):微信支付
0.学习目标
- 微信支付下单
- 生成二维码
- 实现支付回调
- 实现支付状态查询
1.微信支付简介
1.1.介绍
微信支付官方文档:https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F
我们选择开发文档,而后进入选择页面:
选择native支付,就是扫码支付:
此处我们使用模式二来开发:
1.2.开发流程
模式二与模式一相比,流程更为简单,不依赖设置的回调支付URL。
商户后台系统先调用微信支付的统一下单接口,微信后台系统返回链接参数code_url;
商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。
注意:code_url有效期为2小时,过期后扫码不能再发起支付。
流程图:
这里我们把商户(我们)要做的事情总结一下:
- 1、商户生成订单
- 2、商户调用微信下单接口,获取预交易的链接
- 3、商户将链接生成二维码图片,展示给用户;
- 4、支付结果通知:
- 微信异步通知商户支付结果,商户告知微信支付接收情况
- 商户如果没有收到通知,可以调用接口,查询支付状态
- 5、如果支付成功,发货,修改订单状态
在前面的业务中,我们已经完成了:
- 1、生成订单
接下来,我们需要做的是:
- 2、调用微信下单接口,生成链接。
- 3、根据链接生成二维码图片
- 4、支付成功后修改订单状态
2.统一下单(生成支付链接)
按照上面的步骤分析,第一步是要生成支付链接。我们查看下微信官方文档
2.1.API说明
在微信支付文档中,可以查询到下面的信息:
请求路径
URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder
请求参数
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
公众账号ID | appid | 是 | String(32) | wxd678efh56 | 微信支付分配的公众账号ID |
商户号 | mch_id | 是 | String(32) | 1230000109 | 微信支付分配的商户号 |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILT | 随机字符串,长度要求在32位以内。推荐随机数生成算法 |
签名 | sign | 是 | String(32) | C380BEC2B | 通过签名算法计算得出的签名值,详见签名生成算法 |
商品描述 | body | 是 | String(128) | 乐优手机 | 商品简单描述,该字段请按照规范传递,具体请见参数规定 |
商户订单号 | out_trade_no | 是 | String(32) | 20150806125 | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|* 且在同一个商户号下唯一。详见商户订单号 |
标价金额 | total_fee | 是 | Int | 88 | 订单总金额,单位为分,详见支付金额 |
终端IP | spbill_create_ip | 是 | String(16) | 123.12.12.123 | APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP。 |
通知地址 | notify_url | 是 | String(256) | http://www.weixin.qq.com/wxpay/pay.php | 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 |
交易类型 | trade_type | 是 | String(16) | JSAPI | JSAPI 公众号支付;NATIVE 扫码支付;APP APP支付说明详见参数规定 |
这些参数大致分成3类:
appid、mch_id、spbill_create_ip、notify_url、trade_type:是商家自己的信息或固定数据,可以提前配置,因此无需每次请求单独配置,而是统一设置好即可,
nonce_str、sign:是为了保证数据安全而添加的验证数据,根据算法去生成,每次请求自动生成即可。
body、out_trade_no、total_fee:订单相关信息,需要我们自己填写。
2.2.微信SDK
2.2.1.下载
虽然请求参数比较复杂,但官方已经提供了SDK,供我们使用:
我也已经在课前资料提供:
微信没有提供maven仓库坐标,因此我们必须下载使用,建议使用课前资料中,我提供给大家的SDK,其中做了一些必要的设置:
2.2.2.WXPay工具
微信SDK提供了一个统一的微信支付工具类:WXPay:
其中包含这样一些方法:
com.github.wxpay.sdk.WXPay类下提供了对应的方法:
方法名 | 说明 |
---|---|
microPay | 刷卡支付 |
unifiedOrder |
统一下单 |
orderQuery | 查询订单 |
reverse | 撤销订单 |
closeOrder | 关闭订单 |
refund | 申请退款 |
refundQuery | 查询退款 |
downloadBill | 下载对账单 |
report | 交易保障 |
shortUrl | 转换短链接 |
authCodeToOpenid | 授权码查询openid |
- 注意:
- 参数为
Map<String, String>
对象,返回类型也是Map<String, String>
- 方法内部会将参数转换成含有
appid
、mch_id
、nonce_str
、sign_type
和sign
的XML - 通过HTTPS请求得到返回数据后会对其做必要的处理(例如验证签名,签名错误则抛出异常)
- 参数为
我们主要关注其中的unifiedOrder方法,统一下单:
1 | /** |
这里的请求参数是:Map<String, String> reqData,就是官方API说明中的请求参数了,不过并不需要我们填写所有参数,而只需要下面的:
- body:商品描述
- out_trade_no:订单编号
- total_fee:订单应支付金额
- spbill_create_ip:设备IP
- notify_url:回调地址
- trade_type:交易类型
剩下的:appid
、mch_id
、nonce_str
、sign_type
和sign
参数都有WXPay对象帮我们设置,那么问题来了:这些参数数据WXPay是怎么拿到的呢?
其中,
- nonce_str:是随机字符串,因此由WXPay随机生成,
- sign_type:是签名算法,由WXPay指定,默认是HMACSHA256;
- sign:是签名,由签名算法结合密钥加密而来,因此这里的关键是密钥:key
- appid、mch_id是商家信息,需要配置
也就是说,这例需要配置的包括:appid、mch_id、密钥key。这些从哪里来呢?
看下WXPay的构造函数:
1 | public WXPay(final WXPayConfig config) throws Exception { |
这里需要一个WXPayConfig对象,显然是配置对象。
2.2.3..WXPayConfig配置
WXPay依赖于WXPayConfig进行配置,那么WXPayConfig是什么呢?
看下源码中的关键部分:
1 | public abstract class WXPayConfig { |
这不就是WXPay中需要配置的3个属性嘛,当我们实现这个类,并且给出其中的值,把WXPayConfig传递给WXPay时,WXPay就会获取到这些数据:
当我们利用WXPay发送请求时,WXPay就会帮我们封装到请求参数中:
而在我提供给大家的SDK中,就编写了一个WXPayConfig的实现:
1 | package com.github.wxpay.sdk; |
将来我们只需要new出这个实现类对象,并且给这3个参数赋值即可。
2.3.整合到项目中
2.3.1.打包SDK
首先,把我提供的SDK打包并安装到本地的maven仓库,方便在项目中使用。
进入我提供的SDK的项目目录,然后打开黑窗口,输入命令:
1 | mvn source:jar install -Dmaven.test.skip=true |
然后进入本地仓库查看:
2.3.2.配置WXPay
在ly-order中引入坐标:
1 | <dependency> |
我们将这些WXPayConfig中的属性定义到application.yml中
1 | ly: |
将这些属性注入到PayConfig中:
1 | package com.leyou.order.config; |
2.3.4.支付工具类
我们先初始化WXPay对象,并注入到Spring容器中:
1 | package com.leyou.order.config; |
我们定义支付工具类,完成后续操作:
1 | package com.leyou.order.utils; |
2.4.下单并生成支付链接
在订单支付页面,会向后台发起请求,查询支付的URL地址:
我们需要编写controller,来实现这个功能:
- 请求方式:GET
- 请求路径:/order/url/{id}
- 请求参数:id,订单的编号
- 返回结果:url地址
代码如下:
controller:
1 |
|
service,订单支付url的有效期是2小时,因此我们可以获取url后存入redis缓存:
- 先检查redis是否已经有url,有则返回
- 没有,则查询订单信息,校验订单状态是否为已经支付,是则抛出异常
- 如果没有支付,调用PayHelper,生成url
- 将url存入redis,设置有效期为2小时
引入redis依赖:
1 | <dependency> |
配置:
1 | spring: |
代码:
1 |
|
页面响应结果:
3.生成支付二维码
3.1.什么是二维码
二维码又称QR Code,QR全称Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的Bar Code条形码能存更多的信息,也能表示更多的数据类型。
二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的;在代码编制上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设备自动识读以实现信息自动处理:它具有条码技术的一些共性:每种码制有其特定的字符集;每个字符占有一定的宽度;具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化点。
3.2.二维码优势
信息容量大, 可以容纳多达1850个大写字母或2710个数字或500多个汉字
应用范围广, 支持文字,声音,图片,指纹等等…
容错能力强, 即使图片出现部分破损也能使用
成本低, 容易制作
3.3.二维码容错级别
L级(低) 7%的码字可以被恢复。
M级(中) 15%的码字可以被恢复。
Q级(四分)25%的码字可以被恢复。
H级(高)30% 的码字可以被恢复。
3.4.二维码生成插件qrious
qrious是一款基于HTML5 Canvas的纯JS二维码生成插件。通过qrious.js可以快速生成各种二维码,你可以控制二维码的尺寸颜色,还可以将生成的二维码进行Base64编码。官网
qrious.js二维码插件的可用配置参数如下:
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
background | String | “white” | 二维码的背景颜色。 |
foreground | String | “black” | 二维码的前景颜色。 |
level | String | “L” | 二维码的误差校正级别(L, M, Q, H)。 |
mime | String | “image/png” | 二维码输出为图片时的MIME类型。 |
size | Number | 100 | 二维码的尺寸,单位像素。 |
value | String | “” | 需要编码为二维码的值 |
课前资料中给出的案例可以直接生成二维码:
3.5.生成二维码
我们把课前资料中的这个js脚本引入到项目中:
然后在页面引用:
页面定义一个div,用于展示二维码:
然后获取到付款链接后,根据链接生成二维码:
刷新页面,查看效果:
此时,客户用手机扫描二维码,可以看到付款页面。
4.支付结果通知
支付以后,我们后台需要修改订单状态。我们怎么得知有没有支付成功呢?
在我们的请求参数中,有一个notify_url的参数,是支付的回调地址。当用户支付成功后,微信会主动访问这个地址,并携带支付结果信息。
那么,这个notify_url该怎么用呢?
4.1.notify_url
1)什么是notify_url
参数中有一个非常重要的,叫做notify_url的:
基于上文的介绍我们知道,这个地址是在支付成功后的异步结果通知。官网介绍如下:
支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。
所以,此处的地址必须是一个外网可访问地址,而且我们要定义好回调的处理接口。
http://api.leyou.com/api/order-service/notify
2)内网穿透
此处我们肯定不能写:http://api.leyou.com/api/order/,这个域名未经备案,是不被识别的。如何才能获取一个能够外网访问的域名呢?
我们可以通过内网穿透来实现,那么什么是内网穿透呢?
简单来说内网穿透的目的是:让外网能访问你本地的应用,例如在外网打开你本地http://127.0.0.1指向的Web站点。
在这里有一篇播客,详细介绍了几种内网穿透策略:一分钟了解内网穿透
这里我们使用一个免费的内网穿透工具:Natapp:NATAPP官网
详细教程在这里:一分钟的natapp快速新手教程
启动后的样子:
比如此处,我使用的natapp得到的域名是:http://ff7hgc.natappfree.cc,并且我设置指向到`127.0.0.1:10010`位置,也就是我的网关服务。
3)配置回调地址
设置内网穿透地址到配置文件application.yml:
1 | ly: |
4)网关白名单
因为异步回调是微信来访问我们的,因此不应该对登录做校验,我们把这个地址配置到白名单,修改ly-gateway中的application.yml
1 | ly: |
然后,将/api/pay映射到订单微服务:
1 | zuul: |
4.2.支付结果通知API
来看官网关于结果通知的介绍:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_7&index=8
应用场景
支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。
对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒)
注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
特别提醒:商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致
,防止数据泄漏导致出现“假通知”,造成资金损失。
支付完成后,微信服务会自动向notify_url
地址发起POST请求,请求参数是xml格式:
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
返回状态码 | return_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL此字段是通信标识,非交易标识,交易是否成功需要查看trade_state来判断 |
返回信息 | return_msg | 是 | String(128) | OK | 当return_code为FAIL时返回信息为错误原因 ,例如签名失败参数格式校验错误 |
通信成功,会返回下面信息:
签名 | sign | 是 | String(32) | C380BEC2BFD.. | 名,详见签名算法 |
---|---|---|---|---|---|
签名类型 | sign_type | 否 | String(32) | HMAC-SHA256 | 签名类型,目前支持HMAC-SHA256和MD5,默认为MD5 |
业务结果 | result_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL |
错误代码 | err_code | 否 | String(32) | SYSTEMERROR | 错误返回的信息描述 |
错误代码描述 | err_code_des | 否 | String(128) | 系统错误 | 错误返回的信息描述 |
用户标识 | openid | 是 | String(128) | wxd930ea54f | 用户在商户appid下的唯一标识 |
交易类型 | trade_type | 是 | String(16) | JSAPI | JSAPI、NATIVE、APP |
订单金额 | total_fee | 是 | Int | 100 | 订单总金额,单位为分 |
现金支付金额 | cash_fee | 是 | Int | 100 | 现金支付金额订单现金支付金额,详见支付金额 |
微信支付订单号 | transaction_id | 是 | String(32) | 121775250120 | 微信支付订单号 |
商户订单号 | out_trade_no | 是 | String(32) | 12123212112 | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。 |
我们需要返回给微信的结果:
1 | <xml> |
4.3.编写回调接口
先分析接口需要的四个数据:
- 请求方式:官方文档虽然没有明说,但是测试得出是POST请求
- 请求路径:我们之前指定的notify_url的路径是:/pay/wx/notify
- 请求参数:是xml格式数据,包括支付的结果和状态
- 返回结果:也是xml,表明是否成功
因为要接收xml格式数据,因此我们需要引入解析xml的依赖:
1 | <dependency> |
然后编写controller:
1 | package com.leyou.order.web; |
因为需要对结果的签名进行验证,所以在PayHelper
中定义一个校验签名的算法:
1 | public void isValidSign(Map<String, String> result) throws Exception { |
另外,支付是否成功,需要校验业务状态才知道,我们在PayHelper
编写一个校验业务状态的方法:
1 | public void checkResultCode(Map<String, String> result) { |
Service 代码:
service中需要完成下列代码;
- 签名校验
- 数据校验
- 订单号码校验
- 订单金额校验
- 更新订单状态
1 |
|
5.支付状态查询
当用户扫码支付成功,会自动调用回调接口,从而修改订单状态,完成订单支付。
但是,页面上并不知道支付是否成功。怎么办?
5.1.页面查询支付状态
因为不知道用户什么时候会支付,也不知道支付有没有成功,因此页面会采用定时任务,不断查询订单支付的状态:
1 | // 开启定时任务,查询付款状态 |
每隔5秒就会查询一次,为了防止用户一直不支付的情况,又设置了一个定时任务,10分钟后跳转到支付失败页面。
5.2.支付状态查询接口
上面的查询请求 分析:
- 请求方式:Get
- 请求路径 :/state/{id}
- 请求参数:订单id
- 返回结果:1或者其它,1代表未支付,其它是已经支付
controller:
1 |
|
service:
1 | public Integer queryPayStatus(Long orderId) { |