大实训 · 从零创建 Flask 项目(4 小节连堂)
13 章学完,这次不看骨架、不抄代码,从空文件夹开始亲手搭一个能跑、能调、能验收的 Flask 后端。四小节分别完成:①最小可运行 → ②工程化拆分 → ③数据库与登录鉴权 → ④调试与 curl 验收。每一步都把前面章节的知识点用回来,每行代码都有注释。
实训规则:① 全程只允许对照本页,不允许打开课程骨架 code/hiflask/(做完再对标);② 每做完一个步骤必须用 curl 验证一次,不验证不进下一步;③ 出错先自己读报错信息,再查「常见报错速查表」,最后才举手。
四小节总览
每小节 45 分钟:讲 10 + 跟做 25 + 小结 10
第 1 小节从空文件夹到「能跑」
虚拟环境 → 装依赖 → 最小 app.py → 启动服务 → 第一次 curl 请求。
→
第 2 小节工程化拆分
配置中心 → 扩展集中 → 统一响应 → 蓝图拆分 → 应用工厂,目录初具骨架。
→
第 3 小节数据库与登录鉴权
建库 → ORM 模型 → 注册/登录接口 → JWT 签发与校验 → 受保护接口。
→
第 4 小节调试与验收
调试模式与日志 → 常见报错速查 → 一键 curl 验收脚本 → 对标课程骨架。
知识关联地图
这次实训把哪些「学过的东西」串了起来
| 前序课程 / 章节 | 在本实训中的用武之地 |
| 网站开发(HTTP / JSON) | curl 发 GET/POST、看状态码(200/400/401/404)、读懂 Content-Type: application/json |
| Python 课(装饰器 / 类 / 模块) | @app.route 与 @jwt_required 都是装饰器;配置用类;项目按包(package)组织 |
| MySQL 课(库表 / 编码) | 第 3 小节建库 wenxue_zero、utf8mb4 编码、主键自增 |
| Flask 第 1~3 章 | 应用实例、路由装饰器、变量规则、request 取参数、jsonify 返回响应 |
| Flask 第 5 章 蓝图与工程化 | 蓝图拆分 views/、配置中心 config.py、应用工厂 create_app |
| Flask 第 6 章 ORM | SQLAlchemy 模型类 ↔ 数据库表、db.session 增查 |
| Flask 第 7 章 统一响应 | 所有接口都用 ok() / fail(),结构永远是 code/msg/data |
| Flask 第 8 章 JWT 鉴权 | 注册登录、签发 token、保护接口、异常统一成 401 |
| Flask 第 12 章 接口测试 | curl 就是命令行版 Postman;验收脚本 = 一组有序用例 |
第 1 小节 · 从空文件夹到「能跑」
目标:服务跑起来,curl 收到第一份 JSON
1
本节结束时你将拥有:一个名叫 wenxue-api 的项目文件夹、一个隔离的虚拟环境、一个只有一个接口的最小 Flask 服务,并用 curl 亲眼看到它返回 JSON。
0
课前自检(2 分钟)
四条命令确认工具都在。任何一条报「command not found」,先解决再继续。
# 逐条运行($ 开头是提示符,不用输入)
$ python3 --version # 看 Python 版本,需要 3.10 及以上
$ pip3 --version # 确认包管理器 pip 可用
$ mysql --version # 可选:第 3 小节才用到 MySQL,没有可先跳过
$ curl --version # 确认 curl 可用——本实训全程用它调试接口
Python 3.12.4 / pip 24.0 …
1
创建项目文件夹与虚拟环境
$ mkdir wenxue-api && cd wenxue-api # 新建项目文件夹并进入;&& 表示前一条成功才执行后一条
$ python3 -m venv .venv # 在项目内创建虚拟环境 .venv——项目的依赖装在这里,不污染系统
$ source .venv/bin/activate # 激活(Windows PowerShell: .venv\Scripts\Activate.ps1)
(.venv) $ # 命令行前多了 (.venv),说明此后安装的东西只属于本项目
$ pip install flask flask-sqlalchemy flask-jwt-extended pymysql python-dotenv
# flask 本体 / SQLAlchemy(ORM)/ JWT(登录令牌)/ pymysql(Python 连 MySQL 的驱动)/ dotenv(读 .env 配置)
$ pip freeze > requirements.txt # 把当前环境的依赖及版本写进清单,别人 pip install -r 即可复现
为什么必须用虚拟环境:这学期你装过的库版本可能和新项目冲突。虚拟环境相当于「每个项目一个独立书包」,第 1 小节结束前若忘了激活,装错地方,后面所有 ModuleNotFoundError 都源于此。
2
写最小 app.py(先「能跑」再谈架构)
在项目根目录新建 app.py,输入以下全部内容(含注释):
# app.py —— 项目入口。第 1 小节所有代码都先挤在这一个文件里,第 2 小节再拆分
from flask import Flask, jsonify # Flask 是应用类;jsonify 把字典转成 JSON 响应(自动加 Content-Type 头)
app = Flask(__name__) # 创建应用实例;__name__ 帮 Flask 定位项目根目录(找静态文件、模板用)
app.json.ensure_ascii = False # 让 JSON 里的中文原样显示,而不是 "\u53e4\u5178" 这样的转义
@app.route("/") # 路由装饰器:把下面的函数绑定到根路径 / —— curl 或浏览器访问 / 就会执行它
def index():
# 返回统一结构 {code, msg, data}:这是第 7 章的规范,从第一个接口就养成习惯
return jsonify({
"code": 200, # 业务状态码:200 = 成功
"msg": "success", # 给人看的提示信息
"data": { # 给程序用的具体数据
"service": "古典文学网 API", # 服务名——本实训的贯穿项目
"version": "0.1", # 版本号,每次大改动手动 +0.1,方便确认「跑的是新代码」
},
})
if __name__ == "__main__": # 只有直接运行本文件才执行(被 import 时不执行)
app.run(debug=True, port=5000) # 启动开发服务器;debug=True:改代码自动重启、出错给调试页
3
启动服务
$ python app.py # 运行入口文件
* Serving Flask app 'app'
* Debug mode: on
* Running on http://127.0.0.1:5000 ← 看到这行 = 服务已在本机 5000 端口待命
4
用 curl 发出第一个请求(本节验收点)
新开一个终端窗口(原窗口要留着跑服务),逐条执行:
$ curl http://127.0.0.1:5000/ # 向本机 5000 端口的 / 发 GET 请求(curl 默认就是 GET)
{"code":200,"msg":"success","data":{"service":"古典文学网 API","version":"0.1"}}
$ curl -i http://127.0.0.1:5000/ # 加 -i:连响应头一起打印
HTTP/1.1 200 OK ← 状态码
Content-Type: application/json ← 内容类型,网站开发课讲过:JSON 响应的身份证
$ curl -i http://127.0.0.1:5000/not-exist # 故意访问不存在的路径,认识 404
HTTP/1.1 404 NOT FOUND
- 终端 1:服务运行中,无红色报错
- 终端 2:curl 收到
{"code":200,...},且中文没有变成 \uXXXX
- 能说出:curl 默认发的是 GET;-i 的作用是什么
第 2 小节 · 工程化拆分
对应第 5 章蓝图与工程化 + 第 7 章统一响应
2
为什么不能一直挤在一个文件里:接口会越写越多。第 5 章讲过蓝图,这一小节把 app.py 拆成「配置 / 扩展 / 工具 / 视图」四层,接口功能不变、结构脱胎换骨——拆完再用 curl 验证一遍,结果必须和第 1 小节一模一样。
5
先规划目录(照着建空文件)
wenxue-api/
├── app.py # 改造成「应用工厂」:只组装,不写接口
├── config.py # 配置中心:开发/生产配置收拢在此
├── extensions.py # 扩展实例集中地:db、jwt(防循环导入,见下)
├── requirements.txt
├── models/ # 模型层:一个表一个文件(第 3 小节填充)
│ └── __init__.py # 空文件:让 Python 把 models 当作一个包
├── utils/
│ ├── __init__.py
│ └── response.py # 统一响应 ok() / fail()
└── views/ # 视图层:一个业务模块一个蓝图
├── __init__.py # register_blueprints():集中注册所有蓝图
├── home.py # 首页模块
└── user.py # 用户模块(第 3 小节实现,本节可先空着)
6
config.py —— 配置中心
# config.py —— 配置中心:把写死的配置收拢到一个文件,换环境只改这里
import os # 标准库:读取环境变量,避免把数据库密码硬编码进代码
from dotenv import load_dotenv # python-dotenv:启动时把 .env 文件里的键值对装进环境变量
load_dotenv() # 执行加载:之后 os.getenv("XXX") 就能读到 .env 里的 XXX 了
class BaseConfig: # 基础配置:开发和生产共用的部分
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key") # Flask 签名用密钥;第二个参数是没有环境变量时的兜底值
SQLALCHEMY_DATABASE_URI = os.getenv( # SQLAlchemy 连库地址(URI):驱动+用户名+密码+主机+库名
"DATABASE_URL",
"mysql+pymysql://root:123456@127.0.0.1:3306/wenxue_zero?charset=utf8mb4",
) # 默认连本机 MySQL——把 123456 换成你的密码;没装 MySQL 的同学见下方「SQLite 退路」
SQLALCHEMY_TRACK_MODIFICATIONS = False # 关闭模型变更追踪通知:我们用不到,还费内存
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "jwt-dev-secret") # 签发 JWT 的密钥:泄露 = 别人能伪造登录态
class DevConfig(BaseConfig): # 开发环境:继承基础配置,只写差异项
DEBUG = True # 开调试模式:出错给交互式调试页、改代码自动重启
class ProdConfig(BaseConfig): # 生产环境
DEBUG = False # 生产绝不能开调试页——它会把源码和变量暴露给任何访问者
config_map = { # 名字 → 配置类 的映射表:应用工厂按名字取用
"dev": DevConfig,
"prod": ProdConfig,
}
没装 MySQL?先用 SQLite 退路:把 URI 换成 sqlite:///wenxue.db 即可——SQLAlchemy 的价值就在这里:换数据库只改这一行连接串,模型和接口代码一行不动。
7
extensions.py —— 扩展实例集中地(防循环导入)
# extensions.py —— db 等扩展实例在这里创建
# 为什么单独建文件:db 要被 models 和 views 同时 import。如果它在 app.py 里创建,
# 就会出现 app.py → views/user.py → app.py 的「循环导入」——Python 直接报 ImportError。
# 把扩展放在独立的第三方中立文件里,大家 import 它,它谁也不 import,环就断了。
from flask_sqlalchemy import SQLAlchemy # ORM 扩展:用 Python 类操作数据库表
from flask_jwt_extended import JWTManager # JWT 扩展:负责签发与校验登录令牌
db = SQLAlchemy() # 创建“裸”的 db 实例——此刻还没绑定任何应用,所以谁 import 都安全
jwt = JWTManager() # 同理,JWT 管理器也先创建,等 app 工厂里再 init_app 绑定
8
utils/response.py —— 统一响应(第 7 章)
# utils/response.py —— 统一响应格式:所有接口长一个样,前端只写一套处理逻辑
from flask import jsonify # 字典 → JSON 响应
def ok(data=None, msg="success", code=200):
"""成功响应:调用方只管给数据,外壳自动补齐"""
return jsonify({"code": code, "msg": msg, "data": data}), code # 返回元组:第二项是 HTTP 状态码
def fail(msg="error", code=400, data=None):
"""失败响应:错误信息也包进同样结构,前端只需判断 code 字段"""
return jsonify({"code": code, "msg": msg, "data": data}), code
9
views/home.py + views/__init__.py —— 蓝图拆分
# views/home.py —— 首页模块蓝图
from flask import Blueprint # Blueprint:可整体注册到主应用的“路由包”
from utils.response import ok # 复用统一响应——接口里不再手写 jsonify 外壳
home_bp = Blueprint("home", __name__) # 创建蓝图;第一个参数是蓝图名,供 url_for 反查用
@home_bp.get("/") # 等价于 @home_bp.route("/", methods=["GET"]) —— .get/.post 是快捷写法
def index():
"""首页探活接口:服务是否活着,看它就行"""
return ok({"service": "古典文学网 API", "version": "0.2"}) # 版本号提到 0.2:拆分完成的标记
# views/__init__.py —— 视图包的“总装配车间”
def register_blueprints(app):
"""把所有蓝图集中注册到 app:以后新增模块,只需在这里多 import 一行"""
from views.home import home_bp # 注意:在函数体内 import(延迟导入)——进一步避免循环导入
app.register_blueprint(home_bp) # 注册后,home_bp 里的路由才真正生效
10
app.py 改造成应用工厂(create_app)
# app.py —— 应用工厂:只负责“组装零件”,不再写任何具体接口
from flask import Flask # 应用类
from config import config_map # 配置映射表
from extensions import db, jwt # 扩展实例(第 3 小节会用 jwt,现在先绑上)
from views import register_blueprints # 蓝图总注册函数
def create_app(config_name="dev"):
"""应用工厂:每次调用返回一个按指定配置组装好的 Flask 应用"""
app = Flask(__name__) # 创建应用实例
app.json.ensure_ascii = False # 中文原样显示:第 1 小节配置过,拆分后要跟着搬过来,否则中文变 \uXXXX
app.config.from_object(config_map[config_name]) # 按名字加载配置类,如 "dev" → DevConfig
db.init_app(app) # 把 db 绑定到这个 app——工厂模式的关键一步:先有裸实例,此处再绑定
jwt.init_app(app) # 同样绑定 JWT 管理器
register_blueprints(app) # 注册全部蓝图——路由从这一刻起生效
return app # 把组装好的应用交出去
app = create_app() # 模块级调用一次:供 python app.py / flask --app app 使用
if __name__ == "__main__": # 直接运行本文件时才启动开发服务器
app.run(debug=True, port=5000) # debug 也可以从 config 读,这里显式写方便第 1 次理解
✓
本节验收:拆完必须还是「能跑」
# 先在运行服务的终端按 Ctrl + C 停掉旧服务,再重新运行
$ python app.py
$ curl http://127.0.0.1:5000/ # 结构变了,响应必须一字不差
{"code":200,"msg":"success","data":{"service":"古典文学网 API","version":"0.2"}}
- 目录里出现 config.py / extensions.py / utils/ / views/,各文件职责能一句话说清
- curl 返回 version 0.2——功能没变,但代码已分层(这就是「重构」)
- 能回答:db 为什么放在 extensions.py 而不是 app.py(防循环导入)
第 3 小节 · 数据库与登录鉴权
对应 MySQL 课 + 第 6 章 ORM + 第 8 章 JWT
3
本节目标:让数据真正落进 MySQL;实现注册、登录两个公开接口和「查看自己」一个受保护接口;用 curl 走完「注册 → 登录拿 token → 带 token 访问」的完整链路。
11
建库(MySQL 课知识回收)
$ mysql -u root -p # 登录 MySQL,回车后输入密码
-- 建库:utf8mb4 是完整 UTF-8 编码(MySQL 课讲过:utf8 是残缺版,存不了 emoji)
CREATE DATABASE wenxue_zero DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE wenxue_zero; # 切换到新库
-- 表不用手动建:下一节的 User 模型会由 db.create_all() 自动建表(ORM 的意义)
12
models/user.py —— 用户模型
# models/user.py —— 一个类对应一张表,一个属性对应一列
from werkzeug.security import check_password_hash, generate_password_hash
# ^ werkzeug 是 Flask 自带依赖,不用额外安装:一对密码「散列/比对」工具
from extensions import db # 从 extensions 导入 db——绝不从 app 导入,防止循环导入
class User(db.Model): # 继承 db.Model 后,SQLAlchemy 才把它当表模型管理
__tablename__ = "user" # 指定表名;不写则默认用类名小写
id = db.Column(db.Integer, primary_key=True, autoincrement=True) # 主键,自增编号
username = db.Column(db.String(32), unique=True, nullable=False) # 用户名:唯一 + 非空,重复插入直接报错
password_hash = db.Column(db.String(256), nullable=False) # 存哈希!数据库里永远不出现明文密码
nickname = db.Column(db.String(32), default="") # 昵称:可选字段,默认空字符串
def set_password(self, raw):
"""注册/改密时调用:明文 → 单向散列"""
self.password_hash = generate_password_hash(raw) # 单向:能从明文算出哈希,不能反推
def check_password(self, raw):
"""登录时调用:把用户输入同样散列后与库里的哈希比对"""
return check_password_hash(self.password_hash, raw) # 相同明文 → 相同哈希 → True
def to_dict(self):
"""序列化:模型对象 → 字典。注意:绝不把 password_hash 放进来"""
return {"id": self.id, "username": self.username, "nickname": self.nickname}
13
views/user.py —— 注册 / 登录 / 查看自己
# views/user.py —— 用户模块:两公开一保护
from flask import Blueprint, request
# ^ request 对象封装了本次 HTTP 请求的一切:JSON 体、查询串、请求头(第 3 章核心)
from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required
# ^ JWT 三件套:签发令牌 / 从令牌读身份 / 保护接口(第 8 章核心)
from extensions import db # db.session:数据库会话(暂存区 + 提交)
from models.user import User # 用户模型
from utils.response import fail, ok # 统一响应
user_bp = Blueprint("user", __name__, url_prefix="/api/user")
# ^ url_prefix:此蓝图下所有路由自动带 /api/user 前缀——接口分组从蓝图名开始
@user_bp.post("/register") # POST /api/user/register(写数据必须用 POST)
def register():
data = request.get_json(silent=True) or {}
# ^ 解析 JSON 请求体;silent=True:body 不是合法 JSON 时不抛异常而是返回 None,再 or {} 兜底成空字典
username = (data.get("username") or "").strip() # 取用户名并去掉首尾空格
password = data.get("password") or "" # 取密码;字段缺失时 or "" 防止 None 引发异常
if not username or not password: # 服务端必须再校验一次——前端校验可以被绕过
return fail("用户名和密码不能为空", code=400) # 400 = 请求参数有问题
if len(password) < 6: # 最简单的强度规则
return fail("密码至少 6 位", code=400)
if User.query.filter_by(username=username).first(): # 按用户名查库:first() 取第一条,没有则 None
return fail("用户名已被占用", code=400)
user = User(username=username) # 创建模型对象——此刻只存在于内存,尚未写库
user.set_password(password) # 明文 → 哈希后再保存
db.session.add(user) # 放进会话(相当于 git add 的暂存区)
db.session.commit() # 提交事务,真正写入 MySQL
return ok({"id": user.id, "username": user.username}, msg="注册成功") # 只回非敏感字段
@user_bp.post("/login") # POST /api/user/login
def login():
data = request.get_json(silent=True) or {} # 同上:安全解析请求体
user = User.query.filter_by(username=(data.get("username") or "").strip()).first()
if user is None or not user.check_password(data.get("password") or ""):
# ^ 用户不存在 或 密码不符——故意合并成一句提示,不给撞库者「哪个字段错了」的线索
return fail("用户名或密码错误", code=401) # 401 = 未通过身份认证
token = create_access_token(identity=str(user.id))
# ^ 签发 JWT:把用户 id 塞进令牌(identity 要求字符串,所以要 str())
# ^ 令牌由 JWT_SECRET_KEY 签名,客户端改一个字节都会校验失败
return ok({"token": token, "nickname": user.nickname}, msg="登录成功")
# ^ 前端把 token 存起来(localStorage),之后每个受保护请求都带上它
@user_bp.get("/me") # GET /api/user/me —— 受保护接口
@jwt_required() # 守门装饰器:请求头没有合法 token,直接 401,函数体根本不会执行
def me():
uid = get_jwt_identity() # 从 token 里取回签发时存的 identity(用户 id 字符串)
user = db.session.get(User, int(uid)) # 按主键查询(get 走主键,最快)
if user is None: # token 合法但用户已被删——边界情况也要兜住
return fail("用户不存在", code=404)
return ok(user.to_dict()) # 返回个人信息(不含密码哈希)
14
注册蓝图 + 建表(改两处既有文件)
# views/__init__.py —— 增加 user_bp
def register_blueprints(app):
from views.home import home_bp
from views.user import user_bp # 新增:导入用户蓝图
app.register_blueprint(home_bp)
app.register_blueprint(user_bp) # 新增:注册用户蓝图
# app.py —— create_app 里、register_blueprints(app) 之后,加上自动建表
with app.app_context(): # 需要应用上下文:离开它 db 不知道自己服务于哪个 app
db.create_all() # 扫描全部模型,表不存在就建(教学用;生产环境用 Flask-Migrate 管理变更)
15
完整链路 curl 测试(本节验收点)
# 重启服务后,按顺序执行:
# ① 注册(POST + JSON 请求体;-H 指定内容类型,-d 是数据)
$ curl -X POST http://127.0.0.1:5000/api/user/register \
-H "Content-Type: application/json" \
-d '{"username":"zhangsan","password":"123456"}'
{"code":200,"msg":"注册成功","data":{"id":1,"username":"zhangsan"}}
# ② 重复注册 → 400(服务端唯一性校验生效)
$ curl -X POST http://127.0.0.1:5000/api/user/register -H "Content-Type: application/json" -d '{"username":"zhangsan","password":"123456"}'
{"code":400,"msg":"用户名已被占用","data":null}
# ③ 错误密码登录 → 401
$ curl -X POST http://127.0.0.1:5000/api/user/login -H "Content-Type: application/json" -d '{"username":"zhangsan","password":"wrong!"}'
{"code":401,"msg":"用户名或密码错误","data":null}
# ④ 正确登录拿 token:-s 静默模式,管道交给 python 从 JSON 里抠出 token 存进变量
$ TOKEN=$(curl -s -X POST http://127.0.0.1:5000/api/user/login \
-H "Content-Type: application/json" -d '{"username":"zhangsan","password":"123456"}' \
| python3 -c "import sys,json;print(json.load(sys.stdin)['data']['token'])")
$ echo $TOKEN # 打印看看,应该是一串 xxx.yyy.zzz 三段式 JWT
# ⑤ 不带 token 访问受保护接口 → 401
$ curl -i http://127.0.0.1:5000/api/user/me
HTTP/1.1 401 UNAUTHORIZED
# ⑥ 带 token(Authorization 头 + Bearer 前缀,一个空格都不能少)→ 200
$ curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:5000/api/user/me
{"code":200,"msg":"success","data":{"id":1,"username":"zhangsan","nickname":""}}
Windows 同学注意:PowerShell 里单引号 JSON 会出问题,改用 curl.exe -d "{\"username\":\"zs\",\"password\":\"123456\"}",或直接用课程第 12 章装过的 Apifox/Postman 等价操作。
- 注册 → 登录 → 带 token 访问,六步 curl 全部得到上注的期望响应
- 用 MySQL 课的客户端
SELECT * FROM user; 亲眼看到:库里存的是哈希,不是明文
- 能回答:为什么 token 里不直接存用户名而存 id;为什么登录失败提示不区分「用户不存在 / 密码错误」
第 4 小节 · 调试、排错与验收
对应第 10 章钩子 + 第 12 章接口测试
4
本节目标:把 JWT 异常统一成规范响应(第 10 章钩子思想)、学会用日志代替 print 调试、掌握常见报错速查表,最后写一个一键 curl 验收脚本并全绿通过。
16
JWT 异常统一 + 日志
在 app.py 的 create_app 内(jwt.init_app 之后)注册异常处理器——这就是第 10 章「请求钩子/统一处理」的实战:
# app.py —— create_app 内新增(放在 jwt.init_app(app) 之后)
from utils.response import fail # 文件顶部别忘了导入
@jwt.unauthorized_loader # 钩子①:请求头压根没带 token 时触发
def missing_token(reason):
return fail("请先登录", code=401) # 把框架默认的英文报错,统一成我们的响应结构
@jwt.expired_token_loader # 钩子②:token 过期时触发(header/payload 是 JWT 两段内容)
def expired_token(header, payload):
return fail("登录已过期,请重新登录", code=401) # 前端据此跳转登录页
调试用日志,不用 print(print 没有时间、级别,也没法一键关闭):
# views/user.py —— 顶部加:
import logging # 标准库日志模块
logger = logging.getLogger("wenxue") # 获取名为 wenxue 的日志器
# 在 register() 的 db.session.commit() 之后加一行:
logger.info("用户 %s 注册成功,id=%s", username, user.id)
# ^ 占位符式写法:值与模板分离,防注入也更易读;控制台会输出带时间的日志行
17
常见报错速查表(遇到先查这里)
| 症状(报错关键字) | 原因 | 解决 |
Address already in use | 5000 端口被上一个没关的服务占着 | mac/Linux:lsof -i :5000 找到 PID 后 kill PID;Windows:netstat -ano | findstr 5000 |
ModuleNotFoundError: No module named 'flask' | 虚拟环境没激活,包装到别处去了 | source .venv/bin/activate(确认命令行有 (.venv) 前缀)再 pip install -r requirements.txt |
ImportError: cannot import name 'db' | 循环导入:views 反过来 import 了 app | 检查是否按第 2 小节把 db 放进了 extensions.py、蓝图中用延迟导入 |
OperationalError (1045) | MySQL 用户名/密码不对 | 改 config.py 里的连接串,或用 mysql -u root -p 验证密码 |
OperationalError (1049) Unknown database | 库没建 | 回步骤 11 执行 CREATE DATABASE |
curl 收到 405 Method Not Allowed | 用 GET 去访问了只收 POST 的接口 | 补 -X POST;对照接口定义核对方法 |
带 token 仍 401 | 头格式错:缺 Bearer 前缀 / Bearer 后少空格 / token 变量没取到 | echo $TOKEN 先确认非空,再原样对照 "Authorization: Bearer $TOKEN" |
curl 报 JSON decode error / 后端收到的字段是 null | Windows 引号转义问题,JSON 没发对 | 见第 3 小节 Windows 提示,或改用 Apifox |
18
一键 curl 验收脚本(本节验收点)
在项目根目录新建 scripts/acceptance.sh——把散落的测试固化成脚本,就是第 12 章「接口测试用例」的命令行版:
# scripts/acceptance.sh —— 一键验收:按顺序走完 6 条用例,任何一条失败立即停
# 运行方式:bash scripts/acceptance.sh
set -e # 任何命令返回非 0,整个脚本立即退出——不把错误留到最后
BASE=http://127.0.0.1:5000 # 服务地址,集中定义一处
# 用例 1:探活 —— 期望 code=200
curl -sf $BASE/ > /dev/null && echo "✓ 1.探活 通过"
# ^ -f:状态码 ≥400 时 curl 以失败退出,正好配合 set -e
# 用例 2:注册新用户(换一个用户名,脚本可反复跑)
curl -sf -X POST $BASE/api/user/register -H "Content-Type: application/json" \
-d '{"username":"tester01","password":"123456"}' > /dev/null \
&& echo "✓ 2.注册 通过"
# 用例 3:错误密码必须被拒(期望 HTTP 401,所以用不带 -f 的 curl 抓状态码)
CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST $BASE/api/user/login \
-H "Content-Type: application/json" -d '{"username":"tester01","password":"bad"}')
[ "$CODE" = "401" ] && echo "✓ 3.错误密码拒绝 通过" || { echo "✗ 用例3失败:得到 $CODE"; exit 1; }
# 用例 4:正确登录,抠出 token
TOKEN=$(curl -s -X POST $BASE/api/user/login -H "Content-Type: application/json" \
-d '{"username":"tester01","password":"123456"}' \
| python3 -c "import sys,json;print(json.load(sys.stdin)['data']['token'])")
[ -n "$TOKEN" ] && echo "✓ 4.登录签发 通过"
# 用例 5:无 token 访问受保护接口,期望 401
CODE=$(curl -s -o /dev/null -w "%{http_code}" $BASE/api/user/me)
[ "$CODE" = "401" ] && echo "✓ 5.未登录拦截 通过" || { echo "✗ 用例5失败:得到 $CODE"; exit 1; }
# 用例 6:带 token 访问,期望 200
CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TOKEN" $BASE/api/user/me)
[ "$CODE" = "200" ] && echo "✓ 6.鉴权放行 通过" || { echo "✗ 用例6失败:得到 $CODE"; exit 1; }
echo "全部用例通过,验收完成 🎉"
$ bash scripts/acceptance.sh
✓ 1.探活 通过
✓ 2.注册 通过
✓ 3.错误密码拒绝 通过
✓ 4.登录签发 通过
✓ 5.未登录拦截 通过
✓ 6.鉴权放行 通过
全部用例通过,验收完成 🎉
19
对标课程骨架 + 提交
现在可以打开 code/hiflask 对照了——你从零搭出的结构和它完全一致,只是它多了 article / comment / favorite 模块(第 13 章内容)。检查三件事:
- 目录分层一致:app 工厂 / config / extensions / models / utils / views
- 响应结构一致:code / msg / data;JWT 异常都收敛成 401
- 把
requirements.txt、acceptance.sh 运行结果截图、笔记.md(记 3 个你踩过的坑)一起打包提交
下一步:这套后端正是 Vue 前端课与
综合实战·项目整合与部署 的地基——部署到服务器后,用同样的 curl 命令把 BASE 换成公网地址即可验收线上服务。
四小节一句话复盘
- 第 1 节:虚拟环境隔离依赖,最小 app.py 先让服务「能跑」,curl 看到第一份 JSON。
- 第 2 节:配置、扩展、响应、蓝图四层拆分,应用工厂组装——功能不变,结构升级。
- 第 3 节:ORM 把类变表,JWT 把登录变令牌,「注册→登录→带 token 访问」全链路打通。
- 第 4 节:钩子统一异常、日志代替 print、速查表排错、验收脚本兜底——会写更要会调。