文章目录
1. 微信支付
商户系统和微信支付系统主要交互:
1、商户server调用统一下单接口请求订单,api参见公共api【统一下单API】
2、商户server接收支付通知,api参见公共api【支付结果通知API】
3、商户server查询支付结果,api参见公共api【查询订单API】
问题1:用户下单到完成支付的执行流程?
1、下单:下的是支付订单,需要在业务订单生成后(会生成业务订单号)将业务订单号提供给支付订单,选择性地将部分业务信息提供给支付订单,签名后提交给微信服务器,如此完成了支付的下单步骤。
2、支付确认:用户支付成功/失败微信都会将信息同步给服务器,服务器收到微信传回的信息需要验证签名并更新订单状态。
用户下单到完成支付的流程:调用微信支付系统的统一下单API生成预付单,并返回预付单信息:
① 商户后台系统会生成二维码,微信支付用户扫描二维码打开商户的H5页面,
② 微信客户端网页内请求生成支付订单,商户后台系统生成支付订单,调用统一下单API
③ 微信支付系统生成预付单,返回预付单信息(prepay_id)
④ 商户后台系统生成JSAPI页面调用的支付参数并签名,将支付参数返回为微信客户端
⑤ 微信支付用户发起支付后, 微信支付系统检查参数合法性和授权权限,返回验证结果并要求授权
⑥ 用户输入密码,确认支付,提交支付授权,微信支付系统验证授权,异步通知商户支付结果
⑦ 微信支付系统向微信客户端返回微信支付结果,并发消息提示
问题2:用户下单成功但是没有接受到支付怎么处理?
3、实现支付订单的查询:
查询订单应用场景:查询订单接口提供所有微信支付订单的查询,商户可以通过查询订单接口主动查询订单状态,完成下一步的业务逻辑,需要调用查询接口的情况:
◆ 当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知;
◆ 调用支付接口后,返回系统错误或未知交易状态情况;
① 微信支付系统一般会异步通知商户后台系统支付结果,即商户被动的接收微信服务器传回的支付通知
② 如果用户下单成功后,商户迟迟没有收到维信传回的消息,商户后台系统可以主动调用微信支付系统的查询订单API,查询支付结果,在网络环境不出现异常的情况下,该步骤是可以省略的。
③ 微信支付系统就会将支付结果返回给微信客户端
问题3:微信支付重复支付?
4、进行支付订单的关闭操作,确保完成整个支付流程的闭环:
我们看一下微信的官方解释:以下情况需要调用关单接口:商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。注意:订单生成后不能马上调用关单接口,最短调用时间间隔为5分钟。
1.1 调用统一下单接口–生成预付单
统一下单应用场景:户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、JSAPI(微信扫码支付)、APP等不同场景生成交易串发起支付。
① 商户server调用微信支付系统的统一下单接口,微信支付系统生成预支付订单后返回预付单信息
② 商户server生成JSAPI页面调用的支付参数并签名,并向微信客户端返回支付参数(prepay_id,paySign等信息)
统一下单API的请求参数:
字段名 | 变量名 | 必填 |
---|---|---|
公众账号ID | appid | 是 |
商户号 | mch_id | 是 |
随机字符串 | nonce_str | 是 |
签名 | sign | 是 |
商品描述 | body | 是 |
商户订单号 | out_trade_no | 是 |
标价金额 | total_fee | 是 |
终端IP | spbill_create_ip | 是 |
通知地址 | notify_url | 是 |
交易类型 | trade_type | 是 |
用户标识 | openid | 否 |
上面的参数将会分为两个部分传给统一下单接口,发起支付:
① 根据业务订单orderId
查询出来的订单和商品信息:openid、orderAmount、orderId、orderName、paytype;r在PayService接口中将订单和商品信息封装为payRequest对象传给bestPayService.pay(payRequest);
② 业务订单不包含但是下单接口需要的请求参数:通过配置文件和配置类封装
-
首先通过 WechatAccountConfig ,将配置文件中配置的请求参数和该类的属性绑定在一起
-
在WechatPayConfig中,将配置文件中的属性封装为wxPayH5Config对象,然后传给
bestPayService.setWxPayH5Config(wxPayH5Config())
③ 最后在BestPayService
类中调用wxPayService.pay(request)
,并在该方法内将上面的参数信息封装为WxPayUnifiedorderRequest
对象,统一下单发起支付;
1.1.1 配置信息的封装
统一下单接口API:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
微信支付秘钥信息:https://git.imooc.com/coding-117/coding-117/src/develop/doc/%e5%be%ae%e4%bf%a1%e6%94%af%e4%bb%98%e5%af%86%e9%92%a5%e4%bf%a1%e6%81%af.md
① 在application.properties文件中配置支付所需要的请求参数:
# 公众账号ID
wechat.mp-app-id=wxd898fcb01713c658
# 公众账号秘钥
wechat.mp-app-secret=46088f58a30ffb26b12d921735d26691
# 商户号
wechat.mch-id=1483469312
# 商户秘钥
wechat.mch-key=098F6BCD4621D373CADE4E832627B4F6
# 商户证书路径
wechat.key-path=G:\\imooc_springboot_wechat\\certificate\\h5.p12
# 微信支付异步通知
wechat.notify-url=http://heng.nat300.top/sell/pay/notify
# 开放平台id
wechat.open-app-id=wx6ad144e54af67d87
# 开放平台秘钥
wechat.open-app-secret=91a2ff6d38a2bbccfb7e9f9079108e2e
② 将配置文件中的参数和配置类中的绑定在一起:
@Data
@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatAccountConfig {
//公众号appid
private String mpAppId;
//公众号appSecret
private String mpAppSecret;
//商户号
private String mchId;
//商户秘钥
private String mchKey;
//商户证书路径
private String keyPath;
//微信支付异步通知
private String notifyUrl;
//开放平台id
private String openAppId;
//开放平台秘钥
private String openAppSecret;
}
③ 利用WxPayH5Config封装配置文件中的配置信息,传给bestPayService.setWxPayH5Config(wxPayH5Config())
文档:https://github.com/Pay-Group/best-pay-sdk/blob/develop/doc/use.md
@Configuration
public class WechatPayConfig {
@Autowired
private WechatAccountConfig accountConfig;
//将bestPayService交给Spring容器来管理
@Bean
public BestPayServiceImpl bestPayService(){
BestPayServiceImpl bestPayService = new BestPayServiceImpl();
bestPayService.setWxPayH5Config(wxPayH5Config());
return bestPayService;
}
//将这个对象交给Spring容器来管理
@Bean
public WxPayH5Config wxPayH5Config(){
WxPayH5Config wxPayH5Config = new WxPayH5Config();
wxPayH5Config.setAppId(accountConfig.getMpAppId());
wxPayH5Config.setAppSecret(accountConfig.getMpAppSecret());
wxPayH5Config.setMchId(accountConfig.getMchId());
wxPayH5Config.setMchKey(accountConfig.getMchKey());
wxPayH5Config.setKeyPath(accountConfig.getKeyPath());
wxPayH5Config.setNotifyUrl(accountConfig.getNotifyUrl());
return wxPayH5Config;
}
}
1.1.2 PayService调用统一下单接口
在PayService中创建支付订单,然后调用统一下单接口发起支付
@Slf4j
@Service
public class PayServiceImpl implements PayService {
//封装了的方法,也是很关键的方法
@Autowired
private BestPayServiceImpl bestPayService;
private static final String ORDER_NAME="微信购物订单";
//创建一个支付订单,注意是支付订单,不是业务订单
@Override
public PayResponse create(OrderDTO orderDTO) {
//封装支付请求参数
PayRequest payRequest = new PayRequest();
//openid
payRequest.setOpenid(orderDTO.getBuyerOpenid());
//订单总金额OrderAmount
payRequest.setOrderAmount(orderDTO.getOrderAmount().doubleValue());
//订单id
payRequest.setOrderId(orderDTO.getOrderId());
//订单名称
payRequest.setOrderName(ORDER_NAME);
//支付类型,代表微信公众号支付
payRequest.setPayTypeEnum(BestPayTypeEnum.WXPAY_H5);
//调用支付接口发起支付
bestPayService.pay(payRequest);
log.info("【微信支付】request = {}", JsonUtil.toJson(payRequest));
//支付后的响应结果
PayResponse payResponse = bestPayService.pay(payRequest);
log.info("【微信支付】response】= {}",JsonUtil.toJson(payResponse));
return payResponse;
}
}
1.1.3 PayController
需要传入两个参数:一个是orderId,用来查询业务订单信息,并封装为支付订单,另一个是支付成功后需要跳转的订单详情页面
@Controller
@RequestMapping("/pay")
public class PayController {
//订单业务层接口
@Autowired
private OrderService orderService;
//支付业务层接口
@Autowired
private PayService payService;
//创建订单
@GetMapping("/create")
public ModelAndView create(@RequestParam("orderId")String orderId,
@RequestParam("returnUrl")String returnUrl,
Map<String,Object> map) throws UnsupportedEncodingException {
//1.根据订单id查询订单
OrderDTO orderDTO = orderService.findOnes(orderId);
if(orderDTO==null){
throw new SellException(ResultEnum.ORDER_NOT_EXIST);
}
//2. 发起支付
PayResponse payResponse = payService.create(orderDTO);
map.put("payResponse",payResponse);
map.put("returnUrl",returnUrl);
//跳转到模板引擎页面create.ftlh
return new ModelAndView("pay/create",map);
}
}
1.1.4 测试生成预付单并返回支付参数
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
class PayServiceImplTest {
@Autowired
private OrderService orderService;
@Autowired
private PayService payService;
@Test
void create() {
OrderDTO orderDTO = orderService.findOnes("1234578");
payService.create(orderDTO);
}
}
测试结果:
2020-08-04 11:38:08,815 - 【微信支付】request = {
"payTypeEnum": "WXPAY_H5",
"orderId": "1234578",
"orderAmount": 2.4,
"orderName": "微信购物订单",
"openid": "oTgZpwenC6lwO2eTDDf_-UYyFtqI"
}
2020-08-04 11:38:09,085 - 【微信支付】response】= {
"appId": "wxd898fcb01713c658",
"timeStamp": "1596512289",
"nonceStr": "qfMjhWSuB05DzQKv",
"packAge": "prepay_id\u003dwx04113820473075f8f7d7b9e31376781400",
"signType": "MD5",
"paySign": "61D6E03FC2424441DCF454C7339180FC"
}
1.2 统一下单接口—微信内H5调起支付
微信内h5调起支付:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6
在微信浏览器里面打开H5网页中执行JS调起支付。接口输入输出数据格式为JSON。
注意:WeixinJSBridge内置对象在其他浏览器中无效。
getBrandWCPayRequest参数以及返回值定义:
① 网页端接口请求参数列表**(参数需要重新进行签名计算,参与签名的参数为:appId、timeStamp、nonceStr、package、signType,参数区分大小写。)**
名称 | 变量名 | 必填 | 类型 | 描述 |
---|---|---|---|---|
公众号id | appId | 是 | String(16) | 商户注册具有支付权限的公众号成功后即可获得 |
时间戳 | timeStamp | 是 | String(32) | 当前的时间,其他详见时间戳规则 |
随机字符串 | nonceStr | 是 | String(32) | 随机字符串,不长于32位。推荐随机数生成算法 |
订单详情扩展字符串 | package | 是 | String(128) | 统一下单接口返回的prepay_id参数值,提交格式如:prepay_id=*** |
签名方式 | signType | 是 | String(32) | 签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致 |
签名 | paySign | 是 | String(64) | 签名,详见签名生成算法 |
② 返回结果值说明
返回值 | 描述 |
---|---|
get_brand_wcpay_request:ok | 支付成功 |
get_brand_wcpay_request:cancel | 支付过程中用户取消 |
get_brand_wcpay_request:fail | 支付失败 |
调用支付JSAPI缺少参数:total_fee | 1、请检查预支付会话标识prepay_id是否已失效 2、请求的appid与下单接口的appid是否一致 |
下面是PayController
类中跳转的微信H5网页发起支付页面:create.ftlh
,在这个页面中发起支付,支付完成后将跳转到订单详情页面returnUrl
<script>
function onBridgeReady(){
//动态的从ModelAndView中提取
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
//公众号名称,由商户传入
"appId": "${payResponse.appId}",
//时间戳,自1970年以来的秒数
"timeStamp": "${payResponse.timeStamp}",
//随机串
"nonceStr": "${payResponse.nonceStr}",
"package":"${payResponse.packAge}",
//微信签名方式
"signType": "MD5",
//微信签名
"paySign": "${payResponse.paySign}"
},
function(res){
// 在用户支付成功后跳转的通知地址
location.href="${returnUrl}";
});
}
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady();
}
</script>
1.3 用户下单到支付总流程
① 首先用户在微信客户端访问下面这个请求连接:该请求连接中需要传入openid
、orderId
、returnURL
http://proxy.springboot.cn/pay?openid=oTgZpwenC6lwO2eTDDf_-UYyFtqI&orderId=12345890&returnUrl=http://www.imooc.com
② 当我们访问上面的链接时就会自动跳回到下面的链接,访问PayController
类中的create()
方法,创建预支付订单,并将预支付订单信息通过ModelAndView
传给create.ftlh
页面
http://heng.nat300.top/sell/pay/create?openid=oTgZpwenC6lwO2eTDDf_-UYyFtqI&orderId=12345890&returnUrl=http://www.imooc.com
③ 在create.ftlh
页面发起支付,支付成功后点击完成跳转到renturnURL
对应的订单详情页地址
需要注意的是:同一个业务订单不能创建两次支付订单,同一个支付订单不能重复支付,否则会报下满两个错误
java.lang.RuntimeException: 【微信统一支付】发起支付, resultCode != SUCCESS, err_code = INVALID_REQUEST err_code_des=201 商户订单号重复
java.lang.RuntimeException: 【微信统一支付】发起支付, resultCode != SUCCESS, err_code = ORDERPAID err_code_des=该订单已支付
控制台输出的日志:
2020-08-04 13:09:24,133 - 【微信支付】request = {
"payTypeEnum": "WXPAY_H5",
"orderId": "12345890",
"orderAmount": 0.01,
"orderName": "微信购物订单",
"openid": "oTgZpwenC6lwO2eTDDf_-UYyFtqI"
}
2020-08-04 13:09:24,379 - 【微信支付】response】= {
"appId": "wxd898fcb01713c658",
"timeStamp": "1596517764",
"nonceStr": "ruvUKmQiksofX79M",
"packAge": "prepay_id\u003dwx0413093581507588decc69891653366600",
"signType": "MD5",
"paySign": "5D47C3CBF2CB8745895DF414AF19985A"
}
支付结果:
说明:支付成功后微信支付系统向微信客户端返回微信支付结果,并发消息提示,其实我们点击完成后应该跳转到订详情页面的,但是因为公众号是借用的,因此暂时先跳转到慕课页面
从支付成功后跳转的支付详情页面可以看到,明明已经支付成功了,但是订单状态显示的仍然是待支付状态,这是因为支付完成后,我们并没有修改订单状态,因此需要发送一条通知异步通知商户支付结果
1.4 异步通知商户支付结果
当用户支付成功后,微信支付系统需要异步通知商户支付结果:
1.4.1 PayController调用异步通知接口
因为我们在请求参数中设置了异步接收微信支付结果通知的回调地址,该通知url必须为外网可访问的url,不能携带参数。
wechat.notify-url=http://heng.nat300.top/sell/pay/notify
因此写个接口路径为:/sell/pay/notify
@Controller
@RequestMapping("/pay")
public class PayController {
@Autowired
private OrderService orderService;
@Autowired
private PayService payService;
// 微信异步通知
@PostMapping("/notify")
public ModelAndView notify(@RequestBody String notifyData) {
payService.notify(notifyData);
}
}
1.4.2 PayServiceImpl
当用户支付成功后,微信支付系统回会异步通知商户订单支付结果从而商户后天系统需要修改订单的支付状态,将payStatus
从0
改为1
:
@Service
@Slf4j
public class PayServiceImpl implements PayService {
@Autowired
private BestPayServiceImpl bestPayService;
@Autowired
private OrderService orderService;
//异步通知
@Override
public PayResponse notify(String notifyData) {
//异步通知响应结果
PayResponse payResponse = bestPayService.asyncNotify(notifyData);
log.info("【微信支付】异步通知, payResponse={}", JsonUtil.toJson(payResponse));
//查询订单
OrderDTO orderDTO = orderService.findOne(payResponse.getOrderId());
//判断订单是否存在
if (orderDTO == null) {
log.error("【微信支付】异步通知, 订单不存在, orderId={}", payResponse.getOrderId());
throw new SellException(ResultEnum.ORDER_NOT_EXIST);
}
//判断金额是否一致(0.10 0.1)
if (!MathUtil.equals(payResponse.getOrderAmount(), orderDTO.getOrderAmount().doubleValue())) {
log.error("【微信支付】异步通知, 订单金额不一致, orderId={}, 微信通知金额={}, 系统金额={}",
payResponse.getOrderId(),
payResponse.getOrderAmount(),
orderDTO.getOrderAmount());
throw new SellException(ResultEnum.WXPAY_NOTIFY_MONEY_VERIFY_ERROR);
}
//修改订单的支付状态
orderService.paid(orderDTO);
return payResponse;
}
}
继续访问统一下单接口并完成支付:
http://proxy.springboot.cn/pay?openid=oTgZpwenC6lwO2eTDDf_-UYyFtqI&orderId=12345891&returnUrl=http://www.imooc.com
支付前订单支付状态为0:
支付后订单支付装填为1:
控制台打印日志结果:
2020-08-04 15:15:15,102 - 【微信支付】异步通知, payResponse={
"orderAmount": 0.01,
"orderId": "12345899",
"outTradeNo": "4200000706202008041722824040"
}
2020-08-04 15:15:15,140 - 【订单支付完成】订单支付状态不正确,orderDTO=OrderDTO(orderId=12345899, buyerName=师兄, buyerPhone=13756235489, buyerAddress=北京, buyerOpenid=oTgZpwenC6lwO2eTDDf_-UYyFtqI, orderAmount=0.01, orderStatus=0, payStatus=1, createTime=2020-08-03 18:22:43.0, updateTime=2020-08-03 22:06:38.0, orderDetailList=[OrderDetail(detailId=22222227, orderId=12345899, productId=2, productName=西瓜, productPrice=0.01, productQuantity=2, productIcon=http://chhahh.jps)])
com.hh.exception.SellException: 订单支付状态不正确
总结:当用户支付成功后,微信支付系统回会异步通知商户订单支付结果从而商户后天系统需要修改订单的支付状态,将payStatus
从0
改为1
1.4.3 解决订单支付状态不正确
① 微信支付系统异步通知商户支付结果(用户已经下好单并完成了支付,你处理一下吧)
② 商户就会修改订单的支付状态,商户修改完订单的支付状态后还需要向微信支付系统通知处理结果(我已经将订单状态处理好啦)
返回结果:
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
---|---|---|---|---|---|
返回状态码 | return_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断 |
返回信息 | return_msg | 是 | String(128) | OK | 当return_code为FAIL时返回信息为错误原因 ,例如签名失败参数格式校验错误 |
@Controller
@RequestMapping("/pay")
public class PayController {
@Autowired
private OrderService orderService;
@Autowired
private PayService payService;
// 微信异步通知
@PostMapping("/notify")
public ModelAndView notify(@RequestBody String notifyData) {
payService.notify(notifyData);
//返回给微信处理结果
return new ModelAndView("pay/success");
}
}
向微信返回处理结果:success.ftlh
页面
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
</xml>
2. 微信退款
管方文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
注意:
1、交易时间超过一年的订单无法提交退款
2、微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
3、请求频率限制:150qps,即每秒钟正常的申请退款请求次数不超过150次
错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
4、每个支付订单的部分退款次数不能超过50次
5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
@Slf4j
@Service
public class PayServiceImpl implements PayService {
@Autowired
private BestPayServiceImpl bestPayService;
@Autowired
private OrderService orderService;
/**
* 退款
* @param orderDTO
*/
@Override
public RefundResponse refund(OrderDTO orderDTO) {
RefundRequest refundRequest = new RefundRequest();
refundRequest.setOrderId(orderDTO.getOrderId());
refundRequest.setOrderAmount(orderDTO.getOrderAmount().doubleValue());
refundRequest.setPayTypeEnum(BestPayTypeEnum.WXPAY_H5);
log.info("【微信退款】request={}", JsonUtil.toJson(refundRequest));
RefundResponse refundResponse = bestPayService.refund(refundRequest);
log.info("【微信退款】response={}", JsonUtil.toJson(refundResponse));
return refundResponse;
}
}
证书存放路径:
wechat.key-path=G:\\imooc_springboot_wechat\\certificate\\h5.p12
测试类:
@Test
public void refund() {
OrderDTO orderDTO = orderService.findOne("12345896");
payService.refund(orderDTO);
}
测试结果:
2020-08-04 15:58:30,020 - 【微信退款】request={
"payTypeEnum": "WXPAY_H5",
"orderId": "12345896",
"orderAmount": 0.01
}
java.lang.RuntimeException: 【微信退款】发起退款, returnCode != SUCCESS, returnMsg = 证书已过期