前端支付开发实战指南

前端支付开发实战指南

支付功能是很多Web应用的核心功能。本文将详细介绍支付宝PC支付、微信PC扫码支付和微信公众号H5支付的前端实现方案。

1. 支付宝电脑支付

1.1 开发文档

支付宝开放平台 - 电脑网站支付

1.2 实现流程

  1. 封装请求后台的接口
  2. 后端返回支付宝生成的form表单
  3. 前端打开表单,用户扫码支付

1.3 封装支付接口

import axios from 'axios';
import qs from 'qs';
import store from '@/store';

/**
 * 支付宝支付
 * @param {Number} goodsId - 商品ID,1为自定义
 * @param {Number} quantity - 钻石数量
 */
export function zhifubao(goodsId, quantity) {
  return axios.post(
    // 后端支付接口
    store.state.baseURL + "/sjsvoice/c/pms/create/zfb/pay",
    qs.stringify({
      goodsId: goodsId,
      quantity: quantity,
    }),
    {
      headers: {
        "user-id": store.state.userInfo.userId,
        "user-token": store.state.userToken,
        channel: "pc",
        "app-type": "3",
      },
    }
  );
}

1.4 调用支付接口

// 引入支付接口
import { zhifubao } from '@/api/payment';

export default {
  data() {
    return {
      html: '', // 存储返回的form表单
      showPayPage: false
    }
  },
  methods: {
    handlePay(goodsId, quantity) {
      zhifubao(goodsId, quantity)
        .then((res) => {
          console.log(res);
          if (res.data) {
            this.showPayPage = true;
            // 提前预留一个dom元素区域来展示form表单
            this.html = res.data;

            this.$nextTick(() => {
              // 直接提交form表单,跳转到支付宝支付页面
              document.forms[0].submit();
            });
          }
        })
        .catch((err) => {
          console.log(err);
          this.$message.error('支付失败,请重试');
        });
    }
  }
}

1.5 支付宝返回的表单数据

<form
  name="punchout_form"
  method="post"
  action="https://openapi.alipay.com/gateway.do?charset=utf-8&method=alipay.trade.page.pay&sign=xxx&return_url=https://xxx.com/Recharge&notify_url=https://xxx.com/callback&version=1.0&app_id=xxx&sign_type=RSA2&timestamp=2021-12-31+17%3A45%3A19&alipay_sdk=alipay-sdk-java-dynamicVersionNo&format=json"
>
  <input
    type="hidden"
    name="biz_content"
    value='{"out_trade_no":"zfb1041640943919","total_amount":6,"subject":"支付充值","product_code":"FAST_INSTANT_TRADE_PAY"}'
  />
  <input type="submit" value="立即支付" style="display:none" />
</form>
<script>
  document.forms[0].submit();
</script>

1.6 注意事项

⚠️ 重要:扫码支付成功后,后端会控制回跳地址,这个需要和后端提前协商好

2. 微信PC扫码支付

2.1 准备工作

首先需要准备一个DOM容器,用于展示支付二维码。推荐使用vue-qr组件(支持在二维码中间添加logo):

<template>
  <!-- 微信支付二维码弹窗 -->
  <el-dialog :visible.sync="dialogVisible" title="微信扫码支付">
    <vue-qr
      :logoSrc="logo"
      :text="codeURL"
      :size="200"
    ></vue-qr>
    <div slot="footer" class="dialog-footer">
      <p>请使用微信扫描二维码完成支付</p>
    </div>
  </el-dialog>
</template>

<script>
import vueQr from 'vue-qr';

export default {
  components: {
    vueQr
  },
  data() {
    return {
      dialogVisible: false,
      codeURL: '',
      logo: require('@/assets/logo.png'), // 二维码中间的logo
      timer: null // 支付结果查询定时器
    }
  }
}
</script>

2.2 封装微信支付接口

// 请求后台接口,获取支付二维码URL
export function weixin(goodsId, quantity) {
  return axios.post(
    url,
    qs.stringify({
      goodsId: goodsId,
      quantity: quantity,
    }),
    {
      headers: {}
    }
  );
}

// 查询微信支付结果
export function searchWxResult(outTradeNo) {
  return axios.post(
    '/sjsvoice/h5/sys/wx/pay/result',
    qs.stringify({
      outTradeNo: outTradeNo
    })
  );
}

2.3 完整支付流程

export default {
  methods: {
    handleWxPay(goodsId, quantity) {
      let that = this;

      weixin(goodsId, quantity)
        .then((res) => {
          console.log(res);
          if (res.data.code == 0) {
            this.showPayPage = false;
            this.payMethod = "weixin";
            this.dialogVisible = true;
            this.codeURL = res.data.data.codeUrl; // 获取二维码URL

            // 启动定时器,轮询查询支付结果
            // tradeStatus: -1.失败 0.充值中 1.成功
            this.timer = setInterval(function () {
              searchWxResult(res.data.data.outTradeNo)
                .then((res) => {
                  console.log(res);
                  if (res.data.data.tradeStatus == 1) {
                    // 支付成功
                    clearInterval(that.timer);
                    that.dialogVisible = false;
                    that.$message.success('支付成功');
                    that.load(); // 刷新数据
                  } else if (res.data.data.tradeStatus == -1) {
                    // 支付失败
                    clearInterval(that.timer);
                    that.dialogVisible = false;
                    that.$message.error('支付失败');
                  }
                })
                .catch((err) => {
                  clearInterval(that.timer);
                  console.log(err);
                  that.$message.error('查询支付结果失败');
                });
            }, 1000); // 每1秒查询一次
          }
        })
        .catch((err) => {
          console.log(err);
          this.$message.error('发起支付失败');
        });
    },

    // 关闭弹窗时清除定时器
    handleCloseDialog() {
      if (this.timer) {
        clearInterval(this.timer);
        this.timer = null;
      }
      this.dialogVisible = false;
    }
  },

  beforeDestroy() {
    // 组件销毁时清除定时器
    if (this.timer) {
      clearInterval(this.timer);
    }
  }
}

2.4 注意事项

⚠️ 轮询策略:微信扫码支付需要前端轮询查询支付结果,建议间隔1-2秒,避免频繁请求服务器

3. 微信公众号H5支付

3.1 开发文档

3.2 实现流程

  1. 用户打开页面,请求业务接口
  2. 调用微信授权接口,获取code
  3. 通过code获取用户openid
  4. 调用微信支付接口
  5. 使用WeixinJSBridge唤起支付

3.3 引入微信JS-SDK

<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>

3.4 微信授权登录

// 准备页面回跳地址
let url = "https://your-domain.com/pay/callback";

// 请求业务接口,获取微信授权配置
axios
  .post(
    "https://your-api.com/sjsvoice/h5/sys/wechat/jssdk/config/query",
    qs.stringify({ url: url })
  )
  .then((res) => {
    if (res.data.code == 0) {
      // 对回跳地址进行encodeURIComponent编码
      // 如果需要传递参数,可以先拼接好再编码
      let encodedUrl = encodeURIComponent(res.data.data.jsSdkConfig.url);

      // 配置微信JS-SDK
      wx.config({
        debug: false, // 开启调试模式
        appId: "your-appId", // 必填,公众号的唯一标识
        timestamp: res.data.data.jsSdkConfig.timestamp, // 必填,生成签名的时间戳
        nonceStr: res.data.data.jsSdkConfig.nonceStr, // 必填,生成签名的随机串
        signature: res.data.data.jsSdkConfig.signature, // 必填,签名
        jsApiList: ["chooseWXPay"], // 必填,需要使用的JS接口列表
      });

      // 跳转到微信授权页面
      window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx2d5c3d16592f8f3f&redirect_uri=${encodedUrl}&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect`;
    } else {
      this.$toast(res.data.msg);
    }
  })
  .catch((err) => {
    console.log(err);
  });

⚠️ 注意:wx.config在回跳地址时不会输出自己携带的参数,但确实传输过去了,可以使用alert打印调试

3.5 解析回跳参数

// 解析URL参数
let url = window.location.href;
let reg = /[?&][^?&]+=[^?&]+/g;
let arr = url.match(reg);

let params = {};
if (arr) {
  arr.forEach((item) => {
    let tempArr = item.substring(1).split("=");
    let key = tempArr[0];
    let val = tempArr[1];
    params[key] = val;
  });
}

console.log(params); // { code: "xxx", state: "STATE" }

3.6 获取用户openid并调起支付

// 根据code获取用户openid
axios
  .post("https://your-api.com/sjsvoice/h5/sys/wechat/oauth2/access_token", qs.stringify({
    code: params.code
  }))
  .then((res) => {
    console.log(res, "获取用户Token");
    if (res.data.code == 0) {
      const { accessToken } = res.data.data;
      this.userId = params.userID;

      // 构造支付参数
      const payParams = {
        uid: params.userID,
        goodsId: params.goodsId,
        openid: accessToken.openid,
        quantity: params.number,
      };

      // 请求支付接口
      axios
        .post(
          "/sjsvoice/h5/sys/create/wx/pay",
          qs.stringify(payParams),
          {
            headers: {
              channel: "wxH5",
              app_type: "4",
            },
          }
        )
        .then((res) => {
          console.log(res, "支付结果");

          // 调用微信支付
          this.callWeixinPay(res.data.data);
        })
        .catch((err) => {
          console.log(err);
          this.$toast("支付失败");
        });
    }
  })
  .catch((err) => {
    console.log(err);
  });

3.7 调用微信支付API

callWeixinPay(payData) {
  // 微信H5支付API
  function onBridgeReady() {
    WeixinJSBridge.invoke(
      "getBrandWCPayRequest",
      {
        appId: payData.appId, // 公众号ID
        timeStamp: payData.timeStamp, // 时间戳
        nonceStr: payData.nonceStr, // 随机串
        package: "prepay_id=" + payData.package, // ⚠️ 一定要注意这个形式
        signType: "RSA", // 微信签名方式
        paySign: payData.paySign, // 微信签名
      },
      function (res) {
        if (res.err_msg == "get_brand_wcpay_request:ok") {
          // 支付成功
          // 使用以上方式判断前端返回
          // 微信团队郑重提示:res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠
          console.log("支付成功");

          // 跳转到成功页面或刷新数据
          window.location.href = "/pay/success";
        } else if (res.err_msg == "get_brand_wcpay_request:cancel") {
          // 用户取消支付
          console.log("取消支付");
        } else if (res.err_msg == "get_brand_wcpay_request:fail") {
          // 支付失败
          console.log("支付失败");
        }
      }
    );
  }

  // 检测WeixinJSBridge是否就绪
  if (typeof WeixinJSBridge == "undefined") {
    if (document.addEventListener) {
      document.addEventListener(
        "WeixinJSBridgeReady",
        onBridgeReady,
        false
      );
    } else if (document.attachEvent) {
      document.attachEvent("WeixinJSBridgeReady", onBridgeReady);
      document.attachEvent("onWeixinJSBridgeReady", onBridgeReady);
    }
  } else {
    onBridgeReady();
  }
}

3.8 支付方式对比

特性 支付宝PC 微信PC扫码 微信H5
支付方式 跳转支付宝 扫码支付 微信内支付
返回方式 form表单 二维码URL WeixinJSBridge
结果查询 回跳通知 轮询查询 回调通知
适用场景 PC浏览器 PC浏览器 微信内置浏览器

4. 支付安全注意事项

4.1 前端安全

  • ❌ 不要在前端存储支付密钥
  • ❌ 不要在前端计算签名
  • ✅ 所有敏感操作由后端完成
  • ✅ 使用HTTPS协议
  • ✅ 验证支付结果时以服务器为准

4.2 支付流程安全

// ❌ 错误:仅依赖前端回调
if (res.err_msg == "get_brand_wcpay_request:ok") {
  // 直接认为支付成功
}

// ✅ 正确:前端通知 + 后端验证
if (res.err_msg == "get_brand_wcpay_request:ok") {
  // 通知后端验证支付结果
  axios.post('/api/verify/payment', {
    orderId: orderId
  }).then(res => {
    if (res.data verified) {
      // 确认支付成功
    }
  });
}

4.3 用户体验优化

  • 支付前明确展示商品信息和金额
  • 支付过程中显示加载状态
  • 支付结果明确提示用户
  • 提供订单查询功能
  • 异常情况提供重试机制

5. 总结

本文介绍了三种常见的支付方式:

  1. 支付宝PC支付:通过form表单跳转
  2. 微信PC扫码支付:通过二维码展示+轮询查询
  3. 微信H5支付:通过微信授权+JSAPI支付

每种方式都有其适用场景,在实际项目中需要根据用户环境选择合适的支付方式。记住,支付安全永远是第一位的,前端只是支付流程的一部分,真正的验证和资金操作都应该在后端完成。