
前几天RECH训练营第一次作业布置下来的时候群里一片“就这”的轻松语气。等提交截止我看了一圈大家交上来的东西才发现这份作业表面上是写一个任务管理API实际上是在测试你从零搭建一个工程化项目的基本功。有人用五分钟跑通就算完事有人连虚拟环境和Git规范都没搞明白。正好借着复盘这次作业我把整个设计和实现过程拆开讲讲——这个作业到底在考什么每一步为什么要那么做以及那些文档里不会写但实测必须注意的坑。这套作业面向的读者不只是RECH训练营的学员。如果你正在自学编程、准备投简历作品集或者想把手头零散的代码整理成规范的项目这篇内容都值得对照着过一遍。它的核心价值不在于“写一个API”这个结果而在于建立一套可以复用的工程习惯从环境搭建、目录规划、接口设计到测试调优每一步都有明确的取舍逻辑。1. 作业设计拆解第一次作业为什么选“待办事项API”1.1 任务目标与隐藏考点先看看作业本身的要求。题目看起来很简单用Python实现一个待办事项Todo/Task管理接口支持创建任务、查看任务列表、查看任务详情、修改任务状态、删除任务也就是一个基础的CRUD应用。数据存储可以用内存、文件或者数据库不限制框架最终提交项目源码和说明文档。很多同学把这个作业当成“写代码”任务这恰恰是误区。第一次作业的隐藏考点有三个第一你能不能独立把一个项目从零跑起来而不是只会改别人写好的代码第二你有没有基本的工程意识比如目录是否清晰、依赖是否声明、代码是否可维护第三你遇到环境问题、依赖冲突、端口占用这些破事时能不能自己排查解决。这三个能力比CRUD本身重要得多也是RECH后续高阶内容的基础。所以与其说这是“第一次作业”不如说是一次工程素养摸底。后面几周的深度学习内容都会在这套骨架上迭代第一次作业的代码质量直接决定了你后续能不能跟上节奏。1.2 技术选型的取舍标准技术选型是这次作业里第一个需要决策的点。训练营没有硬性规定框架但绝大多数人选择了Flask理由很实在Django太重第一次作业如果陷进它的admin后台、ORM迁移、中间件这些概念里很容易迷失在“框架的魔法”中FastAPI虽然现代且性能好但Pydantic模型、自动文档这些特性对新手来说信息量偏大。Flask则刚好卡在中间——核心逻辑透明路由和请求处理一眼就能看穿又没有太多花活。数据库我选了SQLite。原因只有一个零配置。第一次作业最怕的就是“万事俱备只欠数据库”——MySQL要装服务、配账号、建库PostgreSQL同理而SQLite就是一个文件Python内置模块直接操作适合把所有精力聚焦在业务逻辑上。等后续作业需要更高的并发和复杂查询再平滑迁移到其他数据库也不难。如果用FastAPI也是不错的选择但前提是你已经熟悉了异步和类型注解否则调试成本会翻倍。1.3 评分之外的加分项作业评分标准里明确列了“可运行、代码有注释、文档清晰”但真正拉开差距的是评分标准之外的东西。一份结构化的README写清楚项目是什么、怎么跑、接口有哪些、请求示例是什么。不要用“这是RECH作业”一句话糊弄过去。规范的Git提交记录。一次作业能不能体现你的版本管理习惯至少要有初始项目、实现功能、补充测试这几个清晰的commit而不是一个“final”提交完事。合理的错误处理。接口不只有“成功”一条路参数传错、数据不存在、请求体格式不对这些场景有没有对应的状态码和提示信息这是很多同学最容易忽略的隐藏加分点。我当时提交作业前特地给README加了接口文档表格和curl示例后来收到反馈说这一点在评分里占了不少印象分。别小看这些“表面功夫”工程化本身就是把表面功夫做到极致。2. 环境准备与项目骨架搭建2.1 Python虚拟环境为什么这一步不能省第一次作业遇到最多的“灵异事件”就是“在我电脑上明明能跑”。排除了代码差异之后十有八九是环境问题——两个项目的依赖版本互相覆盖或者系统Python环境里装了一堆不相干的包。虚拟环境就是给每个项目单独隔离出一个“房间”房间里只有这个项目需要的依赖互不干扰。创建虚拟环境的命令很简单cd ~/projects/rech-task-manager python3 -m venv venv然后激活它source venv/bin/activateWindows环境下的激活命令稍有不同是venv\Scripts\activate。激活之后命令行提示符会出现(venv)前缀后续的pip安装都要保证在这个状态下进行。这里有一个经验venv目录不要提交到Git仓库。它动辄几十上百兆而且是从代码中完全恢复的只要有requirements.txt提交它只会让仓库臃肿。在项目根目录创建.gitignore把venv/、__pycache__/、.venv/等通通忽略掉这也是工程化的第一课。2.2 项目目录先把房间收拾干净第一次作业的代码量不会很大但目录结构从一开始就要合理否则后面加功能、写测试代码满天飞就会劝退当时的自己。我采用的结构供你参考rech-task-manager/ ├── app/ │ ├── __init__.py │ ├── db.py │ ├── models.py │ └── routes.py ├── tests/ │ └── test_tasks.py ├── .gitignore ├── README.md └── requirements.txtapp目录放主逻辑代码tests目录放测试根目录放配置和文档。很多人第一版代码喜欢把所有路由、数据库代码写在一个app.py里图省事但后面会越来越难维护。拆文件的核心原则是“各司其职”models.py只管数据模型定义routes.py只管接口路由db.py管理数据库连接或SQLite初始化。哪怕现在每个文件只有几十行这个结构也能帮你快速定位问题。另外__init__.py里创建Flask实例并调用register_routes之类的函数完成路由注册这是从第一天就建立的应用工厂雏形虽然第一次作业不需要完整的工厂模式但习惯很重要。2.3 Git工作流从第一次提交开始养成习惯作业要求里没提Git但我建议所有初学者都把“每完成一个功能模块就提交一次”当成铁律。我这次作业的提交记录大致是这样的feat: 初始化项目结构与依赖feat: 实现任务数据模型与数据库初始化feat: 实现任务增删改查接口test: 添加接口单元测试docs: 补充README与接口文档Commit message用Conventional Commits风格feat、fix、test、docs前缀一目了然将来翻历史记录时可以精准定位每次改动的目的。这个习惯在RECH后面的集体项目作业里会让你受益很大因为你会在同一个仓库里跟别人协作没有规范Git log就是一场灾难。实操命令也不复杂git init git add . git commit -m feat: 初始化项目结构与依赖 git remote add origin 你的仓库地址 git push -u origin main这里有个细节第一次提交建议在“项目骨架刚搭好”的时候进行而不是等所有代码写完再一起提交。Git的核心价值在于记录“变化的节点”骨架搭建就是一个值得记录的变化。后面每次提交前用git status确认修改范围避免把无关文件带进去。3. 核心功能实现任务管理API从零到一3.1 数据模型与配置设计任务管理系统的核心数据模型很简单一个任务包含标题、详情、状态和创建时间。我用SQLAlchemy定义代码贴在下面每一处都值得琢磨from datetime import datetime from sqlalchemy import Column, Integer, String, DateTime, Boolean # 状态用字符串而不用布尔值是为了后续扩展 STATUS_TODO todo STATUS_DONE done class Task(Base): __tablename__ tasks id Column(Integer, primary_keyTrue) title Column(String(200), nullableFalse) description Column(String(500), default) status Column(String(20), defaultSTATUS_TODO, indexTrue) is_deleted Column(Boolean, defaultFalse, indexTrue) created_at Column(DateTime, defaultdatetime.now)说一下每个字段的取舍。title设为nullableFalse因为一个连标题都没有的任务没有意义。status没有用True/False布尔值表示完成状态而是用了字符串原因很简单——任务可能有“进行中”“已完成”“已取消”等多种状态布尔值塞不进去。哪怕此刻只需要两种状态字符串的方案也保留了扩展空间。加indexTrue是因为后续大概率会按状态筛选任务不加索引数据量大了查询会很慢。这里还加了一个is_deleted软删除字段。删除任务时不是物理删除记录而是把is_deleted置为True查询时默认过滤掉。这个设计可能“过度”但考虑到真实系统里经常需要做数据审计从第一次作业就养成“软删除”的思维是件好事。当然如果题目没要求你可以不做但做了并写进文档是体现思考深度的加分项。3.2 创建任务与列表接口实现创建任务是CRUD的入口也是最能体现细节的接口。它的职责是接收客户端提交的JSON数据校验合法性写进数据库然后返回创建后的完整对象状态码用201。from flask import request, jsonify, Blueprint from app.models import Task, STATUS_TODO from app.db import db_session task_bp Blueprint(task, __name__, url_prefix/tasks) task_bp.route(, methods[POST]) def create_task(): data request.get_json() if data is None: return jsonify({error: 请求体必须是合法的JSON}), 400 title data.get(title, ).strip() if not title: return jsonify({error: title不能为空}), 400 task Task( titletitle, descriptiondata.get(description, ).strip(), statusdata.get(status, STATUS_TODO), ) db_session.add(task) db_session.commit() return jsonify(task.to_dict()), 201这里面有几个实战中很容易踩的点。request.get_json()在请求体不是JSON时返回None或者抛异常所以要显式判断。title要strip()去掉首尾空白避免客户端传一个空格过来校验还不报错。状态字段如果客户端传了非法值比如statusxxx当前代码会直接存进去严谨起见应该校验枚举值RECH后续作业会要求更严格这里先留个改进点。列表接口要支持分页和按状态过滤这两个能力会在真实项目中高频出现。我实现得很克制task_bp.route(, methods[GET]) def list_tasks(): page max(request.args.get(page, 1, typeint), 1) per_page min(request.args.get(per_page, 20, typeint), 100) status request.args.get(status) query db_session.query(Task).filter(Task.is_deleted.is_(False)) if status: query query.filter(Task.status status) total query.count() tasks query.order_by(Task.created_at.desc()) \ .offset((page - 1) * per_page).limit(per_page).all() return jsonify({ items: [t.to_dict() for t in tasks], total: total, page: page, per_page: per_page, })max和min的用法是防异常的常见招数——用户传page0或负值强制修正为1用户传per_page10000限制最大100。这些细节单独看不值钱组合在一起才是接口稳健的原因。3.3 详情、更新与删除接口实现详情接口的逻辑最简单但404的处理是重点。查询一个不存在的任务或者已经软删除的任务都要返回404和对应的错误信息而不是返回null或者空对象。task_bp.route(/int:task_id, methods[GET]) def get_task(task_id): task db_session.query(Task).filter( Task.id task_id, Task.is_deleted.is_(False) ).first() if task is None: return jsonify({error: 任务不存在}), 404 return jsonify(task.to_dict())这里有个细节Task.is_deleted.is_(False)就是前面说的软删除过滤。如果漏掉这个条件被删除的任务通过/tasks/1依然能查到那软删除就形同虚设。更新接口处理PUT请求也支持PATCH的局部更新语义。我用简单方式处理只更新请求体里出现的字段没出现的保持原样。这个设计比“全量覆盖”更灵活也更符合实际业务场景。task_bp.route(/int:task_id, methods[PUT, PATCH]) def update_task(task_id): task db_session.query(Task).filter( Task.id task_id, Task.is_deleted.is_(False) ).first() if task is None: return jsonify({error: 任务不存在}), 404 data request.get_json() or {} if title in data: title data[title].strip() if not title: return jsonify({error: title不能为空}), 400 task.title title if description in data: task.description data[description].strip() if status in data: if data[status] not in (STATUS_TODO, STATUS_DONE): return jsonify({error: 无效的状态值}), 400 task.status data[status] db_session.commit() return jsonify(task.to_dict())删除接口推荐用一个结构返回204 No Content不返回具体内容。这个选择背后的HTTP语义是——删除成功后没有实体需要返回204比200更准确。task_bp.route(/int:task_id, methods[DELETE]) def delete_task(task_id): task db_session.query(Task).filter( Task.id task_id, Task.is_deleted.is_(False) ).first() if task is None: return jsonify({error: 任务不存在}), 404 task.is_deleted True db_session.commit() return , 2043.4 错误处理与状态码规范第一次作业里有一个普遍坏习惯不管什么情况都返回200然后靠响应体里的字符串告诉客户端“成功了”或“失败了”。这在开发阶段看起来省事一旦前端要处理不同逻辑比如弹窗提示、跳转登录就完全无从下手。正确的做法是让HTTP状态码承担一部分语义。我梳理了这次作业用到的几个状态码场景状态码说明创建成功201资源已建立更新成功200正常响应返回更新后的资源删除成功204无内容返回请求体不是JSON或缺少必填字段400客户端错误参数问题请求的资源不存在404资源未找到请求方法不被支持405比如对详情接口用POST服务器内部异常500代码报错、数据库异常这个规范不是我一拍脑子定的它就是HTTP协议的通用约定。把状态码用对等于给接口建立了一套机器可读的语言前端、测试工具、监控系统都能直接利用。我额外加了全局错误处理器捕获未处理的异常返回统一的JSON错误格式避免异常栈直接暴露给客户端from flask import jsonify def register_error_handlers(app): app.errorhandler(404) def handle_not_found(e): return jsonify({error: 接口不存在}), 404 app.errorhandler(405) def handle_method_not_allowed(e): return jsonify({error: 请求方法不允许}), 405 app.errorhandler(Exception) def handle_unexpected_error(e): return jsonify({error: 服务器内部错误}), 500有个我要特别说明的坑全局Exception处理器在Flask里要慎用它会吞掉所有异常包括调试阶段本来应该弹出的错误栈。我的经验是开发调试阶段先注释掉这个全局处理器让异常原形毕露功能稳定后再把它加上保证线上返回给用户的错误信息干净得体。4. 测试、调试与提交作业的完整闭环4.1 用 curl 手动验证接口写完代码第一件事不是写测试而是用curl把接口挨个打一遍确认最基础的行为符合预期。我记录一下当时的完整验证过程。启动服务source venv/bin/activate export FLASK_APPrun.py export FLASK_ENVdevelopment flask run --port 5000创建任务curl -X POST http://127.0.0.1:5000/tasks \ -H Content-Type: application/json \ -d {title: 完成RECH第一次作业, description: 包括API设计和文档}正常返回201和任务对象。再试试异常场景curl -X POST http://127.0.0.1:5000/tasks \ -H Content-Type: application/json \ -d {title: }返回400和{error: title不能为空}。再查列表curl http://127.0.0.1:5000/tasks?statusdonepage1per_page10返回分页结构。从命令行验证了一个遍之后再打开Postman做一轮带UI的测试主要是为了看响应时间、Headers这些信息。这一步不是必须的但能把接口的细节看得更直观。手动测试的目的是快速暴露低级错误比如路由写错、JSON格式不对、数据库连接失败。这些问题在自动化测试里会被放大十倍来排查浪费大量时间所以先手动确认主线逻辑通了再说。4.2 用 pytest 写基础自动化测试第二次作业里自动化测试会是重头戏但第一次作业就值得引入pytest。原因很简单API功能以后每次迭代都可能回归手动测试覆盖不了所有分支自动化测试才是可靠的锁。我当时写了三个核心测试用例创建任务成功、创建任务缺title返回400、获取不存在任务返回404。代码不算复杂import pytest from app import create_app from app.db import db_session, init_db pytest.fixture() def client(): app create_app() app.config[TESTING] True with app.test_client() as client: with app.app_context(): init_db() yield client db_session.remove() def test_create_task_success(client): resp client.post(/tasks, json{ title: 写作业, description: 完成API }) assert resp.status_code 201 body resp.get_json() assert body[title] 写作业 assert body[status] todo assert body[id] 0 def test_create_task_missing_title(client): resp client.post(/tasks, json{title: }) assert resp.status_code 400 def test_get_nonexistent_task(client): resp client.get(/tasks/99999) assert resp.status_code 404测试环境和开发环境隔离是关键。我在测试fixture里重新初始化了一次数据库确保用例之间互不干扰。为了不让测试数据污染开发数据数据库文件也要区分比如测试时用:memory:或者临时文件路径这些配置可以在create_app时传入。4.3 常见问题排查速查表这次作业过程中我和群里同学遇到的典型问题整理成了一张排查表这些场景在后续所有Python Web项目里都会反复出现问题现象根本原因快速排查与解决ModuleNotFoundError: No module named flask虚拟环境未激活或依赖未安装检查命令行前缀有没有(venv)执行pip install -r requirements.txt端口5000被占用上次服务没关干净或冲突执行lsof -i :5000Windows用netstat -ano找到PID并kill或换端口flask run --port 5001sqlite3.OperationalError: table tasks has no column named xxx数据库文件是旧版本的schema删掉旧的.db文件重新初始化开发阶段不要在意数据丢失请求返回404但路由看起来没错URL前缀没对上比如Blueprint设置了url_prefix/tasks但请求路径少加了/tasks用flask routes查看实际注册的路由表JSON数据提交后get_json()返回None请求头Content-Type不是application/json检查curl的-H参数或Postman的Header设置修改代码后不生效Flask未开启debug模式或用了flask run但没设FLASK_ENVdevelopment设置export FLASK_ENVdevelopment或使用app.run(debugTrue)中文乱码响应编码或终端编码问题在app.config里设置JSON_AS_ASCIIFalse读取当前配置项并调整排除问题的思路比结果重要。我自己调试时有个固定套路先确认错误发生在哪一层——是请求没到后端还是路由匹配失败还是业务逻辑报错还是数据库异常。分层排查可以缩小范围不会两眼一抹黑。4.4 提交作业前的最后检查清单我提交作业前会对照一份清单逐项打勾这个习惯帮我避免了很多低级失误也分享给你requirements.txt里是否包含所有依赖运行pip list核对一遍特别是Flask和SQLAlchemy版本号是否明确。.gitignore是否忽略掉venv、__pycache__、.db文件在Git仓库里搜索一下有没有误提交。README.md里是否包含项目简介、环境依赖、运行命令、接口文档用一段文字描述“如何从零跑起来”想象你是一个完全没看过代码的人。代码里有没有print调试语句残留、临时注释、无用的测试代码删掉它们再提交。登录远程仓库拉一次代码用全新的目录按README流程走一遍确保新环境能跑通。最后这一步是最考验真实性的。很多人本机因为各种隐性的环境配置能跑换台机器就废了。我在提交前把仓库clone到一个临时目录删掉venv重新装依赖、跑测试全流程走通之后才把链接交上去。这个习惯后来帮我避过很多次“验收现场翻车”。写在最后第一次作业真正教给你的东西我这次RECH第一次作业做下来最大的收获不是学会了Flask也不是写了几个API而是建立了一套“哪怕换个技术栈也能复用”的工程化流程先用虚拟环境隔离依赖、再拆目录各司其职、用Git管理每个里程碑、接口设计必带状态码和错误处理、提交前必须自动化测试加文档验证。这些习惯在后续做数据分析和爬虫项目时也一样适用它们才是这次作业真正的考点。如果你也正卡在“能跑但很乱”的阶段我建议先花一晚上把目录结构和Git记录整理清楚再花一晚上把状态码和错误处理补齐最后写一份看得过去的README。这套动作做完你的项目就从“作业”变成了“作品”——而后者才是训练营真正想看到的东西。