签入版本
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# 8.3 分页查询规范
|
||||
|
||||
:::tip 概述
|
||||
所有分页接口遵循统一的参数命名和返回结构。分页由 `BaseController::_index()` → `BaseLogic::pageList()` 实现,前端只需传标准参数即可获得分页数据。
|
||||
所有分页接口遵循统一的参数命名和返回结构。分页由 `Controller::_page()` → `Service::pageList()` 实现,前端只需传标准参数即可获得分页数据。
|
||||
:::
|
||||
|
||||
## 请求参数
|
||||
@@ -13,14 +13,16 @@
|
||||
| `orderField` | string | - | 排序字段(camelCase,如 `createTime`) |
|
||||
| `orderType` | string | `desc` | 排序方向(`asc` / `desc`) |
|
||||
| `fields` | string | - | 返回字段(逗号分隔,如 `id,username,realname`) |
|
||||
| 其他参数 | - | - | 按 Logic 中配置的 `pageLikeFields` / `pageEqFields` 自动匹配 |
|
||||
| 其他参数 | - | - | 按 Service 中配置的 `pageLikeFields` / `pageEqFields` 自动匹配 |
|
||||
|
||||
### 分页参数边界限制
|
||||
|
||||
| 参数 | 最小值 | 最大值 | 默认值 | 超出处理 |
|
||||
|------|--------|--------|--------|---------|
|
||||
| `pageNo` | 1 | - | 1 | 小于 1 时强制为 1 |
|
||||
| `pageSize` | 1 | 100 | 20 | 超出范围时限制在 1~100 |
|
||||
| `pageSize` | 1 | 100 | 20 | 超出范围时限制在 1~100(防一次拉取过多) |
|
||||
|
||||
(边界由 `Controller::getPageParams()` 统一处理。)
|
||||
|
||||
## 请求示例
|
||||
|
||||
@@ -32,13 +34,13 @@ GET /api/example/page?pageNo=1&pageSize=10
|
||||
GET /api/example/page?pageNo=1&pageSize=10&name=测试
|
||||
|
||||
# 带精确过滤(=)
|
||||
GET /api/example/page?pageNo=1&pageSize=10&status=1&type=2
|
||||
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=sort&orderType=asc
|
||||
GET /api/example/page?pageNo=1&pageSize=10&orderField=createTime&orderType=desc
|
||||
|
||||
# 指定返回字段
|
||||
GET /api/example/page?pageNo=1&pageSize=10&fields=id,name,status
|
||||
@@ -56,12 +58,12 @@ GET /api/user/page?pageNo=1&pageSize=20&username=admin&status=1&deptId=3&orderFi
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"records": [
|
||||
{ "id": 1, "name": "测试", "status": 1, "statusText": "启用", "createTime": "2026-01-01 00:00:00" },
|
||||
{ "id": 2, "name": "示例", "status": 0, "statusText": "禁用", "createTime": "2026-01-02 00:00:00" }
|
||||
{ "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,
|
||||
"size": 10,
|
||||
"current": 1,
|
||||
"size": 10,
|
||||
"pages": 10
|
||||
}
|
||||
}
|
||||
@@ -73,18 +75,18 @@ GET /api/user/page?pageNo=1&pageSize=20&username=admin&status=1&deptId=3&orderFi
|
||||
|------|------|------|
|
||||
| `records` | array | 当前页数据列表 |
|
||||
| `total` | number | 总记录数 |
|
||||
| `size` | number | 每页条数 |
|
||||
| `current` | number | 当前页码 |
|
||||
| `pages` | number | 总页数(`ceil(total / size)`) |
|
||||
| `size` | number | 每页条数 |
|
||||
| `pages` | number | 总页数 |
|
||||
|
||||
## 查询条件匹配
|
||||
|
||||
### pageLikeFields — 模糊匹配(LIKE)
|
||||
|
||||
在 Logic 中配置的字段,前端传参时自动使用 `LIKE '%值%'` 查询:
|
||||
在 Service 中配置的字段,前端传参时自动使用 `LIKE '%值%'` 查询:
|
||||
|
||||
```php
|
||||
class ExampleLogic extends BaseLogic
|
||||
class ExampleService extends BaseService
|
||||
{
|
||||
protected array $pageLikeFields = ['name', 'title'];
|
||||
}
|
||||
@@ -98,10 +100,10 @@ GET /api/example/page?name=测试
|
||||
|
||||
### pageEqFields — 精确匹配(=)
|
||||
|
||||
在 Logic 中配置的字段,前端传参时自动使用 `= 值` 查询:
|
||||
在 Service 中配置的字段,前端传参时自动使用 `= 值` 查询:
|
||||
|
||||
```php
|
||||
class ExampleLogic extends BaseLogic
|
||||
class ExampleService extends BaseService
|
||||
{
|
||||
protected array $pageEqFields = ['status', 'type', 'category_id'];
|
||||
}
|
||||
@@ -119,15 +121,18 @@ GET /api/example/page?status=1&type=2
|
||||
# 模糊 + 精确 + 排序 + 分页
|
||||
GET /api/example/page?pageNo=1&pageSize=20&name=测试&status=1&orderField=sort&orderType=asc
|
||||
|
||||
# 生成 SQL:
|
||||
# SELECT * FROM think_example
|
||||
# 生成 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 → 精确匹配 → 数据权限 → 租户隔离。
|
||||
|
||||
## 排序
|
||||
|
||||
### 前端传参排序
|
||||
@@ -140,31 +145,24 @@ GET /api/example/page?orderField=createTime&orderType=desc
|
||||
GET /api/example/page?orderField=sort&orderType=asc
|
||||
```
|
||||
|
||||
### Logic 默认排序
|
||||
### Service 默认排序
|
||||
|
||||
```php
|
||||
class ExampleLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 单字段排序
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
|
||||
// 单字段排序
|
||||
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
|
||||
|
||||
/**
|
||||
* 多字段排序
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = [
|
||||
['field' => 'sort', 'type' => 'asc'],
|
||||
['field' => 'id', 'type' => 'desc'],
|
||||
];
|
||||
}
|
||||
// 多字段排序
|
||||
protected array $pageOrderBy = [
|
||||
['field' => 'sort', 'type' => 'asc'],
|
||||
['field' => 'id', 'type' => 'desc'],
|
||||
];
|
||||
```
|
||||
|
||||
**优先级:** 前端传参 > Logic 默认配置
|
||||
**优先级:** 前端传参 > Service 默认配置。
|
||||
|
||||
:::warning 排序白名单
|
||||
前端 `orderField` 会先做 camel→snake 转换,再与 **`pageOrderBy` 默认字段 ∪ `orderFieldAllow`** 的并集比对——不在白名单内的字段**直接忽略**(防 SQL 注入)。需要开放更多排序字段请配置 `orderFieldAllow`。见 [3.5 Service 层开发](/zh/guide/logic.html)。
|
||||
:::
|
||||
|
||||
## 字段过滤
|
||||
|
||||
@@ -183,16 +181,16 @@ GET /api/example/page?fields=id,name,status
|
||||
{
|
||||
"records": [
|
||||
{ "id": 1, "name": "测试", "status": 1 },
|
||||
{ "id": 2, "name": "示例", "status": 0 }
|
||||
{ "id": 2, "name": "示例", "status": 2 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**注意:** `id` 字段始终保留,即使未在 `fields` 中指定。
|
||||
(由 `BaseService::filterFields()` 处理,`id` 字段始终保留。)
|
||||
|
||||
## 参数转换
|
||||
|
||||
前端使用 camelCase,后端自动转换为 snake_case:
|
||||
前端使用 camelCase,后端自动转换为 snake_case(`convertQueryParams`):
|
||||
|
||||
| 前端参数 | 转换后 | 说明 |
|
||||
|---------|--------|------|
|
||||
@@ -200,11 +198,12 @@ GET /api/example/page?fields=id,name,status
|
||||
| `pageSize` | `pageSize` | 不转换(排除项) |
|
||||
| `orderField` | `orderField` | 不转换(排除项) |
|
||||
| `orderType` | `orderType` | 不转换(排除项) |
|
||||
| `fields` | `fields` | 不转换(排除项) |
|
||||
| `categoryId` | `category_id` | 自动转换 |
|
||||
| `createTime` | `create_time` | 自动转换 |
|
||||
| `userName` | `user_name` | 自动转换 |
|
||||
|
||||
(排除项由 `config/api.php` 的 `convert_except_params` 配置,默认四项。)
|
||||
|
||||
### 排序字段值也自动转换
|
||||
|
||||
```bash
|
||||
@@ -217,23 +216,14 @@ ORDER BY create_time DESC
|
||||
|
||||
## Controller 层实现
|
||||
|
||||
### 使用 _index 快捷方法(推荐)
|
||||
### 使用 _page 快捷方法(推荐)
|
||||
|
||||
```php
|
||||
class ExampleController extends BaseController
|
||||
#[OperationLog('示例管理-查询分页', OperationLog::TYPE_QUERY)]
|
||||
#[Permission('sys:example:page', '示例分页')]
|
||||
public function page(): JsonResponse
|
||||
{
|
||||
protected ExampleLogic $logic;
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->logic = new ExampleLogic();
|
||||
}
|
||||
|
||||
#[Permission('sys:example:page', '案例分页')]
|
||||
public function page(): Json
|
||||
{
|
||||
return parent::_index($this->logic);
|
||||
}
|
||||
return $this->_page($this->exampleService);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -241,10 +231,10 @@ class ExampleController extends BaseController
|
||||
|
||||
```php
|
||||
#[Permission('sys:user:page', '用户分页')]
|
||||
public function page(): Json
|
||||
public function page(): JsonResponse
|
||||
{
|
||||
// 额外条件:只查启用状态的用户
|
||||
return parent::_index($this->logic, ['status' => 1]);
|
||||
// 额外固定条件:只查启用状态的用户
|
||||
return $this->_page($this->userService, ['status' => 1]);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -252,51 +242,25 @@ public function page(): Json
|
||||
|
||||
```php
|
||||
#[Permission('sys:article:page', '文章分页')]
|
||||
public function page(): Json
|
||||
public function page(): JsonResponse
|
||||
{
|
||||
// 预加载分类关联
|
||||
return parent::_index($this->logic, [], ['category']);
|
||||
// 预加载分类关联(defaultWith 属性也可全局配置)
|
||||
return $this->_page($this->articleService, [], ['category']);
|
||||
}
|
||||
```
|
||||
|
||||
## Logic 层实现
|
||||
## Service 层配置
|
||||
|
||||
```php
|
||||
class ExampleLogic extends BaseLogic
|
||||
class ExampleService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
use HasCrudOperations, HasQueryScopes, HasSerializeMaps, HasUniqueCheck;
|
||||
|
||||
protected string $modelClass = Example::class;
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name', 'title'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageEqFields = ['status', 'type', 'category_id'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
|
||||
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = ['status' => 'example_status'];
|
||||
}
|
||||
```
|
||||
@@ -306,7 +270,7 @@ class ExampleLogic extends BaseLogic
|
||||
### API 函数
|
||||
|
||||
```typescript
|
||||
// src/api/tool/example.ts
|
||||
// ui/src/api/tool/example.ts
|
||||
export function getExamplePage(params?) {
|
||||
return http.request({ url: '/example/page', method: 'GET', params });
|
||||
}
|
||||
@@ -356,16 +320,22 @@ onMounted(() => fetchData());
|
||||
|
||||
**原因:** 字段名未在 `pageLikeFields` 或 `pageEqFields` 中配置。
|
||||
|
||||
**解决:** 在 Logic 中添加对应字段配置。
|
||||
**解决:** 在 Service 中添加对应字段配置。
|
||||
|
||||
### 问题 2:排序不生效
|
||||
|
||||
**原因:** `orderField` 传了数据库不存在的字段名。
|
||||
**原因:** `orderField` 不在排序白名单(`pageOrderBy` ∪ `orderFieldAllow`)内,或字段名转换后与数据库列不匹配。
|
||||
|
||||
**解决:** 确保 `orderField` 传的是数据库字段名的 camelCase 形式。
|
||||
**解决:** 确认传 camelCase 形式且字段在白名单内;开放新字段配置 `orderFieldAllow`。
|
||||
|
||||
### 问题 3:分页数据重复
|
||||
|
||||
**原因:** 排序字段值相同,导致数据库返回顺序不确定。
|
||||
|
||||
**解决:** 排序字段加上唯一字段(如 id)作为第二排序条件。
|
||||
**解决:** 排序字段加上唯一字段(如 id)作为第二排序条件(多字段 `pageOrderBy`)。
|
||||
|
||||
### 问题 4:pageSize 传 1000 无效
|
||||
|
||||
**原因:** `getPageParams()` 将 pageSize 限制在 1~100(防一次拉取过多数据)。
|
||||
|
||||
**解决:** 如需导出大量数据,使用导出接口(`chunkExport` 分批)而非拉大分页。
|
||||
|
||||
Reference in New Issue
Block a user