目录
- 一、支付系统中,前端到底在做什么
- 二、支付系统的完整链路(前端视角)
- 三、支付系统的核心业务对象(前端必须理解)
-
- [1、订单 ≠ 支付](#1、订单 ≠ 支付)
- 2、前端需要理解的关键字段
- 四、支付系统的核心:状态机(前端灵魂)
- 五、前端支付页的模块级拆解
- 六、第三方支付对接(前端重点)
-
- 1、常见支付方式
- [2、支付完成 ≠ 支付成功](#2、支付完成 ≠ 支付成功)
- 七、支付结果确认(前端最难)
- 八、支付安全:前端不是"无关角色"
- 九、企业级支付系统的前端工程能力
- 十、为什么支付系统是前端"天花板级业务"
- 十一、支付系统的异常与兜底
-
- 1、核心结论(极重要)
- 2、支付异常的「总分类模型」(一眼看清全局)
- 3、用户行为异常(出现频率最高)
-
- [(1)、重复点击 / 连续提交](#(1)、重复点击 / 连续提交)
- [(2)、用户中途退出 / 关闭页面](#(2)、用户中途退出 / 关闭页面)
- 4、前端环境异常(最容易被忽略)
-
- [(1)、页面崩溃 / JS 异常](#(1)、页面崩溃 / JS 异常)
- [(2)、浏览器 / WebView 限制](#(2)、浏览器 / WebView 限制)
- 5、网络异常(最典型)
- 6、第三方支付异常(前端必须克制)
-
- (1)、第三方返回"成功",但不可信
- [(2)、第三方无回调 / 卡死](#(2)、第三方无回调 / 卡死)
- 7、系统状态异常(最难、最值钱)
-
- [(1)、支付状态 = UNKNOWN(真实存在)](#(1)、支付状态 = UNKNOWN(真实存在))
- [(2)、成功 / 失败状态"延迟到达"](#(2)、成功 / 失败状态“延迟到达”)
- 8、支付异常兜底「设计原则总结」(⭐️⭐️⭐️⭐️⭐️)
-
- (1)、前端兜底五大铁律
- [(2)、前端兜底 UI 标准文案(实战)](#(2)、前端兜底 UI 标准文案(实战))
- 9、你如果在面试中这样讲(直接高分)(⭐️)
一、支付系统中,前端到底在做什么
前端不是收钱的,而是"控制交易流程的人"
在支付系统里,前端承担的是:
- 支付流程编排者
- 支付状态机的执行层
- 用户行为的约束器
- 风险与异常的第一防线
- 用户体验与安全之间的平衡者
这也是为什么:支付系统是前端最难、最不能出错的业务之一。
二、支付系统的完整链路(前端视角)
一个标准的企业级支付链路:
typescript
确认订单
↓
创建支付单
↓
选择支付方式
↓
发起支付
↓
第三方支付
↓
支付结果确认
↓
订单完成 / 异常处理
三、支付系统的核心业务对象(前端必须理解)
1、订单 ≠ 支付
这是支付系统最重要的认知之一。
| 概念 | 含义 |
|---|---|
| 订单(Order) | 用户买了什么 |
| 支付单(PayOrder) | 一次支付尝试 |
| 交易(Trade) | 钱的流转 |
一个订单可以:
- 多次支付
- 更换支付方式
- 失败后重试
2、前端需要理解的关键字段
typescript
interface PayOrder {
payOrderId: string;
orderId: string;
amount: number; // 单位:分
status: 'INIT' | 'PAYING' | 'SUCCESS' | 'FAIL';
expireTime: number;
channel: 'ALIPAY' | 'WECHAT' | 'BANK';
}
前端展示的所有支付信息,必须来自 payOrder。
四、支付系统的核心:状态机(前端灵魂)
1、为什么支付前端一定要用「状态机」
原因只有一个:支付不是"事件流",而是"状态演进"。
不用状态机写支付,迟早出事故。
支付系统的天然特性
- 不可逆
- 强一致
- 多异常
- 多分支
- 可中断、可恢复
天然适合状态机
2、标准支付状态机(前端必须实现)

前端与状态的关系:
| 状态 | 前端行为 |
|---|---|
| INIT | 可点击 |
| PAY_CREATING | 禁止操作 |
| PAYING | loading / 跳转 |
| SUCCESS | 成功页 |
| FAIL | 错误提示 |
| CAN_RETRY | 允许重试 |
UI = 状态的映射,而不是事件的堆叠
五、前端支付页的模块级拆解
1、金额展示(极其敏感)
核心原则:前端永远不能参与金额计算。
正确做法:
- 金额完全来自后端
- 前端只展示
- 金额单位统一为"分"
typescript
displayAmount = (amount / 100).toFixed(2);
防攻击点:
- 禁止前端传金额
- 禁止浮点数
- 校验金额变更(价格波动)
2、支付方式选择(复杂度极高)
为什么复杂?
- 多支付渠道
- 风控动态可用
- 限额 / 余额
- 渠道互斥
前端建模方式:
typescript
interface PayChannel {
code: string;
name: string;
enabled: boolean;
disabledReason?: string;
}
前端职责:
- 渲染
- 互斥控制
- 引导推荐(后端策略)
3、提交支付(最危险操作)
必须防止的事情:
- 重复提交
- 多次创建支付单
- 状态错乱
前端三重防护:
- 按钮锁
- 状态校验
- 幂等 key
typescript
if (payStatus !== 'INIT') return;
六、第三方支付对接(前端重点)
1、常见支付方式
| 场景 | 前端行为 |
|---|---|
| H5 | 跳转 URL |
| JS SDK | 调起 JSAPI |
| App | Bridge 调用 |
| 小程序 | 平台 API |
示例(H5):
typescript
window.location.href = payUrl;
⚠️ 注意:
- 跳转前必须保存上下文
- 跳转回来不能信任参数
2、支付完成 ≠ 支付成功
这是 支付系统的铁律。
不可信的返回来源:
- URL 参数
- SDK 回调
- 第三方页面提示
唯一可信方式:
- 向后端查询支付状态
七、支付结果确认(前端最难)
1、轮询机制(最常用)
typescript
const timer = setInterval(async () => {
const status = await fetchPayStatus();
if (status === 'SUCCESS') {
clearInterval(timer);
gotoSuccess();
}
}, 2000);
轮询优化:
- 最大次数
- 指数退避
- 页面隐藏暂停
2、异常与中断处理
常见异常:
| 场景 | 前端处理 |
|---|---|
| 页面关闭 | 可恢复 |
| 网络断开 | 重试 |
| 超时 | 引导查看订单 |
| 状态未知 | 风控兜底 |
支付系统必须可恢复。
八、支付安全:前端不是"无关角色"
1、前端安全防护点
- XSS(支付页零容忍)
- CSP
- 防 iframe 嵌套
- 防调试
- 防篡改
2、风控协作
前端参与:
- 设备指纹
- 行为轨迹
- 风险提示
- 二次确认
九、企业级支付系统的前端工程能力
1、架构能力
- 状态机建模
- 多端统一抽象
- 异常兜底设计
2、工程能力
- 高可用
- 可恢复
- 可追踪
- 可监控
十、为什么支付系统是前端"天花板级业务"
| 维度 | 难度 |
|---|---|
| 安全 | 极高 |
| 一致性 | 极高 |
| 复杂度 | 极高 |
| 影响面 | 极大 |
| 出错成本 | 极高 |
能把支付系统前端做好,基本可以覆盖前端 80% 的高阶能力
十一、支付系统的异常与兜底
1、核心结论(极重要)
支付系统不是"成功路径设计",而是"失败路径设计"。
- 成功路径:1 条
- 异常路径:几十条
前端的核心价值:把"不可控异常"变成"可控流程"。
2、支付异常的「总分类模型」(一眼看清全局)
从前端角度,支付异常可以拆成 5 大类:
- 用户行为异常
- 前端环境异常
- 网络异常
- 第三方支付异常
- 系统状态异常(最难)
下面我们 逐类深拆 + 对应兜底策略。
3、用户行为异常(出现频率最高)
(1)、重复点击 / 连续提交
真实场景:
- 用户狂点"立即支付"
- 双击 / 三击
- 手滑刷新
风险:
- 多次创建支付单
- 状态错乱
- 重复扣款风险
前端兜底策略(必须多层):
- 第一层:按钮锁(UI级)
typescript
if (isPaying) return;
isPaying = true;
- 第二层:状态锁(业务级)
typescript
if (payStatus !== 'INIT') return;
- 第三层:幂等 Key(接口级)
typescript
POST /createPayOrder
Headers:
Idempotent-Key: uuid
❗️ 结论:前端永远不能只靠按钮 disabled
(2)、用户中途退出 / 关闭页面
场景:
- 跳转支付宝后关闭浏览器
- 支付中刷新页面
- App 切后台
核心原则:
- 支付流程必须"可恢复"
前端兜底方案:
①、本地保存支付上下文
typescript
localStorage.setItem('pendingPay', JSON.stringify({
orderId,
payOrderId,
timestamp
}));
②、页面重新进入时检测
typescript
if (hasPendingPay()) {
queryPayStatus();
}
③、引导策略
- 已支付 → 成功页
- 未支付 → 继续支付 / 更换方式
4、前端环境异常(最容易被忽略)
(1)、页面崩溃 / JS 异常
场景:
- JS runtime error
- 白屏
- 资源加载失败
前端兜底策略:
①、支付页必须是"最小依赖页面"
- 禁止复杂组件
- 禁止不必要动画
- 禁止重型第三方库
②、全局异常捕获
typescript
window.onerror = (err) => {
reportPayError(err);
showFallbackUI();
};
③、降级 UI
- 简化支付页
- 引导跳转订单中心
(2)、浏览器 / WebView 限制
场景:
- iframe 被禁
- WebView 不支持某 API
- Cookie 丢失
兜底策略:
- 能跳转就跳转(location.href)
- 不依赖 localStorage 单点
- 重要信息后端可查
5、网络异常(最典型)
(1)、创建支付单超时
场景:
- 请求已到后端
- 前端超时未收到响应
‼️ 这是高危场景
❌ 错误做法:
- 直接提示失败
- 允许再次点击
✅ 正确兜底:
typescript
try {
await createPayOrder();
} catch {
queryPayOrderByOrderId();
}
永远先查状态,再决定 UI
(2)、支付过程中网络断开
场景:
- 已跳第三方
- 回跳失败
兜底策略:
- 回到业务系统后:
- 不显示"失败"
- 显示"支付结果确认中"
- 自动轮询
6、第三方支付异常(前端必须克制)
(1)、第三方返回"成功",但不可信
铁律(再强调一次):
- 任何前端拿到的支付成功都不可信
正确兜底流程:
typescript
第三方回跳
↓
前端展示"处理中"
↓
查询后端支付状态
↓
SUCCESS / FAIL
(2)、第三方无回调 / 卡死
场景:
- SDK 没返回
- 页面卡在 loading
兜底设计:
- 超时兜底(如 30s)
- 显示:"支付可能已完成,请前往订单中心确认"
7、系统状态异常(最难、最值钱)
(1)、支付状态 = UNKNOWN(真实存在)
场景:
- 银行处理中
- 风控审核中
- 支付网关延迟
前端策略(非常重要):
❌ 错误
- 直接失败
- 直接成功
✅ 正确
- 状态透明化
typescript
支付处理中
资金到账可能有延迟
请稍后在订单中查看
- 自动轮询
- 手动刷新入口
(2)、成功 / 失败状态"延迟到达"
场景:
- 用户看到失败
- 实际已成功
前端兜底:
- 成功状态优先级最高
- 成功可覆盖失败
- 失败不可覆盖成功
8、支付异常兜底「设计原则总结」(⭐️⭐️⭐️⭐️⭐️)
(1)、前端兜底五大铁律
- 先查状态,再做判断
- 成功必须来自后端
- 失败可以被推翻
- 流程必须可恢复
- 永远给用户退路
(2)、前端兜底 UI 标准文案(实战)
| 场景 | 推荐文案 |
|---|---|
| 不确定 | 支付结果确认中 |
| 网络异常 | 网络异常,未确认支付结果 |
| 超时 | 请前往订单中心查看 |
| 可重试 | 可重新发起支付 |
| 风控 | 为保障安全,请稍后再试 |
9、你如果在面试中这样讲(直接高分)(⭐️)
面试官:
支付失败你们怎么处理?
高级前端答案(模板):
我们不直接认为失败,而是进入一个"支付结果不确定态"。
前端会优先向后端查询支付单状态,如果是 UNKNOWN,会进入轮询;
同时整个支付流程是可恢复的,用户再次进入订单页时可以继续确认状态。
成功态只能来自服务端确认,失败态允许被成功覆盖。
基本稳过。