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(场景驱动版)

上次更新: 9/11/2026, 7:47:52 PM
最近更新
01
interview202601
09-06
02
20-尾声正文
09-01
03
19-第十五章正文
09-01
更多文章>