商品支付流程梳理

发布时间 2023-12-05 17:16:53作者: 柯基与佩奇

微信支付三种方式流程梳理

1. Native 方式 --- PC 端

Native 方式一般是在 PC 端中使用。

先看官方提供的流程图:

![Native 流程图](C:\Users\pc\Desktop\微信支付\Native 流程图.png)

根据官方的图来梳理即可:

1.1 第一步 --- 商户平台内部生成订单信息并展示

这一步一般是在用户进入支付页面时完成的,所谓平台内部生成订单,实际上就是指生成:

  • 订单信息
  • 价格
  • ...

以上这些信息是完全与微信平台无关联的,由商户平台内部完成

1.2 第二步 --- 商户平台调取微信提供的统一下单接口,微信平台生成预支付订单

这一步是在用户在支付页面中,点击微信支付按钮后进行的。

具体到研发,这一步实际上是首先由前端发送请求,后端调取微信提供的接口,进行统一下单,微信平台生成预支付订单,并向后端返回预支付交易链接 code_url

预支付交易链接,实际上就是用户需要跳转完成支付的链接。

1.3 第三步 --- 商户平台前端生成二维码

这一步由商户平台前端完成,在上一步中,前端拿到了微信提供的预支付交易链接 code_url

接下来,由前端借助第三方工具,将 code_url 转换为二维码供用户扫描。

1.4 第四步 --- 用户扫描二维码,进入支付 / 商户平台前端页面展示确认订单按钮

这里,流程分为两条线:

  1. 用户:用户扫描商户平台前端提供的二维码,进入微信支付页,完成或放弃支付,这条线中,仅有用户和微信平台的交互,商户平台并不参与。对应图中的 5~8 步。
  2. 商户平台:商户平台在向用户展示二维码后,还需要提供一个确认订单已完成的按钮。同时,轮询订单的支付状态

用户完成在微信支付页面中的操作后,可能会有两种情况:

  1. 订单未完成/支付失败 --- 此时页面保持轮询订单状态,同时等待用户点击确认支付按钮,待用户点击确认支付按钮后,调取接口查询订单状态接口,确认订单支付状态。发现订单未支付后,向用户输出提示。
  2. 订单已完成 --- 此时页面同样持轮询订单状态,并等待用户点击确认支付按钮。当轮询到用户订单已完成时,进行页面跳转;当轮询到订单状态之前用户点击了确认支付按钮时,调取接口查询订单状态。这种情况下的最终结果就是,发现订单完成,进行下一步操作(主要是页面的跳转)

这里实际上对应的就是流程图中的 10 和 11 步。

2. H5 支付方式 --- 移动端非微信浏览器

H5 方式是移动端的微信支付方式之一,其主要针对移动端在非微信浏览器中进行微信支付的情况。

同样,先看官方流程图:

![H5 流程图](C:\Users\pc\Desktop\微信支付\H5 流程图.png)

同样根据官方的图来梳理:

2.1 第一步 --- 商户平台内部生成订单信息并展示

这一步一般是在用户进入支付页面时完成的,所谓平台内部生成订单,实际上就是指生成:

  • 订单信息
  • 价格
  • ...

以上这些信息是完全与微信平台无关联的,由商户平台内部完成

这一步在三种支付方式中是相同的,因为这一步跟微信平台之间并没有关系。

2.2 第二步 --- 用户点击支付按钮,商户平台生成订单

这一步是在用户点击了支付按钮后进行,由商户平台前端发起请求,后端调用微信提供的 H5 下单 API,生成预支付订单,并向前端返回H5 支付中间页链接 h5_url

h5_url 为拉起微信支付收银台的中间页面,可通过访问该 URL 来拉起微信客户端,完成支付,h5_url 的有效期为 5 分钟。

2.3 第三步 --- 向 code_url 中添加参数,完成跳转

在 Native 方式中,用户要进行支付,需要扫描包含了 code_url 信息的二维码,在手机上完成支付。但在 H5 方式中,由于用户本身就处于移动端,所以不再需要二维码的参与。

但是,与 Native 方式不同的是,H5 支付完成后,需要跳转回商户平台的支付页面。因此,这时,我们需要主动向 h5_url 中添加参数 redirect_url

redirect_url 就是用户完成支付后,跳转回去的地址。在 redirect_url 中,除了目标地址,还可以携带一些我们需要的信息,例如平台内部订单的 uuid、订单生成的时间等等。注意,redirect_url 需要进行 urlencode 处理!

接下来,商户平台主动跳转至拼接好的链接 url

2.4 第四步 --- 用户进行支付操作,跳转回指定页面

用户跳转到微信支付页面后,进行支付操作,订单可能存在两种状态:

  1. 订单未完成/失败
  2. 订单已完成

无论哪种状态,在用户完成操作后,都会跳转至前面指定的 redirect_url

2.5 第五步 --- 回到商户平台支付页面

在微信官方的文档中,给出了 H5 支付完成后,跳回商户平台后的操作标准:

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

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

所以,在具体实现时,我们一般会在 redirect_url 中添加一项标识,用以标记此时是从 h5 支付页跳转回来的情况。然后,弹出弹框,向用户提供【确认订单状态】和【重新支付】的两个按钮,可以参考下图:

支付完成回调

2.6 第六步 --- 商户后台查询订单状态

在用户完成支付后,商户后台会接收到微信平台的通知。

所以,当用户在浏览器中点击【确认订单状态】按钮,进行订单的确认时,商户前台会调用后端提供的查询订单接口。

在商户后台,可以先查询是否接收到微信平台的通知,如果接收到了通知,则直接将订单状态返回给前端;如果还没有接收到通知,则主动调取微信平台的查询订单 API,查询订单状态,然后将订单状态返回给前端。

前端接收到订单状态后:

  • 若订单已完成,则进行下一步操作,一般是跳转页面等
  • 若订单未完成,则停留在当前页面,并提示用户订单未支付,让用户重新支付

当然,也可能用户主动点击【重新支付】按钮,此时商户平台可以根据业务需求,进行处理。

3. JSAPI 方式 --- 移动端微信浏览器

当用户在微信中,打开商户平台时,会进入微信自带的浏览器中,在该情况下进行微信支付时,需要使用微信支付提供的 JSAPI 接口,在支付场景中调起微信支付模块。

官方文档中的流程图如下:

JSAPI 流程图

但在 JSAPI 支付方式中,实际的实现流程可能与官方的流程图不太一样,这里按实际开发中的流程来进行梳理:

3.1 第一步 --- 商户平台内部生成订单信息并展示

同样的,JSAPI 支付中这一步也是必须的,主要目的仍然是展示订单的相关信息

  • 订单信息
  • 价格
  • ...

以上这些信息是完全与微信平台无关联的,由商户平台内部完成

3.2 第二步 --- 用户发起支付,跳转链接完成微信授权

用户点击支付按钮后,商户平台前端需要携带一些信息(例如:回跳链接、标识等),平台内部调取单独微信授权模块模块完成微信授权。(注意,这里的微信授权模块是平台内部封装的)

该模块完成微信授权后,会将微信授权码 auth_code 附带在我们之前传递的回跳链接后面,然后跳转至回跳链接。

在微信支付场景下,这里的回跳链接实际上就是商户平台的支付页。

3.3 第三步 --- 商户平台前端拿到授权码,跳转平台支付模块

在上一步中,商户平台前端拿到了微信授权码 auth_code

接下来,商户平台前端需要调取后端接口,根据当前的 token 获取 ticket。然后,准备好一系列参数 postParam。

其中包括:

  • 平台业务相关参数
  • 用户 ticket
  • 平台内部订单信息
  • 支付方式标识
  • 微信授权码
  • ...

同时,我们还需要准备好完成支付后跳转回支付页面的链接 redirect_url,该链接中需要包含以下信息:

  • 当前页面 url
  • 平台内部订单 uuid
  • 平台内部业务相关参数
  • 一些标识
  • ...

接下来,我们携带着这两项数据,访问平台内部封装好的支付模块。(注意,这两项数据需要进行 urlencode 处理!)

3.4 第四步 --- 平台支付模块调起微信支付

进入平台内部支付模块后,该模块会根据我们传入的数据,调取微信支付提供的 JSAPI,调起微信支付。

3.5 第五步 --- 用户完成支付操作

调起微信支付后,用户进行操作,并最终向微信支付系统发起支付请求,微信支付系统验证支付授权权限后,返回支付授权,然后用户确认支付后,输入密码,提交,微信支付系统进行校验,最终完成支付操作。这里对应的就是官方流程图中的 9~14 步。

当然,这个操作过程中,也可能存在其他情况,例如:用户取消支付、用户支付失败等等。

所以,平台支付模块中会向微信支付提供处理这些情况的回调函数,当出现这些情况时,就会触发回调。在这些回调函数中,平台可以根据业务需求进行处理。

总之,待用户完成支付操作后,最终会跳转回商户平台的支付页面。

3.6 第六步 --- 平台获取支付结果并通知用户

平台获取支付结果,可以分为两个并行的场景:

  1. 用户完成支付后,微信支付系统,异步地通知平台支付结果
  2. 用户主动查询订单支付状态,此时商户平台调取查询订单接口,并返回通知用户

PS

在 JSAPI 支付方式中,【获取微信授权】和【微信支付】这两个平台内部模块的实现比较复杂,后续可以在微信支付实例分析中进行详细的介绍。

微信支付流程实例

0.准备

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

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

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

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

1. Native 方式

1.1 接入前准备

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

1.2 Native 方式流程图

Native 流程图

接下来我们就按步骤一步步进行。

1.3 开发实现

1. 前期准备
  1. 首先我们要拿到 AppId,这是我们后续开发过程中的一个重要参数。但这个参数一般不会由前端获取,而是后端获取后,向前端提供一个拿取的接口。所以这里我们直接调用接口拿到即可。
// 获取微信appId
async getWxAppId() {
  const res = await wxModel.getWxAppId({
    func: 'getWxAppId',
    id: 2,
  });
  this.appId = res.result.appId;
},
  1. 接下来,获取本平台订单的相关信息展示在页面中,另外,页面的设计必须要遵守微信 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="https://oss.stem86.com/images/student-img/v1.0.0-wechat-pay-tip.png"
    />
    <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">该商品一经支付不可退款</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 流程图

接下来按照流程进行实现

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 方式流程图

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) {
      // 支付失败
      // ...
    }
  }
}, []);

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

微信授权代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <title>微信授权</title>
</head>

<body>
  <script>
    const wechatAuth = {
      urlParams: {},
      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;
      },
      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;
        }
      },
      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;
      },
    };
    wechatAuth.getUrlParams();
    wechatAuth.authorize();
  </script>
</body>

</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>
    // 初始化微信模块
    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));
        });
    }

    const postParam = {}

    const redirectParam = {}

    // 从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();
    }

    // 通过 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));
        });
    }

    // 订单支付状态,默认为2 --- 未支付
    let payStatus = 2;

    // 调取后端接口下单
    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);
            payEndDoRedirect();
          }
        })
        .catch(function (error) {
          console.log(JSON.stringify(error));
        });
    }

    // 微信支付
    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) {
          if (res.errMsg == "chooseWXPay:cancel") {
            alert("用户支付失败");
          }
          // 完成后,进行后续跳转
          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;
    }

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

      /*
        postParam 中的 userTicket 信息,换取用户token
        
        --- 为什么?

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

          这里换取token就是为了达成这一目的

      */
      const token = getTokenByUserTicket(postParam.userTicket);

      // 下单
      placeOrder(token, postParam);
    };
  </script>
</head>

<body></body>

</html>