4、请求与响应设计:请求体格式(JSON/XML/Form-Data)、响应体结构设计(统一封装)、分页/排序/过滤参数设计
好,咱们今天聊点实在的。请求和响应,说白了就是API的「门面」。用户怎么把数据递给你,你又怎么把结果还回去,这里面门道不少。我见过太多项目,接口设计得随心所欲,前端对接的时候恨不得骂娘。嗯,咱们今天就把它理清楚。
4.1 请求体格式:JSON、XML、Form-Data
先说说请求体。你想想看,客户端要往服务端塞数据,总得有个格式吧?目前主流就三种:JSON、XML、Form-Data。我个人习惯,90%的场景直接用JSON,简单、轻量、好解析。
4.1.1 JSON(推荐)
JSON现在几乎是RESTful API的标配。结构清晰,Python里用json模块一解析就完事。我在项目中遇到过,前端传了个嵌套对象,后端用request.get_json()直接拿到字典,处理起来非常顺手。
# Flask 示例
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.get_json()
# data 就是一个字典
username = data.get('username')
email = data.get('email')
# 业务逻辑...
return jsonify({'message': 'User created'}), 201
Content-Type: application/json。我曾经见过有人忘了设,Flask解析不出来,debug了半天。
4.1.2 XML(遗留系统常用)
XML现在用得少了,但你要是对接银行、物流之类的老系统,还是得会。解析起来比JSON麻烦,得用xml.etree.ElementTree或者lxml。说实话,我个人不太喜欢XML,太啰嗦了。
# 解析 XML 请求体
import xml.etree.ElementTree as ET
xml_data = request.data # 获取原始字节流
root = ET.fromstring(xml_data)
username = root.find('username').text
lxml 库,容错性更好。
4.1.3 Form-Data(文件上传场景)
Form-Data主要用于文件上传,或者混合了文件和字段的表单。比如用户头像上传,既有图片文件,又有用户ID。这时候用multipart/form-data最合适。
# Flask 处理文件上传
from werkzeug.utils import secure_filename
@app.route('/api/upload', methods=['POST'])
def upload_file():
file = request.files['avatar']
user_id = request.form.get('user_id')
filename = secure_filename(file.filename)
file.save(f'uploads/{filename}')
return jsonify({'filename': filename}), 200
4.2 响应体结构设计:统一封装
响应体设计,我踩过最大的坑就是「每个接口返回格式都不一样」。有的返回{'code': 0, 'data': ...},有的直接返回{'id': 1, 'name': 'xxx'}。前端对接的时候,每个接口都要写不同的解析逻辑,累死人。
所以,我强烈建议:统一响应体结构。不管成功还是失败,格式必须一致。
4.2.1 标准响应体格式
我个人习惯用这个结构:
{
"code": 0, # 业务状态码,0表示成功
"message": "success", # 提示信息
"data": { ... } # 实际数据,可以是对象、列表或null
}
为什么这么设计?
- code:业务状态码,不是HTTP状态码。比如
0成功,1001参数错误,1002未登录。这样前端可以统一判断。 - message:给前端或用户看的提示信息。成功时写"success",失败时写具体原因。
- data:真正的业务数据。没有数据时返回
null,不要返回空字符串或空对象。
# 统一响应封装函数
def success_response(data=None, message="success"):
return jsonify({
"code": 0,
"message": message,
"data": data
})
def error_response(code, message, data=None):
return jsonify({
"code": code,
"message": message,
"data": data
}), 400 # HTTP状态码可以灵活设置
code字段,结果没处理。后来统一用code做业务判断,HTTP状态码只做传输层区分(200成功、400参数错误、500服务器错误)。
4.2.2 错误响应示例
# 参数校验失败
{
"code": 1001,
"message": "用户名不能为空",
"data": null
}
# 未登录
{
"code": 1002,
"message": "请先登录",
"data": null
}
4.3 分页、排序、过滤参数设计
列表接口,几乎每个项目都有。用户不可能一次看几万条数据,所以分页是必须的。排序和过滤,则是让用户能快速找到想要的数据。这三个东西,我建议统一设计,别每个接口各搞一套。
4.3.1 分页参数
我推荐用页码+每页条数的方式,也就是page和page_size。为什么不用游标分页?因为大多数场景下,用户需要跳转到第N页,游标分页做不到。
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| page | int | 1 | 当前页码,从1开始 |
| page_size | int | 20 | 每页条数,建议限制最大值(比如100) |
# 分页参数解析
from flask import request
def get_pagination_params():
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
# 限制 page_size 最大值,防止恶意请求
page_size = min(page_size, 100)
return page, page_size
page_size的最大值。我曾经见过有人传page_size=999999,直接把数据库查崩了。限制到100或200就够用了。
4.3.2 排序参数
排序我习惯用sort_by和sort_order。前者指定字段名,后者指定升序或降序。
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| sort_by | string | created_at | 排序字段,比如 id、created_at、name |
| sort_order | string | desc | 排序方向:asc 或 desc |
# 排序参数解析
def get_sort_params(allowed_fields=None):
if allowed_fields is None:
allowed_fields = ['id', 'created_at', 'updated_at']
sort_by = request.args.get('sort_by', 'created_at')
sort_order = request.args.get('sort_order', 'desc')
# 校验字段是否在白名单内
if sort_by not in allowed_fields:
sort_by = 'created_at'
# 校验排序方向
if sort_order not in ['asc', 'desc']:
sort_order = 'desc'
return sort_by, sort_order
sort_by=password_hash,你的用户密码就暴露了。我见过有人直接把前端传的字段名拼到SQL里,结果被SQL注入,教训深刻。
4.3.3 过滤参数
过滤参数,说白了就是条件查询。我建议用field__operator=value的格式,比如price__gte=100表示价格大于等于100。
| 参数示例 | 说明 |
|---|---|
| status=active | 精确匹配 |
| price__gte=100 | 大于等于(greater than or equal) |
| price__lte=500 | 小于等于(less than or equal) |
| name__contains=张三 | 模糊匹配(like) |
| created_at__gte=2024-01-01 | 日期范围过滤 |
# 过滤参数解析示例(简化版)
def parse_filters(allowed_filters=None):
if allowed_filters is None:
allowed_filters = ['status', 'price__gte', 'price__lte', 'name__contains']
filters = {}
for key, value in request.args.items():
if key in allowed_filters:
filters[key] = value
return filters
name__contains=%,结果SQL里直接拼了LIKE '%%%',查出了所有数据。后来我强制对模糊搜索的输入做转义,把%和_替换掉,才彻底解决。
4.3.4 分页响应结构
分页接口的响应,除了数据本身,还要返回分页信息。我习惯这样设计:
{
"code": 0,
"message": "success",
"data": {
"items": [ ... ], # 当前页的数据列表
"total": 100, # 总记录数
"page": 1, # 当前页码
"page_size": 20, # 每页条数
"total_pages": 5 # 总页数(可选)
}
}
# 分页响应封装
def paginated_response(query, page, page_size):
total = query.count()
items = query.offset((page - 1) * page_size).limit(page_size).all()
# 序列化 items(假设有序列化函数)
serialized_items = [item.to_dict() for item in items]
return success_response({
"items": serialized_items,
"total": total,
"page": page,
"page_size": page_size,
"total_pages": (total + page_size - 1) // page_size
})
好了,这一章的内容就这些。说白了,请求体和响应体设计,核心就是「统一」二字。统一了格式,前后端对接就少了很多扯皮。下一章咱们聊聊认证与授权,那又是另一个大坑了。