2者區別對比分析「微信公眾平台和微信開放平台的區別」

上一篇文章中總結了支付寶支付前後端實現,本篇將對其競爭對手——微信支付進行詳細講解。其中涉及代碼來源於目前正在開發的項目,這個項目涉及PC端、H5移動端及APP三類用戶界面,APP基於Flutter開發,前後端目前都由我一人完成,後續將對這個項目中涉及到的技術進行一步步的總結,感興趣的小夥伴可以關注一下。

1. 微信支付概述

對於線上應用來說,微信支付方式無外乎以下五種:

微信支付前後端實現(Vue+Spring Boot)

APP:適用於第三方APP調用微信支付;PC網站:適用於PC網站發起的微信支付,又稱Native支付,展示一個二維碼頁面,供用戶通過微信客戶端掃描後支付。微信內瀏覽器:即通過微信瀏覽器打開的頁面,通過JSAPI進行支付,可以直接打開微信支付進行直接。這種方式適應於通過微信公眾號打開的頁面,或者是通過微信分享的鏈接點擊後直接在微信內瀏覽器打開的場景。小程序:小程序內的支付。移動端非微信瀏覽器:通過H5支付方式,可直接跳轉微信進行支付。

本文主要講述PC網站、微信內瀏覽器及移動端非微信瀏覽器上的支付實現。

2. 開發前準備

微信體系目前比較混亂,據我目前了解的,除QQ之外,微信自己都有三個不同的管理平台:微信公眾平台、微信開放平台和微信支付商戶平台, 但這三個平台更多的是業務上的區分。我們需要先登錄公眾平台,申請微信支付,同時配置業務域名、JS接口安全域名等,如圖所示:

微信支付前後端實現(Vue+Spring Boot)

公眾號設置

然後登錄微信支付商戶平台,將商戶與微信公眾號做關聯。

如果要使用移動APP進行支付,需要登錄微信開放平台創建應用:https://open.weixin.qq.com/

在此可以創建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(https://pay.weixin.qq.com/wiki/doc/api/wxpay/ch/pages/sdk.shtml),然後在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()")
    @PostMapping("/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地址。用戶的客戶端IPnotify_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,商戶自行定義。

其它參數請參考:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1

生成支付表單後,注意如果是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);
    }
}

接收到消息後更新訂單狀態,並進行其它一些如賬戶餘額修改等處理。

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/281116.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-21 13:16
下一篇 2024-12-21 13:16

相關推薦

發表回復

登錄後才能評論