Study Skills · Expanded Lesson

软件工程两小时入门:从页面到可靠交付

一张宏观地图、六条工程主线、一道综合排错题。重点不是背框架,而是建立“对象—状态—规则—异常—证据”的工程判断框架。

软件工程 · Web · 小程序 · 手机应用120 分钟核心路线图解 v2 · 可读性检查版
本页目录
Primer

① 先认核心术语

先认词:这 12 个词会贯穿全课。先看一句话直觉,细节在后面主线中展开。

Software Engineering · 软件工程把需求、设计、实现、验证、发布和演进组织成可检查的过程。
直觉
不是“会写代码”,而是让软件持续、可靠地满足目标。
边界
代码只是交付链的一部分。
Client / Frontend · 客户端 / 前端用户直接操作的一端:浏览器、小程序和手机 App 都可以是客户端。
当前案例
教练排课页、运动员训练页都属于客户端。
Server / Backend · 服务端 / 后端接收请求、执行可信业务规则、访问数据库并连接外部服务的一端。
边界
关键权限和资金规则不能只放在前端。
Application Programming Interface (API) · 应用程序接口两个程序约定怎样调用、传什么、返回什么。
当前案例
“发布训练计划”“回填成绩”都应对应明确接口合同。
State · 状态描述某个对象此刻情况的信息。
边界
界面状态、请求状态、业务状态不是同一件事。
Invariant · 不变量无论经历什么合法操作都必须保持成立的条件。
当前案例
一次成功支付只能入账一次;其他机构不能读取本机构数据。
Persistence / Database · 持久化 / 数据库把需要长期存在的事实可靠保存下来。
边界
手机本地保存成功不等于服务器已经接受。
Authentication · 身份认证回答“你是谁”。
边界
登录成功不等于有权访问所有资源。
Authorization · 授权 / 鉴权回答“这个身份能否对这个对象执行当前操作”。
当前案例
教练只能修改自己有管理关系的队伍与训练计划。
Transaction · 事务一组关键数据库操作要么整体成立,要么整体不成立。
当前案例
记账与更新余额不能只完成一半。
Idempotency · 幂等性同一个业务动作被重复送达时,不应重复产生副作用。
当前案例
支付平台重复发 Webhook,余额仍只能增加一次。
Verification / Acceptance · 验证 / 验收验证证明技术条件;验收由有权主体决定是否接受结果。
边界
测试全绿、构建成功、推到 main 都不自动等于线上验收完成。
Overview

② 十分钟全景:软件系统不是页面集合

两小时目标:不追求记住所有框架名,而是看到一个功能时,能回答五个问题:谁发起?谁有权?权威数据在哪里?失败怎么恢复?什么证据能证明完成?

统一案例:教练发布一周训练计划 → 运动员手机执行并回填 → 系统生成分析 → 按服务用量计费 → 教练复盘。下面所有主线都围绕这一个闭环。
时间主线结束后能回答
0–10宏观地图页面、后端、数据库、外部平台是什么关系?
10–30界面与请求点按钮后,请求和状态怎样流动?
30–50数据与状态机怎样避免半写入、覆盖和状态越迁?
50–65身份、权限与多租户怎样证明“这个人能改这份数据”?
65–85异步、支付与计费超时、重复通知时怎样不多扣钱?
85–100小程序与手机 App断网、后台、权限、旧版本为什么更难?
100–110开发、测试与发布代码写完以后还缺哪些证据?
110–120综合排错把“看似成功”拆成可验证结论。

宏观模型:客户端负责收集意图与展示反馈;后端负责可信规则;数据库保存权威事实;队列与外部平台完成可重试的后续动作;测试、日志和线上读回负责提供证据。

用户意图→客户端→API→授权 + 业务规则→数据库→异步 / 外部平台→验证与观测
Map

③ 先把知识放进坐标系

分类关系与学习依赖要分开。 分类上,本课属于软件工程中的应用工程;学习依赖上,先理解请求和状态,再理解数据、权限、支付与交付。

观察轴核心问题常见术语
界面用户现在看到什么、能做什么?HTML、CSS、JavaScript、Component、State
通信请求如何从客户端到服务端?HTTP、HTTPS、JSON、API、Webhook
数据事实如何保存且不互相覆盖?Schema、CRUD、Transaction、Lock、Migration
权限谁能对哪个对象做什么?Authentication、Authorization、RBAC、Tenant
可靠性重复、失败、超时如何恢复?Retry、Idempotency、Queue、Audit Log
交付如何证明改动真的在线且正确?Git、Build、QA、E2E、CI/CD、Observability、Rollback
Before Deep Dive · 阅读约定

先学会读本课里的“变量”和伪代码

后面会出现很多像 user_id、tenant_id、version、artifact_digest 这样的名字。它们不是要求你提前会编程,而只是给某个值起的名字。先把这些最基本的写法认清,后面的推理会轻松很多。

写法先把它理解成什么例子
variable / 变量一个“有名字的值”。名字帮助我们在推理时指代某个东西。version = 7 表示当前版本号是 7。
xxx_id某个对象的唯一标识,不是对象本身。user_id 是“这个用户是谁”的编号;plan_id 是某份计划的编号。
=在伪代码里通常读作“等于 / 由……得到”;在赋值代码里也常表示“把右边的值放进左边变量”。View = render(props, state) 读成“界面结果由 render 根据 props 与 state 生成”。
AND逻辑“并且”。所有条件都必须成立。A AND B:A 和 B 都为真才算通过。
OR逻辑“或者”。至少一个条件成立即可。owner OR admin:是 owner 或 admin 任一即可。
→流程顺序,“先发生左边,再进入右边”。点击 → 请求 → 后端 → 数据库。
v3 / v7 / v8version 的缩写,表示对象的第几个版本。v7 就是“版本 7”。
?(SQL 中)参数占位符。真正执行时再把具体值安全地放进去。content = ? 表示“这里稍后传入真实 content 值”。
200 / 403 / 409 / 500HTTP 状态码,表示一次网络请求的结果类别。200 成功;403 无权限;409 冲突;500 服务端错误。
UUIDUniversally Unique Identifier,通用唯一标识符。可以理解为几乎不会重复的随机身份证。手机离线先生成 local_log_uuid,联网后用它识别“这还是同一条训练记录”。
本课规则:后面每个 Deep Dive 都会先给“本节先认词 / 先认变量”,再使用这些词做推理。看到代码块时,不要求你会写代码;目标只是能把每一个名字翻译回业务含义。
Deep Dive 01 · 约 18–22 分钟

深挖一:一个按钮点击以后,前端到底发生了什么?

0. 本节先认词 / 先认变量

词或变量中文直觉本节为什么需要它
Component · 组件页面里可以独立复用的一块界面与逻辑。“发布按钮”“动作编辑卡”“训练日面板”都可以是组件。
props父组件传进来的外部输入,可以理解为“别人给我的参数”。例如父页面把 plan_id、运动员名字传给子组件。
state组件当前自己记住的状态,可以理解为“我现在是什么情况”。例如 Drawer 是否打开、输入框内容、请求是否 loading。
render · 渲染根据当前数据计算“现在应该显示什么界面”的过程。state 变化后,页面需要重新算一遍应该显示什么。
View最终呈现给用户看的界面结果。后面的公式左边只是给“当前界面”起一个名字。
Request · 请求客户端发给服务器的一次消息。点击“发布”后,前端要把“我要发布这份计划”的意图发给后端。
Response · 响应服务器处理请求后回给客户端的结果。前端必须根据 Response 判断成功、冲突还是失败。
Route / Routing · 路由把一个 URL 或导航目标映射到某个页面。决定“用户现在在哪个页面”,不是决定页面内部怎么排版。
HTTPHyperText Transfer Protocol,超文本传输协议;Web 客户端和服务器常用的通信协议。后面的 Method、URL、Status Code 都属于 HTTP 请求/响应的一部分。
Endpoint · 接口地址后端提供某项能力的具体网络入口。例如 POST /api/plans/123/publish。

1. 先把页面理解成“状态 → 界面”的函数

现在再看下面这句:

View = render(props, state)

逐个翻译:

  • View:用户现在看到的界面。
  • render:根据数据生成界面的过程。
  • props:外部传给组件的数据。
  • state:组件当前自己保存的状态。

所以整句不是让你算数学题,而是在说:“页面现在长什么样,由外部输入和当前状态共同决定。”

例如教练正在编辑一周计划,至少同时存在三类状态:

  • 界面状态:周三是否选中、动作 Drawer 是否打开、筛选器是什么。
  • 请求状态:保存请求当前是 idle(还没发)、loading(正在等)、success(服务器确认成功)、error(失败)。
  • 业务状态:这份计划究竟是 draft、published、archived,或者服务器里已经变成了新版本。

最关键的边界是:业务状态不能只听前端自己的。 前端可以临时显示“正在发布”,但最终“是否真的发布成功”要以服务端权威状态为准。

计划要求 · props(props):外层页面提供的计划数据,不由这次填写动作改写。来源:父组件传入;例子:要求 4 组。

已填写 · state(state):训练卡本地暂存的填写进度。来源:当前组件维护;例子:已填写 2 组,随后 3 组。

计算界面 · render(render):根据外部输入与本地状态计算界面。来源:前端渲染过程;例子:输入 4 和 3,显示 3/4。

显示结果 · View(View):用户当前看到的结果。来源:渲染结果;例子:2/4 → 3/4。

下面按具体值读图。图是教学示例,不是生产系统运行记录。

图解 · 教学示例计划仍是 4 组,为什么界面从 2/4 变成 3/4?

左右对照同一行:先看计划要求,再看本地填写进度,最后看显示结果。用户在这两刻之间又填写了一组。

计划仍是 4 组,为什么界面从 2/4 变成 3/4?外部计划保持不变,本地填写进度变化,重新渲染后显示结果随之变化。 ① 再填一组之前 计划要求 · props要求 4 组 已填写 · state已填写 2 组 计算界面 · render根据 4 和 2 计算 显示结果 · View已填写 2/4 组 ② 再填一组之后 计划要求 · props要求 4 组 已填写 · state已填写 3 组(变化) 计算界面 · render根据 4 和 3 重新计算 显示结果 · View已填写 3/4 组
  1. ① 再填一组之前

    计划要求 · props:要求 4 组

    已填写 · state:已填写 2 组

    计算界面 · render:根据 4 和 2 计算

    显示结果 · View:已填写 2/4 组

  2. ② 再填一组之后

    计划要求 · props:要求 4 组

    已填写 · state:已填写 3 组(变化)

    计算界面 · render:根据 4 和 3 重新计算

    显示结果 · View:已填写 3/4 组

查看完整图(可横向滚动)
计划仍是 4 组,为什么界面从 2/4 变成 3/4?外部计划保持不变,本地填写进度变化,重新渲染后显示结果随之变化。 ① 再填一组之前 计划要求 · props要求 4 组 已填写 · state已填写 2 组 计算界面 · render根据 4 和 2 计算 显示结果 · View已填写 2/4 组 ② 再填一组之后 计划要求 · props要求 4 组 已填写 · state已填写 3 组(变化) 计算界面 · render根据 4 和 3 重新计算 显示结果 · View已填写 3/4 组

结论:外部计划保持不变,本地填写进度变化,重新渲染后显示结果随之变化。

边界:图只描述组件中的本地界面变化,不表示已经发送请求,也不证明服务器保存成功。

2. 路由、布局、滚动是三个不同问题

Routing 回答“在哪个页面”;Layout 回答“页面内部怎么排”;Scroll Ownership 回答“内容超出以后到底哪一层滚动”。

场景更合理的布局原因
设置页、长表单content内容多就自然向下长,让浏览器页面滚
聊天、地图、IDEfill工作区需要占满视口,让内部区域滚
统计卡片矩阵Grid它是二维行列关系
Header + Sidebar + MainFlex / Grid固定区域与剩余空间关系清楚

fill 和 content 是项目语义,不是 CSS 官方关键字。你真正要问的是:这个页面是在“读一份会自然增长的内容”,还是在“操作一个需要占满视口的工作台”?

3. 点击“发布”之后发生了什么

先认下面链路里的三个新词:Reverse Proxy(反向代理)是服务端前门;Authentication(身份认证)回答“你是谁”;Authorization(授权)回答“你能不能做这件事”。后面第三章会细讲,这里只看它们在流程中的位置。

用户点击
→ 前端读取 state
→ 构造 HTTP Request
→ 浏览器发送请求
→ Reverse Proxy / API Gateway
→ 后端 Route
→ Authentication
→ Authorization
→ Business Rule
→ Database Transaction
→ HTTP Response
→ 前端更新 request state
→ 显示权威业务状态

4. HTTP Request 里每个变量是什么意思

字段意思示例
Method这次请求想做哪类动作GET 读取;POST 创建/触发动作
URL / Endpoint请求要去哪/api/plans/123/publish
Headers附加的元信息身份 Token、Content-Type
Body请求真正携带的数据备注、目标版本等
Status Code服务器告诉客户端的结果类别200、403、409、500

常见状态码也先翻译成人话:200 大体成功;400 输入本身有问题;401 没有有效身份;403 有身份但没权限;409 当前状态/版本冲突;500 服务端内部出错。

5. 错误提示真正解决的是“用户下一步做什么”

错误真正含义下一步
输入错误用户可以修保留草稿并定位字段
403当前身份没有权限停止提交,不给“重试”假希望
409你手里的版本旧了拉取新版本并比较
Timeout没及时收到结果,不等于没执行查询结果或安全重试
500后端失败保留输入,给错误追踪 ID
练习:用户点“发布”后 8 秒没响应。为什么“直接再点一次”可能危险?

答案:因为客户端只知道“没收到 Response”,不知道服务端是否已经执行。如果第一次其实已经写库并发通知,第二次可能重复产生副作用。后面支付章节会进一步解释“幂等”如何解决这类问题。

Deep Dive 02 · 约 18–22 分钟

深挖二:数据库不是仓库,而是“守住事实”的机器

0. 本节先认词 / 先认变量

词或变量先把它理解成什么例子
Database · 数据库长期保存结构化事实,并负责约束、并发和事务的系统。保存用户、训练计划、订单、训练记录。
Table / Row / Column表 / 行 / 列。表像一类对象,行像一条记录,列像对象的某个属性。plans 表里一行可以是一份训练计划。
Schema数据库结构定义:有哪些表、字段、类型和约束。规定 plan_id、status、version 等字段。
id / Primary Key一条记录自己的唯一身份证。plan_id=123 表示第 123 号计划。
Foreign Key一条记录引用另一条记录的关系。训练记录里的 plan_version_id 指向实际执行的计划版本。
Constraint · 约束数据库自己强制执行的规则。某字段不能为空;某业务键必须唯一。
Transaction · 事务把多步数据库变化当成一个不可分割动作。记账和扣额度要么一起成功,要么一起失败。
status对象当前业务状态。DRAFT、PUBLISHED。
version对象版本号,用来区分“我看到的是第几版”。version=7 表示当前第 7 版。
State Machine · 状态机规定有哪些状态,以及哪些状态之间允许迁移。DRAFT → PUBLISHED 可以;某些反向迁移不允许。
Race Condition · 竞态条件两个并发操作因为执行先后不同,产生了不同或错误结果。两名教练同时改同一份计划。
Migration · 数据库迁移让数据库结构从旧版本安全升级到新版本。给 plans 新增一个字段并处理旧数据。

1. CRUD 只是最表层

CRUD 是 Create(创建)、Read(读取)、Update(更新)、Delete(删除)。它告诉你数据库能做哪四类基础动作,但没有回答“哪些数据可以更新、什么时候能删、两个人一起更新怎么办”。

以训练 SaaS 为例,可以把三个对象分开:

  • plans:计划这个业务对象的“当前入口”。
  • plan_versions:每次发布/修订后形成的历史版本。
  • training_logs:运动员真实执行后产生的记录。

这里 plans、plan_versions、training_logs 只是示例表名。重点是:“计划本身”“计划的版本”“实际执行记录”不是同一个对象。

2. 不变量:真正要守的是规则,而不只是字段

Invariant(不变量)可以理解为:无论系统经历多少合法操作,都必须一直成立的条件。

  • 已经发布的历史版本不能被偷偷改写。
  • 训练记录必须知道自己执行的是哪个计划版本。
  • 同一个支付事件不能生成两条有效账本。
  • 一个机构的数据不能被另一个机构的普通账号读到。

这些规则如果只写在前端 if 里,很脆弱;越关键的不变量,越应该由数据库约束、唯一索引、事务和服务端规则一起兜底。

3. Transaction 为什么解决“一半成功”

先认代码里的两个关键词:BEGIN 表示“事务开始”,COMMIT 表示“确认提交”。

BEGIN
INSERT usage_event ...
INSERT ledger_entry ...
UPDATE quota ...
UPDATE invoice ...
COMMIT

逐行翻译:

  • usage_event:记录这次到底用了多少服务。
  • ledger_entry:账本里新增一条金额/额度变化记录。
  • quota:用户剩余额度。
  • invoice:账单。

如果这四件事在业务上是一体的,就不希望只做完前两件服务器就挂掉。Transaction 的作用就是尽量让它们表现为“整体成功”或“整体失败”。

4. ACID 四个字母先翻译成人话

字母全称人话
AAtomicity · 原子性不留半成品
CConsistency · 一致性操作前后都不能违反约束
IIsolation · 隔离性并发事务尽量不要互相踩坏
DDurability · 持久性一旦提交,结果应可靠保存

5. 状态机不是“有个 status 字段”

DRAFT
  ↓ publish
PUBLISHED
  ↓ execute
IN_EXECUTION
  ↓ complete
COMPLETED
  ↓ review
REVIEWED

这里 publish、execute、complete、review 是“触发状态变化的动作”。状态机真正规定的是:哪些动作在当前状态下合法。

所以“已发布以后为了方便继续改,直接把 status 改回 DRAFT”通常很危险。更合理的是保留已发布的 v7,再创建新的 draft v8。

6. 并发更新为什么会把别人改动覆盖掉

先定义:v7 就是版本 7,v8 是版本 8。A、B 两位教练同时看到 v7。A 先保存,服务器变成 v8。B 还拿着旧 v7 保存,如果服务器不检查版本,就可能把 A 的修改覆盖掉。这叫 Lost Update(丢失更新)。

当前版本(version):服务器目前持久保存的计划版本号。来源:由成功提交原子递增;例子:7 → 8。

期望版本(expected_version):客户端提交修改时声明自己此前读到的版本。来源:客户端读取后随保存请求发送;例子:7。

响应状态码(status_code):409 在此示例表示版本冲突,200 表示请求成功。来源:服务器返回;例子:409:不覆盖当前版本。

下面按具体值读图。图是教学示例,不是生产系统运行记录。

图解 · 教学示例两人都读到版本 7,为什么只有一个保存成功?

从上往下沿三列读。本例画 A 先成功;两次读取的先后并非业务要求,换成 B 先提交,结果对称。

两人都读到版本 7,为什么只有一个保存成功?服务器原子地比较期望版本与当前版本;版本已经改变,就拒绝旧修改,不静默覆盖。 教练 A 服务器 教练 B 读取计划当前版本:7 读取计划当前版本:7 提交修改期望版本:7 原子比较与写入期望:7;当前:7匹配,原子写入 → 8 返回保存结果200 · 已提交 仍提交旧版修改期望版本:7 再次原子比较版本期望:7;当前:8不匹配,拒绝写入当前版本保持 8 返回结果,不覆盖 A409 · 版本冲突
  1. 服务器 → 教练 A

    读取计划

    当前版本:7

  2. 服务器 → 教练 B

    读取计划

    当前版本:7

  3. 教练 A → 服务器

    提交修改

    期望版本:7

  4. 服务器 · 本地步骤

    原子比较与写入

    期望:7;当前:7

    匹配,原子写入 → 8

  5. 服务器 → 教练 A

    返回保存结果

    200 · 已提交

  6. 教练 B → 服务器

    仍提交旧版修改

    期望版本:7

  7. 服务器 · 本地步骤

    再次原子比较版本

    期望:7;当前:8

    不匹配,拒绝写入 当前版本保持 8

  8. 服务器 → 教练 B

    返回结果,不覆盖 A

    409 · 版本冲突

查看完整图(可横向滚动)
两人都读到版本 7,为什么只有一个保存成功?服务器原子地比较期望版本与当前版本;版本已经改变,就拒绝旧修改,不静默覆盖。 教练 A 服务器 教练 B 读取计划当前版本:7 读取计划当前版本:7 提交修改期望版本:7 原子比较与写入期望:7;当前:7匹配,原子写入 → 8 返回保存结果200 · 已提交 仍提交旧版修改期望版本:7 再次原子比较版本期望:7;当前:8不匹配,拒绝写入当前版本保持 8 返回结果,不覆盖 A409 · 版本冲突

结论:服务器原子地比较期望版本与当前版本;版本已经改变,就拒绝旧修改,不静默覆盖。

边界:冲突检测不等于自动合并。B 需要先读取新版、比较改动,再决定如何重新提交;示例省略身份认证和权限检查。

Optimistic Locking(乐观锁)的一种写法:

UPDATE plans
SET content = ?, version = 8
WHERE id = 123 AND version = 7;

逐个解释:content 是计划内容;? 是稍后传入真实内容的参数占位符;id=123 指具体计划;version=7 表示“只有服务器仍然是我看到的旧版本 7 时,才允许这次更新”。如果服务器已经是 v8,这次 UPDATE 会匹配不到记录,应返回冲突。

7. Migration:数据库结构自己也有版本

例如现在要给 plans 增加 coach_note。不能只改代码,因为旧数据库没有这个字段。常见安全思路是:

expand → backfill → switch → contract

翻译:先新增兼容结构(expand)→ 给旧数据补值(backfill)→ 新代码切过去(switch)→ 确认旧逻辑不用后再删旧结构(contract)。

练习:为什么“计划已经发布,但为了继续编辑,我们把 status 直接改回 draft”通常是坏设计?

答案:因为这会让历史事实倒退。已经收到或执行 v7 的运动员仍需要一个稳定的 v7。正确思路通常是保留 v7,再创建 v8 draft。

Deep Dive 03 · 约 14–18 分钟

深挖三:鉴权真正难的不是登录,而是“谁能对哪条数据做什么”

0. 本节先认词 / 先认变量

词或变量中文直觉例子
user_id当前用户的唯一编号。区分“是谁在发请求”。
tenant_id当前机构/租户的唯一编号。俱乐部 A 和俱乐部 B 是两个 tenant。
resource_id当前被操作资源的编号。某份计划、某个训练组。
role用户在组织里的角色。coach、athlete、admin。
action用户准备做的动作。read、edit、publish、delete。
relation用户和资源之间的关系。“这个教练是否真的管理这个训练组”。
Authentication · 身份认证确认“你是谁”。登录、Session、Token。
Authorization · 授权确认“你能不能对当前资源做当前动作”。能否发布这份计划。
RBACRole-Based Access Control,基于角色的权限控制。coach 可以 publish,athlete 不可以。
ABACAttribute-Based Access Control,基于属性的权限控制。还要看机构、资源归属、状态等属性。

1. Session、Cookie、Token、JWT 都只是“维持身份”的手段

Session 可以理解为服务端保存的登录会话;Cookie 是浏览器保存并自动携带的小段数据;Token 是客户端主动携带的身份凭证;JWT 是 JSON Web Token,一种带签名的 Token 格式。

它们解决的是“服务端如何知道这个请求来自谁”。但即使 Token 里写着 role=coach,也不能因此允许他编辑所有机构的所有计划。

2. 真实授权往往是多个条件的 AND

先解释下面变量:authenticated 表示身份是否有效;same_tenant 表示用户和资源是否属于同一租户;role_allows(action) 表示角色是否允许这个动作;has_relation 表示用户和资源是否有业务关系;state_allows 表示资源当前状态是否允许这个动作。

allow = authenticated
        AND same_tenant
        AND role_allows(action)
        AND has_relation(user, resource)
        AND state_allows(action)

AND 的意思是“并且”:所有条件都成立,allow 才是真。

例如教练发布训练计划,需要同时满足:账号有效;属于这个机构;角色允许 publish;确实管理这个训练组;计划当前仍处于可发布状态。

3. 为什么 RBAC 一个人通常不够

RBAC 擅长回答“这个角色大体能干什么”,但不能单独回答“能不能操作这一条具体数据”。例如两个用户都是 coach,一个管理短跑一组,一个管理跳远组。只判断角色会让二者都拥有同样的资源范围。

所以真实 SaaS 常常是:Role + Tenant + Resource Relation + Resource State 一起判断。

4. 多租户最怕“改个 ID 就看到别人数据”

假设页面从来不展示机构 B 的计划,但用户手工调用:

GET /api/plans/98765

这里 98765 只是某份计划的 resource_id。如果后端只按这个 ID 查数据库然后返回,就可能出现对象级越权。

正确思路通常是:查询和授权都带上当前 tenant_id,再校验用户和资源的 relation。前端隐藏按钮只是体验优化,不是安全边界。

5. Audit Log 和普通 Debug Log 不一样

Audit Log(审计日志)记录高价值业务动作,用于回答“谁、什么时候、对哪个对象、做了什么”。典型字段包括 user_id、tenant_id、resource_id、action、version、request_id。

request_id 是一次请求的追踪编号,方便把前端报错、后端日志和审计事件串起来。

练习:“普通用户页面里根本看不到删除按钮,所以删除 API 不需要再验权限。”错在哪?

答案:浏览器完全可以被用户控制。用户可以自己构造 HTTP 请求。真正的安全条件必须由服务端针对当前 user、tenant、resource、action 再做一次授权判断。

Deep Dive 04 · 约 18–22 分钟

深挖四:异步、Webhook、幂等与支付,为什么会成为生产级难点

0. 本节先认词 / 先认变量

词或变量中文直觉例子
Synchronous · 同步当前步骤没结束,调用方就继续等。用户点保存,等数据库写完再返回。
Asynchronous · 异步核心动作完成后,后续工作可以慢慢做,不让用户一直等。发布计划成功后,通知 300 个运动员可以后台发送。
Queue · 队列等待后台处理的任务列表。“给运动员发推送”先进入队列。
Worker后台真正拿任务来执行的进程。从 Queue 取一条通知任务并发送。
Retry · 重试失败或超时后重新执行。推送接口暂时失败,过一会再试。
Webhook外部平台主动调用你的 HTTP 接口来通知事件。支付平台通知“这笔付款成功了”。
event_id某个外部事件的唯一编号。用它判断“这个 Webhook 我是不是已经处理过”。
Idempotency · 幂等同一个业务意图重复到达,最终副作用只发生一次。支付成功通知来 5 次,余额也只加一次。
idempotency_key一次业务意图的唯一身份。网络重试时复用同一个 key。
balance账户余额。balance += 100 表示余额增加 100。
quota剩余可用额度。还有多少 API 次数或服务金额。
Ledger · 账本每次余额/额度变化的可追溯记录。不是只保存最终 balance,而是保存每一笔为什么变。

1. 为什么要异步

发布计划时,真正必须同步确认的通常只有核心事实:“这份计划版本是否成功发布”。给 300 个运动员发通知、调用 AI 生成摘要、生成报表,可以放到后台。

HTTP Request
→ 核心事务
→ 写入 Queue
→ Response 返回给用户
              ↓
            Worker
              ↓
     Push / AI / Email

这里 HTTP Request 是用户发来的请求;核心事务 是必须当场确认的数据库变化;Queue 保存待处理任务;Worker 在后台执行。

2. 为什么 Retry 会逼出幂等问题

系统很难保证每个消息“恰好只送一次”。现实里更常见的是:宁愿重复,也别无声丢失。这类语义常被称为 at-least-once delivery,中文可以理解为“至少送达一次,因此可能送达多次”。

于是业务必须能安全处理重复,这就是幂等。

3. 先定义 business key,再谈去重

假设手机离线上传训练记录。先定义两个变量:

  • athlete_id:哪位运动员。
  • local_log_uuid:手机在本地生成的这条训练记录的唯一编号。
business_key = athlete_id + local_log_uuid

这句话读成:“一条训练记录的业务身份,由运动员身份 + 本地唯一记录号共同决定。” 手机重试 5 次都复用同一个 local_log_uuid,数据库对这个业务键做唯一约束,就能避免生成 5 条重复记录。

4. Webhook 为什么不能假设只来一次

先验签并核对支付交易、金额、币种,再处理业务。验签确认通知来自可信平台;下面的数据库事务把唯一业务占位、账本分录、余额变化一起提交。不能先单独写“已处理”,随后才尝试入账。

接收并验证通知
→ 在同一事务内判定是否已提交该业务效果
→ 首次:唯一占位 + 写账本 + 改余额一起提交
→ 重复且已提交:不再入账
→ 提交成功后返回结果

提交前故障不能吞掉重试;提交后响应丢失不能重复记账。真实异步系统还需区分“已持久接收”和“业务已完成”;这里仅展示一个受控的同步事务例子。

通知编号(event_id):支付平台此次通知事件的标识,不等于支付交易编号。来源:支付平台提供;例子:evt_42。

业务键(business_key):渠道、交易、业务效果共同标识的一次入账意图。来源:按渠道与业务规则派生;例子:pay_42_credit。

余额(balance):示例账户当前可用金额,单位元。来源:数据库事务维护;例子:0 → 100 元。

账本(Ledger):逐笔保存金额变化和原因的记录。来源:与余额同事务提交;例子:增加一笔 100 元分录。

下面按具体值读图。图是教学示例,不是生产系统运行记录。

图解 · 教学示例同一笔付款通知两次,为什么余额只增加 100 元?

从上往下比较首次通知与提交后的重复通知。每次数据库处理都有独立行,返回箭头与处理结果分开。

同一笔付款通知两次,为什么余额只增加 100 元?“已收到通知”不等于“业务已提交”。唯一业务占位、账本和余额同事务提交;重复到达不重复记账。 支付平台 接收服务 数据库 首次成功通知通知编号:evt_42 验签、字段校验后请求入账业务键:pay_42_credit 同一数据库事务业务键:pay_42_credit唯一占位、记账、余额一起提交余额:100 元;账本:1 笔 确认事务结果200 · 已提交 返回处理结果200 · 已提交 相同通知再次到达通知编号:evt_42 仍请求同一业务效果业务键:pay_42_credit 核对已提交的业务键业务键:pay_42_credit已提交:本次不再记账余额:100 元;账本:1 笔 确认已经处理200 · 已处理,不重复 返回成功,不再入账200 · 已处理,不重复
  1. 支付平台 → 接收服务

    首次成功通知

    通知编号:evt_42

  2. 接收服务 → 数据库

    验签、字段校验后请求入账

    业务键:pay_42_credit

  3. 数据库 · 本地步骤

    同一数据库事务

    业务键:pay_42_credit

    唯一占位、记账、余额一起提交

    余额:100 元;账本:1 笔

  4. 数据库 → 接收服务

    确认事务结果

    200 · 已提交

  5. 接收服务 → 支付平台

    返回处理结果

    200 · 已提交

  6. 支付平台 → 接收服务

    相同通知再次到达

    通知编号:evt_42

  7. 接收服务 → 数据库

    仍请求同一业务效果

    业务键:pay_42_credit

  8. 数据库 · 本地步骤

    核对已提交的业务键

    业务键:pay_42_credit

    已提交:本次不再记账

    余额:100 元;账本:1 笔

  9. 数据库 → 接收服务

    确认已经处理

    200 · 已处理,不重复

  10. 接收服务 → 支付平台

    返回成功,不再入账

    200 · 已处理,不重复

查看完整图(可横向滚动)
同一笔付款通知两次,为什么余额只增加 100 元?“已收到通知”不等于“业务已提交”。唯一业务占位、账本和余额同事务提交;重复到达不重复记账。 支付平台 接收服务 数据库 首次成功通知通知编号:evt_42 验签、字段校验后请求入账业务键:pay_42_credit 同一数据库事务业务键:pay_42_credit唯一占位、记账、余额一起提交余额:100 元;账本:1 笔 确认事务结果200 · 已提交 返回处理结果200 · 已提交 相同通知再次到达通知编号:evt_42 仍请求同一业务效果业务键:pay_42_credit 核对已提交的业务键业务键:pay_42_credit已提交:本次不再记账余额:100 元;账本:1 笔 确认已经处理200 · 已处理,不重复 返回成功,不再入账200 · 已处理,不重复

结论:“已收到通知”不等于“业务已提交”。唯一业务占位、账本和余额同事务提交;重复到达不重复记账。

边界:前提是已经验签并核对交易、金额和币种。提交前失败须可恢复重试;提交后响应丢失则按已提交业务键去重。不同通知编号也可能描述同一交易,业务键不能简单使用“用户+金额”。

5. “先 SELECT 再 if”为什么仍然会多加钱

请求 A: SELECT processed = false
请求 B: SELECT processed = false
A: balance += 100
B: balance += 100

先解释:processed=false 表示“数据库暂时认为这个事件还没处理”;balance += 100 表示“在原余额基础上再加 100”。

两个并发请求都可能在任何一个写入之前读到 false,于是两个 if 都通过。真正防重不能只靠应用层查询,而要用数据库 UNIQUE、事务、锁等原子机制。

6. Order、Payment、Ledger、Refund 是不同对象

对象它回答什么
Order · 订单用户想买什么、应付多少钱
Payment Transaction · 支付交易支付渠道实际发生了哪次资金尝试
Ledger Entry · 账本分录系统内部余额为什么变化
Refund · 退款原支付的全部或部分资金如何退回
Settlement · 结算支付机构何时把钱结给商户
Reconciliation · 对账你的记录与支付平台记录是否一致

7. Timeout 不是 Failure

Timeout 只表示“规定时间内没收到 Response”。第一次充值请求可能已经成功,只是响应没回来。所以创建支付/订单这类有副作用的动作,要让重试复用同一个 idempotency_key。

练习:用户连续点三次“购买套餐”。怎样区分“同一次购买的网络重试”和“用户真的想买三次”?

答案:一次明确业务意图生成一个 idempotency key;网络重试复用这个 key。用户明确重新发起一次新购买,则生成新的 key。服务端按 key 唯一,而不是简单按“用户+金额”去重。

Deep Dive 05 · 约 14–18 分钟

深挖五:小程序和手机 App,真正多出来的是“平台与现实世界”

0. 本节先认词 / 先认变量

词或变量中文直觉例子
Runtime · 运行时程序真正执行代码、维护状态、调用平台能力的环境。浏览器、微信小程序运行环境、iOS/Android App。
App Lifecycle · 应用生命周期App 从启动、前台、后台、挂起到被系统回收的状态变化。锁屏后 App 可能进入后台甚至被杀死。
Local Storage · 本地存储数据保存在手机本机,而不是服务器。草稿、离线记录。
Sync · 同步把本地和服务器两边的数据重新对齐。恢复网络后上传离线训练记录。
Conflict · 冲突本地和服务器都改了同一个事实,不能简单自动合并。离线期间两端都编辑了同一条备注。
local_uuid手机本地生成的唯一记录 ID。用来识别“重试上传的还是同一条记录”。
plan_version运动员执行时所依据的计划版本。执行 v3,不等于当前服务器计划仍是 v3。
Push Notification · 推送通知服务器通过操作系统/厂商通道给手机发消息。训练提醒。
Permission · 系统权限操作系统是否允许 App 使用某项设备能力。相机、定位、蓝牙、麦克风。

1. 小程序登录为什么比普通 Web 多一层

先定义:code 是微信临时给客户端的一次性登录凭证;openid 可以理解为某个用户在某个小程序里的平台身份标识;user_id 是你自己业务数据库里的用户 ID。

wx.login()
→ 得到临时 code
→ code 发给你的后端
→ 后端向微信验证
→ 得到平台身份 openid
→ 映射到你自己的 user_id
→ 建立业务 Session / Token

这里最重要的边界是:openid 不是你的完整业务用户模型。 同一个微信用户可能在机构 A 是教练,在机构 B 是运动员。

2. 平台能力意味着平台规则

小程序除了你的代码,还受宿主平台约束:合法请求域名、支付 API、隐私接口、审核、WebView 业务域名等。于是“代码逻辑正确”不等于“平台允许这样运行”。

3. App Lifecycle:程序可能突然不再运行

先解释几个状态:Foreground 是 App 正在屏幕前台;Background 是离开当前屏幕但进程可能还在;Suspended 是系统暂时停止它继续执行;Cold Start 是完全没运行时重新启动。

Cold Start
→ Foreground
→ Background
→ Suspended
→ OS 回收进程
→ 用户重新打开
→ 从本地恢复
→ 再和服务器同步

所以关键输入不能只存在内存变量里。否则用户锁屏或系统回收进程后,刚填的训练记录可能直接消失。

4. Offline-first:本地事实和服务器事实怎么合并

假设运动员离线时执行计划 v3,并记录 100m=10.92s。这里 v3 是 plan_version=3。期间教练在服务器发布 v5。

恢复网络后,正确理解不是“手机有 v3,所以把服务器改回 v3”,而是:

  • 服务器当前计划仍然是 v5。
  • 本次训练记录保存 plan_version=3,说明当时执行的是 v3。
  • 这条本地训练记录有稳定 local_uuid,重复上传不会生成多条。
  • 如果同一条记录两端都编辑过,再进入 Conflict Resolution(冲突解决)。

5. Last Write Wins 为什么不是万能答案

Last Write Wins(LWW,最后写入者获胜)就是“谁最后保存,谁覆盖前面”。它对昵称、普通草稿可能够用;对计划版本、审批、支付、训练历史等关键事实通常不够,因为你可能需要保留两边来源和历史。

6. Push accepted 不等于用户已接受业务

先区分三个事实:

  1. 推送平台 accepted:平台接收了你要发消息的请求。
  2. 设备 displayed / received:设备可能已经展示或收到。
  3. 业务 accept:用户本人针对某个具体计划版本明确点击“接受”。

这三件事不能混成一个布尔值。

7. Permission 被拒绝本身就是一种正常状态

定位权限可能是“未请求、允许一次、使用时允许、始终允许、拒绝”。产品逻辑要明确:这个能力是核心必要,还是增强功能。

练习:短跑训练 App 想用 GPS 辅助记录,但用户拒绝定位。要不要阻止整个训练页?

答案:如果核心任务还能通过手动输入完成,就不应把增强能力缺失变成全局阻断。应该说明哪些自动分析会受影响,同时保留手动路径。

Deep Dive 06 · 约 12–16 分钟

深挖六:AI Coding 时代,真正的交付链不是“代码写完 → push”

0. 本节先认词 / 先认变量

词或变量中文直觉例子
Source Code · 源码开发者真正编写和维护的代码。仓库里的前端、后端、配置。
CommitGit 中一版固定历史快照。某次修改对应一个 commit SHA。
Build · 构建把源码变成可运行或可发布产物。生成前端静态文件、Docker image。
Artifact · 构建产物真正准备部署的输出。镜像、二进制、静态站点包。
artifact_digest构建产物的内容指纹。证明“部署的是不是刚才验收的同一份字节”。
Release Candidate · 发布候选已经冻结、准备进入最终验收/发布的一版明确对象。绑定源码 commit、artifact digest、测试结果。
CIContinuous Integration,持续集成。代码变化后自动 build、test、lint。
CDContinuous Delivery / Deployment,持续交付 / 持续部署。把合格候选推进到可发布或生产环境。
Deployment · 部署把某个明确产物放进目标运行环境。把 image digest 部署到生产。
Readback · 读回部署以后从真实环境重新读取版本/页面/接口,确认真的生效。线上 API 返回当前 build SHA。
Observability · 可观测性通过日志、指标、追踪推断系统内部状态。错误率、P95 延迟、request trace。
Rollback · 回滚把运行版本恢复到之前的已知安全状态。切回上一个 image digest。

1. Git 证明“源码历史”,不证明“线上版本”

Commit 只能证明某一版源码存在。main 指向最新 commit,也不能推出生产服务器已经运行它。

至少要区分三个身份:

  • source_commit:源码是哪一版。
  • artifact_digest:构建出的产物是哪一份。
  • running_version:线上当前到底运行哪一版。

这三个变量最好能互相追溯。

2. 测试分层:每种测试证明的东西不同

测试先翻译擅长证明
Unit Test单元测试函数、小算法、小状态转换
Integration Test集成测试数据库、服务、组件之间能否正确协作
Smoke Test冒烟测试系统最关键入口是不是基本活着
E2EEnd-to-End,端到端测试从用户入口穿过多个组件的完整链路
UATUser Acceptance Testing,用户验收测试真实业务场景是否满足使用目标

所以“Mock API 下 E2E 全绿”只证明:在假后端返回可控数据时,前端链路能工作。它不能单独证明真实鉴权、真实数据库、支付平台和生产配置正确。

3. Release Candidate 为什么必须冻结

先定义下面几个变量:

  • source_commit:候选对应的 Git commit。
  • artifact_digest:候选构建产物的指纹。
  • migration_version:数据库结构迁移到哪一版。
  • configuration_version:运行配置是哪一版。
  • verification_report:这版已经跑过哪些检查及结果。
  • known_risks:已知但仍接受的风险。
release_candidate = {
  source_commit,
  artifact_digest,
  migration_version,
  configuration_version,
  verification_report,
  known_risks
}

这段伪代码只是在表达:验收对象必须是一个有明确身份的组合,而不是“最新代码”这种不断变化的概念。

4. CI/CD 是把重复动作变成机器合同

CI 负责在代码变化后统一执行 build、test、lint、type check 等。Continuous Delivery 的目标是“合格候选随时可发布”;Continuous Deployment 再进一步,把生产发布也自动执行。

高风险系统可以保留人工 Acceptance(验收接受)门,但这不妨碍构建、测试、部署准备都自动化。

5. Deployment Readback:真正去线上看一眼

部署命令返回成功只说明“部署工具认为操作完成”。你还应从真实目标环境读回:

  • 页面上是否出现新版本标识。
  • API 是否返回预期 build SHA。
  • 关键接口是否真的能工作。
  • 数据库 migration 是否处于目标版本。

Readback 是把“我执行过发布命令”升级成“我有证据证明目标环境真的变成了这版”。

6. Observability:Logs、Metrics、Traces 分别是什么

  • Logs · 日志:某一时刻发生了什么事件。
  • Metrics · 指标:错误率、请求量、P95/P99 延迟等连续数值。
  • Traces · 链路追踪:把一次请求经过的多个服务串起来。

P95 的意思是“95% 的请求延迟不超过这个值”;P99 类似,只是看更尾部的慢请求。

7. Rollback 为什么也不是万能药

代码版本可以切回去,但已经发生的支付、通知、邮件、数据库不可逆迁移不一定能自动撤销。这类外部副作用可能需要 Compensation · 补偿动作,例如单独发退款,而不是假装旧代码重新上线就把钱退回来了。

AI Coding 的合理分工:Agent 做大量可重复检查;人负责确认需求、不变量、高风险权限/资金边界,以及对一个明确 Release Candidate 作最终 Acceptance。
练习:Agent 说“200 个测试通过、build 成功、已经 push main”。为什么还不能说“线上验收完成”?

答案:还缺明确候选身份、真实后端/目标 Runtime 证据、数据库迁移状态、部署 artifact 身份、线上 readback、关键负向测试,以及有权主体的 Acceptance。

Reconstruct

⑪ 回到原来的主线速览

主线一|10–30 分钟:界面与请求

HTML(HyperText Markup Language,超文本标记语言)描述内容结构;CSS(Cascading Style Sheets,层叠样式表)描述布局与样式;JavaScript处理交互与请求。现代框架把页面拆成组件(Component),并根据状态(State)重新渲染。

最容易混淆的是三种状态:①输入框、抽屉等界面状态;② loading / success / error 等请求状态;③ draft / published / accepted 等业务状态。前两者可以由客户端维护,第三种通常必须以服务端权威记录为准。

路由(Routing)把 URL 或导航目标映射到页面;Flexbox擅长一维排列,Grid擅长二维网格。fill / content 是项目层的布局语义:前者倾向占满可用视口并让内部区域承担滚动,后者让页面随内容自然增长。真正重要的是滚动所有权(Scroll Ownership)——到底浏览器页面滚,还是内部 Pane 滚。

点击“发布计划”后,浏览器通常通过超文本传输协议(HyperText Transfer Protocol,HTTP)发送请求;HTTPS 是加密后的 HTTP。常见请求包含 method、URL、headers 和 body,服务器返回 status code、headers 和 response body。JSON 是常见的数据交换格式。

错误恢复原则:不要只显示“失败”。至少区分可重试、需修改输入、无权限、版本冲突、结果未知。按钮是否禁用、草稿是否保留、是否允许重试,都是业务行为。

主线二|30–50 分钟:数据、事务与状态机

Schema · 数据结构定义决定表和字段;CRUD是 Create / Read / Update / Delete,增删改查;Primary Key · 主键唯一标识一条记录;Constraint · 约束让数据库拒绝不合法状态。

Transaction · 事务让一组相关操作要么全部提交,要么全部回滚。ACID 中的 Atomicity、Consistency、Isolation、Durability 可直观理解为:不半写、守规则、并发尽量互不破坏、提交后不轻易丢失。

State Machine · 状态机把允许的业务迁移写清楚。例如 DRAFT → PUBLISHED → EXECUTED → REVIEWED;若规则没有允许 REVIEWED → DRAFT,接口就不应偷偷这么改。

并发修改时会出现Race Condition · 竞态条件。最常用的入门做法之一是Optimistic Locking · 乐观锁:客户端写入时同时带上自己看到的版本号;服务器只在版本仍相同的情况下更新,否则返回冲突,让用户比较新旧内容。数据库结构升级则通过Migration · 迁移可追踪地完成。

主线三|50–65 分钟:身份、权限与多租户

Authentication · 身份认证回答“你是谁”;Authorization · 授权回答“这个身份现在能否对这个资源做这个动作”。Session、Cookie、Token、JWT 都只是帮助维持身份会话的机制,不能替代真正的资源级判权。

RBAC(Role-Based Access Control,基于角色的访问控制)用 coach / athlete / admin 等角色给出基础权限;ABAC(Attribute-Based Access Control,基于属性的访问控制)进一步考虑组织、资源归属、时间、状态等属性。实际 SaaS 常常是“角色 + 资源关系 + 业务状态”共同决定。

Tenant · 租户通常对应一家机构或客户。多租户系统必须保证机构 A 的账号即使手工改 URL、请求参数,也不能读取机构 B 的数据。隐藏按钮不是安全边界;服务端每次受保护操作都必须重新判权。

一个实用判权公式:允许 = 身份有效 ∧ 属于当前租户 ∧ 对资源具有关系 ∧ 角色允许该动作 ∧ 资源状态允许该动作。

主线四|65–85 分钟:异步、Webhook、支付与计费

Asynchronous · 异步表示当前请求不必等所有后续工作完成。发送通知、生成报告、调用模型等常放进Message Queue · 消息队列,由后台 Worker 消费。失败后可以Retry · 重试,但重试会带来“同一个任务可能执行多次”的现实。

Webhook是外部系统主动调用你的 HTTP 地址通知事件。例如支付平台告诉你“订单已支付”。Webhook 可能重复、延迟、乱序,因此业务不能建立在“它只会来一次”的假设上。

Idempotency · 幂等性的目标是:同一个业务事件重复送达,副作用仍只发生一次。典型做法是给业务事件一个唯一键,并在数据库建立唯一约束;去重记录、账本分录和余额变化放在同一个事务里。

支付应区分:Order · 订单、支付平台的Transaction · 资金交易、Refund · 退款、Settlement · 结算与Reconciliation · 对账。SaaS 进一步还有Billing · 计费、Metering · 计量、Quota · 额度、Subscription 与 Usage-based Billing。

小题:支付平台同一成功通知并发发来两次;两个请求都先查到“未处理”,然后都给余额 +100。哪里错?

答案:“先查再加”不是原子的。应用层 if 不足以挡并发;应以支付业务唯一键建立数据库级唯一约束,并让“确认未入账 → 写账本 → 更新余额”处于同一事务。

主线五|85–100 分钟:小程序与手机 App

小程序比普通 Web 多一层平台约束:平台登录、合法域名、权限接口、审核、支付接口都由宿主平台规定。微信身份通常需要通过登录 code 映射到你自己的用户主键;OpenID 之类的平台标识不应直接等同于完整业务用户模型。

手机 App 额外面对App Lifecycle · 应用生命周期:前台、后台、挂起、系统回收、重新启动。还要处理Local Storage · 本地存储、断网、弱网和Offline-first · 离线优先。

离线同步的核心不是“联网后把本地全部覆盖服务器”,而是明确对象边界与冲突策略。运动员离线执行的是计划版本 3,教练此时已经发布版本 5:正确模型通常是“训练记录引用执行时的 v3”,而不是把服务器计划改回 v3。

Push Notification · 推送通知只说明通知链路的一部分。APNs、FCM 或厂商 Push 接受消息,不等于用户已经看见,更不等于用户接受业务条款。定位、相机、蓝牙、健康数据等还涉及系统权限;权限被拒绝本身也是正常状态。

主线六|100–110 分钟:从代码到可靠交付

Git负责版本历史与协作;Build · 构建把源码变成可运行产物;测试分层验证不同范围。Unit Test 验证小单元,Integration Test 验证组件协作,End-to-End Testing(E2E,端到端测试)从真实入口验证完整用户链路。

Quality Assurance(QA,质量保证)是更广的质量活动;Acceptance · 验收是有权主体对候选作接受/拒绝决定。Continuous Integration(CI,持续集成)把自动检查接入代码变化;Continuous Delivery / Deployment 负责把合格候选推进到可发布或自动上线。

Observability · 可观测性通过日志、指标、追踪等证据帮助回答“线上发生了什么”;Rollback · 回滚用于恢复软件版本,但它不会自动撤销已经发生的资金和数据副作用。

AI Coding 的合理分工:Agent 可以读需求、改代码、跑静态检查、API smoke、Playwright、构建并整理证据;人重点做需求确认、风险判断、权限/资金等关键边界复核,以及最终验收。Agent 自述“完成”不等于证据。
Transfer

⑫ 把所有概念映射回一个完整功能

把六条主线放回同一个真实功能

动作客户端后端 / 数据失败边界与证据
教练发布计划编辑表单、展示提交状态验证身份、租户、角色、计划版本;事务写入发布版本版本冲突不能静默覆盖;读回发布版本
运动员执行手机可离线记录成绩成绩引用具体计划版本同步重试不能产生重复训练记录
生成分析展示 pending / ready / failed后台任务调用模型并保存结果Worker 可重试;任务有稳定业务键
按量计费只展示服务端账本结果计量、账本、额度更新保持原子与幂等重复事件不重复扣费;可对账
教练复盘展示当前版本和历史Review / Receipt 等业务对象有明确状态机ack 与 accept 不能混为一个布尔值
上线新版本目标浏览器 / 小程序 / App 验证API、迁移、权限与监控一起验证构建成功之外还要部署读回和验收

遇到陌生系统时的通用排错顺序

  1. 先描述用户动作和期望结果,不先猜框架。
  2. 定位对象:用户、组织、资源、订单、计划、训练记录分别是谁。
  3. 定位状态:现在处于什么状态,允许迁移到哪里。
  4. 定位权威数据:客户端缓存、数据库、支付平台谁能证明什么。
  5. 定位异常:重复、并发、超时、断网、旧版本、越权。
  6. 最后选择证据:日志、数据库查询、负向权限测试、故障注入、真实浏览器或目标 Runtime。
Attempt

⑬ 综合训练

综合题|110–120 分钟

下面六项都“看起来做完了”。请指出违反了哪条工程规则、你会怎样修、以及至少一条可重复验收条件。

编号观察
A机构甲页面看不到机构乙按钮,但手工请求乙的资源 URL 返回 200 和完整数据。
B两位教练同时基于版本 7 编辑,两个请求都 200,后写覆盖先写。
C同一笔 100 元支付两个 Webhook 并发到达,余额最终 +200。
D运动员离线执行计划 v3;服务器已有 v5;同步后服务器计划被手机覆盖回 v3。
E推送平台返回“accepted”,系统立即把运动员业务回执写为“已接受”。
FAgent 报告:Mock API 下 E2E 全绿、build 成功、代码已推 main,所以线上验收完成。

建议先自己写四列:错误结论 / 应守住的不变量 / 修复位置 / 验收证据。

Hint → Answer

⑭ 分级提示与实际答案

Hint 1 · 方向

分别寻找:跨租户授权、并发丢失更新、资金幂等、对象边界、通知语义、证据范围。先问“哪个事实被错误地等同于另一个事实”。