ecommerce-architecture-first-principles
# C端电商系统前端技术架构:第一性原理设计
方法论:不从"功能清单"出发,而从物理约束与业务本质出发,推导出不可妥协的设计原则,再将具体业务场景作为原则的应用实例。
声明:本文不重复常规的"React选型""SSR原理"等内容,而是聚焦于"为什么必须这样做"的根本推导。
# 目录
- 引言:为什么要从第一性原理出发
- 第一章:五条公理——不可违背的物理与业务约束
- 第二章:从公理推导出的六条设计定律
- 第三章:定律的工程化落地——架构分层与核心机制
- 第四章:业务场景一——首页与商品浏览
- 第五章:业务场景二——秒杀
- 第六章:业务场景三——下单
- 第七章:业务场景四——支付
- 第八章:业务场景五——支付后导航与订单全生命周期
- 第九章:整体架构蓝图
- 第十章:迭代演进路线
- 附录:设计定律速查与决策树
# 引言:为什么要从第一性原理出发
常规的架构文档从功能出发:"首页要加载快,秒杀要防重,支付要安全"。这种思路的隐患是:每个功能都找到了一个解法,但解法之间可能矛盾,且遇到新场景时无从推导。
第一性原理的思路是:先找到不可违背的物理与业务约束(公理),从公理推导出设计定律,再让所有具体方案成为定律的自然推论。这样,遇到任何新场景,都能从定律推导出正确的方案,而不是靠经验拍脑袋。
# 第一章:五条公理——不可违背的物理与业务约束
# 公理一:网络是不可靠的
物理事实:
· 光速有限 → 延迟永远 > 0
· 带宽有限 → 大数据传输必然慢
· 网络会断开 → 请求会丢失
· TCP重传 → 请求可能重复到达
· 路由不对称 → 请求和响应可能走不同路径,时序不保证
推论:
· 你永远无法100%确认一个请求"是否到达了服务端"
· 你永远无法100%确认一个响应"是否是最新状态"
· 超时 ≠ 失败(请求可能已到达并执行成功,只是响应丢了)
# 公理二:客户端是不可信的
安全事实:
· 所有前端代码都是公开的(可被审查、篡改、注入)
· 用户输入是恶意的(必须视为攻击者)
· localStorage/IndexedDB 可被手动修改
· 请求参数可以被拦截和篡改(Fiddler/Charles)
· 客户端时间可以被修改(系统时间设置)
推论:
· 永远不要在客户端做安全判定(时间校验、价格计算、权限判断)
· 客户端只负责"展示"和"传输",不负责"裁决"
· 所有关键数据必须服务端验证
# 公理三:用户感知是主观的,焦虑会放大问题
认知科学事实:
· < 100ms:用户感觉"瞬间"(无需 loading)
· 100ms - 1s:用户感觉"快"(可接受轻度 loading)
· 1s - 3s:用户感觉"慢"(需要进度反馈)
· > 3s:用户感觉"卡"(大量用户流失)
· 不确定性 > 等待本身(没有反馈的等待比有反馈的慢更让人焦虑)
· 焦虑的用户会疯狂点击/返回(放大系统压力)
推论:
· "展示什么"比"有多快"更重要(有骨架屏的3秒 > 白屏的2秒)
· 任何操作必须立即反馈(哪怕只是按钮变灰 + "处理中")
· 中间态必须有明确文案("支付处理中"而非无限 loading)
# 公理四:交易是不可逆的
业务事实:
· 多扣一分钱 = 客诉 + 赔偿 + 信任崩塌
· 超卖一件商品 = 直接经济损失
· 漏发一个订单 = 运营事故
· 退款金额算错 = 财务对不上
推论:
· 交易链路的容错标准是"零"(不是"低概率")
· 宁可下单失败,不能重复下单
· 宁可支付超时,不能重复扣款
· 每一步都必须有"事实记录",可追溯、可对账
# 公理五:系统资源是有限的
工程事实:
· JavaScript 是单线程的(一个长任务阻塞所有交互)
· 内存有限( leaks 会导致崩溃)
· DOM 操作是昂贵的(每次 reflow/repaint 都有成本)
· 并发连接数有限(HTTP/2 也不是无限的)
· 服务端 QPS 有上限(瞬时洪峰会击穿系统)
推论:
· 渲染必须分优先级(用户看得见的优先渲染)
· 请求必须分优先级(核心链路优先)
· 流量必须被削峰(瞬时洪峰不能直达后端)
# 第二章:从公理推导出的六条设计定律
# 定律一:服务端是唯一事实源
推导路径:公理二(客户端不可信)+ 公理四(交易不可逆)
含义:
· 时间 → 以服务端为准
· 价格 → 以服务端计算为准
· 库存 → 以服务端校验为准
· 订单/支付状态 → 以服务端查询为准
应用:
· 倒计时不信任本地时间 → 同步服务端 offset
· 下单不传价格 → 只传商品ID和优惠券ID
· 支付成功不以三方回调为准 → 以查单为准
· 是否可下单不看前端倒计时 → 看服务端时间窗 + 资格令牌
# 定律二:幂等是一切可靠性的基石
推导路径:公理一(网络不可靠)+ 公理四(交易不可逆)
含义:
· 网络不可靠 → 请求会重发
· 重发不能导致重复执行
· 每个交易操作都必须幂等:执行一次和执行多次效果相同
实现方式:
· 幂等键(Idempotency-Key):前端生成 UUID,后端以 (userId, operation, key) 去重
· 一次性 Token:后端签发的短TTL凭证,使用后失效
· 状态机约束:订单状态只能单向流转,拒绝回退
关键区分:
· 幂等键 → 解决"同一次操作重复提交"
· 业务Token → 解决"请求是否合法/是否被篡改"
· 两者配合 = 双Token防重体系
# 定律三:任何操作必须有确定性终态
推导路径:公理一(超时 ≠ 失败)+ 公理三(不确定性放大焦虑)
含义:
· 由于"超时 ≠ 失败",前端永远不能因为超时就判定操作失败
· 必须有机制让操作最终收敛到一个确定的终态(成功 or 失败)
· 收敛的唯一可靠方式 = 查询服务端的"事实记录"
实现方式:
· 超时 → 进入 UNKNOWN 态 → 展示"处理中"
· 持续轮询/SSE 查询订单状态
· 直到服务端返回 SUCCESS 或 FAILED
· 如果超时阈值到了仍未收敛 → "如已扣款将在X小时内对账"
一句话:
前端永远不"猜"结果,只"查"结果。
# 定律四:永远给用户"看到什么",而不是"看到白屏"
推导路径:公理三(用户感知主观)+ 公理五(资源有限)
含义:
· 数据没到 → 展示骨架屏(而非白屏)
· 数据过期 → 展示旧数据 + 后台更新(而非清空等待)
· 图片没到 → 展示占位图(而非空白区域)
· 网络断了 → 展示离线缓存(而非"网络错误")
分层策略:
第0层:骨架屏(静态 HTML,SSG/SSR 即可产出,0ms 可见)
第1层:缓存数据(SWR stale-while-revalidate,先展示旧数据)
第2层:最新数据(接口返回后平滑替换)
第3层:降级数据(接口失败 → 隐藏模块或展示兜底)
核心:感知性能 > 实际性能
用户觉得"快"比实际"快"更重要。
# 定律五:核心链路与非核心链路必须物理隔离
推导路径:公理四(交易不可逆)+ 公理五(资源有限)
含义:
· 秒杀按钮不能因为推荐接口挂了就点不动
· 支付不能因为评论加载慢就被阻塞
· 核心交易链路的资源(请求并发、渲染优先级)不能被非核心功能挤占
实现方式:
· 请求优先级队列:P0 交易类 > P1 业务类 > P2 埋点类
· 组件级降级:非核心组件崩溃由 ErrorBoundary 隔离,不影响核心
· 资源级隔离:核心 JS bundle 独立打包,不被非核心代码拖累
· 域名级隔离:交易 API 与商品 API 分域名,互不影响
# 定律六:防御纵深——不信任任何单层防护
推导路径:公理二(客户端不可信)+ 公理四(交易不可逆)
含义:
· 单层防护总有一天会被突破
· 必须多层防护,层层校验
· 前端防护 + 网关防护 + 服务端防护 = 纵深
应用(以"防重复下单"为例):
第1层(前端UI):isSubmitting 状态锁,按钮置灰
第2层(前端请求):幂等Key注入,同一次意图复用同一Key
第3层(前端跨页):BroadcastChannel 同步,多标签页互斥
第4层(网关层):频率限制,同用户 N 秒内限 M 次
第5层(服务端幂等):(userId, operation, key) 唯一约束
第6层(服务端状态机):订单状态单向流转,拒绝非法迁移
即使第1-4层全部被绕过,第5-6层仍能兜底。
# 第三章:定律的工程化落地——架构分层与核心机制
# 3.1 从定律到架构:映射关系
定律一(服务端是事实源) ──→ 时间同步机制 + 状态查询机制
定律二(幂等是基石) ──→ 幂等Key管理 + 双Token体系
定律三(确定性终态) ──→ 状态机(FSM)+ 查单收敛机制
定律四(永远有内容展示) ──→ 分层缓存 + 骨架屏体系 + 降级策略
定律五(核心链路隔离) ──→ 请求优先级队列 + 组件降级隔离
定律六(防御纵深) ──→ 多层校验链 + 安全SDK
↓ 工程化
┌──────────────────────────────────────────────────┐
│ 架构分层 │
│ │
│ 视图层 ← 定律四(骨架/降级)+ 定律五(隔离) │
│ 状态层 ← 定律三(FSM) │
│ 请求层 ← 定律二(幂等)+ 定律五(优先级) │
│ 缓存层 ← 定律四(分层缓存) │
│ 安全层 ← 定律一(服务端为准)+ 定律六(纵深) │
│ 监控层 ← 定律三(可观测终态收敛) │
└──────────────────────────────────────────────────┘
# 3.2 核心机制一:时间同步(定律一落地)
问题推导:
· 公理二 → 客户端时间不可信(可被篡改)
· 秒杀倒计时必须准确 → 需要可信的时间源
· 唯一可信时间源 = 服务端
· 但不能每秒请求服务端(会压垮服务器)
方案推导:
· 不信任本地绝对时间 → 信任"本地时间的相对流逝"
· 从服务端拿到权威时间 → 计算 offset = serverTime - localTime
· 用 performance.now()(单调时钟)做时间累计 → 不受系统时间修改影响
· 倒计时 = endTime - (localNow + offset)
· 定期校准 offset(补偿时钟漂移)
时间同步链路(四步):
Step 1:请求服务端时间
· 记录 sendTime(请求发出前)
· 记录 receiveTime(响应到达后)
· 网络延迟 RTT = receiveTime - sendTime
· 假设单向延迟 = RTT / 2
· serverTime = responseTime - RTT / 2
Step 2:计算偏移量
· offset = serverTime - localTime
· 后续计算只用 offset 和 performance.now(),不读 Date.now()
Step 3:单调时钟 tick
· startPerfTime = performance.now()
· 每次 tick:elapsed = performance.now() - startPerfTime
· currentServerTime = startServerTime + elapsed
· remaining = endServerTime - currentServerTime
Step 4:动态校准
· visibilitychange(从后台回来)→ 立即重新校准
· 临界点(最后3-5秒)→ 加密校准频率
· 常规期间 → 每30秒轻量校准一次
# 3.3 核心机制二:幂等管理(定律二落地)
问题推导:
· 公理一 → 网络会重发请求
· 公理四 → 交易不可重复执行
→ 需要:重复请求 = 只执行一次
幂等Key生命周期管理:
用户产生一次"意图"(如:点击"提交订单")
│
├── 前端生成 Idempotency-Key(UUID v4)
├── 将 (intentId, key) 存入内存
│
├── 发送请求(Header: X-Idempotency-Key)
│
├── 超时?→ 重试,复用同一个 Key
│ (不是生成新Key,因为这是"同一次意图"的重试)
│
├── 成功?→ 清除 Key,流程完成
│
└── 失败?→ 用户手动重试 → 生成新 Key
(因为这是"新的意图",不是重试)
关键区分:
· "重试"(系统发起,如超时重试)→ 复用 Key
· "重新操作"(用户发起,如点击"再试一次")→ 新 Key
双Token体系:
Token 1: Idempotency-Key(前端生成)
· 解决"同一次操作重复提交只算一次"
· 生命周期:一次用户意图
Token 2: payToken / purchaseToken(后端签发)
· 解决"请求是否合法/参数是否被篡改"
· 生命周期:短TTL一次性使用
· 绑定 userId + orderId + channel
# 3.4 核心机制三:状态机 + 查单收敛(定律三落地)
问题推导:
· 公理一 → 超时 ≠ 失败
· 公理三 → 用户需要确定性终态
→ 需要:任何操作最终收敛到"成功"或"失败"
方案推导:
· 不能信任请求响应(可能超时、丢失)
· 唯一可信的事实源 = 服务端记录
· 持续查询服务端,直到拿到终态
有限状态机(FSM)通用模型:
通用交易状态机:
IDLE ──→ INITIATING ──→ PROCESSING ──→ SUCCESS(终态)
│ │
│ └── FAILED(终态)
│
└──→ UNKNOWN(不确定态)
│
│ 持续查单
│
├──→ SUCCESS
└──→ FAILED
状态机核心规则:
· 终态不可回退(SUCCESS 后不接受任何其他状态写入)
· UNKNOWN 必须能收敛(通过查单机制)
· 非法迁移被拒绝(防止竞态条件导致的脏状态)
查单收敛机制:
· 进入 UNKNOWN 后,启动轮询(间隔递增:3s → 5s → 10s)
· 每次查询 /api/status → 返回 PROCESSING / SUCCESS / FAILED
· PROCESSING → 继续轮询
· SUCCESS / FAILED → 状态机迁移到终态,停止轮询
· 超时阈值(如5分钟)→ 停止轮询,展示"如已操作,系统将自动对账"
# 3.5 核心机制四:请求调度器(定律五落地)
问题推导:
· 公理五 → 连接数有限、服务端QPS有限
· 秒杀场景 → 瞬时大量请求
→ 需要:请求优先级调度 + 限流削峰
请求调度器架构:
┌──────────────────────────────────────────┐
│ 请求入口 │
│ 每个请求携带优先级标签 │
│ P0=交易类 P1=业务类 P2=辅助类 │
└─────────────────┬────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 优先级队列(堆) │
│ │
│ P0: [支付提交, 下单, 抢购] ← 最高优先级 │
│ P1: [商品列表, 详情, 搜索] │
│ P2: [埋点, 推荐, 评论] ← 可延迟/丢弃 │
└─────────────────┬────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 调度器 │
│ · 全局并发上限(如 maxConcurrent = 6) │
│ · 域名分组(交易API / 商品API 独立限流) │
│ · 熔断器(错误率 > 阈值 → 短路 → 降级) │
└─────────────────┬────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 响应处理链 │
│ · 幂等去重(同Key返回缓存结果) │
│ · Token刷新(401 → 静默刷新 → 重发) │
│ · 错误标准化 │
└──────────────────────────────────────────┘
# 3.6 核心机制五:渲染分层(定律四 + 定律五落地)
问题推导:
· 公理三 → 白屏是最差的体验
· 公理五 → DOM操作昂贵、JS单线程
→ 需要:分层渲染 + 优先级控制
渲染分层模型:
第0层:静态骨架(0ms 可见)
· SSG/SSR 产出,CDN 缓存
· 骨架与最终布局像素级一致(CLS ≈ 0)
· 即使所有接口都挂了,用户也能看到"页面存在"
第1层:首屏核心内容(LCP 目标 < 2s)
· SSR 返回的 P0 数据
· 或 SWR 缓存的旧数据(stale-while-revalidate)
第2层:交互组件(Islands 水合)
· 只对需要交互的组件 hydrate
· 非交互区域保持纯 HTML(零 JS 开销)
第3层:可视区扩展内容(IntersectionObserver 触发)
· 列表数据分页加载
· 图片懒加载
第4层:空闲时加载(requestIdleCallback)
· 推荐数据
· 预获取下一页资源
渲染性能隔离(定律五):
· 高频更新(倒计时)→ 原生 DOM 逃生舱,绕过框架 Diff
· 长列表 → 虚拟列表(>200条时),只渲染可视区
· 组件级 ErrorBoundary → 非核心组件崩溃不影响核心
# 第四章:业务场景一——首页与商品浏览
本章视角:不是"首页要做什么功能",而是"定律四和定律五在首页场景中如何应用"。
# 4.1 本质问题
首页的本质问题是:在有限的网络和计算资源下,如何在用户打开页面的瞬间给出"有用且正确"的内容。
# 4.2 用定律推导方案
定律四(永远有内容展示)推导出的加载策略:
用户打开首页 → 不能白屏
→ 第0层:SSG 骨架屏即时可见(CDN 返回 HTML < 50ms)
→ 第1层:BFF 聚合接口返回首屏 P0 数据
→ 第2层:可视区外的模块延后加载
→ 第3层:个性化推荐等 P2 数据空闲时加载
如果接口失败 → 不能白屏
→ 展示 Service Worker 缓存的上次数据
→ 标记"数据可能不是最新"
→ 联网后静默刷新
定律五(核心链路隔离)推导出的模块策略:
首页模块优先级划分:
P0(首屏必须可见):
· 页面框架、导航
· 轮播图(运营入口,LCP 候选元素)
P1(首屏可见区域):
· 秒杀单品入口(交易入口,高价值)
· 商品列表第一屏
P2(可视区外/空闲加载):
· 个性化推荐
· 分类导航
· 客服入口
隔离规则:
· P2 模块的加载失败绝不影响 P0/P1
· P2 模块用 ErrorBoundary 包裹
· P2 模块的请求优先级最低
# 4.3 关键设计决策
# 决策1:BFF 接口聚合
问题推导:
· 公理一 → 弱网下每个请求的 RTT 很大
· 首页需要 5+ 个接口(轮播/秒杀/列表/推荐/用户)
· 5 个串行请求 = 5 × RTT(弱网下可能 3-5 秒)
→ 需要:把多个接口合并为 1 个
BFF 聚合方案:
Client ──→ BFF (1 请求) ──→ 并行调用后端微服务
├── /banner
├── /seckill
├── /products
└── /recommend
←── 合并返回
容错设计(定律四):
· BFF 用 Promise.allSettled(非 Promise.all)
· 部分接口失败 → 返回 { banner: ok, seckill: null, degraded: ['seckill'] }
· 前端根据 degraded 隐藏对应模块,不报错
# 决策2:LCP 元素优化
问题推导:
· 公理三 → LCP(最大内容绘制)直接影响用户感知速度
· 首页 LCP 元素通常是轮播图首图或秒杀主视觉
→ LCP 元素必须最快加载
优化链路:
1. 确定LCP元素(Lighthouse 分析)
2. preload 标记最高优先级
<link rel="preload" as="image" href="banner.webp" fetchpriority="high" />
3. 图片格式 WebP/AVIF + CDN 实时裁剪
4. preconnect 图片域名
<link rel="preconnect" href="https://cdn.example.com" />
5. LCP 图片不做懒加载(首屏图必须立即可见)
# 决策3:二次进入秒开
问题推导:
· 公理三 → 用户第二次进入时期望"秒开"
· 公理一 → 即使网络快,请求+渲染也需要时间
→ 需要:用缓存消除等待
二次进入策略(定律四分层缓存):
第0层:Service Worker 缓存 App Shell(HTML骨架+核心CSS/JS)→ Cache-First
第1层:Service Worker 缓存上次 P0 数据 → Stale-While-Revalidate
· 先秒返缓存数据(< 300ms 可见)
· 后台静默请求最新数据
· 数据到了平滑替换
第2层:SWR 内存缓存 → 商品列表 staleTime=60s
结果:
第一次进入:~1-2s(SSG + 接口)
第二次进入:< 300ms(SW 缓存 + SWR)
# 4.4 商品列表滚动加载
问题推导:
· 公理五 → DOM 节点越多,渲染越慢
· 商品可能有成百上千条
→ 需要:只渲染必要的 DOM + 按需加载
分阶段策略:
数据量 < 50:普通列表 + 图片懒加载(IntersectionObserver)
数据量 50-200:普通列表 + 图片懒加载 + React.memo
数据量 > 200:虚拟列表
无限滚动加载链路:
· 首屏 SSR 返回前 20 条(利于 SEO)
· IntersectionObserver 监听列表底部哨兵
· 哨兵进入视口 → 触发加载下一页
· 加载中显示骨架卡片(与正式卡片布局一致)
· 预取策略:滚动到 3/4 时预取下一页
图片加载 CLS 防护(定律四):
· 图片容器预设 aspect-ratio(固定宽高比)
· 先加载 LQIP(< 1KB 模糊占位图)
· 真实图片加载完淡入
· 图片加载失败 → 默认占位图
→ CLS ≈ 0,布局不抖动
# 4.5 弱网与断网
问题推导:
· 公理一 → 弱网/断网是常态,不是异常
· 公理三 → 白屏 = 用户流失
→ 需要:弱网下的分层降级展示
弱网分级应对:
良好(4G+, RTT<50ms):
· 正常加载,高清图片
弱网(3G, RTT<500ms):
· 首屏接口强制走 BFF 聚合(减少请求数)
· 图片降清晰度(CDN ?w=200&format=webp)
· 关闭装饰性动效
· 骨架屏优先,数据延后
断网:
· Service Worker 返回离线 App Shell + 上次缓存数据
· 全局标记"离线浏览中"
· 核心操作(下单/支付)按钮置灰 + "网络不可用"提示
· 非关键操作(加购物车)写入 IndexedDB 待发队列
· 网络恢复 → 静默刷新 + 补发待发操作
# 第五章:业务场景二——秒杀
本章视角:秒杀场景是所有六条定律集中体现的"极限测试"。
# 5.1 本质问题
秒杀的本质问题是:在瞬时极端流量下,保证时间公平、库存准确、交易不重复。
# 5.2 用定律推导方案
定律一(服务端是事实源)→ 倒计时设计:
问题:
· 秒杀倒计时必须准确(差1秒就是业务漏洞)
· 公理二 → 客户端时间不可信
方案:
· 倒计时 = 服务端时间 - 本地时间(offset 补偿)
· 用 performance.now() 单调时钟做 tick(不受系统时间修改影响)
· 定期校准 offset(补偿时钟漂移)
· 后台恢复后立即重新校准
关键原则:
· 客户端倒计时只是"展示提示"
· 是否真的可下单 → 以服务端校验为准(时间窗 + 资格令牌 + 库存)
· 即使用户篡改本地时间让倒计时归零,服务端仍然会拒绝
定律五(核心链路隔离)→ 倒计时渲染性能:
问题:
· 秒杀会场可能有几十甚至几百个倒计时同时运行
· 每秒 tick 都触发框架 re-render → 性能灾难
方案(渐进式渲染隔离):
Level 1(常规):倒计时组件独立 memo,父组件不因 tick 重渲染
Level 2(中等):倒计时值存 data-attr,CSS 伪元素展示
Level 3(极限):requestAnimationFrame + 原生 DOM 逃生舱
· 直接 ref.innerText = '00:15:30'
· 完全绕过 React/Vue 的虚拟 DOM Diff
· 性能开销接近 0
判断标准:
倒计时数量 < 10 → Level 1
倒计时数量 10-50 → Level 2
倒计时数量 > 50 → Level 3
定律二(幂等)+ 定律六(防御纵深)→ 防重复抢购:
防重六道防线:
第1层(前端UI):isSubmitting 状态锁,按钮置灰
第2层(前端请求):Idempotency-Key 注入
第3层(前端跨页):BroadcastChannel 同步多标签页状态
第4层(网关层):同用户 N 秒内限 M 次请求
第5层(服务端幂等):(userId, activityId, key) 唯一约束
第6层(服务端状态机):库存原子扣减 + 订单状态单向流转
关键设计——为什么不用 debounce?
· debounce 会吃掉弱网下的合法重试
· 用户网络慢 → 第一次请求还没返回 → debounce 期过了 → 用户再点
· 如果 debounce 吃掉了这次点击,用户合法的抢购就失败了
· 正确做法:状态锁(isSubmitting)—— 锁住直到接口返回或超时
定律三(确定性终态)→ 抢购超时处理:
问题:
· 公理一 → 抢购请求可能超时
· 超时 ≠ 失败(可能已经抢到了,只是响应丢了)
方案:
· 超时不判失败 → 进入 UNKNOWN 态
· 展示"抢购处理中,请稍候"
· 用幂等Key持续查询抢购结果
· 收敛到 SUCCESS(跳转结算)/ FAILED(提示售罄/失败)
# 5.3 秒杀按钮完整状态机
从"用户不可操作"到"用户可操作"再到"操作完成"的完整生命周期:
NOT_AUTH(未登录)
│ ── 按钮展示"登录后抢购"
│ ── 点击跳登录页
│
▼ (登录成功)
NOT_READY(活动未开始)
│ ── 按钮展示倒计时
│ ── 不可点击
│
▼ (服务端时间到达)
READY(可抢购)
│ ── 按钮高亮"立即抢购"
│
│ 用户点击
▼
SUBMITTING(提交中)
│ ── 按钮置灰 + "提交中..."
│ ── 生成 Idempotency-Key
│ ── BroadcastChannel 通知其他标签页
│ ── 发送抢购请求(携带 purchaseToken + Key)
│
├── SUCCESS ──→ 跳转结算页
│
├── SOLD_OUT ──→ 按钮变为"已售罄"(终态)
│
├── LIMIT_REACHED ──→ "您已抢购过"(终态)
│
├── NEED_CAPTCHA ──→ 弹出验证码 ──→ 验证通过回到 SUBMITTING
│
├── QUEUED ──→ 展示排队进度 ──→ 叫号后提交
│
├── TIMEOUT ──→ UNKNOWN ──→ 轮询查结果 ──→ SUCCESS/FAILED
│
└── FAILED ──→ "抢购失败,重试" ──→ 用户手动重试生成新Key
# 5.4 零点请求风暴防护
问题推导:
· 10万用户倒计时同时归零 → 10万请求瞬间到达后端
· 即使前端防重,后端也会被洪峰击穿
方案推导(定律五:流量必须削峰):
削峰三层:
层1:资格预申请(开抢前1-2分钟)
· 前端预申请 purchaseToken
· 后端绑定 userId + activityId + SKU
· 有效期短(如5分钟TTL)
· 无Token的请求 → 网关直接拦截
层2:排队前置(点击抢购后)
· 不直接下单,先进入排队队列
· POST /queue/join → 返回 queueId + 预计等待时间
· SSE/WebSocket 订阅叫号
· 前端展示"排队中... 预计30s"
· 叫号后才真正提交下单
层3:请求打散(Jitter)
· 倒计时归零后按钮不立即激活
· 添加客户端随机延迟(0-500ms)
· 打散请求到达服务端的时间
# 第六章:业务场景三——下单
本章视角:下单的核心是"在不可靠环境中创建一个可靠的交易意图"。
# 6.1 本质问题
下单的本质问题是:把用户的"购买意图"转化为一个"不可重复、价格正确、库存锁定"的订单。
# 6.2 用定律推导方案
定律一(服务端是事实源)→ 价格防篡改:
问题:
· 公理二 → 用户可以篡改请求参数
· 如果前端传 price=0.01,后端不校验 → 超级漏洞
方案:
· 前端只传"意图":{ items, addressId, couponId }
· 绝不传 price / shippingFee / discountAmount
· 后端根据 items + couponId 重新计算所有价格
· 前端展示的价格只是"预估"(给用户即时反馈)
· 提交后以服务端返回的"权威价格"为准
价格展示流程:
用户选优惠券 → 前端乐观更新"预估价" → 同时请求后端计算
→ 后端返回权威价 → 前端用权威价覆盖预估价
→ 如有差异,平滑过渡(不突兀跳变)
定律二(幂等)→ 下单防重:
下单幂等流程:
用户点击"提交订单"
│
├── 前端生成 Idempotency-Key
├── isSubmitting = true(按钮锁定)
│
├── POST /api/order/create
│ Header: X-Idempotency-Key: <uuid>
│ Body: { items, addressId, couponId }(不传价格!)
│
├── 超时?→ 不判失败,用 Key 查询订单是否创建成功
│
├── 成功?→ 跳转支付,记录 orderId
│
└── 库存不足?→ 提示并引导返回
竞态防护(状态机约束):
· 状态机拒绝非法迁移:SUCCESS 后不接受 FAILED 覆盖
· 即使两个请求并发返回(一成功一失败),状态机保证最终只有成功生效
定律一(服务端是事实源)→ 库存锁定与释放:
库存锁定机制:
下单成功
├── 后端:Redis 原子预扣库存
├── 后端:创建订单,设置过期时间(30分钟)
└── 前端:启动支付倒计时(30:00)
支付倒计时进行中
├── 前端:复用秒杀倒计时方案(服务端时间为准)
├── 最后5分钟:强提示"订单即将关闭"
└── 后端:定时扫描即将过期的订单
倒计时归零 / 用户取消
├── 后端:关闭订单,释放库存
└── 前端:引导重新下单
关键原则:
· 库存校验永远在服务端(前端展示库存只是参考)
· 库存锁定由服务端完成(前端不能"锁库存")
· 前端只负责展示倒计时和引导
# 6.3 地址变更的竞态处理
问题推导:
· 公理一 → 多个异步请求并发返回,顺序不保证
· 用户快速切换地址 A → B → C
· A 的响应最后才回来 → 价格被错误覆盖为 A 的运费
方案(请求版本号忽略过期响应):
let calcVersion = 0;
async function onAddressChange(addressId) {
const myVersion = ++calcVersion;
// 乐观更新(先展示新地址,给用户即时反馈)
updateAddressUI(addressId);
// 发起价格重算 + 运费查询 + 库存校验
const result = await Promise.allSettled([
api.calcPrice({ items, addressId, couponId }),
api.calcShipping({ items, addressId }),
api.checkRegionStock({ skuIds, addressId }),
]);
// 版本号不匹配 = 这是一次过期响应,忽略
if (myVersion !== calcVersion) return;
// 版本号匹配 = 这是最新请求的响应,应用
applyResult(result);
}
为什么不用 AbortController?
· AbortController 可以取消请求,但某些场景(如库存校验)不希望取消
· 版本号方案更灵活:请求继续执行,但忽略过期响应
· 也可以两者结合:AbortController 取消旧请求 + 版本号兜底
# 第七章:业务场景四——支付
本章视角:支付是"在最高安全要求下的最终一致性收敛"。
# 7.1 本质问题
支付的本质问题是:在不可靠的网络环境中,保证"钱"的准确——不少扣、不多扣、不漏记,且用户始终知道当前状态。
# 7.2 用定律推导方案
定律二(幂等)+ 定律六(防御纵深)→ 防重复扣款:
防重复扣款的六层防御:
第1层(前端UI):
· 点击支付 → isSubmitting = true → 按钮置灰
· 直到终态才释放
第2层(前端幂等):
· 生成 payAttemptId(UUID)
· 存入 localStorage:orderId → payAttemptId
· 刷新后读取同一 payAttemptId 继续流程
第3层(前端跨页):
· BroadcastChannel 广播"正在支付"
· 其他标签页收到后禁用支付按钮
第4层(网关限流):
· 同一订单 N 秒内最多 1 次支付请求
第5层(服务端幂等):
· (userId, orderId, payAttemptId) 唯一约束
· 重复请求 → 返回已有结果,不二次扣款
第6层(服务端事实):
· 支付单表记录每笔支付尝试的状态
· INIT → PROCESSING → SUCCESS/FAILED
· 只在 INIT → PROCESSING 时调用三方扣款
· 已处理的支付单 → 直接返回结果
双Token体系:
Idempotency-Key(前端生成)+ payToken(后端签发)
· Idempotency-Key 解决"重复提交只算一次"
· payToken 解决"请求是否合法/参数是否被篡改"
· payToken 一次性使用,绑定 userId + orderId + channel
定律三(确定性终态)→ 支付超时的最终一致:
问题:
· 公理一 → 支付请求可能超时
· 超时 ≠ 没扣款(可能三方已经扣了,只是回调丢了)
· 公理四 → 绝不能因为前端展示"失败"而让用户重复支付
方案:以服务端订单状态为唯一事实源
支付超时 → 进入 UNKNOWN 态
│
├── 展示"支付处理中,请稍候"
├── 不展示"失败",不展示"成功"
├── 提供"继续等待"和"返回订单页"选项
│
├── 后台持续轮询 /api/order/status
│ · PROCESSING → 继续等待
│ · SUCCESS → 跳转成功页
│ · FAILED → 提示失败
│
├── 超时阈值(5分钟)→ 停止轮询
│ · 展示"如已扣款,系统将在X小时内自动对账"
│ · 引导查看订单状态
│
└── 用户主动查单
· 随时可查 /api/order/status
· 以查询结果为准
后端最终一致保障:
· 支付单表(事实源):每笔支付都有完整状态记录
· 三方回调(Webhook):三方主动通知支付结果
· 主动查单(补偿):定时扫描 PROCESSING 太久的单
· 每日对账:与三方对账文件核对
定律一(服务端是事实源)→ 支付状态以查单为准:
核心原则:前端永远不"判定"支付结果,只"展示"服务端的状态
❌ 错误做法:
· 三方支付SDK返回 success → 直接展示"支付成功"
· 原因:SDK返回可能不准确,或者回调可能丢失
✅ 正确做法:
· 三方支付SDK返回 → 触发"查单"
· 查 /api/order/status → 以结果为准
· 查到 SUCCESS → 展示"支付成功"
· 查到 PROCESSING → 继续等待
· 查到 FAILED → 展示"支付失败"
# 7.3 支付状态机完整设计
支付FSM(9状态完整模型):
IDLE(未发起)
│
│ 用户点击"确认支付"
│ 生成 payAttemptId,持久化到 localStorage
│
▼
CREATING(创建支付单)
│ POST /api/pay/create { orderId, channel }
│ Header: X-Idempotency-Key: payAttemptId
│ 后端返回 { payId, payToken, expiresAt }
│
├── CREATE_FAIL ──→ FAILED(可重试,新 payAttemptId)
│
▼
QUEUEING(高峰排队,可选)
│ SSE/WebSocket 订阅排队状态
│ 展示"排队中... 前面还有N人"
│
├── CALLED ──→ AWAITING_CHANNEL
├── QUEUE_TIMEOUT ──→ UNKNOWN
└── CANCEL ──→ CANCELLED
│
▼
AWAITING_CHANNEL(拉起收银台/三方SDK)
│ 使用 payToken 拉起微信/支付宝
│
├── CHANNEL_OPENED ──→ PROCESSING
├── CHANNEL_FAIL ──→ FAILED(可换渠道重试)
└── USER_CANCEL ──→ CANCELLED
│
▼
PROCESSING(支付处理中)
│ 轮询/SSE 查询 /api/order/status
│
├── STATUS=SUCCESS ──→ SUCCESS(终态)
├── STATUS=FAILED ──→ FAILED(终态)
└── TIMEOUT/NETWORK ──→ UNKNOWN
│
│ 持续查单收敛
│
├──→ SUCCESS
└──→ FAILED
终态:SUCCESS / FAILED / CANCELLED
不确定态:UNKNOWN(必须收敛到终态)
FSM核心规则:
· 终态不可迁移(SUCCESS 后任何事件都被拒绝)
· UNKNOWN 必须能收敛(查单机制)
· 每次迁移都记录日志(可追溯)
# 7.4 支付安全
从公理二(客户端不可信)推导的安全清单:
数据安全:
· 敏感信息脱敏:银行卡显示后四位,姓名掩码
· 支付金额、商户名强展示(防替换/中间人)
· 日志/埋点不记录完整卡号、CVV
通信安全:
· 全链路 HTTPS + HSTS
· 接口签名(ts + nonce + sign),防重放防篡改
· APP端 SSL证书固定(防中间人)
页面安全:
· CSP(限制脚本来源,防XSS)
· X-Frame-Options: DENY(防点击劫持)
· Referrer-Policy(防信息泄露到外域)
APP端额外安全:
· 禁用截屏/录屏(FLAG_SECURE)
· 水印(用户ID + 时间戳)
· Root/越狱/模拟器检测 → 风控拦截
# 第八章:业务场景五——支付后导航与订单全生命周期
本章视角:支付成功不是终点,"如何优雅地让用户离开支付链路"同样重要。
# 8.1 本质问题
支付后导航的本质问题是:如何让用户从"支付链路"中干净地退出,不会误入已经过期的中间页面。
# 8.2 从第一性原理分析"回退问题"
问题推导:
· 公理三 → 用户支付后焦虑,会习惯性点返回
· 浏览器 history 栈记录了:首页→列表→详情→结算→收银台→结果页
· 用户点返回 → 回到收银台 → 收银台可能重新加载 → 触发二次支付
· 即使有幂等保护不会重复扣款,也会造成状态混乱和体验恶化
根本原因:
· 结算页和收银台是"中间态页面",不应该出现在回退路径中
· 用户完成支付后,这些页面已经"过期",不应该再被访问
解决思路:
· 支付链路的跳转用 replace 而非 push
· 结果页做导航守卫,防止回退到已过期的页面
· 提供"正确的"下一步导航入口
# 8.3 方案设计
定律三(确定性终态)→ 路由清理:
支付链路的路由策略:
正常浏览路径(push,保留 history):
首页 → 列表 → 详情
history: [首页, 列表, 详情]
交易链路(replace,不保留 history):
详情/购物车 → 结算页(push 或 replace,取决于业务)
结算页 → 收银台(replace!结算页不应回退)
收银台 → 结果页(replace!收银台不应回退)
最终 history:
[首页, 列表, 详情, 结果页]
用户点返回 → 详情 → 列表 → 首页
✅ 不会经过收银台和结算页
关键点:
· replace 的本质 = "这个页面不需要被回退到"
· 交易中间页(结算/收银台)都是一次性页面
· 只有结果页和正常浏览页需要保留在 history 中
结果页导航守卫:
设计目标:即使用户按物理返回键,也不能回到支付链路中间页
方案1:行动按钮优先 + 不提供返回
结果页只提供两个大按钮:
· "查看订单" → navigate('/orders/' + orderId, { replace: true })
· "返回首页" → navigate('/', { replace: true })
不提供返回按钮,引导用户使用以上按钮
方案2:popstate 拦截兜底
// 监听浏览器返回
window.addEventListener('popstate', (event) => {
// 用户按了返回键
// 强制跳转到安全页面(订单详情或首页)
const target = getSafeReturnTarget();
window.history.replaceState(null, '', target);
navigate(target);
});
方案3:history 缓冲层
// 进入结果页时注入一个"缓冲历史记录"
window.history.pushState({ buffer: true }, '');
// 用户第一次返回 → 消耗缓冲,展示引导提示
// 用户第二次返回 → 才真正返回到安全页面
源路由感知:
// 下单时记录来源,决定"返回"去哪
sessionStorage.setItem('payment_source', source);
// 从购物车来 → 回首页(购物车已清空)
// 从商品详情来 → 回首页
// 从订单列表来 → 回订单列表
// 从秒杀来 → 回首页
支付结果页的清理职责:
支付成功进入结果页时,必须执行的清理:
1. 停止所有轮询/定时器
· 支付状态轮询 → 停止
· 排队订阅 → 关闭 SSE/WebSocket
· 防止内存泄漏和无效请求
2. 清理支付相关状态
· localStorage 中的 payAttemptId → 清除
· 内存中的 isSubmitting → 释放
· BroadcastChannel → 关闭
3. 清理交易中间页的状态
· 结算页的表单数据 → 清除(防止回退后残留)
· 收银台的渠道选择 → 清除
4. 路由清理
· 用 replace 进入结果页
· 确保 history 中没有中间页
这些清理看似琐碎,但每一条都是防止"边界情况下状态混乱"的必要措施。
# 8.4 订单全生命周期
订单状态流转(从服务端视角定义,前端只展示):
待付款(PENDING_PAYMENT)
· 已创建,库存已锁定
· 前端展示:支付倒计时 + "去支付"按钮
· 超时(30分钟)→ 自动关闭,释放库存
待发货(PAID)
· 支付成功
· 前端展示:等待商家发货
待收货(SHIPPED)
· 已发货
· 前端展示:物流跟踪 + "确认收货"
已完成(COMPLETED)
· 确认收货 / 超时自动确认
· 前端展示:评价入口 + 再次购买 + 售后入口
已关闭/已取消(CLOSED/CANCELLED)
· 超时关闭 / 用户取消
· 库存已释放
售后中(AFTER_SALE)
· 退款/退货申请处理中
· 前端展示:售后进度
订单列表设计要点:
· SWR 缓存 + staleTime=0(每次进入刷新)
· 但用 keepPreviousData(切Tab时保留旧数据,新数据到了再覆盖)
· 待付款订单展示倒计时
· 多Tab切换用 AbortController 取消旧请求
· 每个 Tab 独立分页状态
订单详情设计要点:
· 状态进度条(可视化流转)
· 待付款倒计时(复用秒杀倒计时方案)
· 物流时间线
· 价格明细完整展示
· 按状态动态显示操作按钮
# 第九章:整体架构蓝图
# 9.1 技术架构分层图
┌──────────────────────────────────────────────────────────────┐
│ 用户终端 │
│ H5 (Next.js) | 小程序 (Taro) | APP (Webview) │
├──────────────────────────────────────────────────────────────┤
│ 视图层 │
│ │
│ 页面组件 | 业务组件 | 基础UI组件 | 模板 │
│ │
│ 渲染策略: │
│ SSG (首页/秒杀壳) | SSR (列表/搜索) | ISR (详情) | CSR (交易)│
│ Islands Architecture (部分水合,减少JS开销) │
├──────────────────────────────────────────────────────────────┤
│ 状态管理层 │
│ │
│ ┌──────────┐ ┌───────────┐ ┌───────────────────────┐ │
│ │ Zustand │ │ SWR/Query │ │ XState (状态机) │ │
│ │ 全局状态 │ │ 服务端状态 │ │ 下单FSM | 支付FSM │ │
│ │ 用户/购物车│ │ 商品/订单 │ │ 倒计时/库存锁定 │ │
│ └──────────┘ └───────────┘ └───────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ 请求层 (Request SDK) │
│ │
│ 拦截器链 │
│ ┌────────┬────────┬───────────────┬────────┬────────┐ │
│ │ auth │ trace │ idempotency │ retry │ sign │ │
│ │Token注入│链路追踪│ 幂等Key注入 │重试策略 │ 签名 │ │
│ └────────┴────────┴───────────────┴────────┴────────┘ │
│ │
│ 调度器 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 全局并发上限 | 优先级队列(P0/P1/P2) | 熔断器 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ 响应处理链 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 业务码路由 | Token静默刷新 | 幂等去重 | 错误标准化│ │
│ └──────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ 缓存层 │
│ │
│ ┌────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐ │
│ │ CDN │ │ Service │ │ SWR │ │IndexedDB │ │
│ │ 边缘缓存│ │ Worker │ │ 内存缓存 │ │持久化缓存 │ │
│ │ 静态资源│ │ App Shell │ │stale- │ │购物车/ │ │
│ │ HTML/JS│ │ 离线兜底 │ │while- │ │支付状态 │ │
│ │ │ │ SWR策略 │ │revalidate│ │ │ │
│ └────────┘ └──────────────┘ └──────────┘ └──────────┘ │
├──────────────────────────────────────────────────────────────┤
│ 安全层 │
│ │
│ 接口签名(ts+nonce+sign) | 敏感信息脱敏 | CSP | HSTS │
│ 防重放 | 防篡改 | 防爬虫 | 风控(设备指纹+行为检测) │
├──────────────────────────────────────────────────────────────┤
│ 监控层 │
│ │
│ 错误监控 | 性能RUM | 业务埋点(转化漏斗) | 告警 | 会话回放 │
├──────────────────────────────────────────────────────────────┤
│ BFF 层 (Node.js) │
│ │
│ 接口聚合 | 数据裁剪 | SSR渲染 | 缓存代理 | 限流降级 │
├──────────────────────────────────────────────────────────────┤
│ 后端微服务 │
│ │
│ 商品服务 | 交易服务 | 支付服务 | 用户服务 | 营销服务 │
└──────────────────────────────────────────────────────────────┘
# 9.2 交易核心链路完整时序
秒杀 → 下单 → 支付 → 结果 全链路时序
用户 前端 BFF/API 后端微服务 三方支付
│ │ │ │ │
│ 进入秒杀会场 │ │ │ │
│──────────────→│ │ │ │
│ │ 预申请资格令牌 │ │ │
│ │──────────────────→│─────────────────→│ │
│ │ purchaseToken │ │ │
│ │←──────────────────│←─────────────────│ │
│ │ │ │ │
│ 点击"立即抢购"│ │ │ │
│──────────────→│ │ │ │
│ │ ① 生成幂等Key │ │ │
│ │ ② 按钮状态锁 │ │ │
│ │ ③ BroadcastChannel │ │ │
│ │ ④ POST /seckill │ │ │
│ │ X-Idempotency-Key│ │ │
│ │──────────────────→│─────────────────→│ │
│ │ │ 原子扣库存 │ │
│ │ │ 创建订单 │ │
│ │ │←─────────────────│ │
│ │ {orderId} │ │ │
│ │←──────────────────│ │ │
│ │ │ │ │
│ │ replace → 结算页 │ │ │
│ 确认下单 │ │ │ │
│──────────────→│ │ │ │
│ │ ① 生成下单Key │ │ │
│ │ ② POST /order/create │ │
│ │ (只传items+地址+券,不传价格!) │ │
│ │──────────────────→│─────────────────→│ │
│ │ │ 锁定库存 │ │
│ │ │ 设过期时间(30min) │ │
│ │ │←─────────────────│ │
│ │ {orderId,expireAt}│ │ │
│ │←──────────────────│ │ │
│ │ │ │ │
│ │ replace → 收银台 │ │ │
│ 确认支付 │ │ │ │
│──────────────→│ │ │ │
│ │ ① 生成payAttemptId │ │ │
│ │ ② POST /pay/create│ │ │
│ │ X-Idempotency-Key│ │ │
│ │──────────────────→│─────────────────→│ │
│ │ │ 创建支付单 │ │
│ │ │ 签发payToken │ │
│ │ │←─────────────────│ │
│ │ {payId,payToken} │ │ │
│ │←──────────────────│ │ │
│ │ │ │ │
│ │ 拉起三方支付 │ │ │
│ │────────────────────────────────────────────────────────→│
│ │ │ │ 扣款处理 │
│ 输入支付密码 │ │ │←───────────────│
│──────────────────────────────────────────────────────────────────────→ │
│ │ │ │ 支付成功 │
│ │ │ │←───────────────│
│ │ │ │ 回调更新订单 │
│ │ │ │ │
│ │ 轮询订单状态 │ │ │
│ │ GET /order/status │ │ │
│ │──────────────────→│─────────────────→│ │
│ │ │ │ 返回SUCCESS │
│ │ │←─────────────────│ │
│ │ {status:SUCCESS} │ │ │
│ │←──────────────────│ │ │
│ │ │ │ │
│ │ replace → 结果页 │ │ │
│ │ ① 停止所有轮询 │ │ │
│ │ ② 清理支付状态 │ │ │
│ │ ③ 展示成功+导航 │ │ │
│ │ │ │ │
│ 查看订单 │ │ │ │
│──────────────→│ replace → 订单详情 │ │ │
│ │ │ │ │
│ 按返回键 │ │ │ │
│──────────────→│ popstate守卫 │ │ │
│ │ → replace到安全页 │ │ │
│ │ ✅ 不回退到收银台 │ │ │
# 9.3 关键决策矩阵
| 决策点 | 选项 | 决策 | 理由(基于哪条定律) |
|---|---|---|---|
| 倒计时时间源 | 本地时间 / 服务端时间 | 服务端时间 + offset | 定律一:服务端是事实源 |
| 下单防重 | debounce / 状态锁 | 状态锁 + 幂等Key | 定律二:debounce会吃掉合法重试 |
| 支付超时处理 | 判失败 / 判处理中 | 判处理中 + 查单收敛 | 定律三:超时≠失败 |
| 首页数据加载 | 全量等待 / 分层渐进 | 分层渐进(SSG→缓存→最新) | 定律四:永远有内容展示 |
| 推荐接口挂了 | 全页报错 / 隐藏推荐 | 隐藏推荐,不影响核心 | 定律五:核心链路隔离 |
| 下单价格 | 前端传 / 后端算 | 后端算,前端只传意图 | 定律一+定律六:不信任客户端 |
| 支付链路跳转 | push / replace | replace(中间页不留history) | 定律三:防止回退到过期页面 |
| 倒计时渲染 | 框架Diff / 原生DOM | 按数量分级,极限用原生DOM | 定律五:渲染资源隔离 |
# 第十章:迭代演进路线
# 设计理念
迭代不是"先做功能A再做功能B",而是先建立定律的基础设施,再逐步在上面构建业务场景。每个阶段完成后,定律的工程化落地更加完善。
# Phase 1:定律基础设施
目标:建立六条定律的工程化基础
定律一落地:
· 时间同步SDK(offset计算 + 单调时钟 + 动态校准)
· 状态查询机制(/api/status 统一接口规范)
定律二落地:
· 幂等Key管理器(生成、复用、持久化、清理)
· 请求拦截器自动注入Idempotency-Key
定律三落地:
· XState 集成(交易状态机框架)
· 查单收敛机制(轮询 + 超时兜底)
定律四落地:
· SSG/SSR 基础框架
· 骨架屏组件库
· Service Worker 缓存基础
定律五落地:
· 请求调度器(优先级队列 + 并发控制 + 熔断)
· 组件级 ErrorBoundary
定律六落地:
· 接口签名SDK(ts + nonce + sign)
· 敏感信息脱敏组件
交付标准:
· 六条定律的工程化基础全部就位
· 核心链路可跑通(浏览→下单→支付)
· 幂等率 100%,状态可收敛
# Phase 2:性能与体验
目标:定律四(永远有内容展示)和定律五(核心隔离)的深度优化
首页性能:
· BFF 接口聚合
· 图片优化体系(WebP/AVIF + srcset + LQIP + CLS防护)
· 二次进入秒开(SW + SWR缓存)
· 性能预算 + CI门禁
· RUM监控(按网络/机型分桶)
弱网/断网:
· 网络分级策略
· 断网离线展示
· 操作暂存与恢复
列表性能:
· 无限滚动 + 虚拟列表
· 滚动位置恢复
· 竞态防护
交付标准:
· 首页 FCP < 1.0s / LCP < 2.0s(WiFi)
· 弱网(4G) LCP P75 < 3.0s
· 二次进入 < 300ms
# Phase 3:交易核心深化
目标:秒杀→下单→支付全链路的极致可靠性
秒杀模块:
· 倒计时组件(时间同步 + 原生DOM渲染)
· 秒杀按钮状态机(完整9状态)
· 资格令牌预申请
· 排队削峰(SSE/WebSocket)
· 风控配合(设备指纹 + 行为检测 + 验证码)
下单模块:
· 下单FSM(XState)
· 库存锁定与超时释放
· 地址变更竞态防护
· 价格防篡改
支付模块:
· 支付FSM(9状态完整模型)
· 双Token防重体系
· 超时查单收敛
· 支付渠道容灾
· 支付安全加固
交付标准:
· 幂等率 100%(压测验证)
· 支付状态最终一致
· 倒计时精度误差 < 100ms
· 安全审计通过
# Phase 4:全链路体验闭环
目标:支付后导航 + 订单全生命周期 + 购物车
支付后导航:
· 路由 replace 策略
· 结果页导航守卫
· 源路由感知返回
· 状态清理机制
订单管理:
· 订单列表(多Tab + SWR缓存)
· 订单详情(状态进度条 + 倒计时 + 物流跟踪)
· WebSocket状态推送
购物车:
· 本地+服务端同步
· 离线编辑 + 联网同步
· 失效商品处理
售后模块:
· 退款/退货流程
· 售后进度状态机
交付标准:
· 支付后回退不触发二次支付
· 订单状态展示准确
· 全链路体验闭环
# Phase 5:安全与监控
目标:定律六(防御纵深)的完善 + 定律三(可观测性)
安全加固:
· XSS体系化治理
· CSRF防护
· 敏感信息脱敏(全链路)
· 防爬虫策略
· 日志/埋点脱敏
监控完善:
· 全链路 traceId 贯通
· 性能告警(P75退化阈值)
· 业务告警(下单/支付失败率)
· 会话录屏回放
· 应急响应(远程开关 + 灰度回滚)
交付标准:
· 安全扫描无高危
· 核心链路告警覆盖 100%
· 合规审计通过
# Phase 6:持续演进
目标:长期可维护,持续迭代
· 微前端拆分(按业务域独立部署)
· E2E测试覆盖核心链路
· 性能基准自动化回归
· A/B实验框架
· AI辅助开发
· 边缘计算(Edge SSR)
# 迭代节奏
Phase 1 Phase 2 Phase 3 Phase 4 Phase 5 Phase 6
定律基础设施 性能体验 交易深化 全链路闭环 安全监控 持续演进
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
[基础] [快] [准] [完整] [安全] [演进]
每个Phase的验收维度:
· 功能:核心场景跑通
· 性能:指标达标不回退
· 安全:无高危风险
· 质量:CR通过 + 测试覆盖
# 附录:设计定律速查与决策树
# 六条设计定律
| 定律 | 核心表述 | 一句话 |
|---|---|---|
| 定律一 | 服务端是唯一事实源 | 时间、价格、库存、状态,一切以服务端为准 |
| 定律二 | 幂等是一切可靠性的基石 | 每个交易操作执行一次和多次效果相同 |
| 定律三 | 任何操作必须有确定性终态 | 前端永远不"猜"结果,只"查"结果 |
| 定律四 | 永远给用户"看到什么" | 有骨架的3秒 > 白屏的2秒 |
| 定律五 | 核心链路与非核心链路物理隔离 | 推荐挂了不影响下单 |
| 定律六 | 防御纵深,不信任任何单层 | 多层校验,层层兜底 |
# 架构决策树
遇到新场景时的决策路径:
Q: 这个操作涉及交易/金钱吗?
├── YES → 应用定律二(幂等)+ 定律三(确定性终态)+ 定律六(防御纵深)
│ ├── 需要幂等Key吗? → YES(所有写操作)
│ ├── 需要状态机吗? → YES(有多步骤流转的场景)
│ ├── 超时怎么处理? → 进入UNKNOWN,查单收敛
│ └── 前端传什么参数? → 只传意图,不传计算结果(价格等)
│
└── NO → 应用定律四(分层展示)+ 定律五(优先级隔离)
├── 数据加载策略? → 骨架屏 → 缓存 → 最新数据
├── 接口失败怎么办? → 降级展示(隐藏模块/默认值)
└── 请求优先级? → P0核心 > P1业务 > P2辅助
# 从公理到定律到方案的完整推导链
公理 定律 方案
───────────── ───────────── ──────────────
公理一:网络不可靠 ──→ 定律二:幂等 ──→ 幂等Key管理器
公理四:交易不可逆 双Token防重体系
公理一:网络不可靠 ──→ 定律三:确定性终态 ──→ 状态机(FSM)
公理三:焦虑放大 查单收敛机制
UNKNOWN态处理
公理二:客户端不可信 ──→ 定律一:服务端为准 ──→ 时间同步(offset)
公理四:交易不可逆 价格防篡改
库存服务端校验
公理三:感知主观 ──→ 定律四:永远有内容 ──→ 骨架屏体系
公理五:资源有限 分层缓存(SW/SWR)
渐进式渲染
公理四:交易不可逆 ──→ 定律五:核心链路隔离 ──→ 请求优先级队列
公理五:资源有限 组件级降级
原生DOM逃生舱
公理二:客户端不可信 ──→ 定律六:防御纵深 ──→ 多层校验链
公理四:交易不可逆 安全SDK
接口签名
文档版本:v2.0(第一性原理版) 方法论:从物理约束与业务本质出发,推导设计原则,再落地工程方案 最后更新:2026-08-10 关联文档:interview2026.md | ecommerce-frontend-architecture.md(场景驱动版)