【微信支付】商品支付流程实例

微信支付流程实例

0.准备

三种支付方式的流程梳理可见:微信支付三种常见支付方式流程梳理

微信支付准备过程中的重要参数

  1. 选择【接入模式】
  2. 参数申请:
    • 【AppId】
    • 【商户号 mchid】
    • 完成以上两项参数的申请后,还需要绑定 AppId 与 mchid
  3. 配置【API key】,即 API v3 密钥,用于平台证书的解密、回调信息解密等
  4. 配置商户证书

以上操作均由平台完成,与实际开发步骤无关,但开发过程中可能会使用到这些参数

1. Native 方式

1.1 接入前准备

要接入 Native 方式支付,需要完成第一节中的全部4个步骤。

1.2 Native 方式流程图

在这里插入图片描述
接下来我们就按步骤一步步进行。

1.3 开发实现
1. 前期准备
  1. 首先我们要拿到 AppId,这是我们后续开发过程中的一个重要参数。但这个参数一般不会由前端获取,而是后端获取后,向前端提供一个拿取的接口。所以这里我们直接调用接口拿到即可。

    // 获取微信appId
    async getWxAppId() {
        const res = await wxModel.getWxAppId({
            func: 'getWxAppId',
            id: 2,
        });
        this.appId = res.result.appId;
    },
    
  2. 接下来,获取本平台订单的相关信息展示在页面中,另外,页面的设计必须要遵守微信 Native 方式的设计要求!

    JS

    // 获取订单信息
    async prePlaceOrder() {
     const res = await publicModel.xxxxxxxxx({
         func: 'xxxxxxxxx',
     });
     this.orderInfo = { ...res.result };
    },
    

    HTML

    <div class="member-pay">
        <div class="desc">
            <div class="left">
                <span>有效期至:</span>
                <span>金额:</span>
                <span>收款方:</span>
                <span v-if="orderInfo.firstBuy">优惠券码:</span>
                <span>实际金额:</span>
            </div>
            <div class="right">
                <span>{{ orderInfo.expirationTimeOfMembership }}</span>
                <span class="money">
                    <strong>{{ (orderInfo.price || 0) / 100 }}元</strong>
                </span>
                <span>{{ orderInfo.payee || '' }}</span>
                <span class="code" v-if="orderInfo.firstBuy">
                    <el-input
                              v-model="orderInfo.couponCode"
                              placeholder="请输入优惠券码"
                              size="medium"
                              clearable
                              :disabled="orderInfo.lockCouponCode"
                              style="width: 232px; height: 38px"
                              ></el-input>
                    <span
                          class="coupon-code"
                          :class="{ lockCode: orderInfo.lockCouponCode }"
                          @click="validateCouponCode"
                          >校验</span
                        >
                </span>
                <span class="money">{{ (orderInfo.finallyVipPrice || 0) / 100 }}元</span>
            </div>
     	</div>
     	<div class="code-img">
            <img
                 v-show="!showQR"
                 src="xxx"
                 />
            <VueQr
                   v-cloak
                   v-show="showQR"
                   :correct-level="3"
                   :text="wechatPayUrl"
                   :size="198"
                   :margin="0"
                   />
        </div>
        <div class="btn">
            <LButton
                     class="pay-btn"
                     v-show="!showQR"
                     tip="微信支付"
                     type="stema"
                     @click="placeOrder"
                     />
            <LButton
                     class="pay-btn"
                     v-show="showQR"
                     tip="我已支付"
                     type="stema"
                     @click="checkOrder"
                     />
        </div>
        <div class="refund-paperwork">xxxxxxxxx</div>
    </div>
    

    这里使用了一个vue的二维码插件 vue-qr 来生成微信支付的二维码。(PS: LButton是自己封装的组件,用其他组件库里的一样)

2. 具体流程实现

根据我们之前梳理的 Native 支付流程,接下来需要对用户点击微信支付按钮进行处理。

当用户点击微信支付时,前端需要调用后端接口,后端调用微信平台API,进行下单,并返回预支付交易链接 code_url。这里我们只关注前端的开发实现:

// placeOrder 方法
placeOrder() {
    // 请求后端接口,获取code_url
    const res = await placeOrderInterface({
        // 这里需要传递一些参数
        func: 'placeOrder',
        payMethod: 'NATIVE'	// NATIVE支付方式
        // 还可能会需要传一些业务相关的参数,例如:
        // coupon: xxx,  使用优惠券
        // category: xxx, 商品的类型
        // isActivity: xxx, 是否是平台活动期间的订单
        // ...
    })
    
    /*
    	后端会返回三个重要的参数:
    		1. uuid --- 订单在商户平台中的订单号
    		2. code_url --- 预支付交易链接
    */
    const { uuid, code_url } = res;
    // 先将这两个数据存放在组件中
    this.orderUuid = uuid;
    this.code_url = code_url;
    
    // 展示支付二维码
    this.showQR = true
}

接下来,需要根据我们拿到的 code_url 生成微信支付二维码,这里使用 vue-qr 插件直接生成然后展示出来即可。

同时,在二维码的下方展示一个【我已支付】按钮。

然后,根据梳理的流程,用户需要扫描二维码,在手机上进行支付操作,待用户完成操作后,可以主动点击按钮,查询订单状态,所以这里我们需要准备一个查询订单的方法:

// 调用后端接口,查询订单状态(后端实际上是调取的微信提供的查单API)
async checkOrder = () => {
    const res = await checkOrderInterface({
        func: 'xxxCheckOrder',
        uuid: this.orderUuid,	// 这里的uuid是商户平台内部的订单id
    })
    if(res.code === 0) {
        const { status } = res.result
        // 注意,这里的订单状态码是前后端约定的,与微信平台没有关系
        // 这里我们约定:0 --- 未支付;1 --- 已支付;2 --- 订单已关闭
        
        if(status === 0) {
            // 未支付时,输出提示,但不要关闭弹窗,因为此时并不是真的确定了用户没有支付成功
            // 如果用户想要关闭弹窗重新支付,需要点击【我已支付】下方的【支付遇到问题...】按钮来关闭弹窗
            this.$message({
              message: '订单未支付',
              type: 'warning',
            });
        } else if (status === 1) {
            // 已支付时,首先输出提示,然后关闭弹窗
            this.$message({
              message: this.orderInfo.firstBuy ? '订单已支付' : '续费成功',
              type: 'success',
            });
            this.modalCheckOrder = false;
            // 后续根据业务需求进行下一步处理
            ...
            
        } else if (status === 2) {
            // 订单已关闭时,输出提示,同样不要关闭弹窗
            this.$message({
              message: '订单已关闭',
              type: 'warning',
            });
        }
    } else {
        // 错误处理
        ...
    }
}

这样,就完成了一个 Native 方式的简单 demo。在 Native 方式中,很多工作都是由后端完成的,前端并没有承担很多工作量。

但在实际开发中,由于存在不同的需求,所以可能会需要对具体的支付流程进行各种修改。但 Native 方式内部的核心流程是基本上不会变动的。

2. H5 方式

2.1 接入前准备

在 H5 方式中,除了需要完成我们前面所说的4个步骤,还需要前往微信支付商户平台,开通H5支付。

2.2 H5 方式流程图

在这里插入图片描述

接下来按照流程进行实现。

另外,这里由于H5、JSAPI支付一般是针对移动端,所以后面的具体实现中,都是在 React 中进行的,当然也可以在其它框架中实现,其核心流程是不变的。

2.3 开发实现
1. 前期准备

后端需要准备好 appId 等参数,在调取 H5 支付API时使用。

首先同样需要拿到平台内部的订单数据,展示在页面中:

const index = () => {
    const [orderInfo, setOrderInfo] = useState({})

    const getOrderInfo = async () => {
        const { orderInfo } = await prePlaceOrderInfo()
        setOrderInfo(orderInfo)
    }

    useEffect(() => {
        getOrderInfo()
    }, [])
    
    ...
    
    return (
    	<div className='pay'>
            <Form>
                <FormItem label="商品名称" name="name">
                    {orderInfo.spuName}
                </FormItem>
                <FormItem label="金额" name="price">
                    <span className="price">
                        {orderInfo.price / 100}</span>
                </FormItem>
                <FormItem label="收款方" name="payee">
                    xxxxx公司
                </FormItem>
                ...
            </Form>
            <div className='toPay'>
            	<div className='tip'>该商品一经购买xxxxxxxx</div>
                <div className='finalPrice'>总计{orderInfo.finalPrice / 100}</div>
                <div className='btn' onClick={toPay}>立即购买</div>
            </div>
            ...
        </div>
    )
}
2. 具体流程实现

根据梳理好的流程,接下来,用户点击【立即支付】按钮,商户平台前端发起请求,后端调用微信提供的 H5 下单API,生成预支付订单,并向前端返回一个H5支付中间页链接 h5_url

那么我们前端拿到 h5_url 后,还不能直接跳转,因为相比于 Native 方式,用户是在PC上扫描二维码,然后在手机上完成支付操作,并不存在后续的跳转逻辑。但在 H5 方式中则不同,用户的下单、支付、查单操作均在手机上进行,所以我们需要在用户支付完成后,让其跳转回到我们的页面中。这一点也是 Native 方式与 H5 方式的主要区别。

所以,后续我们需要向 h5_url 中添加一些参数。

const [orderUuid, setOrderUuid] = useState(0)

const toPay = async () => {
    // 调取后端下单接口,在商户平台内部进行下单,同时后端调取微信API生成预付单
    const {h5_url, orderUuid, needPay} = await placeOrder({
        payMethod: 'MWEB',
        // 这里可能会有一些业务相关的参数,比如:
        categroyId: xxx,	// 商品类型id
        orderType: xxx,		// 订单类型
        couponId: xxx,		// 优惠券
        ...,
    })
    
    // 首先把订单id存起来,后续查单时需要用到
    setOrderUuid(orderUuid)
    
    // 判断是否需要支付
    if(needPay) {
        // 需要支付时
        // 1. 准备好重定向链接,这里添加了一个标志位flag,用于标明是h5支付后跳转回来的情况
        const redirect_url = `${window.location.href}?flag=1&orderUuid=${orderUuid}`
        // 2. 向 h5_url 中添加参数
        const toPayUrl = `${h5_url}&redirect_url=${encodeURIComponent(redirect_url)}&date=${Date.now()}`
        // 3. 跳转支付中间页
        window.location.href = toPayUrl
    } else {
        // 不需要支付时,直接进行下一步操作即可
        ...
    }
}

接下来,用户跳转支付中间页,调起微信支付,进行支付操作,此时订单可能处于三种状态:

  1. 未完成/支付失败
  2. 已完成
  3. 已关闭

无论处于哪种状态,一旦用户完成操作后,都会跳转至我们指定的 redirect_url,即,回到我们的前端页面。

接下来的操作,微信的官方文档中给出了标准:

由于设置 redirect_url 后,回跳指定页面的操作可能发生在:

  • 微信支付中间页调起微信收银台后超过5秒。
  • 用户点击“取消支付”或支付完成后点击“完成”按钮。因此无法保证页面回跳时,支付流程已结束,所以商户设置的redirect_url地址不能自动执行查单操作,应让用户去点击按钮触发查单操作。

所以我们需要在跳转回来时弹出一个弹框,供用户主动触发查单操作:

首先准备好弹框,这里是直接用的组件库里弹框

<Dialog
   id="checkOrderDia"
   title="订单支付"
   showCancelButton
   showConfirmButton
   closeOnClickOverlay={false}
   cancelButtonText="支付失败,重新支付"
   confirmButtonText="我已支付"
   show={modalCheckOrder}
   onCancel={repayment}
   onConfirm={checkOrder}
>
    <div style={{ textAlign: "center" }}>请确认微信支付是否已经完成</div>
</Dialog>

接下来,我们需要准备跳转回来后弹框的逻辑,由于是跳转回来的,所以直接加在页面加载的时候就可以了:

// 控制弹框显隐
const [modalCheckOrder, setModalCheckOrder] = useState(false)

// 从路径中获取参数的方法
const getQuery = (url = window.location.search, key?: string) => {
	var query = {}
	var locationSearch = url || window.location.search
	if (locationSearch) {
		locationSearch = locationSearch.replace('?', '')
		var params = locationSearch.split('&')
		params.forEach((param) => {
			var keyValue = param.split('=')
			if(keyValue[0])query[keyValue[0]] = decodeURIComponent(keyValue[1])
		})
	}
	return key ? query[key] : query
}

useEffect(() => {
    getOrderInfo()
    
    // 1. 从链接中获取参数
    const searchUrl = window.location.href.split("?")[1]
    const flag = getQuery(searchUrl, 'flag')
    const orderUuid = getQuery(searchUrl, 'orderUuid')
    
    // 2. 判断是否是跳转回来的情况
    if(flag) {
        // 此时是从h5支付跳转回来的情况,更新订单id
        if(orderUuid) {
            setOrderUuid(orderUuid)
        }
        // 弹框
        setModalCheckOrder(true)
    }
}, [])

这样就完成了弹框的展示,但我们还需要准备两个处理函数,分别对应用户点击【我已支付】和【支付失败,重新支付】按钮的情况:

// 点击我已支付时,调取接口查单
const checkOrder = async () => {
    const res = await checkOrderInterface({ uuid: orderUuid})
    // 这里的订单状态码是前后端共同约定的,与微信平台无关
    // 比如这里:0 --- 订单未支付;1 --- 订单已完成;2 --- 订单支付失败
    if(res.status == 0) {
        // 订单未支付时,输出提示
        this.showToast({ type: 'error', content: '订单未支付'})
    } else if(res.status == 2) {
        // 订单支付失败时,同样,输出提示
        this.showToast({ type: 'error', content: '订单支付失败'})
    } else if(res.status == 1) {
        // 订单支付成功,此时输出提示,然后进行下一步处理
        this.showToast({ type: 'success', content: '订单支付成功'})
        ...
    }
}

// 点击支付失败重新支付
const repayment = () => {
    setModalCheckOrder(false)
    // 这里可以根据业务需求进行不同的处理,一般是重新进入支付页面,或者退出但是保留订单为未支付状态
    ...
}

3. JSAPI 方式

3.1 接入前准备

在 JSAPI 方式中,除了需要完成选择接入模式、申请参数以外,还需要进入微信支付商户平台进行相关配置。

  1. 设置支付授权目录
  2. 设置授权域名

具体的设置要求可见:微信JSAPI方式接入前准备

3.2 JSAPI 方式流程图

在这里插入图片描述

3.3 开发实现
1. 前期准备

与 Native、H5 方式不同,使用 JSAPI 方式时,我们需要提前准备好两个模块:

  • 微信授权模块 — 获取微信授权码
  • 微信支付模块

为什么需要这两个模块?

因为在上面的接入前准备中,我们也提到过,需要配置支付授权目录以及授权域名。但是考虑下面这种情况:商户分为多个端和平台,各端中均需要支持微信支付,那么我们应该如何配置这两项呢?不可能为每一个端的域名都配置一次,所以,我们最好的解决方法就是封装两个独立的模块,拥有独立的域名,拿着这两个模块的域名进行配置,然后各端需要进行微信授权、微信支付时,直接带参数访问这两个模块的域名,然后在这两个模块中实现授权即可。

下面对这两个模块的实现进行说明:

1.1 微信授权模块

由于模块中的内容并不多,所以放在一个 .html 文件中即可。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>微信授权</title>
    </head>

    <body>
        <script>
            ...
        </script>
    </body>

</html>

接下来首先准备好一个对象,用于存放一系列从路由中获取的数据以及方法

const wechatAuth = {
    urlParams: {},
    ...,
}

然后,由于我们从其它页面跳转至该模块时,会携带一些参数,所以我们这里需要一个从url中获取参数的方法:

const wechatAuth = {
    urlParams: {},
    getUrlParams: function() {
        // 先用 location.search 获取url中的参数字符串,例如:'?a=xx&b=xx&c=cc&...'
        // 并用 substring 处理掉最前面的'?'
        let paramStr = loction.search.substring(1)
        let keyValueList = paramStr.split('&')	// 将参数字符串根据&分割得到一个键值对数组
        // 处理参数数组中的各键值对
        for (let i = 0; i < keyValueList.length; i++) {
            // 找到键值对中'='的位置
            let equalIndex = keyValueList[i].indexOf('=')
            if(equalIndex === -1){
                // 找不到等号位置时,直接进行下一轮循环,去处理下一对键值对
                continue
            }
            // 找到等号后,只需要将键值对转换为对象属性即可
            // 等号前的为key
            let key = keyValueList[i].substring(0, equalIndex)
            // 等号后的为value,但需要注意,我们从其它页面中携带参数跳转进来时,参数是经过了 urlencode 处理的!
            let encodedValue = keyValueList[i].substring(equalIndex + 1)
            let decodedValue = decodeURIComponent(encodedValue)
            wechatAuth.urlParams[key] = decodedValue
        }
    }
}

// 进入模块时,从路径上获取参数
wechatAuth.getUrlParams();

后续,我们需要跳转微信授权页面完成授权操作,但是跳转时我们同样需要在路径中携带参数,所以这里先准备一个向路径中添加参数的方法:

const wechatAuth = {
	...,
	appendParamsToUrl: function(url, params) {
		if(params) {
            // 传入的url中可能携带有 wechat_redirect 标识,这里先将其拿出来
            let baseWithSearch = url.split('#')[1] === 'wechat_redirect' ? url.split('#')[0] : url
            let hash = url.split('#')[1]
            // 处理传入的需要向url上添加的参数对象
            for (let key in params) {
                // 拿到参数值
                let attrValue = params[key]
                if(attrValue !== undefined) {
                    // 构建键值对
                    let newParam = key + "=" + attrValue
                    // 检查此时的 baseWithSearch 中是否已经添加了参数
                    if (baseWithSearch.indexOf('?') > 0) {
                        // 若添加了参数,则后续的参数需要用 '&' 连接
                    } else {
                        // 若还没有添加参数,则将当前的参数作为第一个参数通过 '?' 连接到 baseWithSearch 上
                        baseWithSearch += "?" + newParam
                        let hasOldParamReg = new RegExp('^' + key + '=[-%.!~*\'\(\)\\w]*', 'g')
                        // 检查现在的 baseWithSearch 上是不是已经有与当前要添加的参数同名的旧参数
                        if (hasOldParamReg.test(baseWithSearch)) {
                            // 若有,则用新参数替换
                            baseWithSearch = baseWithSearch.replace(hasOldParamReg, newParam)
                        } else {
                            // 没有,则直接用 '&' 连接上去
                            baseWithSearch += "&" + newParam
                        }
                    }
                }
            }
            // 将前面取出的标识加回去
            if (hash === 'wechat_redirect') {
                url = baseWithSearch + '#' + hash;
            } else {
                url = baseWithSearch;
            }
        }
        return url
	}
}

接下来,就可以编写进行授权操作的函数了,该函数中有两种情况:

  1. 此时是完成了授权,从微信授权页中跳转回来的,当前的路径参数中有授权码code字段
  2. 此时是刚从其它页面跳转至该模块,还没有进行授权操作,即当前路径参数中没有授权码code字段

所以,我们需要根据当前路径中有没有code参数,分情况进行处理

const wechatAuth = {
    urlParams: {...},
    ...,
    authorize: function () {
            // 首先拿到我们前面从路径中取出的参数
            const code = wechatAuth.urlParams["code"];
            // appId 是其它页面在微信浏览器中的唯一应用编号,是必须的!
            const appId = wechatAuth.urlParams["appid"];
            const scope = wechatAuth.urlParams["scope"];
            const state = wechatAuth.urlParams["state"];
            /*
                params 是我们从其它页面中跳转进入该模块时携带的各种参数
                由于无法保证各页面跳转时携带的参数都是统一的,所以将这些参数统统放在 params 中。
                同时,其它页面跳转进来时,之所以要携带这些参数,往往是为了从该模块跳转回去时使用
                所以我们也不需要进行处理,只需要在跳转回去时给它携带上即可
            */
            const params = wechatAuth.urlParams["params"];
            // 准备好跳转的 toUrl、baseUrl 以及重定向所需的 redirect_uri
            let baseUrl;
            let redirectUri;
            let toUrl;
            // 此时是否拿到了code
            if (!code) {
                // 情况1 --- 没有拿到code,需要跳转微信授权页,获取code
                // 设置 baseUrl 为微信开发平台授权链接
                baseUrl =
                    "https://open.weixin.qq.com/connect/oauth2/authorize#wechat_redirect";
                // 设置目标链接
                toUrl = wechatAuth.appendParams(baseUrl, {
                    appid: appId,     // appId
                    // 由于现在是去微信平台授权,所以将重定向地址设置为当前模块的域名即可,完成授权后才是跳转其它页面
                    redirect_uri: encodeURIComponent(location.href),
                    response_type: "code",
                    // 这些原来进入模块时携带的参数也要带上,不然等会儿跳转回来的时候参数就丢失了
                    scope: scope,
                    state: state,
                    params: params,
                });
            } else {
                // 情况2 --- 已经拿到了code,需要跳转回其它页面
                // 此时需要将 baseUrl 置为我们进入该模块的页面链接,即我们在params中携带的 origin 字段
                const redirectParams = JSON.parse(params)
                const origin = redirectParams.origin
                // 将 redirectParams 中的 origin 字段删掉
                delete redirectParams.origin
                // 设置跳转链接,此时用我们的origin作为baseUrl
                toUrl = wechatAuth.appendParams(origin, {
                    // 带上授权得到的授权码
                    code: code,
                    // 从其它页面进入该模块时的参数也携带上,不要丢弃了
                    state: state,
                    ...redirectParams
                })
            }
            // 跳转
            location.href = toUrl
        },
}

这样,就实现了一个公共的微信授权模块。

完整代码见:公共微信授权模块demo

1.2 微信支付模块

与授权模块相同,支付模块的内容直接放在一个 .html 文件中即可

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>微信支付</title>
    <!-- 引入axios用于发送请求 -->
	<script src="https://web-common-resource.stem86.com/axios/axios-1.4.0.min.js"></script>
    <!-- 引入微信依赖 -->
	<script src="https://web-common-resource.stem86.com/wechat/weixin-1.6.0.min.js"></script>
	<script>
		...
	</script>
</head>
<body></body>
</html>

首先我们需要引入 axios,以及微信支付的依赖包。

然后,我们需要在模块加载时,调取平台后端接口,获取平台的微信相关配置

function initWxConfig() {
    // 请求接口获取平台微信配置
    axios
        .post(
        "https://api.xxx.com",
        {
            func: "wxConfig",
            url: window.location.href.split("?")[0], // 这里将模块的域名传递过去,用于校验
        },
        {
            headers: {
                "Content-Type": "application/json",
            },
        }
    )
        .then((response) => {
        // 调取微信API添加微信配置信息,这些信息都是由后端调取微信API获得的
        wx.config({
            debug: false, // 是否开启调试模式
            appId: response.data.result.appId, // 必填,唯一应用标识
            signature: response.data.result.signature, // 必填,签名,即微信的认证信息
            timestamp: response.data.result.timestamp, // 必填,生成签名的时间戳
            nonceStr: response.data.result.nonceStr, // 必填,生成签名的随机串
            jsApiList: [
                // 需要调用的JS接口列表
                "chooseWXPay",
            ],
        });
    })
        .catch((err) => {
        console.log(JSON.stringify(err))
    })
}

window.onload = function() {
    initWxConfig()
    ...
}

由于从其它页面跳转至该模块时,会在路径中携带参数,而最后重定向离开时,又需要将参数添加回去,所以我们需要准备一个从路径中获取参数的方法以及一个向路径中添加参数的方法

// 从url中获取指定参数
function getUrlParams(paramKey) {
    let paramStr = location.href.slice("?")[1];
    let searchParams = new URLSearchParams(paramStr);
    // 这里通过 entries() 方法获取 searchParams 对象的迭代器
    // 然后通过 fromEntries() 方法,将 searchParams 这一键值对列表转换为对象
    let result = Object.fromEntries(searchParams.entries());
    return result[paramKey];
}

// 向url中添加参数
function addParams2Url(url, params) {
    // 取出baseUrl
    var baseUrl = url.split("?")[0];
    // 取出后续的参数部分
    var searchParams = new URLSearchParams(url.split("?")[1]);
    // 向 searchParams 键值对列表中添加参数
    for (var key in params) {
        searchParams.set(key, params[key]);
    }
    return baseUrl + "?" + searchParams.toString();
}

然后,从路径中获取其它页面跳转至该模块时携带的参数:

const postParam = {}
const redirectParam = {}

window.onload = function() {
    // 1. 初始化微信模块
    initWxConfig();
    // 2. 从路径中取出参数
    let params = getUrlParams("params"); // 进入该模块时携带的参数
    // 取出调取接口时需要传递的参数
    postParam = JSON.parse(params).postParam;
    // 取出重定向时需要的参数
    redirectParam = JSON.parse(params).redirectParam;
    ...
}

但此时,我们还不能进行下单,因为有一个问题还未解决:我们如何知道当前的下单用户是谁呢?

如果我们不知道是谁下的单,那还怎么调取接口?

所以,为了解决这一问题,我们需要拿 postParam 中的 userTicket 去找后端换取用户 token,先来看实现,然后再解释原因:

// 通过 userTicket 换取用户token
function getTokenByUserTicket(userTicket) {
    axios
        .post(
        "https://api.xxx.com",
        {
            func: "getTokenByUserTicket",
            userTicket: userTicket,
        },
        {
            headers: {
                "Content-Type": "application/json",
            },
        }
    )
        .then(function (response) {
        const token = response.data.result.token;
        return token
    })
        .catch(function (error) {
        console.log(JSON.stringify(error));
    });
}

下面来解释一下原因:

由于平台各端内部都是使用 token 来标识当前使用用户,但当各端互通时,不可能将 token 通过 url 进行传递。所以,在某一端需要跳转至另一个端时,需要将 token 通过平台内部的处理兑换为 ticket,然后通过 url 跳转并携带 ticket。 当另一端接收到 ticket 后,再使用平台内部的相同逻辑将 ticket 兑换为 token,这样不同端之间就能识别到同一个用户了。

这里其它页面跳转时携带 userTicket 以及模块内部拿 userTicket 换取 token 就是该过程的具体体现。

那么,问题解决了,接下来我们就可以拿到用户的下单信息,进行真正的下单操作了:

// 调取后端接口下单
function placeOrder(token, postParam) {
    axios
        .post(
        "https://api.xxx.com",
        {
            func: "placeOrder",
            ...postParam,
            token: token,
        },
        {
            headers: {
                "Content-Type": "application/json",
            },
        }
    )
        .then((response) => {
        if (response.data.code == 0) {
            // 下单成功,进行支付
            payByWeChat(response.data);
        } else {
            // 下单失败,报错,然后跳转回其它页面
            alert(response.data.message);
            payEndRedirectUrl();
        }
    })
        .catch(function (error) {
        console.log(JSON.stringify(error));
    });
}

window.onload = function() {
    ...
    // 下单
	placeOrder(token, postParam)
}

完成下单后,若下单成功,则调取微信支付的方法:

// 微信支付
function payByWeChat({result}) {
    wx.chooseWXPay({
        // 支付签名时间戳,注意微信JS SDK中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
        timestamp: result.timeStamp,
        // 支付签名随机串,不长于 32 位
        nonceStr: result.nonceStr,
        // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
        package: result.package,
        // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
        signType: result.signType,
        // 支付签名
        paySign: result.paySign,
        // 支付成功后的回调函数
        success: function (res) {
            // res.errMsg === 'chooseWXPay:ok'方式判断前端返回, 微信团队郑重提示:
            // res.errMsg将在用户支付成功后返回ok,但并不保证它绝对可靠,切记。
            if (res.errMsg === "chooseWXPay:ok") {
                alert("支付成功");
                // 支付成功后,将支付状态置为1,并进行后续跳转
                payStatus = 1;
                payEndDoRedirect();
            }
        },
        // 接口调用完成时执行的回调函数,无论成功或失败都会执行
        complete: function (res) {
            // 进行收尾工作
            ...
            // 完成后,进行后续跳转
            payEndDoRedirect();
            /**
             * iOS和Android支付成功点击“完成”后都会进入success和complete函数,都返回'chooseWXPay:ok'
             * 但也有人说Android支付成功不进入success函数,
             * 原因是【iOS和Android返回数据不同。支付成功后Android返回 {"errMsg":"getBrandWCPayRequest:ok"},iOS返回{"err_Info":"success","errMsg":"chooseWXPay:ok"},故Android找不到success方法,导致失败】
             * */
        },
        // 支付取消回调函数
        cancel: function (res) {
            alert("用户取消支付");
            // 支付取消后,进行后续跳转
            payEndDoRedirect();
        },
        // 支付失败回调函数
        fail: function (res) {
            alert("接口调用失败");
            // 失败后,进行后续跳转
            payEndDoRedirect();
        },
    });
}

接下来,页面会调起微信支付页面,用户完成操作后,可能会有以下几种情况:

  • 支付成功
  • 支付失败
  • 支付取消

这三种情况均需要我们配置相应的回调函数。

另外,在支付的调用完成后,还会触发一个【接口调用完成】的回调函数,其无论支付成功或是失败,都会执行,我们一般在其中进行一些收尾性的工作。

在这些回调函数中,我们最后一般会进行跳转回到进入本模块的页面中:

// 完成支付后进行跳转
function payEndDoRedirect() {
    // 从重定向参数中取得一开始跳转到该模块的页面的域名
    const origin = redirectParam.origin;
    delete redirectParam.origin;
    // 向重定向路径中添加订单支付状态 payStatus,后续支付页面可以拿该字段进行进一步的操作
    redirectParam.payStatus = payStatus;
    // 将这些参数添加到要跳转的域名后面
    const originUrl = addUrlParams(origin, redirectParam);
    window.location.href = originUrl;
}

这样,就实现了一个简单的公共JSAPI支付模块。

代码可见:公共微信JSAPI支付模块demo

1.3 将模块域名加入微信平台授权目录中

这一步一般是由商户平台统一完成。

2. 具体流程实现

完成以上两个模块后,就可以在具体的支付页面中实现 JSAPI 支付流程了。

根据流程图以及我们梳理好的支付流程,首先由用户点击【立即支付】按钮,发起支付,此时我们还不能跳转微信支付模块,而是需要先去授权模块,拿到微信授权码:

const is_weixin = () => {
  var ua = navigator.userAgent.toLowerCase();
  return ua.indexOf('micromessenger') !== -1;
}

const toPay = async () => {
	// 首先需要判断是否处于微信浏览器中
    if(is_weixin()) {
        // 是在微信浏览器中,则使用 JSAPI 支付,否则可以选择其它方式
        // 准备好参数,跳转授权模块域名,获取微信授权码
        const params = {
            // 当前页面的地址,用于重定向回来
            origin: window.location.href.split("?")[0],
            ...
        };
        // appId
        const appId = 'wx...........'
        // 注意params要做urlencode处理
        const to = 'https://agent.xxx.com/getwxcode.html?appid=' + appId + '&params=' + encodeURIComponent(JSON.stringify(params))
        
        // 跳转授权
        window.location.href = to
    }
}

然后进入我们前面准备好的授权模块中,拿到微信授权码 auth_code,再跳转回来,并携带授权码以及我们传递的一些参数,所以,我们下一步实际上是需要在本页面加载的时候进行处理,判断此时是否从授权模块跳转回来,并进行进一步的处理:

useEffect(() => {
    // 初次加载时,可能会需要进行一些业务相关的处理,需要与我们的支付逻辑区分开来
    ...
    
    // 从路径中获取参数
    const urlParams = window.location.href.split("?")[1];
    const authCode = getQuery(urlParams, "code");	// 授权码
    ...		// 可能有其它参数
    
    // 判断路径参数中是否有 authCode
    if(authCode) {
        // 有authCode说明是从授权模块且拿到了授权码
        // 1. 用当前平台内部的token换取用户ticket
        const { userTicket } = await getUserTicket();
        // 2. 准备好支付需要的参数
        const params = {
            // 下单需要的相关参数
            postParam: {
                userTicket,				// 用户ticket
                payMethod: "JSAPI",      // 支付方式
                loginCode: authCode,     // 授权码
                ...,                     // 还可以带一些参数,比如商品种类等,这些参数都是平台内部下单时要使用的
            },
            // 重定向时需要的相关参数
            redirectParam: {
                origin: window.location.href.split("?")[0],	// 当前页面的地址
                ...,	// 其它业务相关参数
            },
        }
        
        // 3. 跳转支付模块
        const to = 'https://pay.xxx.com/wechat/pay.html?params=' + encodeURIComponent(JSON.stringify(params))
        window.location.href = to
    }
}, [])

根据我们上面的微信支付模块中的逻辑,接下来就是调起微信支付,由用户进行操作完成支付,完成操作后,无论支付成功、失败或是被取消,最后都会跳转回进入模块的页面,也就是本页面。

同时,还会携带一个 payStatus 字段,这样,我们就可以判断订单的状态并进行相应的处理了。

由于这里也是跳转回来,所以也需要在页面加载时进行处理:

useEffect(() => {
	// 业务逻辑
    ...
    
    // 从路径中获取参数
    const urlParams = window.location.href.split("?")[1];
    const authCode = getQuery(urlParams, "code");           // 授权码
    const payStatus = getQuery(urlParams, "payStatus");     //订单支付状态
    ...
    
    // 拿到授权码后跳转支付逻辑
    if(authCode) {
        ...
    }
        
    // 支付完成后跳转回来的处理逻辑
    if(payStatus !== undefined) {
        if (payStatus == 1) {
            // 支付成功
            ...
        } else if (payStatus == 2) {
            // 支付失败
            ...
        }
    }
}, [])

注意,这里在页面初次加载时有三个处理逻辑,需要彼此进行区分。

(style) hcq_donghonglai@f940780da57b:~/multi_pose_vton-main$ export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 (style) hcq_donghonglai@f940780da57b:~/multi_pose_vton-main$ torchrun --nproc_per_node=1 --master_port=29502 train_warping.py --batchSize 1 --image_size 256 usage: train_warping.py [-h] [--local_rank LOCAL_RANK] [--batchSize BATCHSIZE] [--dataroot DATAROOT] [--datapairs DATAPAIRS] [--phase PHASE] train_warping.py: error: unrecognized arguments: --image_size 256 ERROR:torch.distributed.elastic.multiprocessing.api:failed (exitcode: 2) local_rank: 0 (pid: 4472) of binary: /public/home/hcq_donghonglai/.conda/envs/style/bin/python Traceback (most recent call last): File "/public/home/hcq_donghonglai/.conda/envs/style/bin/torchrun", line 8, in <module> sys.exit(main()) File "/public/home/hcq_donghonglai/.conda/envs/style/lib/python3.8/site-packages/torch/distributed/elastic/multiprocessing/errors/__init__.py", line 345, in wrapper return f(*args, **kwargs) File "/public/home/hcq_donghonglai/.conda/envs/style/lib/python3.8/site-packages/torch/distributed/run.py", line 719, in main run(args) File "/public/home/hcq_donghonglai/.conda/envs/style/lib/python3.8/site-packages/torch/distributed/run.py", line 710, in run elastic_launch( File "/public/home/hcq_donghonglai/.conda/envs/style/lib/python3.8/site-packages/torch/distributed/launcher/api.py", line 131, in __call__ return launch_agent(self._config, self._entrypoint, list(args)) File "/public/home/hcq_donghonglai/.conda/envs/style/lib/python3.8/site-packages/torch/distributed/launcher/api.py", line 259, in launch_agent raise ChildFailedError( torch.distributed.elastic.multiprocessing.errors.ChildFailedError: ============================================================ train_warping.py FAILED ------------------------------------------------------------ Failures: <NO_OTHER_FAILURES> ------------------------------------------------------------ Root Cause (first observed failure): [0]: time : 2025-04-02_11:08:24 host : f940780da57b rank : 0 (local_rank: 0) exitcode : 2 (pid: 4472) error_file: <N/A> traceback : To enable traceback see: https://pytorch.org/docs/stable/elastic/errors.html ============================================================根据你的提供的修改方案之后出现这个问题,接下来怎么处理
04-03
<think>好的,用户遇到了一个错误,他们在运行训练脚本时添加了`--image_size 256`参数,但程序提示无法识别的参数。我需要仔细分析这个问题,找出可能的原因并提供解决方案。 首先,用户执行的命令是: ``` torchrun --nproc_per_node=1 --master_port=29502 train_warping.py --batchSize 1 --image_size 256 ``` 而错误信息显示`unrecognized arguments: --image_size 256`,这说明`train_warping.py`脚本并不接受`--image_size`这个参数。可能的原因有几个: 1. **参数名称错误**:可能用户记错了参数名,正确的参数名可能类似`--img_size`或者`--size`等。需要检查脚本的参数定义。 2. **脚本版本问题**:用户可能根据某个修改方案添加了这个参数,但实际脚本中并未正确实现。需要确认该参数是否在代码中被正确解析。 3. **环境问题**:可能用户修改了代码但没有保存,或者没有正确安装依赖,导致参数未被识别。需要确认代码修改是否生效。 接下来,我需要指导用户如何排查这些问题: - **检查可用的参数**:用户可以通过`python train_warping.py -h`查看所有支持的参数,确认是否存在`--image_size`或者类似的参数。 - **查看脚本源代码**:如果用户有权限访问`train_warping.py`,可以检查`argparse`部分,查看定义的参数名称。例如: ```python parser.add_argument('--image_size', type=int, default=256) ``` 如果这里没有定义`--image_size`,就会报错。 - **建议的替代参数**:如果确认参数名称错误,可以尝试常见的替代名称,比如`--img_size`、`--input_size`等。 另外,用户使用了`torchrun`来启动脚本,可能需要确保参数传递的正确性,特别是分布式训练时的参数处理。有时,参数需要放在脚本名之后,确保被正确解析。 如果用户是根据某个教程或文档添加的参数,可能需要回查文档是否有误,或者联系原作者确认参数的正确性。 最后,如果用户确实需要`image_size`参数但脚本不支持,可能需要自行修改脚本的`argparse`部分,添加对应的参数,并确保在代码中使用该参数调整输入图像的尺寸。</think>根据错误信息,问题出在`--image_size`参数未被脚本识别。以下是分步解决方案: $\textcolor{green}{步骤 1:验证参数合法性}$ 执行命令查看所有合法参数: ```bash python train_warping.py -h ``` 观察输出中是否包含`--image_size`或类似参数名称 $\textcolor{green}{步骤 2:常见参数名尝试}$ 若未找到`--image_size`,可尝试这些替代参数: ```bash torchrun ... train_warping.py --img_size 256 torchrun ... train_warping.py --input_size 256 torchrun ... train_warping.py --size 256 ``` $\textcolor{green}{步骤 3:检查源码(如有权限)}$ 在`train_warping.py`中搜索`argparse`配置,确认是否包含类似: ```python parser.add_argument('--image_size', type=int, default=256) ``` 若不存在则说明参数名称错误 $\textcolor{green}{步骤 4:调整执行顺序}$ 确保参数传递位置正确: ```bash torchrun --nproc_per_node=1 --master_port=29502 \ train_warping.py \ --batchSize 1 \ [其他合法参数] \ --img_size 256 # 使用验证后的正确参数名 ``` $\textcolor{green}{附加建议}$ 1. 检查GitHub仓库的issues区或文档确认参数规范 2. 若需强制设置图像尺寸,可修改数据预处理代码中的`transforms.Resize()` 3. 分布式训练时确保所有节点使用相同的参数配置 示例成功命令结构: ```bash torchrun --nproc_per_node=1 --master_port=29502 \ train_warping.py \ --batchSize 4 \ --dataroot ./dataset \ --img_size 256 # 假设正确参数名为--img_size ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值