您的位置 首页 java

微信支付前后端实现(Vue+Spring Boot)

上一篇文章中总结了支付宝支付前后端实现,本篇将对其竞争对手——微信支付进行详细讲解。其中涉及代码来源于目前正在开发的项目,这个项目涉及PC端、H5移动端及APP三类用户界面,APP基于Flutter开发,前后端目前都由我一人完成,后续将对这个项目中涉及到的技术进行一步步的总结,感兴趣的小伙伴可以关注一下。

1. 微信支付概述

对于线上应用来说,微信支付方式无外乎以下五种:

微信支付前后端实现(Vue+Spring Boot)

  • APP:适用于第三方APP调用微信支付;
  • PC网站:适用于PC网站发起的微信支付,又称Native支付,展示一个二维码页面,供用户通过微信客户端扫描后支付。
  • 微信内浏览器:即通过微信浏览器打开的页面,通过JSAPI进行支付,可以直接打开微信支付进行直接。这种方式适应于通过微信公众号打开的页面,或者是通过微信分享的链接点击后直接在微信内浏览器打开的场景。
  • 小程序:小程序内的支付。
  • 移动端非微信浏览器:通过H5支付方式,可直接跳转微信进行支付。
  • 本文主要讲述PC网站、微信内浏览器及移动端非微信浏览器上的支付实现。

    2. 开发前准备

    微信体系目前比较混乱,据我目前了解的,除QQ之外,微信自己都有三个不同的管理平台:微信公众平台、微信开放平台和微信支付商户平台, 但这三个平台更多的是业务上的区分。我们需要先登录公众平台,申请微信支付,同时配置业务域名、JS接口安全域名等,如图所示:

    微信支付前后端实现(Vue+Spring Boot)

    公众号设置

    然后登录微信支付商户平台,将商户与微信公众号做关联。

    如果要使用移动APP进行支付,需要登录微信开放平台创建应用:

    在此可以创建APP、网站应用、小程序等,同时可以绑定公众平台中的公众号应用。

    微信支付前后端实现(Vue+Spring Boot)

    开放平台配置

    我这个项目当前使用的都是公众平台应用来进行支付的,不需要登录开放平台进行配置;目前正在开发APP,因此也开始在开放平台上申请了一个移动应用,等待腾讯审核。

    配置完后就可以通过公众平台获取到应用id及secret,并在商户平台中获取商户号,在后续的开发中会使用到。

    3. 微信支付简要流程

    一个简单的微信支付流程如下所示(按我的项目来的,实际每个项目的下单流程肯定会不一样,但关于微信支付的部分基本是一致的):

    微信支付前后端实现(Vue+Spring Boot)

    微信支付流程

  • 用户购买商品;
  • 后端生成订单号
  • 订单信息确认,用户下单
  • 后端生成订单,同时调用微信接口生成微信端订单,并返回订单信息给前端
  • 前端根据微信订单信息跳转微信支付页面(或者加载二维码)
  • 用户支付完成
  • 微信端跳转到支付前页面(如未指定重定向页面),同时会推送支付结果给后端应用
  • 4. 支付过程示例图如下所示:

  • 商品选择
  • 微信支付前后端实现(Vue+Spring Boot)

    商品选择

  • 生成订单号并确认
  • 微信支付前后端实现(Vue+Spring Boot)

    订单确认

  • 支付界面(PC)
  • 微信支付前后端实现(Vue+Spring Boot)

    PC扫码支付

  • 支付界面(微信内)
  • 微信支付前后端实现(Vue+Spring Boot)

    微信内支付

    5. 具体实现

    考虑代码量太大影响展示,因此下面会将无关代码隐藏,如果有问题可以私信。

    5.1 前端订单确认与下单

     <template>
        <div class="buy-vip-confirm-page p-1">
            <template v-if="!plan.payMoney">订单已失效或已支付完成</template>
            <template v-else>
                <van-cell-group title="订单信息">
                    <van-cell title="订单号" :value="orderNo" />
                    <van-cell title="订单金额(元)" :value="plan.refillMoney" />
                    <van-cell
                        title="获得T币"
                        :value="plan.payMoney * (plan.isVip ? 1.5 : 1)"
                    />
                </van-cell-group>
    
                <div class="buttons">
                    <van-button type="primary" @click="doBuy" class="mb-1"
                        >确认并支付</van-button
                    >
                    <van-button @click="$goBack()">取消</van-button>
                </div>
            </template>
        </div>
    </template>
    
    <script>
    export default {
        components: {},
        data() {
            return {
                plan: {},
                orderNo: "",
                payChannel: "wx",
            };
        },
        mounted() {
            this.plan = this.$route.query || {};
    
            // 生成订单号
            this.$get("/trade/generate-order-no").then((resp) => {
                this.orderNo = resp;
            });
        },
    
        methods: {
            doBuy() {
                var platform = 0;
                // 0表示PC支付,1表示微信内支付; 2表示MWEB支付
                // 此处是H5端的页面,因此没有0的情况,0的情况的PC端处理
                platform = this.$isWx() ? 1 : 2;
    
                this.$post("/pay/refill", {
                    orderNo: this.orderNo,
                    fee: this.plan.refillMoney,
                    channel: this.payChannel === "wx" ? 2 : 1,
                    tbAmount: this.plan.payMoney,
                    tbSentAmount: this.plan.isVip ? this.plan.payMoney * 0.5 : 0,
                    platform: platform,
                }).then((resp) => {
                    // 生成支付表单成功后跳转支付页面
                    resp.platform = platform;
                    this.$goPath("/user/pay/wx-pay", resp);
                });
            },
        },
    };
    </script>  

    注意isWx是全局的判断是否是微信浏览器的方法,实现如下:

     // 判断是否是微信浏览器 
    Vue.prototype.$isWx = () => {
        let UA = navigator.userAgent.toLocaleLowerCase();
        return UA.indexOf("micromessenger") !== -1;
    }  

    用户在确认订单后点击“发起订单并支付”,将调用后台/pay/refill接口生成订单。

    5.2 后台订单生成

    开发之前需要先下载微信提供的SDK(,然后在MAVEN中进行配置:

     <dependency>
        <groupId>com.github.wxpay</groupId>
        <artifactId>wxpay-sdk</artifactId>
        <version>0.0.3</version>
        <scope>system</scope>
        <systemPath>${project.basedir}/src/main/resources/jar/wxpay-sdk-0.0.3.jar</systemPath>
    </dependency>  

    然后定义WXPayConfigImpl类如下:

     package com.ttcn.front.common.config;
    
    import com.github.wxpay.sdk.WXPayConfig;
    
    import java.io.InputStream;
    
    public class WXPayConfigImpl implements WXPayConfig {
        public WXPayConfigImpl() {
        }
    
        public String getAppID() {
            return "***";
        }
    
        public String getMchID() {
            return "***";
        }
    
        public String getKey() {
            return "***";
        }
    
        public InputStream getCertStream() {
            return null;
        }
    
        public int getHttpConnectTimeoutMs() {
            return 10000;
        }
    
        public int getHttpReadTimeoutMs() {
            return 0;
        }
    }
      

    注意需要将APPID及商户号、Key配置成从公众平台、商户平台中获取的值。

    完成后就可以继续编码了。

    /pay/refill接口实现如下:

     /**
         * 充值
         *
         * @param refill 充值
         * @return 支付相关信息
         */    @PreAuthorize("isAuthenticated()")
        @Post Map ping("/refill")
        public PayDTO refill(@RequestBody @Validated RefillDTO refill) {
            UserDTO user = this.getLoginUserOrThrow();
    
            if (null == tradeService.findByNo(refill.getOrderNo())) {
                // 保存订单
                TradeDTO tradeDTO = new TradeDTO();
                ...
    
                tradeService.save(user, tradeDTO);
            }
    
            // 获取支付二维码
            String  openId  = user.getWxOpenId();
            if (refill.getPlatform().equals(1)) {
                openId = user.getMpOpenId();
            }
            return wxPayService.getPayUrl(openId, refill.getOrderNo(), refill.getFee(), TradeType.REFILL, refill.getPlatform());
        }  

    wxPayService.getPayUrl即用于调用微信接口生成微信端订单,实现如下:

     /**
         * 查询支付页面地址
         *
         * @param platform 0 : WEB端;1: 微信内支付;2: MWEB支付(即移动端非微信内支付)
         * @return 支付页面地址
         */    public PayDTO getPayUrl(String openId, String orderNo, double fee, TradeType tradeType, Integer platform) {
            platform = Optional.ofNullable(platform).orElse(0);
            boolean isJsPay = platform.equals(1);
    
            String ip;
            try {
                ip = InetAddress.getLocalHost().getHostAddress();
            } catch (UnknownHostException e) {
                logger.error("获取ip地址失败", e);
                throw BusinessException.create("生成支付二维码失败,请稍后重试");
            }
            String feeStr = String.format("%.0f", fee * 100D);
    
            Map<String, String> params = MapEnhancer.<String, String>create()
                    .put("body", tradeType.getName())
                    .put("nonce_str", orderNo)
                    .put("out_trade_no", orderNo)
                    .put("total_fee", feeStr)
                    .put("spbill_create_ip", ip)
                    .put("notify_url", notifyUrl)
                    .put("trade_type", isJsPay ? "JSAPI" : (platform == 0 ? "NATIVE" : "MWEB"))
                    .putNotNull("openid", isJsPay ? openId : null)
                    .put("product_id", String.valueOf(tradeType.ordinal()))
                    .get();
    
            if (logger.isDebugEnabled()) {
                logger.debug("微信支付下单参数: {}", params);
            }
    
            Map<String, String> result;
            try {
                result = wxPay.unifiedOrder(params);
            } catch (Exception e) {
                logger.error("生成微信支付二维码失败", e);
                throw BusinessException.create("生成微信支付二维码失败,请稍候重试");
            }
            if (logger.isDebugEnabled()) {
                logger.debug("发送微信支付订单结果: {}", result);
            }
    
            String resultCode = MapUtils.getString(result, "result_code");
            if ("SUCCESS".equals(resultCode)) {
                if (logger.isDebugEnabled()) {
                    logger.debug("发送订单成功");
                }
    
                PayDTO payDTO = new PayDTO();
                payDTO.setFee(fee);
                payDTO.setOrderNo(orderNo);
    
                payDTO.setCodeUrl(MapUtils.getString(result, isJsPay ? "prepay_id" : (platform == 0 ? "code_url" : "mweb_url")));
    
                // 如果是JSPay
                if (isJsPay) {
                    // 需要组装参数并签名
                    // 签名
                    Map<String, String> signParams = new TreeMap<>();
                    signParams.put("appId", config.getAppID());
                    signParams.put("timeStamp", String.valueOf(LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8))));
                    signParams.put("nonceStr", UUID.randomUUID().toString().replaceAll("-", ""));
                    signParams.put("package", "prepay_id=" + MapUtils.getString(result, "prepay_id"));
                    signParams.put("signType", "MD5");
                    try {
                        String sign = WXPayUtil.generateSignature(signParams, config.getKey());
                        signParams.put("paySign", sign);
                    } catch (Exception e) {
                        logger.error("签名失败", e);
                        throw BusinessException.create("签名失败");
                    }
    
                    payDTO.setParams(signParams);
                }
    
                return payDTO;
            }
    
            logger.error("发送微信支付订单失败,返回结果:{}", result);
            throw BusinessException.create("生成微信支付二维码失败,请重试或联系管理员");
        }  

    可以看到上面主要是组装参数然后调用wxPay.unifiedOrder接口生成支付表单;

    涉及的参数如下:

    body商品简单描述 nonce_str随机字符串,长度要求在32位以内 out_trade_no商户系统内部订单号,要求32个字符内,且在同一个商户号下唯一 接收支付结果通知时会包括这个参数,因此可以将通知结果与之前的订单关联上; total_fee订单总金额,单位为分 spbill_create_ip支持IPV4和IPV6两种格式的IP地址。用户的客户端IP notify_url异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 需要在微信公众平台中配置相关域名,否则会报异常 trade_type交易类型,JSAPI/NATIVE/APP/MWEB等 JSAPI用于微信内浏览器打开的界面支付 Native用于PC端支付 APP用于单独的APP应用中进行的支付 MWEB用于H5在非微信浏览器中打开的支付 openidtrade_type=JSAPI时(即JSAPI支付),此参数必传,此参数为微信用户在商户对应appid下的唯一标识。 注意只有在微信浏览器支付中才传输该值,其它的不要传,否则会报异常 product_idtrade_type=NATIVE时,此参数必传。此参数为二维码中包含的商品ID,商户自行定义。

    其它参数请参考:

    生成支付表单后,注意如果是JSAPI(也就是微信内浏览器打开的场景)需要进行签名,代码参考上面。

    生成的支付表单示例如下(PC):

      {nonce_str=HCF8vr2sG5XnAKFY, code_url=weixin://wxpay/bizpayurl?pr=VIarG6Jzz, appid=**, sign=***, trade_type=NATIVE, return_msg=OK, result_code=SUCCESS, mch_id=1501105441, return_code=SUCCESS, prepay_id=wx2020212657557887ba533cfa23a2be0000}  

    5.3 前端跳转支付界面

    在5.1中调用/pay/refill接口并返回后,会带上返回的支付表单信息跳转到新的界面:

     this.$post("/pay/refill", {
                    ...
    }).then((resp) => {
        resp.platform = platform;
        this.$goPath("/user/pay/wx-pay", resp);
    });  

    跳转后的wx-pay界面实现如下:

     <template>
        <div class="wx-pay-page">
            <div class="code-image p-1 mt-2">
                <div class="bottom">
                    请确认支付已完成,如有异议,请在<span
                        @click="$goPath('/feedback')"
                        class="underline"
                        >服务中心</span
                    >中进行反馈
                </div>
            </div>
        </div>
    </template>
    
     <script>
    export default {
        components: {},
        props: [],
        data() {
            return {
                getResultInterval: null,
                payDialogVisible: false,
                orderNo: null,
                isWx: false,
                payInfo: null,
                params: null,
            };
        },
        mounted() {
            this.isWx = this.$isWx();
            this.orderNo = this.$route.query.orderNo;
            this.payInfo = this.$route.query.codeUrl;
    
            // 支付方式,1:微信内支付,2:MWEB支付
            this.platform = this.$route.query.platform;
            this.params = this.$route.query.params;
    
            this.doPay();
        },
        destroyed() {
            if (this.getResultInterval) {
                clearInterval(this.getResultInterval);
            }
        },
        methods: {
            doPay() {
                if (this.platform === 1 || this.platform === "1") {
                    // 微信内支付
                    if (typeof WeixinJSBridge == "undefined") {
                        if (document.addEventListener) {
                            document.addEventListener(
                                "WeixinJSBridgeReady",
                                this.onBridgeReady,
                                false
                            );
                        } else if (document.attachEvent) {
                            document.attachEvent(
                                "WeixinJSBridgeReady",
                                this.onBridgeReady
                            );
                            document.attachEvent(
                                "onWeixinJSBridgeReady",
                                this.onBridgeReady
                            );
                        }
                    } else {
                        this.onBridgeReady();
                    }
                } else {
                     // 非微信内支付(MWEB)
                    var url = this.payInfo;
                    window.open(url, "_self");
                }
            },
    
            onBridgeReady() {
                let _this = this;
    
                window.WeixinJSBridge.invoke(
                    "getBrandWCPayRequest",
                    this.params,
                    function (res) {
                        if (res.err_msg == "get_brand_wcpay_request:ok") {
                            // 使用以上方式判断前端返回,微信团队郑重提示:
                            //res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
                            alert(
                                "支付成功,您可以在账户中心/消费记录中查看历史订单"
                            );
                            _this.$goPath("/user");
                        }
                    }
                );
            },
        },
    };
    </script>   

    对于微信内支付,可以通过其浏览器的WeixinJSBridge对象调起微信支付界面。

    非微信浏览器,将表单中codeURL(二维码)加载到页面中即可。 (目前我的项目代码在移动端非微信浏览器中展示的仍旧是二维码,暂未做改造,所以上面非微信浏览器的与PC端处理基本一样,后续会对这部分进行改造)

    到此就等用户支付完成即可。

    5.4 接收支付结果

    当用户支付完成后,会跳转到支付前的页面,这个时候可以在这个页面中做一些操作,来查询订单状态并展示给用户。

    在5.2生成订单的参数中,我们指定了notify_url,那么在支付成功后微信也会同时往这个所配的地址推送支付结果,代码实现如下:

     /**
         * 接收微信支付结果通知
         *
         * @param body 微信支付结果
         */@PostMapping("wx-notify")
    public String wxNotify(@RequestBody String body) {
        wxPayService.parseAndSaveTradeResult(body);
        return "success";
    }  
     /**
         * 支付结果解析
         */public void parseAndSaveTradeResult(String body) {
        try {
            if (logger.isDebugEnabled()) {
                logger.debug("接收到微信支付结果通知: {}", body);
            }
    
            Map<String, String> map = WXPayUtil.xmlToMap(body);
            String tradeNo = MapUtils.getString(map, "out_trade_no");
            if (StringUtils.isEmpty(tradeNo)) {
                logger.warn("微信通知消息中订单号为空");
                return;
            }
    
            TradeDTO trade = tradeService.findByNo(tradeNo);
            if (null == trade) {
                logger.warn("交易不存在,通知消息:{}", body);
                return;
            }
    
            if (trade.getState() == 1) {
                if (logger.isDebugEnabled()) {
                    logger.debug("订单已成功: {}", body);
                }
    
                return;
            }
    
            String result = MapUtils.getString(map, "result_code", "");
            if ("SUCCESS".equals(result)) {
                // 支付成功
                tradeService.tradeSuccess(trade);
            } else {
                logger.warn("支付失败,返回消息:{}", body);
    
                tradeService.tradeFailed(trade);
            }
        } catch (Exception e) {
            logger.error("返回结果:{}", body);
            logger.error("XML转换成Map异常", e);
        }
    }  

    接收到消息后更新订单状态,并进行其它一些如账户余额修改等处理。

    文章来源:智云一二三科技

    文章标题:微信支付前后端实现(Vue+Spring Boot)

    文章地址:https://www.zhihuclub.com/188123.shtml

    关于作者: 智云科技

    热门文章

    网站地图