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.

269 lines
7.9 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
HTTP 响应包装工具
提供统一的 API 响应格式包装函数和分页工具
"""
from typing import Any, Optional, Union
from flask import request
from urllib.parse import urlencode
import math
# ==================== 包装函数 ====================
def success(data: Any = None, message: str = "成功", code: int = 200) -> dict:
"""
成功响应包装
Args:
data: 返回数据
message: 提示信息(默认 "成功"
code: 业务状态码(默认 200
Returns:
符合 BaseResponse 格式的字典
Example:
>>> return success({'id': 1, 'name': 'test'})
{'data': {'id': 1, 'name': 'test'}, 'code': 200, 'message': '成功'}
>>> return success([1, 2, 3], message='查询成功')
{'data': [1, 2, 3], 'code': 200, 'message': '查询成功'}
"""
return {"data": data, "code": code, "message": message}
def fail(message: str = "操作失败", code: int = 500, data: Any = None) -> dict:
"""
失败响应包装HTTP 状态码保持 200由前端根据 code 判断)
Args:
message: 错误信息
code: 业务错误码(默认 500
data: 额外数据(如验证错误详情)
Returns:
符合 BaseResponse 格式的字典
Example:
>>> return fail('参数错误', code=400)
{'data': None, 'code': 400, 'message': '参数错误'}
>>> return fail('未找到资源', code=404)
{'data': None, 'code': 404, 'message': '未找到资源'}
>>> return fail('验证失败', code=422, data={'username': ['必填项']})
{'data': {'username': ['必填项']}, 'code': 422, 'message': '验证失败'}
"""
return {"data": data, "code": code, "message": message}
def page(
items: Union[list, Any],
pagination: Union[dict, Any, None] = None,
message: str = "成功",
code: int = 200,
) -> dict:
"""
分页响应包装(智能识别多种输入方式)
支持三种调用方式:
1. 传入 SQLAlchemy Pagination 对象(自动解析)
>>> db_pagination = User.query.paginate(page=1, per_page=10)
>>> return page(db_pagination)
2. 传入数据列表 + 分页信息字典
>>> users = [{'id': 1}, {'id': 2}]
>>> pagination_info = pagination_builder(None, page=1, size=10, total=100)
>>> return page(users, pagination_info)
3. 传入数据列表 + SQLAlchemy Pagination 对象
>>> db_pagination = User.query.paginate(page=1, per_page=10)
>>> users = UserSchema(many=True).dump(db_pagination.items)
>>> return page(users, db_pagination)
Args:
items: 数据列表 或 SQLAlchemy Pagination 对象
pagination: 分页信息字典 或 SQLAlchemy Pagination 对象 或 None
message: 提示信息
code: 状态码
Returns:
符合 BaseResponse 格式的分页数据
Example:
{
'data': {
'items': [...],
'page': {
'page': 1,
'size': 10,
'pages': 10,
'total': 100,
'current': '...',
'next': '...',
'prev': None,
'first': '...',
'last': '...'
}
},
'code': 200,
'message': '成功'
}
"""
# 方式1: items 是 SQLAlchemy Pagination 对象
if pagination is None and hasattr(items, "items") and hasattr(items, "total"):
pagination_obj = items
items = pagination_obj.items
pagination = pagination_builder(pagination_obj)
# 方式2: pagination 是 SQLAlchemy Pagination 对象
elif (
pagination is not None
and hasattr(pagination, "items")
and hasattr(pagination, "total")
):
pagination_obj = pagination
pagination = pagination_builder(pagination_obj)
# 方式3: pagination 已经是字典,直接使用
# items 已经是列表,直接使用
return {
"data": {"items": items, "page": pagination},
"code": code,
"message": message,
}
# ==================== 分页构建器(参考 APIFlask helpers.py====================
def pagination_builder(
pagination: Any,
*,
page: Optional[int] = None,
size: Optional[int] = None,
total: Optional[int] = None,
pages: Optional[int] = None,
) -> dict:
"""
构建分页信息(参考 APIFlask 实现)
支持两种调用方式:
1. 传入 SQLAlchemy Pagination 对象(自动解析)
>>> db_pagination = User.query.paginate(page=1, per_page=10)
>>> pagination_info = pagination_builder(db_pagination)
2. 传入自定义参数(手动构建)
>>> pagination_info = pagination_builder(
... None,
... page=1,
... size=10,
... total=100
... )
Args:
pagination: SQLAlchemy Pagination 对象 或 None
page: 当前页码(手动模式)
size: 每页数量(手动模式)
total: 总记录数(手动模式)
pages: 总页数(可选,自动计算)
Returns:
符合 PaginationSchema 的字典
Example:
# 自动模式
>>> db_pagination = User.query.paginate(page=1, per_page=10)
>>> info = pagination_builder(db_pagination)
{'page': 1, 'size': 10, 'pages': 10, 'total': 100, ...}
# 手动模式
>>> info = pagination_builder(None, page=1, size=20, total=200)
{'page': 1, 'size': 20, 'pages': 10, 'total': 200, ...}
"""
# 自动模式:从 SQLAlchemy Pagination 对象提取
if pagination is not None and hasattr(pagination, "page"):
page = pagination.page
size = pagination.per_page
pages = pagination.pages
total = pagination.total
has_prev = pagination.has_prev
has_next = pagination.has_next
prev_num = pagination.prev_num if has_prev else None
next_num = pagination.next_num if has_next else None
# 手动模式:使用传入的参数
else:
page = page or 1
size = size or 10
total = total or 0
# 自动计算总页数
if pages is None:
pages = math.ceil(total / size) if size > 0 else 0
# 计算上下页
has_prev = page > 1
has_next = page < pages
prev_num = page - 1 if has_prev else None
next_num = page + 1 if has_next else None
# 生成分页 URL参考 APIFlask 实现)
current_url = _generate_page_url(page, size)
next_url = _generate_page_url(next_num, size) if has_next else None
prev_url = _generate_page_url(prev_num, size) if has_prev else None
first_url = _generate_page_url(1, size)
last_url = _generate_page_url(pages, size) if pages > 0 else None
return {
"page": page,
"size": size, # ✅ 使用 size 代替 per_page
"pages": pages,
"total": total,
"current": current_url,
"next": next_url,
"prev": prev_url,
"first": first_url,
"last": last_url,
}
def _generate_page_url(page_num: Optional[int], page_size: int) -> Optional[str]:
"""
生成分页 URL内部辅助函数
参考 APIFlask 的 URL 生成逻辑
Args:
page_num: 页码
page_size: 每页数量
Returns:
完整的分页 URL 或 None
"""
if page_num is None:
return None
try:
# 获取当前请求的 URL 和查询参数
base_url = request.base_url
args = request.args.copy()
# 更新分页参数
args["page"] = page_num
args["size"] = page_size # ✅ 使用 size 参数名
# 构建完整 URL
if args:
return f"{base_url}?{urlencode(args)}"
return base_url
except RuntimeError:
# 在请求上下文之外调用时返回 None
return None