分页、过滤与排序标准化
没有标准化的列表 API,每个接口的参数命名各不相同——page、pageNum、current_page……前端苦不堪言。本节建立 TaskFlow 的统一列表接口规范。
分页策略
// Laravel 提供三种分页:
// 1. 页码分页(最常用)
$tasks = Task::paginate(20);
// GET /api/tasks?page=2
// 响应:{ data: [...], links: {...}, meta: { current_page, last_page, total, per_page } }
// 2. 简单分页(只有上一页/下一页,无总数,性能更好)
$tasks = Task::simplePaginate(20);
// 适合:不需要显示总页数的无限滚动场景
// 3. Cursor 分页(最高性能,基于唯一 cursor 而非 OFFSET)
$tasks = Task::cursorPaginate(20);
// GET /api/tasks?cursor=eyJpZCI6...
// 适合:实时数据流、百万级数据集的分页
// 限制:不能跳转到特定页码
统一过滤:SpatieLaravel QueryBuilder
手动写过滤逻辑很繁琐,spatie/laravel-query-builder 提供了声明式的过滤方案:
composer require spatie/laravel-query-builder
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\AllowedSort;
// app/Http/Controllers/Api/V1/TaskController.php
public function index(Request $request)
{
$tasks = QueryBuilder::for(Task::class)
// 允许的过滤字段
->allowedFilters([
AllowedFilter::exact('status'), // ?filter[status]=pending
AllowedFilter::exact('priority'), // ?filter[priority]=5
AllowedFilter::scope('due_before', 'dueBefore'), // ?filter[due_before]=2024-12-31
AllowedFilter::partial('title'), // ?filter[title]=会议(模糊搜索)
AllowedFilter::custom('search', new TaskSearchFilter()),
])
// 允许的排序字段
->allowedSorts([
'created_at', // ?sort=created_at(升序)或 ?sort=-created_at(降序)
'due_date',
'priority',
AllowedSort::field('updated', 'updated_at'), // 别名
])
// 允许的关联预加载(防止客户端随意 include 任何关联)
->allowedIncludes(['user', 'tags', 'project', 'comments'])
// 默认排序
->defaultSort('-created_at')
// 只查询当前用户的任务
->where('user_id', auth()->id())
->paginate($request->input('per_page', 20))
->appends($request->query()); // 把过滤参数带入分页链接
return TaskResource::collection($tasks);
}
# 请求示例:
GET /api/v1/tasks?filter[status]=pending&filter[priority]=5&sort=-due_date&include=user,tags&page=2
# 响应:
{
"data": [
{
"id": 42,
"title": "完成报告",
"status": "pending",
"priority": 5,
"user": { "id": 1, "name": "Admin" },
"tags": [...]
}
],
"links": {
"first": "http://taskflow.app/api/v1/tasks?page=1",
"last": "http://taskflow.app/api/v1/tasks?page=8",
"prev": "http://taskflow.app/api/v1/tasks?page=1",
"next": "http://taskflow.app/api/v1/tasks?page=3"
},
"meta": {
"current_page": 2,
"from": 21,
"last_page": 8,
"per_page": 20,
"to": 40,
"total": 150
}
}
自定义过滤器(搜索)
// app/Filters/TaskSearchFilter.php
use Spatie\QueryBuilder\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
class TaskSearchFilter implements Filter
{
public function __invoke(Builder $query, $value, string $property): void
{
$query->where(function ($q) use ($value) {
$q->where('title', 'ilike', "%{$value}%") // PostgreSQL 不区分大小写
->orWhere('description', 'ilike', "%{$value}%");
});
}
}
// 使用
->allowedFilters([
AllowedFilter::custom('search', new TaskSearchFilter()), // ?filter[search]=报告
])
不用 QueryBuilder 的手动过滤
// 如果不想引入额外包,手动写也很简洁(用 when())
public function index(Request $request)
{
$request->validate([
'status' => ['nullable', 'in:pending,in_progress,done,cancelled'],
'priority' => ['nullable', 'integer', 'between:1,5'],
'sort' => ['nullable', 'in:created_at,-created_at,due_date,-due_date'],
'per_page' => ['nullable', 'integer', 'between:1,100'],
]);
$tasks = Task::query()
->where('user_id', auth()->id())
->when($request->status, fn($q, $s) => $q->where('status', $s))
->when($request->priority, fn($q, $p) => $q->where('priority', $p))
->when($request->search, fn($q, $s) => $q->where('title', 'ilike', "%{$s}%"))
->when($request->due_before, fn($q, $d) => $q->where('due_date', '<=', $d))
->when($request->sort, function ($query, $sort) {
$direction = str_starts_with($sort, '-') ? 'desc' : 'asc';
$column = ltrim($sort, '-');
$query->orderBy($column, $direction);
}, fn($q) => $q->latest())
->with(['user:id,name', 'tags:id,name'])
->withCount('comments')
->paginate($request->input('per_page', 20));
return TaskResource::collection($tasks);
}
列表接口规范文档模板
GET /api/v1/tasks
认证:Bearer Token(Required)
查询参数:
filter[status] string 过滤状态(pending/in_progress/done)
filter[priority] integer 过滤优先级(1-5)
filter[search] string 标题/描述模糊搜索
sort string 排序字段(前缀 - 为降序)
可选值:created_at, -created_at, due_date, -due_date, priority
include string 逗号分隔的关联(user,tags,project)
page integer 页码(默认 1)
per_page integer 每页数量(默认 20,最大 100)
响应 200:
data[].id integer
data[].title string
data[].status string
data[].priority integer
data[].due_date string|null (YYYY-MM-DD)
data[].created_at string (ISO 8601)
meta.total integer 总记录数
meta.per_page integer 每页数量
meta.current_page integer 当前页
下一章:测试:Feature Test、Unit Test 与 Mock——API 设计规范了,现在用测试保证它的行为正确。Pest 让测试代码像描述文档一样可读——这是 Laravel 社区现在最推荐的测试框架。