订单中心集成:浏览器联调与消息队列入门
今天从哪里开始
昨天受保护的接口能区分未登录和越权,今天让运营通过页面使用它。页面显示成功,数据库就一定正确吗?先把请求、响应和页面状态连起来,再处理通知太慢的问题。
前半段借助现有页面壳完成 API 调用与排错;后半段单独演示跨进程通知队列。队列实验帮助判断哪些任务可以延后,不要求把订单系统改造成复杂消息平台。
今天要学会什么
应用既学 Python 模型/异常/事务/async/线程进程边界;fetch 四态、Cookie/CSRF/CORS;MQ 生产消费和 ACK。新增能力都落到:真实浏览器订单调用链与独立通知队列实验。
- fetch 与请求四态
- Cookie、CSRF 与 CORS
- 前后端容器联调
- 生产者、Broker、消费者、ACK 与幂等
课堂结束时,用自己的项目检查:
- 能做出来:跟踪一条通知消息;说明入队不等于通知完成,并展示正常输入的结果。
- 能查出问题:针对“用 create_task 假装持久队列、先 ACK 后处理、无界重试”中的风险,说明输入、预期与实际差异;失败后核对数据没有被误改。
- 能解释修改:提交代码、测试和修改前后结果,讲清自己接受或拒绝了哪一处 AI 建议。
把 Python 的处理结果与浏览器看到的结果对齐
我沿一笔订单定位四处代码:页面 request 发请求,后端输入模型校验,业务服务在工作单元中执行,返回模型把 Decimal 转为金额字符串。字典字段、异常、with、async/await 都是前三周已经学过的工具,今天用它们排一条真实调用链。
| 我做的变化 | 为什么值得做 | 在商城哪里观察 |
|---|---|---|
| 超库存下单 | 页面成功不能掩盖事务失败 | Network 的 409、错误码、原库存和无残单 |
| 只读账号取消 | UI 与服务端权限必须一致 | 403、审计/服务日志、订单仍未取消 |
| 输入不存在订单号 | 缺失对象要与网络故障区分 | 404 和页面失败态,不能渲染 undefined |
| 模拟写请求连接中断 | 未收到响应不等于未写入 | 先按订单事实核对,禁止无条件再提交 |
先在本人后端跑原有回归,再在日包 frontend/src/main.js 补登录和订单查询 TODO。api.js 已给请求支架,我要解释 credentials、CSRF 与 response.ok,不重写一套请求库。运行 npm.cmd ci、npm.cmd run build 只能证明依赖安装和构建;四态页面、Cookie 和真实事务必须在浏览器与数据库分别核对。
三种“队列”放在商城哪里
| 机制 | 数据在哪里 | 本周的用途 |
|---|---|---|
| queue.Queue | 当前 Python 进程 | D01 线程之间传递查询结果;退出不保留 |
| 进程池工作队列 | 父子进程之间 | D02 把报表批次交给计算进程;不是订单持久任务系统 |
| Redis Streams | 独立 Redis 服务 | 本日虚构通知交接;恢复还取决于保存、消费和确认配置 |
我先画出下单事务提交和通知入队之间的失败窗口,再运行独立通知实验。现有商城下单路径没有自动发布这条实验消息,不能用两个分别成功的程序冒充端到端通知。W02 已讲过 async 的首次语法,本日只比较等待、后台执行和可靠交接的业务含义。
Node.js、npm 与 Vite 第一次使用
今天不教授前端体系,但 AI(人工智能) 生成的候选仍需要 Node.js、npm 和 Vite 才能真实运行。先按 Node.js 与 npm 安装验收手册 完成环境门:
node --version
npm.cmd --version
where.exe node
Set-Location -LiteralPath .\frontend
node -e "const p=require('./package.json'); console.log(p.engines); console.log(p.scripts)"
npm.cmd ci
npm.cmd run build当前壳要求 Node.js >=24.14.0 <25、npm >=11.9.0 <12。没有 lockfile 时第一次使用 npm.cmd install 生成并审查 package-lock.json;已有可信 lockfile 时使用 npm.cmd ci。不要提交 node_modules/,也不要为了绕过 EBADENGINE 删除 engines。
前端只做最小 API Consumer
今天不教授 React。
固定技术壳:
- Vite vanilla JavaScript;
- 端口 5173;
- API 端口 8000;
- API base 只读
VITE_API_BASE_URL; - 最小 HTML、按钮和结果区;
- 使用 fetch 调真实 API。
前端是调用与调试窗口。
不是本周新职业方向。
从 OpenAPI 生成候选
这张图的重点是“每轮只改一种东西”。OpenAPI(接口描述规范) 和示例 JSON(结构化数据交换格式) 先固定事实,AI 只生成候选;学生要读 diff(代码差异)、看 Network(网络)、修一个真实错误,再决定是否进入下一轮。
全周使用同一份 商城前端 Vibe Coding 完整提示词。今天依次完成商品 GET、员工登录、订单查询与受保护写操作,每完成一段就检查 Network 和失败状态。先有你后端合同,再接受 AI 的局部页面修改。最终独立故障与交接在下次课完成。
给 AI 的输入:
GET /products合同;GET /orders/{order_id}合同;ErrorResponse;- 只修改
src/main.js和必要 HTML; - 不引入 React;
- 不写 Mock(模拟替身) 数据;
- API base 从环境变量读取;
- 输出 diff 和运行步骤。
AI 只提供候选。
学生负责读、改、运行和验证。
fetch 最小结构
下面是页面模块里的调用片段,依赖 Vite 环境配置和已有 renderProducts(渲染商品)函数。先读请求到响应的顺序,再接入页面壳;它不是可以直接粘进 Python 终端的独立程序。
const apiBase = import.meta.env.VITE_API_BASE_URL;
const response = await fetch(`${apiBase}/products`);
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.message ?? "请求失败");
}
renderProducts(payload);你要能解释:
- 请求发到哪里;
- response.ok 依据什么;
- JSON 在哪里解析;
- 错误为什么不能吞掉;
- 最后怎样更新 DOM。
Origin 和 CORS
商品请求在命令行正常,浏览器却报错。先看页面和接口的协议、主机、端口,判断是否跨源;不要为了消掉提示就关闭昨天的身份检查。
Origin 由:
- scheme;
- host(主机名);
- port(端口)
共同决定。
http://127.0.0.1:5173 和 http://127.0.0.1:8000 端口不同,是不同 Origin。
浏览器会执行跨源规则。
CORS(跨源资源共享) 是 Cross-Origin Resource Sharing,跨源资源共享。
它不是登录,也不是鉴权。
先故意移除 CORS
让页面从 5173 请求 8000。
观察:
- Network 是否出现请求;
- Console 报什么;
- API 服务日志有没有收到;
- Postman 或 curl 是否仍能请求。
这能说明 CORS 的限制主要发生在浏览器环境。
不要看到 CORS 就说“后端挂了”。
最小 CORS 配置
只允许课堂需要的 Origin:
allow_origins=[
"http://localhost:5173",
"http://127.0.0.1:5173",
]localhost 与 127.0.0.1 虽指向本机,host 字符串不同,所以是两个 Origin;完整实现显式允许这两个开发入口。生产使用实际前端域名。不要为了省事在带凭据场景随意使用 *。
昨天已经建立教学员工登录、Cookie(会话凭证载体)与服务端角色检查。今天浏览器调用受保护接口时要携带凭据,写请求还要提供 CSRF(跨站请求伪造)防护令牌;CORS 配置不能替代这些检查。
Network 排错路径
| Network 中看到的现象 | 接着核对什么 |
|---|---|
| 没有请求 | 事件绑定、JavaScript 错误和 API base |
| 浏览器提示跨源拦截 | Origin、凭据及响应头;再查服务端是否已收到请求 |
| 401/403 | 会话是否有效、角色与 CSRF 是否满足要求 |
| 其他 4xx/5xx | 错误响应、request_id、业务状态及服务日志 |
| 2xx 但页面不对 | 响应字段、JSON 解析和 DOM 更新 |
CORS、认证和授权分别检查;今天沿用 W03-D03 已建立的登录与服务端角色规则。
页面空白时:
- 请求发出了吗;
- 是否被 CORS 拦截;
- 状态码是什么;
- 响应体是什么;
- JSON 是否解析;
- DOM 是否更新。
同时对照服务日志和 MySQL。
不要只刷新页面碰运气。
页面失败提示与订单真实结果不一致时,首先核对哪些证据?
查看浏览器 Network(网络面板)的请求、状态码和响应,再用请求标识关联后端日志及订单状态;不能凭页面提示直接重试写操作。
Bad Case:API base 写死
错误:
fetch("http://localhost:8000/products")问题:
- 环境切换要改源码;
- localhost 和 127.0.0.1 可能造成 Origin 混乱;
- Docker / 同伴环境难复现。
修复:
const apiBase = import.meta.env.VITE_API_BASE_URL;并提供 .env.example。
Bad Case:不判断 response.ok
fetch 遇到 404 或 409 不会自动抛出网络异常。
如果代码直接:
const payload = await response.json();
renderOrder(payload);ErrorResponse 可能被当成 Order 渲染。
必须先根据状态和合同区分成功与失败。
明天的完整交付旅程会复用它处理库存不足的 409 响应。
fetch 成功返回 Promise 是否代表 HTTP 业务成功?
不代表,404/403/500 也可能正常得到 Response。检查 status/ok,区分未登录、越权、输入错误和未知写入结果,避免把所有错误都提示成网络失败。
先接好浏览器的身份和错误状态
我使用已提供的运营页面模板,把精力放在请求契约、加载/成功/空结果/失败、Cookie、CSRF 和权限反馈。页面通过同源 /api 调用服务;本地跨源开发需要 credentials 与明确来源配置。学生独立修改一个查询或详情功能,不从头生成整套后台。
浏览器跨域失败、HTTP 403 与连接断开,应该都显示成同一种错误吗?
不应该。跨域涉及浏览器访问策略,403 表示无权操作,连接断开可能无法确认结果;分别检查来源、凭据、状态码和接口地址,给出可操作提示。
从同步通知到消息队列
页面已经能正确显示订单结果。现在增加一个允许稍后完成的通知任务:如果发送很慢,是否必须让下单请求一直等待?这才是接下来讨论队列的原因。
订单已经提交,发送通知还要等待外部系统。如果通知允许稍后完成,就可以把它交给独立消费者。必须先区分三层:
| 方式 | 任务在哪里运行 | 应用进程退出后 |
|---|---|---|
| 同步调用 | 当前请求调用链 | 请求未返回,调用方仍在等待 |
| async/await、进程内后台任务 | 当前应用进程的任务调度 | 未完成任务通常随进程结束,不能承诺恢复 |
| 消息队列+消费者 | Broker 保存消息,独立进程消费 | 是否恢复取决于持久化、确认、重试与运行配置 |
异步语法描述程序如何等待;消息队列描述工作如何跨进程交接。二者可以组合,不能互相替代。
asyncio.create_task 为什么不是消息队列?
它创建当前进程里的任务;进程退出通常任务也消失。跨进程可靠任务还需要 Broker、保存策略、消费确认、重试和幂等。
生产者、Broker、消费者与确认
flowchart LR P[生产者:提交通知事件] --> Q[Broker:保存消息] Q --> C[消费者:处理通知] C -->|处理成功后 ACK| Q C --> F[失败或退出:保留待处理状态]
生产者提交事件,Broker 管理消息,消费者执行工作,ACK 告诉 Broker 这次处理已完成。消息被接收不等于通知已发送。若接口返回 202,应表达任务已接受并给出查询入口,不能把待处理说成业务完成。本演示独立运行,不改变订单 API 已有成功语义。
把通知放到后台后,什么时候才能告诉运营“已经发送”?
任务入队只代表接收阶段;还需消费执行结果或发送回执。状态应区分待处理、成功和失败,重试需避免重复业务效果。
生产者收到入队确认,是否代表通知已经完成?
没有。生产者确认与消费者 ACK 是不同阶段;消费失败可能重投,所以业务需去重。数据库提交和入队之间的缺口可通过 Outbox 等机制处理,今天只认识边界。
用已有 Redis 做最小队列演示
课堂复用 Redis Streams,避免再安装一个消息服务。由讲师投屏运行独立机制文件 message_queue.py;选做时仅领取该实验脚本及 MQ_LAB.md,不领取教师商城实现。命令从该脚本所在目录执行:
python message_queue.py produce --event-id lesson-event-1
python message_queue.py consume
python message_queue.py pending生产者和消费者在两个终端、两个独立进程运行。演示只写入虚构通知记录,不发送真实邮件或消息。重复发布相同 event_id,消费者识别重复并只保留一份通知记录。
随后发布新事件,让消费者使用 --crash-before-ack 读取后退出;查看 pending,等待它满足重新领取时间,再运行 reclaim。消息存在与消息已经处理是两个状态。每个实验使用自己的 namespace,避免同学互相消费消息。
为什么需要重试、幂等和失败出口
消费者可能完成工作后、ACK 前退出,重投会导致重复处理。幂等要求同一个业务事件重复到达时,不产生重复业务效果。示例把去重标记、虚构通知记录和 ACK 放在 Redis 原子操作中;真实邮件系统不在这个原子边界内,不能据此宣称端到端 exactly-once。
生产系统还需限制重试次数、安排退避和人工处理/死信出口。永久无效消息不能无限重试。消息积压长期增加说明消费能力不足或下游故障,队列只缓冲工作,不凭空增加处理能力。
订单落库和消息发布之间也可能失败。事务发件箱 Outbox 的思路是在同一数据库事务保存业务变化和待发事件,再由投递器发送。本周要求能画出这个失败窗口,不实现完整 Outbox、死信系统或高可用 MQ。
自主学习任务:从最小示例走向常用消息系统
核心提交是一张选型与风险卡:哪一步可以延后、调用方拿到什么状态、失败如何发现、重复如何处理。使用自己的订单通知场景,不能只抄术语。
进阶选做:阅读 RabbitMQ Work Queues 官方教程,运行生产者和消费者;消费者收到消息后退出,观察未确认消息的去向。比较消息持久化、消费者确认与发布者确认各自解决什么问题。提交运行环境、关键输出和一处失败观察,未运行就写未执行。
挑战选做:比较 Redis Streams 与 RabbitMQ 在任务确认、积压观察和团队运维上的差异。Kafka 作为日志流平台的延伸阅读,不要求本周安装。所有自学任务都有边界与产物,不额外占用必修验收。
参考:Redis Streams、RabbitMQ Work Queues、RabbitMQ 确认机制。
学生自己动手,AI 只能改你允许改的地方
我先保存当前可运行版本,再完成“跟踪一条通知消息;说明入队不等于通知完成”。让 AI 一次只修改负责该行为的文件;修改前预测正常与失败结果,检查差异,再运行回归。每人需要解释一处接受或拒绝的建议。
错误与边界怎样验收
今天重点检查:用 create_task 假装持久队列、先 ACK 后处理、无界重试。保留输入、预期、实际输出和修复后的结果,不能只贴全绿截图。未验证的外部依赖如实记为待验证。
留给下一阶段
今天把浏览器结果与订单事实对齐,并明确通知入队和发送完成是两件事。明天不再增加新组件,按成功、失败、权限和恢复路径把前三周的系统完整走一遍。