ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

10天从零搭建FastAPI记账项目:工程闭环与部署实战

10天从零搭建FastAPI记账项目:工程闭环与部署实战 很多“要变强”的念头都是从一句半开玩笑的话开始的兄弟以为给自己 10 天时间学学框架、写写接口就能变成那种什么问题都能秒解的技术超人。结果真正走到第 10 天自己还在调一个没有输出内容的接口或者被服务器上的 500 错误按在地上摩擦。差距不在天赋而在预期和现实之间缺了一条可以复现、可以验证、可以排错的工程路径。这篇博客把“10 天变成技术超人”拉回到地面上用一个 10 天可完成的最小记账项目从环境隔离、数据库、接口、日志、测试一直做到本地到生产环境的差异处理。文章里给出的代码用来演示思路实际项目要结合自己的包名、路径和依赖版本调整但这条主线可以直接带走。1. 为什么“10 天后我就成技术超人”这句话热血执行时却会翻车先说通俗版的结论人很容易把“看懂了”当成“会了”把“跑通了”当成“交付了”。教程里每段代码都有上下文而真实项目里一个报错背后可能同时牵扯环境、依赖、路径、权限和网络。10 天计划如果只指向“变得更厉害”缺少明确的验收标准那第 10 天大概率只是在原地转圈。1.1 从“我以为”到“实际上”之间隔着一整个调试过程“我以为 10 天后可以写出完整项目”和“实际上 10 天后我在处理 ImportError”这两句话放在一起看很搞笑但它是非常典型的学习曲线。前几天的学习节奏会让人产生掌控感因为课程内容是被设计成连续的到了自己写项目时计算机不再按教程顺序执行而是按代码的真实逻辑执行。少一个冒号、多一个空格、数据库里某个字段名拼错都会让程序直接报错。真实工程里这种落差会被放得更大。学习环境里你只需要在本地跑通生产环境里还要考虑进程怎么保持运行、日志写到哪、数据库文件能不能被多个进程安全访问、端口被占用时怎么处理。指望“懂语法”就能处理这些问题就像指望会踩油门就能参加拉力赛。所以“技术超人”不是 10 天练成的但“我能用 10 天理解一个完整闭环并能解决运行中的问题”是可以练成的。1.2 工程能力不是知识量而是闭环能力一个合格的工程闭环至少包括六个环节环境搭建、编码实现、运行验证、错误排查、部署上线和回滚维护。很多人为了提高“知识量”一直在学习新框架新工具却很少把一个功能从脑袋里一直推到服务器上跑通。知识量高但没有闭环能力的人面对真实项目时会发现每一步都在卡为什么库装不上、为什么接口拿到 405、为什么本机能跑服务器不能跑。对“10 天变超人”这件事来说最重要的不是背下多少 API而是能不能把下面这几个环节串起来能力项学习环境下常见的做法生产环境至少要补上的内容环境安装直接 pip install / npm install固定依赖版本使用虚拟环境或容器隔离启动运行前台启动看到日志就算成功后台进程、重启策略、健康检查数据存储用本地文件或内存数据数据库迁移、备份和连接管理日志输出print 或者不输出结构化日志、日志滚动、错误追踪测试验证手动浏览器点一点自动化测试、断言关键结果部署发布本机访问成功服务器路径、权限、端口、反向代理、回滚这个表本身就是一套能力地图。如果你发现自己只占了左边一列那“10 天计划”最该做的不是学更多框架而是把右边这一列补起来。1.3 把一个“超人计划”翻译成六个工程前提继续把口号落地。你不需要在第 1 天就冲进代码先确认六件事环境隔离项目依赖不能和系统全局 Python 混在一起否则装一个包影响一个环境。版本锁定安装完成后把依赖列表保存下来保证别人或者一周后的自己能复现。配置外置数据库路径、端口、日志级别不要写死在代码里用环境变量控制。日志留痕程序出问题时有日志可以查而不是黑屏或一屏红色堆栈。验证维度不只验证正常输入还要验证异常输入和边界输入。回滚路径部署新版本之前知道怎么回到上一个可用版本。这六件事会在后面的项目里逐个出现。先不要着急一步到位把每一件事都变成一个可执行动作第 10 天复盘时才会发现自己已经不像第一天那样只会“感觉能写”。2. 对照这个 10 天安排第一天先别急着写代码很多人在第 1 天就打开编辑器开始敲这是最不推荐的做法。10 天周期本来就短前两个小时应该用来确定“做出什么东西”而不是“用哪个框架”。没有明确目标后面每一次环境报错都会变成放弃的理由。这里给出一张可以直接照做的 10 天排期表主线是一个个人记账本项目。2.1 10 天任务拆解表天主要任务可验收结果第 1 天确认需求、设计数据表和接口有完整接口清单和数据表字段定义第 2 天创建虚拟环境搭建 FastAPI 最小工程项目能启动健康检查接口有返回第 3 天初始化数据库完成建表和基础查询能往表里写入一条数据并查出来第 4 天实现新增、列表、汇总、删除接口接口可以通过 HTTP 请求调用第 5 天加入参数校验和异常处理非法参数返回 422业务错误返回 404第 6 天加入结构化日志复盘一个真实报错日志能定位问题发生的具体请求第 7 天编写自动化测试pytest 跑通至少覆盖新增和异常场景第 8 天在本机用生产方式启动服务使用多进程方式启动并验证负载第 9 天处理部署差异路径、权限、日志、备份按检查清单操作并记录上线记录第 10 天写 README 和复盘文档生成依赖清单别人按文档能完整复现项目这个排期并不夸张。它没有规划任何“高并发”“微服务”“分布式”内容只做了一件真实项目都会做的普通功能但它把工程闭环走完了。别小看这件普通功能过程中会遇到的环境问题、数据问题、接口问题和部署问题已经覆盖了新手到独立开发之间的大部分台阶。2.2 选技术栈时先回答三个问题选型不是越新越好也不是越流行越好。这个项目选择 Python FastAPI SQLite原因有三个代码量少一个文件就能把服务跑起来适合 10 天周期。数据存储逻辑直观SQLite 不需要单独安装数据库服务降低部署复杂度。参数校验由 Pydantic 完成能很快看到合法输入和非法输入的差别。如果你自己的项目要用其他技术栈判断标准也不变第一你能不能在一个小时内搭出最小可运行版本第二你选择的数据库在部署环境里是否容易备份和迁移第三遇到问题时网上有没有足够多的同场景资料。技术栈的热度不是核心核心是你在这个技术栈里能否快速完成一个闭环。2.3 最小可行项目个人记账本的接口和数据结构这个项目的名字可以叫 moneybook功能不用多四个接口就够接口方法说明/healthGET服务健康检查/expensesPOST新增一条支出记录/expensesGET查询支出列表支持按分类筛选/expenses/{id}DELETE删除一条记录/expenses/totalGET按分类统计支出总额数据表设计也尽量简化CREATE TABLE IF NOT EXISTS expenses ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, amount REAL NOT NULL, note TEXT DEFAULT , created_at TEXT DEFAULT (datetime(now, localtime)) );字段说明category记录分类比如餐饮、交通、学习。amount记录金额这里用REAL只是为了演示真实财务系统不要用浮点数存储金额应该使用精确十进制类型。note是备注允许为空。created_at使用 SQLite 当前时间应用层不需要额外处理。只要把这张表建好、接口对得上第 2 天到第 4 天的目标就全部落在代码上了。3. 从零跑通 FastAPI 项目前 3 天只解决“能运行”“能运行”是第一个工程里程碑。这一步的目标不是实现业务逻辑而是让一个空的 Web 服务能够被浏览器或者 curl 访问。3.1 创建虚拟环境并固定依赖先在项目目录里创建虚拟环境。虚拟环境的作用是隔离当前项目的 Python 包避免和系统全局环境冲突。这一点在学习阶段经常被忽略等到两台机器上运行结果不一致时才后悔。mkdir moneybook cd moneybook python3 -m venv .venv source .venv/bin/activate python -m pip install --upgrade pip python -m pip install fastapi uvicorn[standard] python-dotenv pytest python -m pip freeze requirements.txt注意如果原始依赖没有明确版本落地前先确认当前系统可以安装的版本。上面的命令安装的是最新稳定版本不保证未来仍相同。安装完成后执行pip freeze把版本信息记录下来这一步是“可复现”而不是“可运行”的关键。检查点命令行提示符前出现(.venv)说明当前使用的是虚拟环境中的 Python。3.2 项目目录结构一个 10 天项目不需要太复杂但也不该把所有代码堆在一个文件里。推荐这样组织moneybook/ ├── .venv/ ├── app/ │ ├── __init__.py │ ├── config.py │ ├── database.py │ ├── main.py │ ├── models.py │ ├── schemas.py │ └── routers/ │ ├── __init__.py │ └── expenses.py ├── tests/ │ └── test_expenses.py ├── .env.example ├── requirements.txt └── README.mdapp目录放 Web 服务代码tests放测试代码.env.example记录需要配置的环境变量样例requirements.txt保存依赖。初次看到这个结构可能会觉得文件多但实际上每个文件只负责一件事配置、数据库、路由、数据校验、启动入口。3.3 最小应用启动、健康检查、第一个接口先写最小的app/main.pyfrom fastapi import FastAPI app FastAPI(titlemoneybook) app.get(/health) def health(): return {status: ok}启动命令uvicorn app.main:app --host 0.0.0.0 --port 8000 --reloadapp.main:app的意思是“从 app 包的 main 模块导入 app 对象”。--reload只在开发时使用文件变化后服务会自动重启。如果是在生产环境这个参数应该去掉。验证方式curl http://127.0.0.1:8000/health预期输出{status:ok}能拿到这个输出说明环境、依赖、启动方式三条路都通了。这就是“能运行”的最小含义不是看到代码而是 HTTP 请求有响应。3.4 配置外置让数据库路径和日志级别可以改把配置直接写在代码里的问题是换一台机器运行就要改代码。更合适的做法是用环境变量保存配置项。创建.env.exampleDATABASE_PATHmoneybook.db LOG_LEVELINFO接着写app/config.pyimport os from dotenv import load_dotenv load_dotenv() DATABASE_PATH os.getenv(DATABASE_PATH, moneybook.db) LOG_LEVEL os.getenv(LOG_LEVEL, INFO)这样数据库路径和日志级别就从代码里移到了环境。本机调试时可以默认使用moneybook.db部署到服务器后可以通过环境变量指定绝对路径避免进程工作目录不一致导致找不到数据库文件。注意.env文件不要提交到代码仓库仓库里只保留.env.example作为模板。真实生产环境直接用系统环境变量或配置管理工具注入。3.5 前 3 天最常见的四个坑现象常见原因检查方式处理建议启动报 No module named app当前命令不在项目根目录查看当前目录pwd切换到moneybook根目录再执行改了代码但没有生效启动时没加--reload或进程没重启查看启动命令开发环境加--reload生产环境手动重启端口被占用8000 端口被其他服务使用使用 netstat -tunlpgrep 8000 查看请求返回 403 或 500 但没有日志应用内异常被静默吞掉检查终端是否有完整 traceback先加中间件记录异常再定位具体代码这四条覆盖了新手阶段最常见的环境问题。遇到报错不要先怀疑“框架太难”先把命令、路径、端口这三个基础项检查完再往业务代码里看。4. 第 4-6 天把核心业务代码写完并让错误有“出口”项目能启动之后就要开始写实际业务。这个阶段最重要的不是把每个接口都写得漂亮而是保证两件事数据能正确写入数据库错误能被日志和异常处理捕获。4.1 数据库连接与初始化在app/database.py中实现建表和连接管理import sqlite3 from app.config import DATABASE_PATH def get_connection(): conn sqlite3.connect(DATABASE_PATH) conn.row_factory sqlite3.Row return conn def init_db(): conn get_connection() try: with conn: conn.executescript( CREATE TABLE IF NOT EXISTS expenses ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, amount REAL NOT NULL, note TEXT DEFAULT , created_at TEXT DEFAULT (datetime(now, localtime)) ); ) finally: conn.close()这里的关键点是conn.row_factory sqlite3.Row它让查询结果可以通过字段名访问而不是只能按下标取。with conn是事务控制放在try/finally里保证连接一定关闭。SQLite 默认是文件数据库单机项目完全够用如果将来换成 PostgreSQL路由代码里的 SQL 要做相应调整。在app/main.py中加入启动初始化from contextlib import asynccontextmanager from fastapi import FastAPI from app.database import init_db asynccontextmanager async def lifespan(app: FastAPI): init_db() yield app FastAPI(titlemoneybook, lifespanlifespan)这样服务启动时自动建表不需要手动执行 SQL 文件。4.2 数据校验模型在app/schemas.py中定义请求和响应模型from pydantic import BaseModel, Field class ExpenseCreate(BaseModel): category: str Field(..., min_length1, max_length20, description支出分类) amount: float Field(..., gt0, description支出金额) note: str Field(, max_length200, description备注) class ExpenseOut(ExpenseCreate): id: int created_at: strgt0是一个很容易被忽略但价值很高的参数。它把“金额不能小于等于 0”这条业务规则前移到接口层调用方一旦传入负数FastAPI 会直接返回 422并且在响应里说清楚是哪个字段不满足哪条规则。这比在业务代码里写一堆if amount 0更直接也能省掉大量手工防御代码。4.3 路由接口和统一错误处理在app/routers/expenses.py中实现业务路由。先写新增和列表接口from fastapi import APIRouter, HTTPException import sqlite3 from app.database import get_connection from app.schemas import ExpenseCreate, ExpenseOut import logging logger logging.getLogger(moneybook) router APIRouter(tags[expenses]) router.post(/expenses, response_modelExpenseOut) def create_expense(expense: ExpenseCreate): conn get_connection() try: with conn: cur conn.execute( INSERT INTO expenses (category, amount, note) VALUES (?, ?, ?), (expense.category, expense.amount, expense.note), ) last_id cur.lastrowid row conn.execute( SELECT id, category, amount, note, created_at FROM expenses WHERE id ?, (last_id,), ).fetchone() except sqlite3.Error as exc: logger.exception(create expense failed) raise HTTPException(status_code500, detail数据库写入失败) finally: conn.close() return ExpenseOut( idrow[id], categoryrow[category], amountrow[amount], noterow[note], created_atrow[created_at], ) router.get(/expenses, response_modellist[ExpenseOut]) def list_expenses(category: str | None None, limit: int 50): conn get_connection() try: if category: rows conn.execute( SELECT id, category, amount, note, created_at FROM expenses WHERE category ? ORDER BY id DESC LIMIT ?, (category, limit), ).fetchall() else: rows conn.execute( SELECT id, category, amount, note, created_at FROM expenses ORDER BY id DESC LIMIT ?, (limit,), ).fetchall() finally: conn.close() return [dict(row) for row in rows]再写删除和汇总接口router.delete(/expenses/{expense_id}) def delete_expense(expense_id: int): conn get_connection() try: with conn: cur conn.execute(DELETE FROM expenses WHERE id ?, (expense_id,)) if cur.rowcount 0: raise HTTPException(status_code404, detail支出记录不存在) finally: conn.close() return {deleted: expense_id} router.get(/expenses/total) def total_expenses(category: str | None None): conn get_connection() try: if category: row conn.execute( SELECT SUM(amount) AS total FROM expenses WHERE category ?, (category,), ).fetchone() else: row conn.execute(SELECT SUM(amount) AS total FROM expenses).fetchone() finally: conn.close() return {total: row[total] or 0}注意/expenses/total必须写在/expenses/{expense_id}之前。否则请求/expenses/total时FastAPI 会把total当成expense_id传入删除或查询逻辑导致路由不匹配。最后把路由注册到app/main.pyfrom app.routers import expenses app.include_router(expenses.router)4.4 一个“复现、修复、回归”的真实排错案例第 5 天可以专门安排一次故障演练。比如把list_expenses的 SQL 中字段名created_at错写成create_at运行后的现象是sqlite3.OperationalError: no such column: create_at排查步骤看完整堆栈定位到expenses.py中的list_expenses函数。对比 SQL 字段与数据表字段发现create_at和created_at不一致。修复 SQL 字段名。再执行一次列表请求确认返回 200。把这条 case 写进测试用例避免后续改动再次触发。这个套路就是“复现、修复、回归”。任何 bug 都值得这样处理不要改完代码不验证就继续写下一个功能否则第 10 天你会同时面对多个问题根本不知道哪个改动导致什么结果。5. 第 7 天写测试验证的是“人都会犯错”这件事很多人觉得写测试浪费时间但 10 天项目写测试不是为了形式而是为了让回归验证成本降低。手动点一遍浏览器只要 1 分钟但当接口多起来后每改一次代码都手动验证10 分钟内就会觉得无聊。测试能把这些重复劳动脚本化。5.1 先选三个最容易出错的场景写测试不要追求行覆盖率先覆盖最容易出错的三个场景正常新增一条支出返回结果里的amount和category与请求一致。金额传负数或 0接口返回 422。删除不存在的记录接口返回 404。这三条分别对应正常流程、参数校验和业务异常已经覆盖了 Web 服务最核心的判断逻辑。5.2 使用 FastAPI TestClient 写测试在tests/test_expenses.py中写入from fastapi.testclient import TestClient from app.main import app client TestClient(app) def test_create_expense(): resp client.post(/expenses, json{ category: food, amount: 25.5, note: 午饭 }) assert resp.status_code 200 body resp.json() assert body[amount] 25.5 assert body[category] food def test_invalid_amount(): resp client.post(/expenses, json{ category: food, amount: -1 }) assert resp.status_code 422 def test_delete_not_found(): resp client.delete(/expenses/999999) assert resp.status_code 404运行测试pytest -q5.3 把测试变成命令而不是心理安慰如果测试依赖同一个moneybook.db多次运行会造成数据堆积。为了让测试可重复运行前设置独立的数据库路径DATABASE_PATH/tmp/moneybook_test.db pytest -q这一步把测试环境与开发环境隔离开来也让“验证”变成每次提交前固定执行的动作。10 天项目结束时你可以对任何人说这个项目不是“看起来能跑”而是有自动化测试能证明核心接口的行为符合预期。6. 第 8-9 天从本地到生产换一个运行环境就会发现一堆问题本机能跑通只在学习环境里有意义。第 8 天开始需要把服务切换到更接近生产的方式运行。6.1 本地环境与生产环境的差异表维度本地开发生产环境启动方式uvicorn --reloadgunicorn 多 worker日志终端输出文件 日志轮转数据路径相对路径 moneybook.db绝对路径有备份机制端口8000 随便用端口可能被占用需确认防火墙环境变量.env 文件系统环境变量或配置中心回滚不存在回滚问题需要保留上一版本和备份数据6.2 使用 Gunicorn 启动 FastAPI先安装 Gunicornpython -m pip install gunicorn生产模式启动命令gunicorn app.main:app -w 2 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000参数解释-w 2表示启动 2 个 worker 进程能利用多核 CPU。-k uvicorn.workers.UvicornWorker指定 worker 类型因为 FastAPI 是 ASGI 应用不能用默认的同步 worker。-b 0.0.0.0:8000表示监听所有网卡地址。只在本机调试时可以改成127.0.0.1服务器上需要配合反向代理使用。这里有一个容易被忽视的问题多个 worker 同时读写同一个 SQLite 文件虽然不会立刻崩掉但在写并发超过临界点后可能出现 “database is locked”。SQLite 适合低并发场景如果你的服务将来有多人同时提交数据就需要考虑迁移到 PostgreSQL 或 MySQL。6.3 日志、数据库备份和健康检查生产环境不能只靠终端输出。Gunicorn 启动时可以通过参数把访问日志和错误日志分开gunicorn app.main:app \ -w 2 \ -k uvicorn.workers.UvicornWorker \ -b 0.0.0.0:8000 \ --access-logfile logs/access.log \ --error-logfile logs/error.log数据库备份至少要做两件事每天定时备份moneybook.db文件在业务低峰期备份避免备份过程影响写入。备份时不要只复制一份文件建议保留最近 N 份备份防止数据库文件意外损坏后无法追溯。健康检查接口/health在部署后可以被监控系统或负载均衡器使用。注意别把这个接口省略它是最低成本发现“服务还活着”的方式。6.4 生产上线前的检查清单[ ] 依赖版本已进入requirements.txt不是靠当前环境临时安装。[ ]DATABASE_PATH已设置为绝对路径有备份计划。[ ] 日志目录已创建且有权限写入。[ ] 已使用 Gunicorn 多 worker 启动不用--reload。[ ] 防火墙和云安全组只开放必要端口。[ ] 本地和服务器上分别跑一次/health验证。[ ] 保留上一版本的代码和数据库备份能随时回滚。这张清单可以在你的下一个真实项目里直接复用。发布不是把代码复制到服务器上跑一次就结束可回滚、可观察、可复现才是生产环境的基本要求。7. 第 10 天复盘时不要比代码量要比“可复现性”第 10 天不是终结而是把 10 天经验打包成可复用流程的时间。你可以写一份 README明确写清楚项目是做什么的、需要什么依赖、如何安装、如何启动、如何测试、数据库文件在哪、日志在哪、如何部署。如果别人拿到你的项目后照着 README 三十分钟内无法复现说明项目文档还不够可执行。7.1 把这次交付打包成一套可复制流程复盘时不要问“我写了多少行代码”要问四个问题我把项目从一个空目录跑到能提供服务的完整流程是否每步都有记录我遇到过的所有报错是否都能在下一次遇到时快速定位我写的测试能否证明核心功能没有坏掉如果服务器上出现问题我能从哪里看到日志、怎么回滚这四个问题直接对应环境、排错、验证和运维恰好是“技术超人”和“看起来会技术”的分界线。7.2 被报错“打脸”的正确姿势世界以痛吻我我却报之以歌最后一个主题落到这句“世界以痛吻我我却报之以歌”上。编程里天天都在被“打脸”接口报错、依赖冲突、部署失败、日志里出现一堆看不懂的英文。初期的正常反应是烦躁、怀疑自己是不是不适合干这行。但成熟的开发者很清楚报错本身就是软件开发中的一部分它不是为了打击你而是为了告诉你好一个明确的信息某个输入和你预期的不一致某个假设不成立。“报之以歌”在工程里的意思不是唱歌而是用一套稳定方法来回应记录现场、抽象问题、拆解假设、逐个验证、修复后回归。这里的“歌”就是你手里那份日志、测试用例、部署清单以及被验证过的排错步骤。有了这套东西报错依然会来但你不再害怕它因为每一次“世界以痛吻我”都将变成一次可以积累的改进机会。10 天练习结束后最值得保留的不是某个框架的语法也不是那几条接口代码而是这种面对不确定问题时依然能一步步定位、修复和验证的反应方式。它不是天上掉下来的“超人能力”而是在一次次“我以为会”和“实际上报错”之间反复演练出来的工程本能。下一个 10 天可以用同样的方式再做一个小项目把自动化和监控再加进来能力就是这样一步一步从“以为”变成“确实”的。
返回列表