支付模块
一、第三方SDK支付
支付流程:
1、按流程所示,在第2步之后,网页请求生成支付订单,调用统一下单API生成预付单信息:
调用第三方SDK如下:
(1)、配置
@Component
public class WechatPayConfig {
@Autowired
private WechatAccountConfig wechatAccountConfig;
@Bean
public BestPayServiceImpl bestPayService(){
BestPayServiceImpl bestPayService = new BestPayServiceImpl();
bestPayService.setWxPayH5Config(wxPayH5Config());
return bestPayService;
}
@Bean
public WxPayH5Config wxPayH5Config() {
WxPayH5Config wxPayH5Config = new WxPayH5Config();
wxPayH5Config.setAppId(wechatAccountConfig.getMpAppId());
wxPayH5Config.setAppSecret(wechatAccountConfig.getMpAppSecret());
wxPayH5Config.setMchKey(wechatAccountConfig.getMchKey());
wxPayH5Config.setMchId(wechatAccountConfig.getMchId());
wxPayH5Config.setKeyPath(wechatAccountConfig.getKeyPath());
wxPayH5Config.setNotifyUrl(wechatAccountConfig.getNotifyUrl());
return wxPayH5Config;
}
}
其中wechatAccountConfig通过注入得到,内容如下:
@Component
@Data
@ConfigurationProperties(prefix = "wechat")
public class WechatAccountConfig {
private String mpAppId;
private String mpAppSecret;
/**
* 商户号
*/
private String mchId;
/**
* 商户密钥
*/
private String mchKey;
/**
* 商户证数路径
*/
private String keyPath;
/**
* 微信支付异步通知地址
*/
private String notifyUrl;
}
当然需要在系统配置文件中需要配置上面的字段:
wechat:
mpAppId: wx0807f8636ccc9325
mpAppSecret: 3cce0fb303517513354b12554013ecaf
#商户信息
mchId: qwerrdfd
mchKey: 2fwsdfgsddss
keyPath: /var/weixin_cert/h5.p12
#异步通知地址
notifyUrl: http://wxorder.natapp1.cc/sell/pay/notify
(2)、如上配置好了之后,需要在payservice中注入配置好了的BestPayServiceImpl:
@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) {
//首先要查询与orderId匹配的订单是否存在
OrderDTO orderDTO = orderService.findOrder(orderId);
if (orderDTO==null){
throw new SellException(ResultEums.ORDER_NOT_EXIT);
}
//发起支付
PayResponse payResponse = payService.create(orderDTO);
//将payResponse返回的预付单信息添加近map,用于动态生成getBrandWCPayRequest,页面调起支付
map.put("payResponse",payResponse);
map.put("returnUrl",returnUrl);
//微信内H5调起支付
return new ModelAndView("pay/create",map);
}
}
在上面微信内H5调起支付支付之前需要得到预付单信息,即通过payService返回的response得到,service代码如下:
@Service
@Slf4j
public class PayServiceImpl implements PayService{
private static final String OREDER_NAME = "微信点餐订单";
//注入在(1)中配置好了的BestPayServiceImpl实现类
@Autowired
private BestPayServiceImpl bestPayService;
@Override
public PayResponse create(OrderDTO orderDTO) {
//1、配置,将service做为bean进行配置。注入的BestPayServiceImpl已经完成配置
PayRequest payRequest = new PayRequest();
payRequest.setOrderId(orderDTO.getOrderId());
payRequest.setOpenid(orderDTO.getBuyerOpenid());
payRequest.setOrderAmount(orderDTO.getOrderAmount().doubleValue());
payRequest.setOrderName(OREDER_NAME);
//设置支付方式
log.info("【微信支付】 payRequest={}",JsonUtil.object2Json(payRequest));
payRequest.setPayTypeEnum(BestPayTypeEnum.WXPAY_H5);
//调用统一下单API,生成预付单信息,获取到prepay_id
PayResponse payResponse = bestPayService.pay(payRequest);
log.info("【微信支付】 response={}", JsonUtil.object2Json(payResponse));
//唤起支付
return payResponse;
}
}
(3)、根据统一下单API返回的预付单信息payResponse动态生成的onBridgeReady,即Conreoller中的ModelAndView create.ftl:
<script>
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":"${payResponse.appId}", //公众号名称,由商户传入
"timeStamp":"${payResponse.timeStamp}", //时间戳,自1970年以来的秒数
"nonceStr":"${payResponse.nonceStr}", //随机串
"package":"${payResponse.package}",
"signType":"MD5", //微信签名方式:
"paySign":"${payResponse.paySign}" //微信签名
},
function(res){
//if(res.err_msg == "get_brand_wcpay_request:ok" ) {} // //使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回
//ok,但并不保证它绝对可靠。
location.href="${payResponse.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>
(4)、在支付成功后重新请求到returnUrl--------location.href="${payResponse.returnUrl}"
在前台页面配置文件中需要配置: wechatPayUrl: 'http://wxorder.natapp1.cc/sell/pay/create'
module.exports = {
build: {
env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
productionSourceMap: false,
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
port: 9000,
sellUrl: 'http://sell.com',
openidUrl: 'http://wxorder.natapp1.cc/sell/wechat/authorize',
wechatPayUrl: 'http://wxorder.natapp1.cc/sell/pay/create'
},
需改完成之后需要重新构建前台项目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| build config data.json dist index.html LICENSE node_modules package.json prod.server.js README.md src static [root@localhost sell_fe_buyer]# cd config/ [root@localhost config]# ls dev.env.js index.js prod.env.js test.env.js [root@localhost config]# vim index.js [root@localhost config]# ls dev.env.js index.js prod.env.js test.env.js [root@localhost config]# npm run build
> sell@1.0.0 build /opt/code/sell_fe_buyer > node build/build.js
Tip: Built files are meant to be served over an HTTP server. Opening index.html over file:// won't work.
Hash: 1dcb99e7e52120a718fd Version: webpack 1.15.0 Time: 10929ms Asset Size Chunks Chunk Names static/js/manifest.f13f2efeee0c038fa8f6.js 819 bytes 0 [emitted] manifest static/js/vendor.e8bcf9a796b8d5dbca42.js 155 kB 1, 0 [emitted] vendor static/js/app.6fb014b91af2bc6acd56.js 42.4 kB 2, 0 [emitted] app static/css/app.fc95ecb15d823cfb3172a7b93aaa741d.css 231 kB 2, 0 [emitted] app index.html 616 bytes [emitted] [root@localhost config]# cd .. [root@localhost sell_fe_buyer]# ls build config data.json dist index.html LICENSE node_modules package.json prod.server.js README.md src static [root@localhost sell_fe_buyer]# cp -r dist/* /opt/data/wwwroot/sell/ cp: overwrite ‘/opt/data/wwwroot/sell/index.html’? y cp: overwrite ‘/opt/data/wwwroot/sell/static/css/reset.css’? y cp: overwrite ‘/opt/data/wwwroot/sell/static/css/app.fc95ecb15d823cfb3172a7b93aaa741d.css’? y cp: overwrite ‘/opt/data/wwwroot/sell/static/js/vendor.e8bcf9a796b8d5dbca42.js’? y [root@localhost sell_fe_buyer]# ls build config data.json dist index.html LICENSE node_modules package.json prod.server.js README.md src static
|
2、支付成功之后还需要修改数据库的支付状态,但是需改订单支付状态的依据不能是getBrandWCPayRequest的返回值get_brand_wcpay_request:ok。而是以后端的异步通知。
(1)、返回异步通知,需要提前配置设置异步通知的Url,该url在统一下单API中作为参数传给微信后台:
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
对于在项目中的配置如上,已经配置完成。
支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。
对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒)
注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
特别提醒:商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。
代码实现:
对于Controller:
/**
* w微信异步通知
* @param notifyData
*/
@RequestMapping("/notify")
public ModelAndView notify(@RequestBody String notifyData) {
payService.notify(notifyData);
//收微信异步通知,更新完订单状态,但是微信后台依然会不停的发送异步通知,调用该代码,所有根据文档要求。处理完异步通知后需要给微信后台返
// 回处理结果。
return new ModelAndView("pay/success");
}
}
实际是调用了 payService.notify(notifyData)方法:
@Autowired
private BestPayServiceImpl bestPayService;
@Autowired
private OrderServiceImpl orderService;
@Override
public PayResponse notify(String notifyData) {
//1、验证签名,SDK已经完成
//2、支付状态验证,SDK已完成
// 3、验证支付金额
//4、支付人验证
// 获取微信后台支付异步通知信息
PayResponse payResponse = bestPayService.asyncNotify(notifyData);
log.info("【微信支付】 异步通知,payResponse={}",JsonUtil.object2Json(payResponse));
//判断订单是否存在
OrderDTO orderDTO = orderService.findOrder(payResponse.getOrderId());
if (orderDTO==null) {
log.error("【微信支付】 异步通知,订单不存在,orderId={}",orderDTO.getOrderId());
}
//验证金额是否一致
if (!MathUtil.equals(orderDTO.getOrderAmount().doubleValue(),payResponse.getOrderAmount())) {
log.error("【微信支付】 异步通知,订单金额不一致,orderId={},微信通知金额={},系统金额={}",
payResponse.getOrderId(),payResponse.getOrderAmount(),
orderDTO.getOrderAmount());
throw new SellException(ResultEums.WECHAT_PAY_AMOUNT_ERROR);
}
//修改订单的支付状态
orderService.pay(orderDTO);
return payResponse;
}
在处理完以异步通知后,需要向微信后台发送处理结果:
商户处理后同步返回给微信参数:
return_code:SUCCESS/FAIL,SUCCESS表示商户接收通知成功并校验成功;
return_msg:返回信息,如非空,为错误原因:签名失败、参数格式校验错误
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
</xml>
在本controller是通过ModelAndView返回:return new ModelAndView("pay/success");
(success.ftl)
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
</xml>