Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_NaiveVue/zh/api-spec/pagination.md
T
2026-10-08 17:50:29 +08:00

342 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 8.3 分页查询规范
:::tip 概述
所有分页接口遵循统一的参数命名和返回结构。分页由 `Controller::_page()` → `Service::pageList()` 实现,前端只需传标准参数即可获得分页数据。
:::
## 请求参数
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `pageNo` | number | 1 | 页码(最小为 1) |
| `pageSize` | number | 20 | 每页条数(1~100) |
| `orderField` | string | - | 排序字段(camelCase,如 `createTime`) |
| `orderType` | string | `desc` | 排序方向(`asc` / `desc`) |
| `fields` | string | - | 返回字段(逗号分隔,如 `id,username,realname`) |
| 其他参数 | - | - | 按 Service 中配置的 `pageLikeFields` / `pageEqFields` 自动匹配 |
### 分页参数边界限制
| 参数 | 最小值 | 最大值 | 默认值 | 超出处理 |
|------|--------|--------|--------|---------|
| `pageNo` | 1 | - | 1 | 小于 1 时强制为 1 |
| `pageSize` | 1 | 100 | 20 | 超出范围时限制在 1~100(防一次拉取过多) |
(边界由 `Controller::getPageParams()` 统一处理。)
## 请求示例
```bash
# 基本分页
GET /api/example/page?pageNo=1&pageSize=10
# 带模糊搜索(LIKE)
GET /api/example/page?pageNo=1&pageSize=10&name=测试
# 带精确过滤(=)
GET /api/example/page?pageNo=1&pageSize=10&status=1
# 模糊 + 精确组合
GET /api/example/page?pageNo=1&pageSize=10&name=测试&status=1
# 自定义排序
GET /api/example/page?pageNo=1&pageSize=10&orderField=createTime&orderType=desc
# 指定返回字段
GET /api/example/page?pageNo=1&pageSize=10&fields=id,name,status
# 多条件组合
GET /api/user/page?pageNo=1&pageSize=20&username=admin&status=1&deptId=3&orderField=id&orderType=desc
```
## 返回结构
```json
{
"code": 0,
"ok": true,
"msg": "操作成功",
"data": {
"records": [
{ "id": 1, "name": "测试", "status": 1, "statusText": "在用", "createTime": "2026-01-01 00:00:00" },
{ "id": 2, "name": "示例", "status": 2, "statusText": "停用", "createTime": "2026-01-02 00:00:00" }
],
"total": 100,
"current": 1,
"size": 10,
"pages": 10
}
}
```
### 返回字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `records` | array | 当前页数据列表 |
| `total` | number | 总记录数 |
| `current` | number | 当前页码 |
| `size` | number | 每页条数 |
| `pages` | number | 总页数 |
## 查询条件匹配
### pageLikeFields — 模糊匹配(LIKE)
在 Service 中配置的字段,前端传参时自动使用 `LIKE '%值%'` 查询:
```php
class ExampleService extends BaseService
{
protected array $pageLikeFields = ['name', 'title'];
}
```
```bash
# name 字段自动使用 LIKE 查询
GET /api/example/page?name=测试
# 生成: WHERE name LIKE '%测试%'
```
### pageEqFields — 精确匹配(=)
在 Service 中配置的字段,前端传参时自动使用 `= 值` 查询:
```php
class ExampleService extends BaseService
{
protected array $pageEqFields = ['status', 'type', 'category_id'];
}
```
```bash
# status 和 type 字段自动使用 = 查询
GET /api/example/page?status=1&type=2
# 生成: WHERE status = 1 AND type = 2
```
### 条件组合
```bash
# 模糊 + 精确 + 排序 + 分页
GET /api/example/page?pageNo=1&pageSize=20&name=测试&status=1&orderField=sort&orderType=asc
# 生成 SQL(buildQuery 组装):
# SELECT * FROM laravel_example
# WHERE is_delete = 0
# AND name LIKE '%测试%'
# AND status = 1
# AND tenant_id = 1 ← 租户隔离(启用时)
# ORDER BY sort ASC
# LIMIT 20 OFFSET 0
```
`buildQuery` 叠加顺序(见 [4.7 横切机制](/zh/architecture/cross-cutting.html)):额外固定条件 → LIKE → 精确匹配 → 数据权限 → 租户隔离。
## 排序
### 前端传参排序
```bash
# 按创建时间倒序
GET /api/example/page?orderField=createTime&orderType=desc
# 按排序字段正序
GET /api/example/page?orderField=sort&orderType=asc
```
### Service 默认排序
```php
// 单字段排序
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
// 多字段排序
protected array $pageOrderBy = [
['field' => 'sort', 'type' => 'asc'],
['field' => 'id', 'type' => 'desc'],
];
```
**优先级:** 前端传参 > Service 默认配置。
:::warning 排序白名单
前端 `orderField` 会先做 camel→snake 转换,再与 **`pageOrderBy` 默认字段 ∪ `orderFieldAllow`** 的并集比对——不在白名单内的字段**直接忽略**(防 SQL 注入)。需要开放更多排序字段请配置 `orderFieldAllow`。见 [3.5 Service 层开发](/zh/guide/logic.html)。
:::
## 字段过滤
### fields 参数
前端可通过 `fields` 参数指定只需要的字段,减少数据传输量:
```bash
# 只返回 id、name、status 三个字段
GET /api/example/page?fields=id,name,status
```
**响应:**
```json
{
"records": [
{ "id": 1, "name": "测试", "status": 1 },
{ "id": 2, "name": "示例", "status": 2 }
]
}
```
(由 `BaseService::filterFields()` 处理,`id` 字段始终保留。)
## 参数转换
前端使用 camelCase,后端自动转换为 snake_case(`convertQueryParams`):
| 前端参数 | 转换后 | 说明 |
|---------|--------|------|
| `pageNo` | `pageNo` | 不转换(排除项) |
| `pageSize` | `pageSize` | 不转换(排除项) |
| `orderField` | `orderField` | 不转换(排除项) |
| `orderType` | `orderType` | 不转换(排除项) |
| `categoryId` | `category_id` | 自动转换 |
| `createTime` | `create_time` | 自动转换 |
| `userName` | `user_name` | 自动转换 |
(排除项由 `config/api.php` 的 `convert_except_params` 配置,默认四项。)
### 排序字段值也自动转换
```bash
# 前端传入 camelCase
GET /api/example/page?orderField=createTime
# 后端自动转换为 snake_case
ORDER BY create_time DESC
```
## Controller 层实现
### 使用 _page 快捷方法(推荐)
```php
#[OperationLog('示例管理-查询分页', OperationLog::TYPE_QUERY)]
#[Permission('sys:example:page', '示例分页')]
public function page(): JsonResponse
{
return $this->_page($this->exampleService);
}
```
### 带额外查询条件
```php
#[Permission('sys:user:page', '用户分页')]
public function page(): JsonResponse
{
// 额外固定条件:只查启用状态的用户
return $this->_page($this->userService, ['status' => 1]);
}
```
### 带关联查询
```php
#[Permission('sys:article:page', '文章分页')]
public function page(): JsonResponse
{
// 预加载分类关联(defaultWith 属性也可全局配置)
return $this->_page($this->articleService, [], ['category']);
}
```
## Service 层配置
```php
class ExampleService extends BaseService
{
use HasCrudOperations, HasQueryScopes, HasSerializeMaps, HasUniqueCheck;
protected string $modelClass = Example::class;
protected array $pageLikeFields = ['name', 'title'];
protected array $pageEqFields = ['status', 'type', 'category_id'];
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
protected array $serializeMaps = ['status' => 'example_status'];
}
```
## 前端集成
### API 函数
```typescript
// ui/src/api/tool/example.ts
export function getExamplePage(params?) {
return http.request({ url: '/example/page', method: 'GET', params });
}
```
### 组件使用
```vue
<script setup>
import { getExamplePage } from '@/api/tool/example';
const tableData = ref([]);
const pageNo = ref(1);
const pageSize = ref(20);
const total = ref(0);
const searchForm = ref({ name: '', status: '' });
const fetchData = async () => {
const res = await getExamplePage({
pageNo: pageNo.value,
pageSize: pageSize.value,
...searchForm.value,
});
tableData.value = res.records;
total.value = res.total;
};
// 搜索
const handleSearch = () => {
pageNo.value = 1;
fetchData();
};
// 翻页
const handlePageChange = (page) => {
pageNo.value = page;
fetchData();
};
onMounted(() => fetchData());
</script>
```
## 常见问题
### 问题 1:搜索条件不生效
**原因:** 字段名未在 `pageLikeFields` 或 `pageEqFields` 中配置。
**解决:** 在 Service 中添加对应字段配置。
### 问题 2:排序不生效
**原因:** `orderField` 不在排序白名单(`pageOrderBy` ∪ `orderFieldAllow`)内,或字段名转换后与数据库列不匹配。
**解决:** 确认传 camelCase 形式且字段在白名单内;开放新字段配置 `orderFieldAllow`。
### 问题 3:分页数据重复
**原因:** 排序字段值相同,导致数据库返回顺序不确定。
**解决:** 排序字段加上唯一字段(如 id)作为第二排序条件(多字段 `pageOrderBy`)。
### 问题 4:pageSize 传 1000 无效
**原因:** `getPageParams()` 将 pageSize 限制在 1~100(防一次拉取过多数据)。
**解决:** 如需导出大量数据,使用导出接口(`chunkExport` 分批)而非拉大分页。