You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
21 KiB
21 KiB
HTTP 响应包装工具使用指南
📖 概述
本工具库提供统一的 API 响应格式包装函数和分页工具,基于 APIFlask 框架设计,简化 API 开发流程。
核心特性:
- ✅ 统一返回格式:
{data, code, message} - ✅ 智能分页支持:自动识别 SQLAlchemy Pagination 对象
- ✅ 灵活调用方式:支持多种参数传递方式
- ✅ 完整类型提示:TypeScript 级别的类型安全
- ✅ 参考 APIFlask 标准:与框架保持一致
📦 安装位置
src/applications/common/utils/http.py
🎯 核心函数
1. success() - 成功响应
签名:
def success(data: Any = None, message: str = '成功', code: int = 200) -> dict
参数:
data:返回数据(任意类型)message:提示信息(默认'成功')code:业务状态码(默认200)
返回格式:
{
"data": <返回数据>,
"code": 200,
"message": "成功"
}
示例:
from iti.applications.common.utils import success
from iti.applications.extensions.http import BaseResponse
@app.get('/api/users/<int:user_id>')
@app.output(BaseResponse)
def get_user(user_id):
user = User.query.get(user_id)
if not user:
return fail('用户不存在', code=404)
# 返回单个对象
return success({
'id': user.id,
'name': user.name,
'email': user.email
})
@app.get('/api/products')
@app.output(BaseResponse)
def get_products():
products = Product.query.limit(10).all()
# 返回列表
return success(
[{'id': p.id, 'name': p.name} for p in products],
message='获取产品列表成功'
)
2. fail() - 失败响应
签名:
def fail(message: str = '操作失败', code: int = 500, data: Any = None) -> dict
参数:
message:错误信息code:业务错误码(默认500)data:额外数据(如验证错误详情)
特点:
- ⚠️ HTTP 状态码保持 200,由前端根据
code字段判断业务状态 - 💡 适用于统一错误处理,避免 HTTP 层面的错误拦截
返回格式:
{
"data": null,
"code": 500,
"message": "操作失败"
}
示例:
from iti.applications.common.utils import fail
@app.post('/api/users')
@app.output(BaseResponse)
def create_user():
username = request.json.get('username')
# 参数验证失败
if not username:
return fail('用户名不能为空', code=400)
# 资源不存在
if User.query.filter_by(username=username).first():
return fail('用户名已存在', code=409)
# 服务器错误
try:
user = User(username=username)
db.session.add(user)
db.session.commit()
except Exception as e:
return fail(f'创建失败: {str(e)}', code=500)
return success(user_to_dict(user), message='创建成功')
@app.post('/api/login')
@app.output(BaseResponse)
def login():
data = request.json
# 验证失败,返回详细错误
errors = validate_login(data)
if errors:
return fail(
message='验证失败',
code=422,
data=errors # {'username': ['必填项'], 'password': ['长度不足']}
)
# ... 登录逻辑
3. page() - 分页响应
签名:
def page(
items: Union[list, Any],
pagination: Union[dict, Any, None] = None,
message: str = '成功',
code: int = 200
) -> dict
支持三种调用方式:
方式 1:传入 SQLAlchemy Pagination 对象(最简)
from iti.applications.common.utils import page
@app.get('/api/users')
@app.output(BaseResponse)
def get_users():
page_num = request.args.get('page', 1, type=int)
page_size = request.args.get('size', 10, type=int)
# SQLAlchemy 分页查询
db_pagination = User.query.paginate(page=page_num, per_page=page_size)
# ✅ 直接传入 Pagination 对象,自动解析
return page(db_pagination, message='获取用户列表成功')
返回格式:
{
"data": {
"items": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
],
"page": {
"page": 1,
"size": 10,
"pages": 10,
"total": 100,
"current": "http://api.example.com/api/users?page=1&size=10",
"next": "http://api.example.com/api/users?page=2&size=10",
"prev": null,
"first": "http://api.example.com/api/users?page=1&size=10",
"last": "http://api.example.com/api/users?page=10&size=10"
}
},
"code": 200,
"message": "获取用户列表成功"
}
方式 2:传入数据列表 + Pagination 对象
适用于需要先序列化数据的场景:
from iti.applications.common.utils import page
from iti.applications.schemas.user import UserSchema
@app.get('/api/users')
@app.output(BaseResponse)
def get_users():
db_pagination = User.query.paginate(page=1, per_page=10)
# 先序列化数据(使用 Marshmallow Schema)
users = UserSchema(many=True).dump(db_pagination.items)
# ✅ 传入序列化后的数据 + Pagination 对象
return page(users, db_pagination, message='获取用户列表成功')
方式 3:手动构建分页信息
适用于非 SQLAlchemy 数据源(如 Redis、外部 API):
from iti.applications.common.utils import page, pagination_builder
@app.get('/api/stats')
@app.output(BaseResponse)
def get_stats():
# 从 Redis 获取数据
all_data = redis_client.lrange('stats', 0, -1)
page_num = request.args.get('page', 1, type=int)
page_size = request.args.get('size', 20, type=int)
# 手动分页
start = (page_num - 1) * page_size
end = start + page_size
items = all_data[start:end]
# ✅ 手动构建分页信息
pagination_info = pagination_builder(
None,
page=page_num,
size=page_size,
total=len(all_data)
)
return page(items, pagination_info, message='获取统计数据成功')
🔧 辅助函数
pagination_builder() - 分页信息构建器
签名:
def pagination_builder(
pagination: Any,
*,
page: Optional[int] = None,
size: Optional[int] = None,
total: Optional[int] = None,
pages: Optional[int] = None,
) -> dict
参数说明:
pagination:SQLAlchemy Pagination 对象 或Nonepage:当前页码(手动模式)size:每页数量(手动模式)total:总记录数(手动模式)pages:总页数(可选,自动计算)
注意:
- 第一个参数后使用
*,强制后续参数必须使用关键字传参 - 参考 APIFlask 的
helpers.py实现
示例 1:自动模式(SQLAlchemy Pagination)
from iti.applications.common.utils import pagination_builder
@app.get('/api/products')
def get_products():
db_pagination = Product.query.paginate(page=1, per_page=10)
# ✅ 自动从 Pagination 对象提取信息
pagination_info = pagination_builder(db_pagination)
return {
'items': db_pagination.items,
'pagination': pagination_info
}
示例 2:手动模式(自定义数据源)
from iti.applications.common.utils import pagination_builder
@app.get('/api/external-data')
def get_external_data():
# 从外部 API 获取数据
response = requests.get('https://api.example.com/data')
total_count = response.headers.get('X-Total-Count', 0)
items = response.json()
# ✅ 手动构建分页信息
pagination_info = pagination_builder(
None, # 第一个参数传 None
page=1,
size=20,
total=int(total_count)
)
return page(items, pagination_info)
📊 Schema 定义
PaginationSchema - 分页信息 Schema
class PaginationSchema(Schema):
"""自定义分页信息 Schema"""
page = Integer() # 当前页码
size = Integer() # 每页数量(重命名自 per_page)
pages = Integer() # 总页数
total = Integer() # 总记录数
current = URL() # 当前页URL
next = URL() # 下一页URL
prev = URL() # 上一页URL
first = URL() # 首页URL
last = URL() # 末页URL
特点:
- ✅ 与 APIFlask 原生
PaginationSchema保持一致 - ✅ 仅将
per_page重命名为size
PageDataSchema - 分页数据 Schema
class PageDataSchema(Schema):
"""分页数据包装 Schema"""
items = List(Nested(Field())) # 数据列表
page = Nested(PaginationSchema) # 分页信息(字段名为 page)
使用示例:
from iti.applications.common.utils import PageDataSchema
# 在路由中使用(可选,主要用于文档生成)
@app.get('/api/users')
@app.doc(responses={200: PageDataSchema})
def get_users():
...
🎨 完整示例
示例 1:用户管理 CRUD
from flask import request
from iti.applications.common.utils import success, fail, page
from iti.applications.extensions.http import BaseResponse
from iti.applications.models.user import User
from iti.applications.schemas.user import UserSchema, UserCreateSchema
# ========== 列表(分页) ==========
@app.get('/api/users')
@app.output(BaseResponse)
def get_users():
"""获取用户列表"""
page_num = request.args.get('page', 1, type=int)
page_size = request.args.get('size', 10, type=int)
db_pagination = User.query.paginate(page=page_num, per_page=page_size)
return page(db_pagination, message='获取用户列表成功')
# ========== 详情 ==========
@app.get('/api/users/<int:user_id>')
@app.output(BaseResponse)
def get_user(user_id):
"""获取用户详情"""
user = User.query.get(user_id)
if not user:
return fail('用户不存在', code=404)
return success(UserSchema().dump(user))
# ========== 创建 ==========
@app.post('/api/users')
@app.input(UserCreateSchema)
@app.output(BaseResponse)
def create_user(data):
"""创建用户"""
# 检查用户名是否存在
if User.query.filter_by(username=data['username']).first():
return fail('用户名已存在', code=409)
try:
user = User(**data)
db.session.add(user)
db.session.commit()
return success(
UserSchema().dump(user),
message='创建成功',
code=201
)
except Exception as e:
db.session.rollback()
return fail(f'创建失败: {str(e)}', code=500)
# ========== 更新 ==========
@app.patch('/api/users/<int:user_id>')
@app.input(UserCreateSchema)
@app.output(BaseResponse)
def update_user(user_id, data):
"""更新用户"""
user = User.query.get(user_id)
if not user:
return fail('用户不存在', code=404)
try:
for key, value in data.items():
setattr(user, key, value)
db.session.commit()
return success(UserSchema().dump(user), message='更新成功')
except Exception as e:
db.session.rollback()
return fail(f'更新失败: {str(e)}', code=500)
# ========== 删除 ==========
@app.delete('/api/users/<int:user_id>')
@app.output(BaseResponse)
def delete_user(user_id):
"""删除用户"""
user = User.query.get(user_id)
if not user:
return fail('用户不存在', code=404)
try:
db.session.delete(user)
db.session.commit()
return success(None, message='删除成功')
except Exception as e:
db.session.rollback()
return fail(f'删除失败: {str(e)}', code=500)
示例 2:复杂查询与筛选
from sqlalchemy import and_, or_
from iti.applications.common.utils import page, pagination_builder
@app.get('/api/products')
@app.output(BaseResponse)
def get_products():
"""获取产品列表(支持筛选、搜索、排序)"""
# 获取查询参数
page_num = request.args.get('page', 1, type=int)
page_size = request.args.get('size', 20, type=int)
category = request.args.get('category')
search = request.args.get('search')
sort_by = request.args.get('sort', 'created_at')
order = request.args.get('order', 'desc')
# 构建查询
query = Product.query
# 筛选条件
if category:
query = query.filter(Product.category == category)
# 搜索条件
if search:
query = query.filter(
or_(
Product.name.contains(search),
Product.description.contains(search)
)
)
# 排序
if order == 'desc':
query = query.order_by(getattr(Product, sort_by).desc())
else:
query = query.order_by(getattr(Product, sort_by).asc())
# 分页
db_pagination = query.paginate(page=page_num, per_page=page_size)
# 序列化
products = ProductSchema(many=True).dump(db_pagination.items)
return page(products, db_pagination, message='获取产品列表成功')
示例 3:聚合统计(非 ORM 分页)
from sqlalchemy import func
from iti.applications.common.utils import page, pagination_builder
@app.get('/api/stats/daily')
@app.output(BaseResponse)
def get_daily_stats():
"""获取每日统计数据"""
page_num = request.args.get('page', 1, type=int)
page_size = request.args.get('size', 30, type=int)
# 聚合查询(不使用 ORM 分页)
query = db.session.query(
func.date(Order.created_at).label('date'),
func.count(Order.id).label('order_count'),
func.sum(Order.amount).label('total_amount')
).group_by(func.date(Order.created_at))
# 获取总数
total = query.count()
# 手动分页
offset = (page_num - 1) * page_size
items = query.offset(offset).limit(page_size).all()
# 格式化数据
stats = [
{
'date': str(item.date),
'order_count': item.order_count,
'total_amount': float(item.total_amount or 0)
}
for item in items
]
# 手动构建分页信息
pagination_info = pagination_builder(
None,
page=page_num,
size=page_size,
total=total
)
return page(stats, pagination_info, message='获取统计数据成功')
⚙️ 配置说明
BaseResponse Schema
在 applications/extensions/http.py 中定义:
from apiflask import Schema
from apiflask.fields import Integer, String, Field
class BaseResponse(Schema):
"""统一响应格式 Schema"""
data = Field()
code = Integer()
message = String()
def init_http(app):
# 配置 APIFlask 使用自定义响应格式
app.config["BASE_RESPONSE_SCHEMA"] = BaseResponse
app.config["BASE_RESPONSE_DATA_KEY"] = "data"
🔍 URL 生成规则
分页 URL 自动生成逻辑:
- 获取当前请求的
base_url:http://api.example.com/api/users - 复制查询参数:
request.args.copy() - 更新分页参数:
page: 页码size: 每页数量(注意:使用size而非per_page)
- 构建完整 URL:
base_url + ? + urlencode(args)
示例:
请求 /api/users?category=admin&page=2&size=10 时生成的 URL:
{
"current": "http://api.example.com/api/users?category=admin&page=2&size=10",
"next": "http://api.example.com/api/users?category=admin&page=3&size=10",
"prev": "http://api.example.com/api/users?category=admin&page=1&size=10",
"first": "http://api.example.com/api/users?category=admin&page=1&size=10",
"last": "http://api.example.com/api/users?category=admin&page=10&size=10"
}
📝 最佳实践
1. 统一错误码规范
建议定义错误码常量:
# applications/common/constants.py
class ErrorCode:
"""业务错误码"""
SUCCESS = 200
BAD_REQUEST = 400
UNAUTHORIZED = 401
FORBIDDEN = 403
NOT_FOUND = 404
CONFLICT = 409
UNPROCESSABLE_ENTITY = 422
INTERNAL_SERVER_ERROR = 500
# 使用
from iti.applications.common.utils import fail
from iti.applications.common.constants import ErrorCode
return fail('用户不存在', code=ErrorCode.NOT_FOUND)
2. 结合 Marshmallow Schema
from iti.applications.schemas.user import UserSchema
from iti.applications.common.utils import success
@app.get('/api/users/<int:user_id>')
@app.output(BaseResponse)
def get_user(user_id):
user = User.query.get_or_404(user_id)
# ✅ 使用 Schema 序列化
user_data = UserSchema().dump(user)
return success(user_data)
3. 分页参数验证
from iti.applications.common.utils import fail, page
@app.get('/api/users')
@app.output(BaseResponse)
def get_users():
page_num = request.args.get('page', 1, type=int)
page_size = request.args.get('size', 10, type=int)
# 验证分页参数
if page_num < 1:
return fail('页码必须大于 0', code=400)
if page_size < 1 or page_size > 100:
return fail('每页数量必须在 1-100 之间', code=400)
db_pagination = User.query.paginate(page=page_num, per_page=page_size)
return page(db_pagination)
4. 异常统一处理
from iti.applications.common.utils import fail
@app.errorhandler(404)
def handle_404(error):
return fail('资源不存在', code=404)
@app.errorhandler(500)
def handle_500(error):
return fail('服务器内部错误', code=500)
@app.errorhandler(Exception)
def handle_exception(error):
app.logger.error(f'未处理的异常: {error}')
return fail(str(error), code=500)
🆚 对比 APIFlask 原生实现
| 特性 | APIFlask 原生 | 本工具库 |
|---|---|---|
| 分页参数名 | per_page |
size ✅ |
| 返回格式 | 灵活 | 统一 {data, code, message} ✅ |
| 错误处理 | HTTP 状态码 | 业务 code 字段 ✅ |
| 智能识别 | 需手动处理 | 自动识别 Pagination 对象 ✅ |
| URL 生成 | 手动 | 自动生成 ✅ |
🧪 测试
运行测试
# 运行 HTTP 工具测试
hatch run test tests/test_http_utils.py -v
# 运行所有测试
hatch run test
# 测试覆盖率
hatch run cov
测试统计
- 测试用例数量: 33 个
- 测试分类:
success()函数测试: 6 个fail()函数测试: 5 个pagination_builder()测试: 6 个page()函数测试: 7 个- Schema 定义测试: 2 个
- Flask 集成测试: 4 个
- 边界情况测试: 5 个
测试覆盖范围
- ✅ 基础功能测试
- ✅ 参数验证测试
- ✅ 默认值测试
- ✅ 边界情况测试
- ✅ Flask 应用集成测试
- ✅ Schema 定义测试
- ✅ 请求上下文处理测试
添加自定义测试
在 tests/test_http_utils.py 中添加测试:
import pytest
from iti.applications.common.utils import success
def test_my_custom_case():
"""测试自定义场景"""
result = success({'key': 'value'})
assert result['code'] == 200
🐛 常见问题
Q1: 为什么错误也返回 HTTP 200?
A: 这是一种常见的 API 设计模式,优点:
- 前端统一处理,不需要捕获 HTTP 异常
- 避免浏览器/代理对非 200 状态码的拦截
- 业务状态由
code字段表示,更清晰
如需返回 HTTP 错误状态码,可以手动返回元组:
return fail('未找到', code=404), 404 # HTTP 404
Q2: 如何自定义分页 URL 生成逻辑?
A: 修改 _generate_page_url() 函数:
def _generate_page_url(page_num, page_size):
if page_num is None:
return None
# 自定义逻辑
return f"https://custom-domain.com/api?p={page_num}&s={page_size}"
Q3: 分页信息中的 URL 字段可以去掉吗?
A: 可以。修改 pagination_builder() 返回值:
return {
'page': page,
'size': size,
'pages': pages,
'total': total,
# 注释掉 URL 字段
# 'current': current_url,
# 'next': next_url,
# ...
}
📚 参考资料
📅 更新日志
- v1.0.0 (2024-10-14)
- 初始版本
- 实现
success(),fail(),page()函数 - 实现
pagination_builder()分页构建器 - 支持智能识别 SQLAlchemy Pagination 对象
- 自动生成分页 URL
- 参数重命名:
per_page→size
编写人员: AI Assistant
最后更新: 2024-10-14
版本: 1.0.0