📚前言
📒FDE系列内容总纲:
🚄前置课程列表:
见文档结尾附录。
🚀阶段2·Day 49:OpenAPI 文档与接口测试
FDE 学习系列教程 · 第二阶段 · 第 6 周 · Day 4 预计时长:3 小时 | 难度:★★★☆☆ | 前置知识:Day 46 RESTful 与认证、Pydantic 模型、pytest 基础
📌 一句话目标:学会用 FastAPI/Pydantic 写出"自带说明"的接口文档(OpenAPI/Swagger UI),并用 pytest + TestClient 给 API 编写覆盖正常与异常路径的自动化测试。
🧑🤝🧑 开场:代码写完了,怎么让别人会用、怎么保证没改坏
经过前三天,你的系统已经能:对外部提供 RESTful 接口(还带认证)、主动调飞书、接收 Webhook 事件。现在要解决两个交付层面的现实问题。
问题一:别人怎么知道你有哪些接口、参数怎么传?
没有文档的项目,对接就是灾难现场:
同事:你这个创建工单的接口,URL 是啥?要传哪些字段?
你:呃...... /api/ticket?字段我想想...... title、content 还有个啥来着,我看下代码
同事:返回 422 是啥意思?
你:我翻翻......
更糟的是手写文档 :你今天写了一份 Word/Markdown,下周改了接口忘了同步,文档立刻变成"骗人的东西"。FastAPI 的杀手锏是------文档由代码自动生成,永远不会过期。
问题二:改了代码,怎么确定没把旧功能改坏?
现在你只有 5 个接口,手动点一遍还行。等项目有 30 个接口、对接 3 个平台,每次上线都手测一遍是不可能的。自动化测试让你改完代码跑一条命令,几秒钟就知道有没有"按下葫芦浮起瓢"。
今天两件事,正好对应"可交付"和"可维护":
① OpenAPI 文档 ──► 让接口会说话,别人(和未来的你)看得懂、点一下就能试
② pytest 测试 ──► 给接口装上安全网,改代码后一条命令验证有没有改坏
📖 一、认识 FastAPI 自动文档
FastAPI 底层基于一个叫 OpenAPI 的开放规范------它用一份 JSON 把"你有哪些接口、每个接口要什么参数、返回什么结构"完整描述出来。然后有两种网页把这份 JSON 渲染成人类友好的界面:
你的 FastAPI 代码
│(框架自动扫描路由、Pydantic 模型)
▼
openapi.json(机器可读的接口描述)
│
├──► /docs Swagger UI:可交互,能直接在网页上发请求试接口 ⭐
│
└──► /redoc ReDoc:排版漂亮,适合阅读/对外展示
| 地址 | 名称 | 特点 | 用途 |
|---|---|---|---|
/docs |
Swagger UI | 可交互、能在线调试、有 "Try it out" | 开发调试、内部联调最常用 |
/redoc |
ReDoc | 三栏排版、只读、更美观 | 给客户/非技术方阅读 |
/openapi.json |
原始规范 | 纯 JSON | 导入 Postman/Apifox、生成客户端 SDK |
💡 这不是 FastAPI 独有的魔法,而是行业标准(OpenAPI 规范,前身叫 Swagger)。Java 的 SpringDoc、Go 的 swag 都产出同一份规范,所以今天学的概念在任何技术栈都通用。
🖥️ 二、写一个"文档丰富"的工单 API
先建一个待完善的 API,再一步步看文档是怎么自动长出来的。新建 app_docs.py:
"""
带完整 OpenAPI 文档元信息的工单 API
运行:uvicorn app_docs:app --reload
访问:http://127.0.0.1:8000/docs
"""
from fastapi import FastAPI, HTTPException, Header
from pydantic import BaseModel, Field
from typing import Literal
from datetime import datetime
app = FastAPI(
title="FDE 设备工单系统 API",
description=(
"面向工厂数字化工程师(FDE)的设备工单管理服务。\n\n"
"- 支持工单的创建、查询、状态流转\n"
"- 使用 API Key 鉴权(请求头 X-API-Key)\n"
"- 告警工单可由飞书 Webhook 自动创建(见 /webhook)"
),
version="1.2.0",
contact={"name": "FDE 平台组", "email": "fde@example.com"},
license_info={"name": "内部使用"},
)
API_KEY = "fde-secret-2026"
tickets: list[dict] = []
# ---------- 请求模型 ----------
class TicketCreate(BaseModel):
title: str = Field(
..., min_length=2, max_length=50,
description="工单标题,简明描述问题,如:注塑机A3温度超标",
examples=["注塑机A3 温度超标"],
)
device_id: str = Field(
..., pattern=r"^[A-Z]+\d+$",
description="设备编号,格式为 字母+数字,如 A3、B12",
examples=["A3"],
)
priority: Literal["low", "medium", "high"] = Field(
"medium",
description="优先级,默认 medium",
)
description: str | None = Field(
None, max_length=500,
description="详细描述,选填",
)
# ---------- 响应模型 ----------
class Ticket(BaseModel):
id: int = Field(..., description="工单自增 ID")
title: str = Field(..., description="工单标题")
device_id: str = Field(..., description="设备编号")
priority: str = Field(..., description="优先级")
status: Literal["open", "processing", "closed"] = Field(
..., description="工单状态")
created_at: datetime = Field(..., description="创建时间 ISO 格式")
class ErrorResponse(BaseModel):
detail: str = Field(..., description="错误说明")
def _auth(x_api_key: str | None):
if x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="无效或缺失的 API Key")
# ---------- 接口 ----------
@app.post(
"/api/tickets",
response_model=Ticket,
status_code=201,
tags=["工单管理"],
summary="创建工单",
response_description="创建成功,返回完整工单对象",
responses={
401: {"model": ErrorResponse, "description": "API Key 无效"},
422: {"description": "请求参数校验失败"},
},
)
def create_ticket(payload: TicketCreate, x_api_key: str | None = Header(None)):
"""创建一张设备工单。
## 业务规则
- 标题 2~50 字,设备编号必须符合 **字母+数字** 格式
- 优先级 high 的工单会进入值班群加急通道
- 创建后初始状态为 `open`
## 示例场景
车间巡检发现注塑机温度超标时调用本接口,随后可通过
`PATCH /api/tickets/{id}` 推进状态。
"""
_auth(x_api_key)
ticket = {
"id": len(tickets) + 1,
"title": payload.title,
"device_id": payload.device_id,
"priority": payload.priority,
"status": "open",
"created_at": datetime.now(),
}
tickets.append(ticket)
return ticket
@app.get(
"/api/tickets",
response_model=list[Ticket],
tags=["工单管理"],
summary="查询工单列表",
)
def list_tickets(
x_api_key: str | None = Header(None),
status: Literal["open", "processing", "closed"] | None = None,
):
"""查询工单,可按 `status` 过滤;不传则返回全部。"""
_auth(x_api_key)
if status:
return [t for t in tickets if t["status"] == status]
return tickets
@app.patch(
"/api/tickets/{ticket_id}",
response_model=Ticket,
tags=["工单管理"],
summary="更新工单状态",
responses={404: {"model": ErrorResponse, "description": "工单不存在"}},
)
def update_ticket(
ticket_id: int,
status: Literal["open", "processing", "closed"],
x_api_key: str | None = Header(None),
):
"""推进工单状态,例如 open → processing → closed。"""
_auth(x_api_key)
for t in tickets:
if t["id"] == ticket_id:
t["status"] = status
return t
raise HTTPException(404, f"工单 {ticket_id} 不存在")
启动后打开 High Performance Web Crawler API - Swagger UI ,你会看到:
-
顶部是标题、版本、说明(来自
FastAPI(...)的元信息) -
"工单管理" 标签把接口分组(来自
tags) -
每个接口可点开,看到参数说明、示例值、响应结构
-
点右上角 Authorize 或直接在接口里填
X-API-Key请求头,再点 Try it out → Execute,能直接在网页上发真实请求!
📖 三、让文档变好的关键写法
对比"裸奔接口"和"完善接口",文档质量由这些要素决定:
| 写法 | 作用 | 出现在文档哪里 |
|---|---|---|
FastAPI(title=..., version=...) |
项目级标题/版本/说明 | 文档顶部 |
tags=["工单管理"] |
接口分组归类 | 按标签折叠分组 |
summary= |
一句话接口名 | 接口标题栏 |
| 函数 docstring | 详细说明(支持 Markdown 标题/列表) | 接口详情的 Description |
Field(..., description=, examples=) |
字段含义、示例、校验规则 | 请求体/响应体每个字段旁 |
Literal["low","medium"] |
枚举可选值 | 文档显示下拉可选,非法值 422 |
response_model=Ticket |
明确返回结构 | Responses 区展示返回 JSON 结构 |
responses={401:..., 404:...} |
补充错误响应说明 | 文档列出各状态码及含义 |
status_code=201 |
默认成功状态码 | 文档标注 |
文档质量阶梯:
裸奔:@app.post("/api/tickets") → 文档只有个路径,参数靠猜
│
加模型:payload: TicketCreate → 自动有了字段结构和类型
│
加 Field:description + examples → 每个字段有中文解释和示例 ⭐
│
加 tags/summary/responses → 分组清晰、错误码齐全,可对外交付
📌 核心理念:文档即代码(Docs as Code) 。你不是"另外写一份文档",而是在定义模型和接口时顺手把描述、示例、校验写进去,文档自动生成、随代码更新。这彻底解决了"文档和代码不一致"的老问题。
导出规范 & 导入 Postman/Apifox
-
浏览器访问
http://127.0.0.1:8000/openapi.json,保存为 JSON 文件 -
在 Postman:Import → 选择该文件 → 自动生成整个接口集合
-
在 Apifox:导入数据 → OpenAPI/Swagger → 选文件,还能一键生成 Mock 服务
团队对接时,把这份 JSON(或直接给 /docs 地址)发给前端/第三方即可,不用再口头讲接口。
📖 四、接口测试基础:为什么用 TestClient
写 API 测试最朴素的想法是:先启动 uvicorn,再用 httpx 发请求断言。但这样测试依赖服务器在运行,麻烦又慢。
FastAPI 提供 TestClient (基于 httpx),它能不启动真实服务器、在测试进程内部直接"模拟"发请求:
from fastapi.testclient import TestClient
from app_docs import app
client = TestClient(app)
resp = client.post("/api/tickets", json={"title": "测试工单", "device_id": "A1"},
headers={"X-API-Key": "fde-secret-2026"})
assert resp.status_code == 201
assert resp.json()["status"] == "open"
TestClient 的方法和 httpx 一模一样(.get/.post/.patch、json=、headers=、params=),所以你已经会用了。
🖥️ 五、编写完整测试套件
安装:
pip install pytest
项目结构(测试文件以 test_ 开头,pytest 才能自动发现):
fde-api/
├── app_docs.py
└── tests/
├── __init__.py
├── conftest.py # 夹具(fixture)放这里
└── test_tickets.py # 测试用例
夹具 conftest.py------准备可复用的"测试环境"
"""pytest 夹具:多个测试文件共享的初始化逻辑"""
import pytest
from fastapi.testclient import TestClient
from app_docs import app, tickets
@pytest.fixture
def client():
"""每个测试用例拿到一个干净的 TestClient"""
return TestClient(app)
@pytest.fixture(autouse=True)
def reset_db():
"""autouse=True:每个用例执行前自动清空内存工单,保证用例互不干扰"""
tickets.clear()
yield # 这里把控制权交给测试用例
# yield 之后是"收尾",本例无需处理
@pytest.fixture
def auth_headers():
"""带正确 API Key 的请求头"""
return {"X-API-Key": "fde-secret-2026"}
💡
yield把 fixture 分成"准备"和"清理"两半。autouse=True表示不用在每个测试里显式声明,它自动生效------这里用来在每个用例前重置数据,避免"上一个用例创建的工单影响下一个用例的断言",即测试隔离。
测试用例 test_tickets.py
"""工单 API 测试:正常路径 + 异常路径都要覆盖"""
import pytest
# ---------- 创建工单 ----------
def test_create_ticket_success(client, auth_headers):
"""正常创建:201 + 返回结构正确"""
resp = client.post(
"/api/tickets",
json={"title": "注塑机A3温度超标", "device_id": "A3", "priority": "high"},
headers=auth_headers,
)
assert resp.status_code == 201
data = resp.json()
assert data["id"] == 1
assert data["status"] == "open" # 业务默认值
assert data["device_id"] == "A3"
assert "created_at" in data
def test_create_ticket_default_priority(client, auth_headers):
"""不传 priority,应默认 medium"""
resp = client.post(
"/api/tickets",
json={"title": "普通巡检问题", "device_id": "B2"},
headers=auth_headers,
)
assert resp.status_code == 201
assert resp.json()["priority"] == "medium"
# ---------- 鉴权 ----------
def test_create_without_api_key(client):
"""没有 API Key:401"""
resp = client.post("/api/tickets",
json={"title": "测试", "device_id": "A1"})
assert resp.status_code == 401
def test_create_with_wrong_api_key(client):
"""错误 API Key:401"""
resp = client.post(
"/api/tickets",
json={"title": "测试", "device_id": "A1"},
headers={"X-API-Key": "wrong-key"},
)
assert resp.status_code == 401
# ---------- 参数校验 ----------
def test_invalid_device_id_format(client, auth_headers):
"""设备编号不符合 字母+数字:422"""
resp = client.post(
"/api/tickets",
json={"title": "测试", "device_id": "设备-@#"},
headers=auth_headers,
)
assert resp.status_code == 422
def test_title_too_short(client, auth_headers):
"""标题少于 2 字:422"""
resp = client.post(
"/api/tickets",
json={"title": "A", "device_id": "A1"},
headers=auth_headers,
)
assert resp.status_code == 422
def test_invalid_priority_enum(client, auth_headers):
"""priority 不在枚举内:422"""
resp = client.post(
"/api/tickets",
json={"title": "测试工单", "device_id": "A1", "priority": "urgent!!!"},
headers=auth_headers,
)
assert resp.status_code == 422
# ---------- 查询与状态流转 ----------
def test_list_and_filter(client, auth_headers):
"""创建 2 张,按 status 过滤"""
client.post("/api/tickets", json={"title": "工单一", "device_id": "A1"},
headers=auth_headers)
client.post("/api/tickets", json={"title": "工单二", "device_id": "A2"},
headers=auth_headers)
client.patch("/api/tickets/2?status=closed", headers=auth_headers)
all_tickets = client.get("/api/tickets", headers=auth_headers).json()
assert len(all_tickets) == 2
closed = client.get("/api/tickets", params={"status": "closed"},
headers=auth_headers).json()
assert len(closed) == 1
assert closed[0]["id"] == 2
def test_update_nonexistent_ticket(client, auth_headers):
"""更新不存在的工单:404"""
resp = client.patch("/api/tickets/999?status=closed", headers=auth_headers)
assert resp.status_code == 404
# ---------- 参数化:一个测试逻辑跑多组数据 ----------
@pytest.mark.parametrize("device_id,expected_ok", [
("A3", True),
("B12", True),
("ABC", False), # 没有数字
("12A", False), # 数字开头
("a3", False), # 小写字母
])
def test_device_id_pattern(client, auth_headers, device_id, expected_ok):
resp = client.post(
"/api/tickets",
json={"title": "编号校验", "device_id": device_id},
headers=auth_headers,
)
if expected_ok:
assert resp.status_code == 201
else:
assert resp.status_code == 422
运行:
pytest -v
test_tickets.py::test_create_ticket_success PASSED
test_tickets.py::test_create_ticket_default_priority PASSED
test_tickets.py::test_create_without_api_key PASSED
test_tickets.py::test_create_with_wrong_api_key PASSED
test_tickets.py::test_invalid_device_id_format PASSED
...
test_tickets.py::test_device_id_pattern[A3-True] PASSED
test_tickets.py::test_device_id_pattern[12A-False] PASSED
============= 13 passed in 0.5s =============
📖 六、测试设计的核心原则
一个测试只验证一件事 + 测试结构 AAA
每个测试用例遵循 AAA 三段式:
Arrange(准备) 构造数据、请求头
│
Act(行动) 发起一次请求
│
Assert(断言) 检查状态码 + 返回内容
例:
resp = client.post(...) # Act
assert resp.status_code == 201 # Assert
assert resp.json()["status"]=="open" # Assert
正常路径 vs 异常路径(测试价值的分水岭)
新手最容易犯的错:只测"一切正常"的情况。但生产事故几乎都发生在异常分支。看这份覆盖清单:
| 路径类型 | 用例 | 为什么必须测 |
|---|---|---|
| ✅ 正常 | 合法参数创建成功 201 | 基本功能 |
| ✅ 正常 | 默认值、查询过滤、状态流转 | 业务规则 |
| ❌ 鉴权异常 | 无 Key / 错 Key → 401 | 防止安全裸奔 |
| ❌ 入参异常 | 格式错/枚举错/长度错 → 422 | Pydantic 校验是否生效 |
| ❌ 资源异常 | 更新不存在工单 → 404 | 边界条件 |
| 🔁 副作用 | 幂等、重复提交 | Day 48 的重点 |
📌 经验法则 :写接口时每写一个
raise HTTPException或加一条校验规则,就该有对应测试证明它真的会触发。测试不是证明"代码能跑",而是证明"错误被正确拦截"。
参数化测试:消除复制粘贴
@pytest.mark.parametrize 让一组数据跑同一个测试逻辑,上面设备编号的例子用 1 个函数覆盖了 5 种输入。新增边界值只要往列表里加一行,不要复制 5 个几乎相同的函数。
测试隔离
reset_db 夹具在每个用例前清空数据,这很关键。如果测试共享状态,就会出现"单独跑能过、一起跑就挂"的诡异问题------通常是前一个用例的数据污染了后一个。
📖 七、测试覆盖率:知道自己漏测了什么
覆盖率工具统计"测试运行时,代码有多少比例的行被执行到":
pip install pytest-cov
pytest --cov=app_docs --cov-report=term-missing
Name Stmts Miss Cover
---------------------------------
app_docs.py 45 3 93%
--cov-report=term-missing 还会告诉你具体哪几行没被覆盖,直接定位需要补测试的分支。
理性看待覆盖率:
覆盖率 ≠ 测试质量。100% 覆盖也可能断言很弱(只断言不报错)。
它的价值是【发现明显没测到的代码】,而不是追求数字。
FDE 项目建议:核心业务逻辑 80%+,对接/工具代码可适当放宽。
📝 本课小结
| 知识点 | 一句话记住 |
|---|---|
| OpenAPI | 用 JSON 标准描述接口,跨语言通用 |
| /docs | Swagger UI,可交互在线调试 |
| /redoc | 只读美观文档,适合对外 |
| openapi.json | 可导入 Postman/Apifox |
| 文档元信息 | title/version/tags/summary/docstring |
| Field | description + examples + 校验,字段级文档 |
| response_model | 明确返回结构,自动进文档 |
| responses | 补充 401/404/422 等错误响应说明 |
| 文档即代码 | 文档随代码自动生成,永不过期 |
| TestClient | 不起服务器即可测试 API,用法同 httpx |
| fixture | 可复用的准备/清理逻辑,autouse 做隔离 |
| AAA | Arrange 准备 → Act 执行 → Assert 断言 |
| 异常路径 | 401/422/404 必须测,事故都在异常分支 |
| 参数化 | 一组数据跑同一逻辑,加边界值只加一行 |
| 覆盖率 | 发现漏测代码,理性看待数字 |
🧠 核心认知 :在 FDE 的交付现场,文档和测试不是"锦上添花",而是项目能不能交给别人、能不能长期维护的分水岭。用 FastAPI,写好 Pydantic 模型和字段描述,文档是白送的;养成"每写一个接口就补正常+异常测试"的肌肉记忆,你就能放心地反复修改系统------因为有一张自动化的安全网兜底,改坏了立刻报警。
📋 课后练习
练习 1:玩转自动文档(约 20 分钟)
-
启动 app_docs,打开 /docs,用 Authorize 或手动填请求头完成一次创建工单的 Try it out
-
故意在网页上提交一个非法 device_id(如
@@@),观察返回的 422 错误结构 -
打开 /redoc 对比两种文档风格;导出 openapi.json,尝试导入 Postman 或 Apifox
练习 2:补全测试(约 40 分钟)
-
给
list_tickets补一个测试:传非法的 status 枚举值应返回 422 -
给鉴权补测试:不带 Key 查询工单列表 应返回 401
-
用参数化写一个测试:标题长度分别为 1、2、50、51,验证边界(2 和 50 成功,1 和 51 返回 422)
-
跑
pytest --cov=app_docs --cov-report=term-missing,查看是否还有未覆盖的行
练习 3:给昨天的 Webhook 写测试(约 40 分钟)
-
用 TestClient 对 Day 48 的
/webhook/feishu写测试:-
正确 challenge 握手返回原值
-
错误 token 返回 403
-
同一 event_id 连发两次,第二次是 duplicate,工单不重复
-
-
思考:为什么 Webhook 这种"依赖外部平台推送"的接口,尤其需要自动化测试?(提示:你没法等飞书真实推送来验证每次改动)
🔭 下节预告:第二阶段大收官 🎉
第 3 周到第 6 周,你从 SQL 一路打到了 API 双向集成。明天是综合项目日,我们把这四周学的东西串成一个完整作品:
设备告警自动化工单闭环系统:设备数据入库(SQL)→ 超阈值触发告警 → 飞书 API 推送消息(主动调用)→ 群里回复自动建单(Webhook 接收)→ 工单状态流转(RESTful + JWT)→ Docker Compose 一键部署 → OpenAPI 文档 + pytest 测试。
明天不仅是写代码,更重要的是看一个真实 FDE 项目的完整目录结构、模块分层和端到端联调流程,以及第二阶段整体复盘和第三阶段的路线预告。带着这四周的笔记,明天见!
🌍附录:前置课程列表
阶段一:认知启蒙(AI 认知与 FDE 角色)
AI 认知
【FDE系列】阶段1Day 1:AI 层级关系 --- 四个嵌套的圈-CSDN博客
【FDE系列】阶段1Day 2:AI 三阶段发展史 --- 会认 → 会判断 → 会创造-CSDN博客
【FDE系列】阶段1Day 3:符号 AI vs 机器学习 --- 两条路线的本质区别-CSDN博客
【FDE系列】阶段1Day 4:Transformer 的历史意义 --- 2017 年的分水岭-CSDN博客
【FDE系列】阶段1Day 5:本周复习与自测 --- 检验你的 AI 认知地基-CSDN博客
【FDE系列】阶段1Day 6:Transformer 架构 --- 一张图纸盖出千千万万栋楼-CSDN博客
【FDE系列】阶段1Day 7:LLM 本质 --- 文字接龙机器-CSDN博客
【FDE系列】阶段1Day 8:Token --- 模型眼中的最小单位-CSDN博客
【FDE系列】阶段1Day 9:AI 幻觉 --- 为什么会一本正经地胡说八道-CSDN博客
【FDE系列】阶段1Day 10:上下文窗口 --- 模型的记忆力上限 + 本周复习-CSDN博客
【FDE系列】阶段1Day 11:Prompt --- 给模型立规矩-CSDN博客
【FDE系列】阶段1Day 12:Memory --- 让模型记住上下文
【FDE系列】阶段1Day 13:RAG --- 给模型配图书管理员-CSDN博客
【FDE系列】阶段1Day 14:Tool Use --- 让模型动手操作-CSDN博客
【FDE系列】阶段1Day 15:MCP --- 统一的工具接口标准 + 第三周复习-CSDN博客
FDE 基础概念
【FDE系列】阶段1Day 16:什么是 FDE --- 把 AI 变成客户结果的人-CSDN博客
【FDE系列】阶段1Day 17:FDE vs 传统实施 --- 三大本质区别-CSDN博客
【FDE系列】阶段1Day 18:FDE 三重身份 + C6 胜任力模型-CSDN博客
【FDE系列】阶段1Day 19:七阶段行动路径 + 行业经验的价值-CSDN博客
【FDE系列】阶段1Day 20:阶段总结与产出物 --- 第一阶段收官-CSDN博客
阶段二:技术地基(Python + FastAPI + SQL + Docker + API 集成)
Python基础
【FDE系列】阶段2:Day 21:Python 环境搭建 --- 写出你的第一行代码-CSDN博客
【FDE系列】阶段2:Day 22:变量、数据类型、条件判断 --- Python 的"记忆"和"判断"-CSDN博客
【FDE系列】阶段2:Day 23:循环与函数 --- 让代码跑 100 遍、把逻辑打包复用-CSDN博客
【FDE系列】阶段2:Day 24:数据结构 --- 列表、字典、集合、元组-CSDN博客
【FDE系列】阶段2:Day 25:文件读写与 JSON --- 让程序连通外部数据(第一周收官)-CSDN博客
【FDE系列】阶段2:Day 26:模块化编程 --- 把代码拆成"抽屉柜"-CSDN博客
【FDE系列】阶段2:Day 27:异常处理与日志 --- 让程序"摔不烂、查得到"-CSDN博客
FastAPI入门到进阶
【FDE系列】阶段2:Day 28:FastAPI 入门 --- 把你的函数变成 API 服务-CSDN博客
【FDE系列】阶段2:Day 29:FastAPI 进阶 --- Pydantic 模型与完整 CRUD 实战-CSDN博客
【FDE系列】阶段2:Day 30:生产代码规范 --- 测试、类型注解、配置管理(第二周收官)-CSDN博客
SQL基础
【FDE系列】阶段2:Day 31:SQL 基础 --- 增删改查一把梭-CSDN博客
【FDE系列】阶段2:Day 32:多表查询 --- JOIN 与聚合-CSDN博客
【FDE系列】阶段2:Day 33:进阶查询 --- 窗口函数与 CTE-CSDN博客
【FDE系列】阶段2:Day 34:数据清洗 --- 把脏数据捋干净-CSDN博客
【FDE系列】阶段2:Day 35:Python + SQL --- 工单接入 MySQL + 本周收官-CSDN博客
Linux基础
【FDE系列】阶段2:Day 36:Linux 入门与文件操作 --- 扔掉鼠标的第一天-CSDN博客
【FDE系列】阶段2:Day 37:权限、进程与文本三剑客-CSDN博客
【FDE系列】阶段2:Day 38:Shell 脚本 --- 把命令串起来自动跑-CSDN博客
【FDE系列】阶段2:Day 39:Linux 综合实战 --- 让服务无人值守-CSDN博客
【FDE系列】阶段2:Day 40:Shell 进阶 --- 生产级脚本与本周收官-CSDN博客
Docker
【FDE系列】阶段2:Day 41:Docker 入门 --- 把环境装进盒子-CSDN博客