微信点餐系统

支付模块

一、第三方SDK支付

支付流程:

image

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 

image

对于在项目中的配置如上,已经配置完成。

支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。

对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 (通知频率为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>
文章目录
  1. 1. 一、第三方SDK支付