
一.一个系统由哪几块组成前端画界面 收集用户操作后端处理业务逻辑 调数据库 调算法数据库存取数据算法专门算风险分API接口定义前端和后端之间的约定。前端说我要什么后端说给你什么。这个项目里后端定义了 40 多个 API比如POST /api/auth/login—— 登录GET /api/results/b—— 查 B 端结果POST /api/tasks—— 创建算法任务类比菜单。服务员前端照着菜单API向后厨后端点菜。一个 API 长什么样textGET /api/results/b?page1pageSize20GET我要拿数据不是给数据/api/results/b我要 B 端结果?page1pageSize20我要第 1 页每页 20 条二、第二课一个请求的完整旅程现在我们真正打开代码跟着一次点击走完全程。场景你在浏览器里点了一下左侧菜单的B 端结果。【第 0 步】你在浏览器里看到了什么左侧有一排菜单其中一个是B 端结果。你点了一下。这一步的代码在哪web/tobacco_system/frontend/src/layout/MainLayout.vue这个文件就是左侧菜单的图纸。里面定义了 6 个菜单分组、15 个菜单项。B 端结果是其中之一它对应一个路由地址比如/b-results。什么是路由你可以理解成网址里的路径。就像你打开qijiang.com/b-results→ 显示 B 端结果页你打开qijiang.com/c-results→ 显示 C 端结果页路由就是哪个网址显示哪个页面的对照表。【第 1 步】前端从点击到发请求你点击后前端做了两件事① 切换页面src/router/index.ts这个文件里有一张路由表它说text/b-results → 显示 BResults.vue 这个页面组件于是前端把页面换成B 端结果页。② 页面加载时自动去后端要数据src/views/BResults.vueB 端结果页被打开后它内部会执行一段代码大意是javascript// 打开页面时去后端要第 1 页数据 const data await getBResults({ page: 1, pageSize: 20 })这里的getBResults就是一个API 调用函数。所有 API 函数都在这个文件里src/api/tobacco.ts这个文件里有56 个函数每个函数对应后端的一个接口。比如前端函数对应的后端接口getBResultsGET /api/results/bgetCResultsGET /api/results/cloginPOST /api/auth/logincreateTaskPOST /api/tasks【第 2 步】前端真正发出网络请求getBResults内部会调用一个更底层的工具src/api/client.ts这个文件是所有网络请求的总闸。它用的是浏览器自带的fetch函数不是 axios负责拼出完整网址http://你的服务器/api/results/b?page1pageSize20带上身份令牌TokenAuthorization: Bearer xxxxx发送请求接收响应如果返回401未登录自动清空登录状态并跳回登录页这一步发出去的东西就是一个 HTTP 请求。什么是 HTTP 请求你可以把它想象成一封信信封上写地址GET /api/results/b?page1pageSize20我要什么信封里带证件Authorization: Bearer xxxxx我是谁寄出去通过互联网发给服务器服务器收到后回一封信HTTP 响应里面装着你要的数据。请求长这样简化版textGET /api/results/b?page1pageSize20 Authorization: Bearer eyJhbGci...【第 3 步】后端收到请求找到对应的处理函数请求飞到服务器后被FastAPI后端框架接住。FastAPI 做的事情看请求的路径/api/results/b去路由表里找谁能处理这个路径。所有的后端接口都定义在这一个文件里app/api/routes.py这个文件大约1000 行包含40 多个接口。它就像后端的总机。里面有一个函数专门处理/api/results/b大概长这样简化pythonrouter.get(/results/b, response_modelPaginatedBResults) def get_b_results( page: int 1, pageSize: int 20, current_user: CurrentUser Depends(require_user), # ← 先验证登录 ): return query_b_results(pagepage, page_sizepageSize)逐行解释router.get(...)装饰器告诉 FastAPI 这个函数负责处理 GET /results/bresponse_modelPaginatedBResults告诉 FastAPI 返回的数据要符合这个格式page: int 1从网址里读page参数默认是 1Depends(require_user)先检查你有没有登录没登录直接返回 401query_b_results(...)真正去查数据库的函数什么是装饰器就是写在函数上面的xxx那一行。它的作用是给这个函数贴个标签告诉框架这个函数是干嘛的。你不需要理解它的实现只要知道看到router.get(...)就说明这是一个对外的 API。【第 4 步】后端去数据库捞数据query_b_results这个函数在另一个文件里app/services/tobacco_db_service.py这个文件是整个后端最大的一个60KB专门负责从数据库查数据。它内部会做这些事python# 1. 打开数据库连接 with get_anomaly_session() as session: # 2. 写一个查询从 tobacco_b_result 表里按风险分从高到低排序 stmt select(TobaccoBResult).order_by(TobaccoBResult.final_risk_score.desc()) # 3. 加上只查当前批次的条件 stmt stmt.where(TobaccoBResult.batch_id current_batch_id) # 4. 分页跳过前 N 条取 20 条 stmt stmt.offset((page - 1) * page_size).limit(page_size) # 5. 执行查询 rows session.scalars(stmt).all() # 6. 顺便统计一下总共有多少条给前端显示共 XX 条 total session.scalar(select(func.count()).select_from(TobaccoBResult)...)关键点这里出现了ORM对象关系映射。什么是 ORM数据库里的表是二维表格行和列。Python 代码里操作表格很别扭。ORM 的作用是把每一行数据变成一个 Python 对象你就可以像操作普通对象一样操作它。比如数据库里有一行text零售商ID: R001, 最终风险分: 92, 危险等级: L4ORM 会把它变成一个对象pythonrow.零售商ID # R001 row.final_risk_score # 92 row.risk_level # L4这个项目用的 ORM 是 SQLAlchemy。ORM 模型定义在哪app/models/tobacco.py这个文件里定义了所有数据库表的Python 版本。比如pythonclass TobaccoBResult(AnomalyBase): __tablename__ tobacco_b_result # 对应数据库里的表名 id mapped_column(Integer, primary_keyTrue) batch_id mapped_column(String(64)) retailer_id mapped_column(String(64)) # 零售商ID final_risk_score mapped_column(Float) # 最终风险分 risk_level mapped_column(String(16)) # 危险等级 ...记住这个对应关系数据库世界Python 世界表tobacco_b_result类TobaccoBResult一行数据一个对象实例一列一个属性row.final_risk_score【第 5 步】数据返回从数据库 → 后端 → 前端数据库把数据交给后端。后端做两件事① 包装成前端要的格式后端不能直接把数据库对象扔给前端前端不认识。它要把数据转成 JSON。什么是 JSONJSON 就是一种文本格式长得像 Python 的字典和列表。所有语言都能读懂它。例子json{ total: 1234, items: [ { retailerId: R001, finalRiskScore: 92, riskLevel: L4 }, { retailerId: R002, finalRiskScore: 88, riskLevel: L4 } ] }这个转 JSON的过程由 Pydantic 负责。Pydantic 的模型定义在app/schemas/tobacco.py这个文件定义了所有请求长什么样和响应长什么样比如PaginatedBResults分页的 B 端结果。Pydantic 的作用约定数据的格式。它保证后端返回的 JSON字段名和类型都符合前端期望。② 返回响应后端把 JSON 塞进 HTTP 响应发回给前端。【第 6 步】前端拿到数据渲染界面前端收到 JSON 后把它交给Vue处理。Vue 会把items数组渲染成表格的每一行把total显示在共 1234 条把riskLevel渲染成彩色的标签L4 红色、L3 橙色……渲染是什么就是把数据变成你看到的画面。Vue 的核心能力就是这个数据一变页面自动更新。三、完整流程图请截图保存text你点击B端结果 │ ▼ [前端] MainLayout.vue 菜单 → router/index.ts 路由 → BResults.vue 页面 │ ▼ [前端] BResults.vue 调 getBResults() ← src/api/tobacco.ts │ ▼ [前端] client.ts 用 fetch 发出 HTTP 请求 │ GET /api/results/b?page1pageSize20 │ Authorization: Bearer xxx ▼ [网络] ────────────────────────────────────────► │ ▼ [后端] FastAPI 收到请求 → routes.py 找到 get_b_results 函数 │ ▼ [后端] Depends(require_user) 先验证登录 ✅ │ ▼ [后端] 调 query_b_results() ← tobacco_db_service.py │ ▼ [数据库] MySQL 执行 SQL从 tobacco_b_result 表捞出 20 行 │ 表结构定义在 models/tobacco.py ▼ [后端] SQLAlchemy 把每行变成对象 → Pydantic 转成 JSON │ 格式定义在 schemas/tobacco.py ▼ [网络] ◄──────────────────────────────────────── │ HTTP 200 JSON ▼ [前端] BResults.vue 拿到数据Vue 渲染成表格 │ ▼ 你看到了 B 端结果表格 四、一个陷阱提醒现在只需知道不用深究这个项目有个很特别的设计当前批次不存在请求里。一般系统会在请求里带?batchIdxxx但这个系统不是。它把你当前选的批次存在服务器的数据库里tobacco_user_session表的selected_batch_id字段。后端在处理请求时通过app/services/request_context.py这个工具隐式地读到当前批次。后果同一个账号在两个浏览器标签页里不能选不同批次——后选的会覆盖先选的。这个设计新手最容易踩坑先记住现象就行后面专门讲。五、今天的小作业这次作业全部是找文件不用写代码。请你在脑子里或打开项目文件夹回答前端左侧菜单的定义在哪个文件所有 API 调用函数集中在哪个文件发网络请求的总闸是哪个文件后端所有 API 接口定义在哪个文件定义响应数据格式的文件是哪个定义数据库表的文件是哪个流程题用大白话假设你在 B 端结果页把每页改成 50 条。请按顺序说出哪 6 个文件/环节会被涉及到提示参考上面的流程图