码上问学问代码之理,学编程之道

大实训 · 从零创建 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 章 ORMSQLAlchemy 模型类 ↔ 数据库表、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

第 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"}}

第 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 等价操作。

第 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 use5000 端口被上一个没关的服务占着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 / 后端收到的字段是 nullWindows 引号转义问题,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 章内容)。检查三件事:

下一步:这套后端正是 Vue 前端课与 综合实战·项目整合与部署 的地基——部署到服务器后,用同样的 curl 命令把 BASE 换成公网地址即可验收线上服务。

四小节一句话复盘

← 上一章第 13 章 · 项目后端实战