
“AI 软件开发实战教程”系列第 13 篇:把微信沟通后的双方反馈做成明确状态机,并诚实区分顺序测试和真实数据库并发证据。
交换微信号只代表双方可以联系,不代表已经约定同行。若系统在一方点击“已约定”时就减少座位,另一方可能其实已经拒绝,车主信息却会错误地显示满员。
K5 因此坚持一个简单规则:只有双方都选择“已约定同行”,系统才确认配对并占用一个座位。
三个回答,两份独立状态
双方都可以选择:
已约定同行
未约定同行
暂不反馈
车主和乘客的回答分别保存。配对状态再由两份回答推导:
只有一方确认:等待对方,不占座; 双方确认:检查座位后确认,占一个座位; 任一方拒绝:未约定,不占座; 暂不反馈:继续等待; 最后座位已被其他配对占用:座位冲突; 已确认后取消:释放座位。
第一批测试因配对模型和服务不存在而失败。最小实现先让单方、双方和拒绝三条路径转绿,再处理最后座位和取消。
占座必须和双方确认在同一个事务
不能先把配对写成已确认,再单独减少座位。第二步失败会留下“已确认但没占座”的半成品。
服务采用稳定锁顺序:
锁定车主信息
→ 锁定候选
→ 锁定配对
→ 保存当前用户回答
→ 若双方确认,统计该车主信息已确认配对
→ 有座则确认并占座
→ 达到座位数则标记系统满员
整个过程位于一个数据库事务。最后一个座位确认时,配对状态和出行状态一起提交或一起回滚。
系统满员和手动满员不是一回事
车主可能在站外已经坐满,手动把信息标为满员;系统也可能因为确认人数达到座位数自动满员。
出行信息因此保存 full_source:
system:站内确认导致满员
manual:车主手动满员
取消一个已确认配对时,只允许系统满员在仍未出发且重新出现空位后自动恢复。手动满员不能被取消路径偷偷打开,必须由车主明确操作。
测试用一个座位完成双方确认,验证状态变成系统满员;再取消配对,验证座位释放且信息恢复有效。增加座位和完整手动状态页面属于 K6 生命周期节点。
顺序竞争通过,不等于并发通过
测试创建一个只有一座的车主信息和两个乘客候选。先完成第一组双方确认,再完成第二组:
第一组:confirmed
第二组:seat_conflict
已确认总数:1
这证明服务在顺序执行时会重新计算座位,不会盲目确认第二人。
但当前测试数据库是 SQLite。它的锁行为与生产计划中的 PostgreSQL 不同,不能证明两个连接在同一瞬间竞争最后座位时仍然正确。
项目单独保留了 PostgreSQL 标记测试,并在当前环境明确跳过,因为本机没有 Docker 或 PostgreSQL 服务。验收矩阵把 AC-27 写成“部分通过(环境)”,而不是用顺序测试冒充并发证据。
教程尤其需要保留这种差异:测试数量变多,不代表关键风险已经被同等级环境覆盖。
出发时间是确认边界
同行确认截止到双方较早的预计出发时间。到达边界后,再提交“已约定同行”会被服务拒绝,不再改变配对和座位。
用户仍可以自愿反馈“实际是否成行”。这是一张独立事实表,只用于后续质量观察:
不补建配对; 不追溯占座; 不改写满员状态; 同一个候选和用户只有一条,可更新自己的答案。
测试在出发时刻先证明确认被拒绝,再保存实际成行,并断言数据库没有因此出现配对。
正式把等待中的配对落为“确认超时”需要后台时间扫描,由 K6 完成。因此 AC-29 当前只有实时权限证据,尚未完全关闭。
两个浏览器怎样看到不同阶段
双用户 Chromium 旅程在联系方式交换后继续:
乘客选择已约定同行
→ 乘客看到“已记录,等待对方反馈”
车主刷新结果页并选择已约定同行
→ 车主看到“双方已确认同行,已占用 1 个座位”
乘客刷新
→ 乘客看到相同最终结果
这条旅程证明页面没有把单方回答误报成成功,也证明第二方提交后的结果对双方一致。
本节点结果
K5 收口时:
92 个非浏览器测试通过; 1 个 PostgreSQL 专属并发测试明确跳过; 分支覆盖率 89.94%; Ruff、格式、mypy strict、迁移和 Django 检查通过; 4 条 Chromium 旅程通过; 单方、双方、拒绝、座位冲突、取消释放和实际成行都有自动证据; PostgreSQL 并发、WebKit 和微信真机没有被宣称通过; 结果只形成本地提交,不推送。
写在最后
并发错误最危险的地方,是顺序测试看起来全部正确。数据库锁不是写上 select_for_update 就自动获得证明,还需要在计划使用的数据库、真实的两个连接和可控同步点上验证。
当前节点把状态机和事务边界实现完整,同时把缺失环境证据公开留在看板。这比为了显示进度而把 AC-27 涂绿更有价值。
下一篇进入 K6:怎样处理编辑权限、匹配截止、关闭、取消、手动与系统满员、出发过期和延迟后台任务,让页面查询与真实状态即使在任务晚运行时仍保持一致。
关键代码与操作
下面的简化测试展示为什么必须分别检查第一次和第二次反馈后的状态:
deftest_both_accept_and_fill_last_seat(candidate, driver, passenger):
waiting = respond_to_pairing(
candidate_id=candidate.pk,
actor_id=driver.pk,
response="accepted",
now=NOW,
)
assert waiting.status == Pairing.Status.PENDING
confirmed = respond_to_pairing(
candidate_id=candidate.pk,
actor_id=passenger.pk,
response="accepted",
now=NOW,
)
assert confirmed.status == Pairing.Status.CONFIRMED
candidate.driver_trip.refresh_from_db()
assert candidate.driver_trip.status == TripPost.Status.FULL
assert candidate.driver_trip.full_source == TripPost.FullSource.SYSTEM
验证命令:make bugfix TEST=tests/matching/test_pairing.py::test_both_accept_atomically_confirm_and_fill_last_seat
这是顺序事务测试;最后一个座位的真实竞争仍必须在 PostgreSQL 双连接环境验证。
本篇验证摘要
车主和乘客分别保存反馈,只有双方确认才建立配对并占用座位; 配对、座位计算和满员状态在同一个事务中提交或回滚; 系统满员与手动满员分别记录,取消配对不能偷偷重新打开手动满员信息; 出发时间到达后拒绝新的同行确认,但仍允许记录实际成行反馈; 顺序竞争已经通过,最后一个座位的真实并发结论仍等待 PostgreSQL 双连接验证。
附录:相关工具与仓库
gstack
仓库:garrytan/gstack 地址:https://github.com/garrytan/gstack
dev-harness
仓库:Dev-Wiki/dev-harness 地址:https://github.com/Dev-Wiki/dev-harness
UI UX Pro Max Skill
仓库:nextlevelbuilder/ui-ux-pro-max-skill 地址:https://github.com/nextlevelbuilder/ui-ux-pro-max-skill
夜雨聆风