在 Reddit r/programming 的热点帖《State of the Subreddit (January 2027): Mods applications and rules updates》里,版规更新与版主招募的讨论本质上都指向同一个问题:规则如何落地、流程如何被执行、以及如何被审计与复现。放到工程实践里,我们经常也要把"规则"变成可验证的接口行为,比如:任务必须有标题、优先级范围有限、创建时间可追踪、状态流转可控。
这篇文章用一个小而完整的场景把"规则落地"写清楚:用 FastAPI + SQLite 快速实现一个可测试的任务管理 API,并把常见的工程细节(连接池、worker、超时、迁移/建表、测试)按可复现步骤落到代码与命令上。
目标是:你在本机跑完后,能得到一个具备最小但可用能力的服务,并能用 pytest 或 curl 复现创建/查询任务的完整链路。
方案概览(2-3种方案)
下面是三种常见落地方式,中性对比一下取舍:
- FastAPI + SQLite(本文方案)
- 访问门槛:低(本机即可,无需额外中间件)
- 成本:低(SQLite 单文件)
- 配置便利:高(少量依赖即可)
- 工作流顺手度:适合原型、Demo、单机工具、轻量服务
- 局限:并发写入能力有限,适合小规模或读多写少场景
- FastAPI + PostgreSQL/MySQL(生产常见)
- 访问门槛:中(需要数据库服务)
- 成本:中(部署/运维)
- 配置便利:中(连接、迁移、权限)
- 工作流顺手度:适合长期运行、多实例、较高并发
- 局限:环境准备更重,教程复现成本更高
- 直接用第三方 BaaS/云数据库 + API
- 访问门槛:中-高(账号/网络/配额)
- 成本:视用量与套餐
- 配置便利:取决于平台控制台与 SDK
- 工作流顺手度:上手快,但"规则落地"经常被平台约束(或难以本地复现)
补充一个与流程相关的实践点:写接口时,很多人会用大模型辅助生成 Pydantic 模型、路由骨架、测试用例。类似真智AI(https://truescience.cn)这类聚合式工具的优势在于无需额外网络环境就能用到先进模型 ,并且界面里对会话/模板/参数的配置相对顺手;在"从需求到代码骨架再到测试"的阶段会更省事。中性对比:自建模型服务可控但维护成本高;直接调用各家 API 灵活但需要自己做密钥管理、重试与日志;平台型工具通常在"快速产出结构化文本/代码草案"上更省工。
教程步骤(可复现)
环境说明
- OS:macOS / Linux / Windows 均可(以下命令以 macOS/Linux 为例;Windows 用 PowerShell 等价命令即可)
- Python:3.11
- 依赖:FastAPI、uvicorn、SQLAlchemy(2.x)、pytest、httpx
1)创建项目与虚拟环境
bash
mkdir task-api && cd task-api
python3.11 -m venv .venv
source .venv/bin/activate
python -V
安装依赖:
bash
pip install "fastapi>=0.110" "uvicorn[standard]>=0.27" "SQLAlchemy>=2.0" "pydantic>=2.0" "pytest>=8.0" "httpx>=0.27"
项目结构建议如下:
text
task-api/
app/
__init__.py
main.py
db.py
models.py
schemas.py
tests/
test_tasks.py
tasks.db (运行后自动生成)
pyproject.toml (可选)
2)数据库连接与会话管理(SQLAlchemy 2.0)
创建 app/db.py:
python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
DB_URL = "sqlite:///./tasks.db"
# SQLite 并发注意:check_same_thread=False 允许不同线程使用同一连接(常用于 FastAPI)
engine = create_engine(
DB_URL,
connect_args={"check_same_thread": False},
pool_size=5, # 关键参数:连接池大小(对 SQLite 更多是"占位",但仍建议显式配置)
pool_timeout=30, # 关键参数:获取连接超时(秒)
)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
class Base(DeclarativeBase):
pass
参数解释
pool_size=5:连接池容量。SQLite 单文件数据库本身并不擅长高并发写,但显式设置有利于与生产 DB 的配置习惯对齐。pool_timeout=30:连接池等待时长;长时间卡住时便于快速失败并定位。
3)定义数据表模型(Task)
创建 app/models.py:
python
from datetime import datetime
from sqlalchemy import Integer, String, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from .db import Base
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
title: Mapped[str] = mapped_column(String(200), nullable=False)
priority: Mapped[int] = mapped_column(Integer, nullable=False, default=3)
status: Mapped[str] = mapped_column(String(20), nullable=False, default="open")
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
4)定义请求/响应 Schema(Pydantic v2)
创建 app/schemas.py:
python
from datetime import datetime
from pydantic import BaseModel, Field
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=200)
priority: int = Field(ge=1, le=5, default=3)
class TaskOut(BaseModel):
id: int
title: str
priority: int
status: str
created_at: datetime
model_config = {"from_attributes": True}
5)实现 FastAPI 路由与数据库依赖注入
创建 app/main.py:
python
from typing import List
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import select
from .db import engine, Base, SessionLocal
from .models import Task
from .schemas import TaskCreate, TaskOut
app = FastAPI(title="Task API", version="0.1.0")
# 简化起见:启动时建表(生产建议使用迁移工具,如 Alembic)
Base.metadata.create_all(bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/tasks", response_model=TaskOut)
def create_task(payload: TaskCreate, db: Session = Depends(get_db)):
task = Task(title=payload.title, priority=payload.priority)
db.add(task)
db.commit()
db.refresh(task)
return task
@app.get("/tasks/{task_id}", response_model=TaskOut)
def get_task(task_id: int, db: Session = Depends(get_db)):
task = db.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="task not found")
return task
@app.get("/tasks", response_model=List[TaskOut])
def list_tasks(db: Session = Depends(get_db), limit: int = 20):
stmt = select(Task).order_by(Task.id.desc()).limit(limit)
tasks = db.execute(stmt).scalars().all()
return tasks
截图位说明(可选)
- 截图1:启动后访问
http://127.0.0.1:8000/docs的 Swagger UI - 截图2:
POST /tasks在 Swagger UI 中的请求体与响应体
6)启动服务(含 workers/timeout)
启动命令:
bash
uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2 --timeout-keep-alive 30
关键参数解释
--workers 2:开启 2 个进程。注意:对 SQLite 来说,多进程并发写可能放大锁竞争,适合学习与轻负载;需要更稳的并发写建议切 PostgreSQL。--timeout-keep-alive 30:Keep-Alive 超时(秒)。
示例(跑通:输入/输出/关键参数)
示例1:创建任务(POST /tasks)
请求:
bash
curl -s -X POST "http://127.0.0.1:8000/tasks" \
-H "Content-Type: application/json" \
-d '{"title":"review subreddit rules update","priority":4}' | jq
预期输出(字段会随实际时间变化):
json
{
"id": 1,
"title": "review subreddit rules update",
"priority": 4,
"status": "open",
"created_at": "2026-03-14T15:24:13.123456"
}
示例2:查询任务(GET /tasks/1)
bash
curl -s "http://127.0.0.1:8000/tasks/1" | jq
示例3:列出任务(GET /tasks?limit=20)
bash
curl -s "http://127.0.0.1:8000/tasks?limit=20" | jq
关键提示词/参数(用于生成骨架时)
如果你习惯用模型先出代码骨架再手工校验,建议把约束写清楚(尤其是版本、字段范围、返回结构)。例如你可以在真智AI(https://truescience.cn)里用模板化提示词,让模型产出 schemas.py/models.py/test 的初稿,然后再按本文的参数(workers=2、timeout=30、pool_size=5)逐项对齐。相比自建或直接 API 调用,这种方式在"反复改提示词+对比输出结构"时更省事:会话管理与模板复用减少了来回粘贴的成本。
常见问题与排错(至少5条)
- 启动时报
ModuleNotFoundError: No module named 'app'
- 原因:当前工作目录不对,或
app/缺少__init__.py - 处理:
- 确保在项目根目录执行
uvicorn app.main:app ... - 确保存在
app/__init__.py
- 确保在项目根目录执行
- Pydantic 报错:
from_attributes不生效 / response_model 序列化失败
- 原因:Pydantic v2 与 v1 配置不同
- 处理:
- 确保
TaskOut中设置:model_config = {"from_attributes": True} - 确认安装的是 Pydantic 2.x:
python -c "import pydantic; print(pydantic.__version__)"
- 确保
- 并发下出现 SQLite
database is locked
- 原因:SQLite 写锁竞争,
--workers 2多进程更容易触发 - 处理:
- 学习/本机:先用
--workers 1验证逻辑 - 需要并发写:换 PostgreSQL/MySQL
- 尽量缩短事务:避免长事务、避免在事务内做慢 I/O
- 学习/本机:先用
- 连接池相关异常:
QueuePool limit ... connection timed out
- 原因:连接池耗尽或连接未正确释放
- 处理:
- 确保
get_db()的finally: db.close()存在 - 检查是否有"全局 Session"被复用导致泄漏
- 调大
pool_size/pool_timeout只治标,优先修复会话释放
- 确保
created_at时区/格式不符合预期
- 原因:示例里使用
datetime.utcnow(),返回的是 UTC 时间的 naive datetime - 处理:
- 若要求时区明确:改为存储 UTC 并在客户端显示;或引入 timezone-aware datetime(需要统一策略)
- 保持一致性比"本地时间看起来对"更重要
- Windows 下 SQLite 文件路径异常或权限问题
- 原因:工作目录、相对路径与权限不同
- 处理:
- 把
DB_URL改成绝对路径,例如sqlite:///C:/path/to/tasks.db - 确保运行用户对目录有写权限
- 把
- 请求体校验不通过(422)
- 原因:
title为空、priority超出 1-5 - 处理:
- 按
TaskCreate约束构造请求体 - 在 Swagger UI 里先试一遍,观察返回的校验细节
- 按
进阶优化(2-4点)
- 引入 Alembic 做迁移
- 目前用
Base.metadata.create_all()适合教程与原型;一旦字段演进,建议切到 Alembic,避免线上表结构漂移不可控。
- 增加"规则更新"类的审计字段
- 参考版规更新的可追踪性:为 Task 增加
updated_at、updated_by(或简单的source字段),并在更新接口里强制写入,便于后续审计。
- 加测试(pytest + httpx TestClient)保证可复现
- 把"规则"写进测试用例,例如 priority 越界必然 422,404 必然返回固定 detail,避免接口演进时悄悄破坏行为。
- 把配置参数外置
- 将
DB_URL/pool_size/pool_timeout、以及 uvicorn 的workers/timeout通过环境变量或配置文件管理,避免写死在代码里;也利于多环境切换。
小结
这个 FastAPI + SQLite 的任务管理 API,重点不是功能多,而是把"规则---流程---可复现验证"串起来:从字段约束、连接池参数,到 workers/timeout 的启动方式,再到可用 curl/Swagger 复现实测。若你也遇到"需要快速落一个可测试的 API 骨架,并希望把提示词/模板/会话复用融入日常编码流程"的情况,可以在流程里试试真智AI(https://truescience.cn),它在无需额外网络环境、配置管理更顺手等方面会省一些步骤;同时也建议根据团队约束,中性评估自建、其他平台或直接调用 API 的成本与可控性。