# HTTP 响应包装工具使用指南 ## 📖 概述 本工具库提供统一的 API 响应格式包装函数和分页工具,基于 APIFlask 框架设计,简化 API 开发流程。 **核心特性:** - ✅ 统一返回格式:`{data, code, message}` - ✅ 智能分页支持:自动识别 SQLAlchemy Pagination 对象 - ✅ 灵活调用方式:支持多种参数传递方式 - ✅ 完整类型提示:TypeScript 级别的类型安全 - ✅ 参考 APIFlask 标准:与框架保持一致 --- ## 📦 安装位置 ``` src/applications/common/utils/http.py ``` --- ## 🎯 核心函数 ### 1. `success()` - 成功响应 **签名:** ```python def success(data: Any = None, message: str = '成功', code: int = 200) -> dict ``` **参数:** - `data`:返回数据(任意类型) - `message`:提示信息(默认 `'成功'`) - `code`:业务状态码(默认 `200`) **返回格式:** ```json { "data": <返回数据>, "code": 200, "message": "成功" } ``` **示例:** ```python from iti.applications.common.utils import success from iti.applications.extensions.http import BaseResponse @app.get('/api/users/') @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()` - 失败响应 **签名:** ```python def fail(message: str = '操作失败', code: int = 500, data: Any = None) -> dict ``` **参数:** - `message`:错误信息 - `code`:业务错误码(默认 `500`) - `data`:额外数据(如验证错误详情) **特点:** - ⚠️ **HTTP 状态码保持 200**,由前端根据 `code` 字段判断业务状态 - 💡 适用于统一错误处理,避免 HTTP 层面的错误拦截 **返回格式:** ```json { "data": null, "code": 500, "message": "操作失败" } ``` **示例:** ```python 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()` - 分页响应 **签名:** ```python def page( items: Union[list, Any], pagination: Union[dict, Any, None] = None, message: str = '成功', code: int = 200 ) -> dict ``` **支持三种调用方式:** #### **方式 1:传入 SQLAlchemy Pagination 对象(最简)** ```python 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='获取用户列表成功') ``` **返回格式:** ```json { "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 对象** 适用于需要先序列化数据的场景: ```python 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): ```python 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()` - 分页信息构建器 **签名:** ```python 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 对象 或 `None` - `page`:当前页码(手动模式) - `size`:每页数量(手动模式) - `total`:总记录数(手动模式) - `pages`:总页数(可选,自动计算) **注意:** - 第一个参数后使用 `*`,强制后续参数必须使用关键字传参 - 参考 APIFlask 的 `helpers.py` 实现 **示例 1:自动模式(SQLAlchemy Pagination)** ```python 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:手动模式(自定义数据源)** ```python 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 ```python 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 ```python class PageDataSchema(Schema): """分页数据包装 Schema""" items = List(Nested(Field())) # 数据列表 page = Nested(PaginationSchema) # 分页信息(字段名为 page) ``` **使用示例:** ```python from iti.applications.common.utils import PageDataSchema # 在路由中使用(可选,主要用于文档生成) @app.get('/api/users') @app.doc(responses={200: PageDataSchema}) def get_users(): ... ``` --- ## 🎨 完整示例 ### 示例 1:用户管理 CRUD ```python 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/') @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/') @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/') @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:复杂查询与筛选 ```python 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 分页) ```python 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` 中定义: ```python 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 自动生成逻辑: 1. **获取当前请求的 `base_url`**:`http://api.example.com/api/users` 2. **复制查询参数**:`request.args.copy()` 3. **更新分页参数**: - `page`: 页码 - `size`: 每页数量(注意:使用 `size` 而非 `per_page`) 4. **构建完整 URL**:`base_url + ? + urlencode(args)` **示例:** 请求 `/api/users?category=admin&page=2&size=10` 时生成的 URL: ```json { "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. 统一错误码规范 建议定义错误码常量: ```python # 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 ```python from iti.applications.schemas.user import UserSchema from iti.applications.common.utils import success @app.get('/api/users/') @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. 分页参数验证 ```python 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. 异常统一处理 ```python 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 生成 | 手动 | 自动生成 ✅ | --- ## 🧪 测试 ### 运行测试 ```bash # 运行 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` 中添加测试: ```python 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 错误状态码,可以手动返回元组: ```python return fail('未找到', code=404), 404 # HTTP 404 ``` --- ### Q2: 如何自定义分页 URL 生成逻辑? **A:** 修改 `_generate_page_url()` 函数: ```python 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()` 返回值: ```python return { 'page': page, 'size': size, 'pages': pages, 'total': total, # 注释掉 URL 字段 # 'current': current_url, # 'next': next_url, # ... } ``` --- ## 📚 参考资料 - [APIFlask 官方文档](https://apiflask.com/) - [APIFlask helpers.py 源码](https://github.com/apiflask/apiflask/blob/main/src/apiflask/helpers.py) - [Flask-SQLAlchemy Pagination](https://flask-sqlalchemy.palletsprojects.com/en/3.0.x/pagination/) --- ## 📅 更新日志 - **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