签入版本
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# 3.10 完整代码清单
|
||||
|
||||
:::tip 概述
|
||||
本节汇总 example(案例演示)模块涉及的所有文件,方便开发时快速定位。
|
||||
:::
|
||||
|
||||
## 后端文件清单
|
||||
|
||||
| 文件路径 | 说明 | 核心内容 |
|
||||
|----------|------|----------|
|
||||
| `app/model/Example.php` | 数据模型 | 继承 BaseModel,声明表名 `example` |
|
||||
| `app/validate/ExampleValidate.php` | 验证器 | name 字段必填,add/update 场景 |
|
||||
| `app/logic/ExampleLogic.php` | 业务逻辑 | 属性配置 + import/export 方法 |
|
||||
| `app/controller/ExampleController.php` | 控制器 | 9 个接口(page/list/detail/add/update/delete/batchDelete/import/export) |
|
||||
| `route/app.php` | 路由 | 第 371~383 行,9 条路由 |
|
||||
|
||||
## 前端文件清单
|
||||
|
||||
| 文件路径 | 说明 |
|
||||
|----------|------|
|
||||
| `ui/src/api/tool/example.ts` | API 接口模块(9 个函数) |
|
||||
| `ui/src/views/tool/example/index.vue` | 列表页 |
|
||||
| `ui/src/views/tool/example/edit.vue` | 新增/编辑弹窗 |
|
||||
| `ui/src/views/tool/example/detail.vue` | 详情弹窗 |
|
||||
| `ui/src/views/tool/example/columns.ts` | 表格列配置 |
|
||||
| `ui/src/views/tool/example/querySchemas.ts` | 查询条件配置 |
|
||||
|
||||
## 数据库
|
||||
|
||||
| 对象 | 说明 |
|
||||
|------|------|
|
||||
| `think_example` 表 | 案例演示数据表 |
|
||||
|
||||
## 权限节点
|
||||
|
||||
| 权限码 | 说明 |
|
||||
|--------|------|
|
||||
| `sys:example:page` | 案例演示分页列表 |
|
||||
| `sys:example:add` | 添加案例演示 |
|
||||
| `sys:example:update` | 修改案例演示 |
|
||||
| `sys:example:delete` | 删除案例演示 |
|
||||
| `sys:example:import` | 导入案例演示 |
|
||||
| `sys:example:export` | 导出案例演示 |
|
||||
|
||||
## 字典数据
|
||||
|
||||
| 字典编码 | 说明 | 字典项 |
|
||||
|---------|------|--------|
|
||||
| `example_type` | 案例类型 | 按实际配置 |
|
||||
| `example_status` | 案例状态 | 0=禁用, 1=启用 |
|
||||
|
||||
## 代码生成器替代方案
|
||||
|
||||
example 模块本身就是代码生成器的参考模板。如果需要生成类似模块:
|
||||
|
||||
1. 建好数据库表
|
||||
2. 访问代码生成器页面
|
||||
3. 选择表,点击生成
|
||||
4. 自动创建 Controller / Logic / Model / Validate / 前端文件
|
||||
@@ -0,0 +1,305 @@
|
||||
# 3.6 控制器开发
|
||||
|
||||
:::tip 概述
|
||||
控制器是 HTTP 请求的入口,负责参数获取、调用 Logic 层、返回 JSON 响应。通过 `#[Permission]` 和 `#[Log]` 注解实现权限校验和操作日志。
|
||||
:::
|
||||
|
||||
## 控制器文件位置
|
||||
|
||||
```text
|
||||
app/controller/ExampleController.php
|
||||
```
|
||||
|
||||
## 完整代码(来自项目实际代码)
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\controller;
|
||||
|
||||
use app\BaseController; // 控制器基类
|
||||
use app\logic\ExampleLogic; // 案例演示业务逻辑
|
||||
use app\attribute\Log; // 操作日志注解
|
||||
use app\attribute\Permission; // 权限校验注解
|
||||
use think\response\Json; // JSON 响应类型
|
||||
|
||||
/**
|
||||
* 案例演示管理控制器
|
||||
*
|
||||
* 负责案例演示相关的 HTTP 接口入口,包括:
|
||||
* - 分页列表、列表查询、详情查询;
|
||||
* - 新增、修改、删除、批量删除;
|
||||
* - 导入、导出。
|
||||
*
|
||||
* 具体业务逻辑委托给 ExampleLogic 处理,
|
||||
* 并通过注解统一实现日志记录与权限校验。
|
||||
*/
|
||||
class ExampleController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 案例演示业务逻辑层
|
||||
*
|
||||
* @var ExampleLogic
|
||||
*/
|
||||
protected ExampleLogic $logic;
|
||||
|
||||
/**
|
||||
* 控制器初始化
|
||||
*
|
||||
* 在每次请求进入控制器方法前执行,用于实例化业务逻辑层。
|
||||
* ThinkPHP 会自动调用此方法(在构造函数之后)。
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->logic = new ExampleLogic();
|
||||
}
|
||||
|
||||
// ... 以下为各接口方法
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 控制器模式
|
||||
每个控制器遵循固定模式:
|
||||
1. `initialize()` 中实例化 Logic
|
||||
2. 每个方法标注 `#[Log]` 和 `#[Permission]` 注解
|
||||
3. 简单调用 `parent::_index()` / `_detail()` / `_add()` 等通用方法
|
||||
4. 复杂操作(如导入导出)自行处理并 try-catch
|
||||
:::
|
||||
|
||||
```php
|
||||
/**
|
||||
* 案例演示分页列表
|
||||
*
|
||||
* GET /example/page
|
||||
* 对应权限:sys:example:page
|
||||
*
|
||||
* 调用 BaseController::_index(),内部流程:
|
||||
* 1. 合并分页参数(pageNo/pageSize)和普通参数
|
||||
* 2. 调用 ExampleLogic::pageList()
|
||||
* 3. 返回标准分页响应
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
#[Log('案例演示-查询分页记录', Log::TYPE_QUERY)]
|
||||
#[Permission('sys:example:page', '案例演示分页')]
|
||||
public function page(): Json
|
||||
{
|
||||
return parent::_index($this->logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 案例演示列表
|
||||
*
|
||||
* GET /example/list
|
||||
* 返回全量数据(不分页),适用于下拉选择等场景。
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
#[Log('案例演示-查询列表', Log::TYPE_QUERY)]
|
||||
public function list(): Json
|
||||
{
|
||||
return parent::_list($this->logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 案例演示详情
|
||||
*
|
||||
* GET /example/detail/{id}
|
||||
*
|
||||
* 调用 BaseController::_detail(),内部流程:
|
||||
* 1. 调用 ExampleLogic::detail($id)
|
||||
* 2. 查询为空返回 404
|
||||
* 3. 自动补全文件字段域名、枚举显示名
|
||||
* 4. 触发 afterDetail 钩子
|
||||
*
|
||||
* @param int $id 案例演示ID
|
||||
* @return Json
|
||||
*/
|
||||
#[Log('案例演示-查询详情', Log::TYPE_QUERY, '查询案例演示ID:{id}')]
|
||||
public function detail(int $id): Json
|
||||
{
|
||||
return parent::_detail($this->logic, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加案例演示
|
||||
*
|
||||
* POST /example/add
|
||||
* 对应权限:sys:example:add
|
||||
*
|
||||
* 调用 BaseController::_add(),内部流程:
|
||||
* 1. 调用 ExampleLogic::add($data)
|
||||
* 2. 触发 beforeAdd/afterAdd 钩子
|
||||
* 3. 返回新记录 ID
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
#[Log('案例演示-新增记录', Log::TYPE_ADD, '新增案例演示:{name}')]
|
||||
#[Permission('sys:example:add', '添加案例演示')]
|
||||
public function add(): Json
|
||||
{
|
||||
// getJsonBody() 获取前端 POST 提交的 JSON 数据
|
||||
return parent::_add($this->logic, $this->getJsonBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新案例演示
|
||||
*
|
||||
* PUT /example/update
|
||||
* 对应权限:sys:example:update
|
||||
*
|
||||
* 从 JSON 请求体中读取 id,若缺失则直接返回失败。
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
#[Log('案例演示-修改记录', Log::TYPE_UPDATE, '修改案例演示ID:{id}')]
|
||||
#[Permission('sys:example:update', '修改案例演示')]
|
||||
public function update(): Json
|
||||
{
|
||||
$data = $this->getJsonBody();
|
||||
$id = $data['id'] ?? 0;
|
||||
|
||||
// 校验 ID 参数
|
||||
if (empty($id)) {
|
||||
return $this->fail('缺少id参数');
|
||||
}
|
||||
|
||||
return parent::_edit($this->logic, $id, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除案例演示
|
||||
*
|
||||
* DELETE /example/delete/{id}
|
||||
* 对应权限:sys:example:delete
|
||||
*
|
||||
* 调用 BaseController::_remove(),内部流程:
|
||||
* 1. 调用 ExampleLogic::delete($id)
|
||||
* 2. 触发 beforeDelete/afterDelete 钩子
|
||||
* 3. 设置 is_delete=1(软删除)
|
||||
*
|
||||
* @param int $id 案例演示ID
|
||||
* @return Json
|
||||
*/
|
||||
#[Log('案例演示-删除记录', Log::TYPE_DELETE, '删除案例演示ID:{id}')]
|
||||
#[Permission('sys:example:delete', '删除案例演示')]
|
||||
public function delete(int $id): Json
|
||||
{
|
||||
return parent::_remove($this->logic, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除案例演示
|
||||
*
|
||||
* DELETE /example/batchDelete
|
||||
* 对应权限:sys:example:delete
|
||||
*
|
||||
* 从 JSON 请求体中读取 ids 数组。
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
#[Log('案例演示-批量删除记录', Log::TYPE_DELETE)]
|
||||
#[Permission('sys:example:delete', '删除案例演示')]
|
||||
public function batchDelete(): Json
|
||||
{
|
||||
$ids = $this->getJsonBody()['ids'] ?? [];
|
||||
return parent::_batchRemove($this->logic, $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入案例演示
|
||||
*
|
||||
* POST /example/import
|
||||
* 对应权限:sys:example:import
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 从请求中获取上传文件
|
||||
* 2. 校验文件扩展名(仅 xls、xlsx)
|
||||
* 3. 调用 ExampleLogic::import() 解析并导入
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
#[Log('案例演示-导入数据', Log::TYPE_IMPORT)]
|
||||
#[Permission('sys:example:import', '导入案例演示')]
|
||||
public function import(): Json
|
||||
{
|
||||
$file = $this->request->file('file');
|
||||
if (!$file) {
|
||||
return $this->fail('请选择文件');
|
||||
}
|
||||
|
||||
$ext = strtolower($file->getOriginalExtension());
|
||||
if (!in_array($ext, ['xls', 'xlsx'])) {
|
||||
return $this->fail('仅支持 xls、xlsx 格式的文件');
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->logic->import($file->getRealPath());
|
||||
|
||||
$msg = empty($result['errors'])
|
||||
? "成功导入 {$result['count']} 条数据"
|
||||
: "成功导入 {$result['count']} 条,失败 " . count($result['errors']) . " 条";
|
||||
|
||||
return $this->success($result, $msg);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail('导入失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出案例演示
|
||||
*
|
||||
* GET /example/export
|
||||
* 对应权限:sys:example:export
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
#[Log('案例演示-导出数据', Log::TYPE_EXPORT)]
|
||||
#[Permission('sys:example:export', '导出案例演示')]
|
||||
public function export(): Json
|
||||
{
|
||||
try {
|
||||
$filePath = $this->logic->export($this->getParams());
|
||||
return $this->success($filePath, '导出成功');
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail('导出失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注解说明
|
||||
|
||||
### #[Log] 操作日志注解
|
||||
|
||||
```php
|
||||
#[Log('案例演示-新增记录', Log::TYPE_ADD, '新增案例演示:{name}')]
|
||||
// │ │ │
|
||||
// │ │ └── 描述(支持 {param} 占位符)
|
||||
// │ └── 操作类型
|
||||
// └── 操作标题
|
||||
```
|
||||
|
||||
### #[Permission] 权限注解
|
||||
|
||||
```php
|
||||
#[Permission('sys:example:add', '添加案例演示')]
|
||||
// │ │
|
||||
// │ └── 权限名称
|
||||
// └── 权限编码
|
||||
```
|
||||
|
||||
## BaseController 通用方法
|
||||
|
||||
| 方法 | 用途 | 内部调用 |
|
||||
|------|------|----------|
|
||||
| `_index($logic)` | 分页查询 | `$logic->pageList()` |
|
||||
| `_list($logic)` | 全量列表 | `$logic->allList()` |
|
||||
| `_detail($logic, $id)` | 详情查询 | `$logic->detail($id)` |
|
||||
| `_add($logic, $data)` | 新增 | `$logic->add($data)` |
|
||||
| `_edit($logic, $id, $data)` | 修改 | `$logic->update($id, $data)` |
|
||||
| `_remove($logic, $id)` | 删除 | `$logic->delete($id)` |
|
||||
| `_batchRemove($logic, $ids)` | 批量删除 | `$logic->batchDelete($ids)` |
|
||||
@@ -0,0 +1,76 @@
|
||||
# 3.2 数据库建表
|
||||
|
||||
:::tip 概述
|
||||
建表是模块开发的第一步。所有表必须包含公共字段(id、软删除、审计字段),这是 BaseModel 自动处理的基础。
|
||||
:::
|
||||
|
||||
## 公共字段规范
|
||||
|
||||
所有业务表必须包含以下公共字段:
|
||||
|
||||
```sql
|
||||
-- 公共字段(每个表都必须包含)
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||
is_delete TINYINT(1) DEFAULT 0 COMMENT '软删除 0=正常 1=已删除',
|
||||
create_user VARCHAR(50) DEFAULT '' COMMENT '创建人',
|
||||
create_time DATETIME COMMENT '创建时间',
|
||||
update_user VARCHAR(50) DEFAULT '' COMMENT '更新人',
|
||||
update_time DATETIME COMMENT '更新时间'
|
||||
```
|
||||
|
||||
:::warning 为什么必须包含这些字段?
|
||||
- `is_delete`:BaseModel 的全局查询范围 `soft_delete` 会自动过滤 `is_delete=1` 的数据
|
||||
- `create_user` / `create_time`:BaseModel 的 `onBeforeInsert` 事件自动填充
|
||||
- `update_user` / `update_time`:BaseModel 的 `onBeforeUpdate` 事件自动填充
|
||||
:::
|
||||
|
||||
## 建表 SQL 示例(example 模块)
|
||||
|
||||
```sql
|
||||
CREATE TABLE think_example (
|
||||
-- 主键
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||
|
||||
-- 业务字段
|
||||
name VARCHAR(200) NOT NULL COMMENT '案例名称',
|
||||
type TINYINT(1) DEFAULT 0 COMMENT '案例类型(字典 example_type)',
|
||||
status TINYINT(1) DEFAULT 0 COMMENT '案例状态(字典 example_status)',
|
||||
sort INT DEFAULT 0 COMMENT '排序值(越小越靠前)',
|
||||
avatar VARCHAR(500) DEFAULT '' COMMENT '头像(文件路径)',
|
||||
|
||||
-- 公共字段(必须)
|
||||
is_delete TINYINT(1) DEFAULT 0 COMMENT '软删除 0=正常 1=已删除',
|
||||
create_user VARCHAR(50) DEFAULT '' COMMENT '创建人',
|
||||
create_time DATETIME COMMENT '创建时间',
|
||||
update_user VARCHAR(50) DEFAULT '' COMMENT '更新人',
|
||||
update_time DATETIME COMMENT '更新时间',
|
||||
|
||||
-- 索引
|
||||
INDEX idx_type (type),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_sort (sort)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='案例演示表';
|
||||
```
|
||||
|
||||
## 字段与 Logic 属性对应关系
|
||||
|
||||
| 数据库字段 | 类型 | Logic 属性 | 说明 |
|
||||
|-----------|------|-----------|------|
|
||||
| `name` | VARCHAR(200) | `pageLikeFields` | LIKE 模糊搜索 |
|
||||
| `type` | TINYINT(1) | `pageEqFields` + `serializeMaps` | 精确匹配 + 枚举翻译 |
|
||||
| `status` | TINYINT(1) | `pageEqFields` + `serializeMaps` | 精确匹配 + 枚举翻译 |
|
||||
| `sort` | INT | `pageOrderBy` | 默认排序字段 |
|
||||
| `avatar` | VARCHAR(500) | `fileFields` | 文件字段,自动处理 |
|
||||
|
||||
## 常见字段类型参考
|
||||
|
||||
| 用途 | 推荐类型 | 示例 |
|
||||
|------|----------|------|
|
||||
| 主键 | `INT AUTO_INCREMENT` | `id INT PRIMARY KEY AUTO_INCREMENT` |
|
||||
| 外键 | `INT DEFAULT 0` | `category_id INT DEFAULT 0` |
|
||||
| 短文本 | `VARCHAR(n)` | `name VARCHAR(200)` |
|
||||
| 长文本 | `TEXT` | `content TEXT` |
|
||||
| 布尔值 | `TINYINT(1)` | `status TINYINT(1) DEFAULT 0` |
|
||||
| 整数 | `INT` | `sort INT DEFAULT 0` |
|
||||
| 日期时间 | `DATETIME` | `create_time DATETIME` |
|
||||
| 文件路径 | `VARCHAR(500)` | `avatar VARCHAR(500)` |
|
||||
@@ -0,0 +1,93 @@
|
||||
# 3.11 常见问题与排错
|
||||
|
||||
## 后端问题
|
||||
|
||||
### Q: 新增接口返回 401
|
||||
|
||||
A: 检查以下几点:
|
||||
1. 请求头是否携带 `Authorization: Bearer <token>`
|
||||
2. Token 是否过期(默认 2 小时)
|
||||
3. 路由是否在 `route/app.php` 的需认证组中
|
||||
|
||||
### Q: 新增接口返回 403
|
||||
|
||||
A: 权限不足,检查:
|
||||
1. 是否在 Controller 方法上添加了 `#[Permission]` 注解
|
||||
2. 权限码是否在菜单管理中配置
|
||||
3. 用户角色是否包含该权限节点
|
||||
|
||||
### Q: 枚举显示名(typeText/statusText)为空
|
||||
|
||||
A: 检查:
|
||||
1. 字典表中是否存在对应的字典编码(如 `example_type`)
|
||||
2. 字典项的 value 值是否与数据中的值匹配
|
||||
3. Logic 中 `serializeMaps` 配置是否正确
|
||||
|
||||
```php
|
||||
// ExampleLogic 中的配置
|
||||
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = [
|
||||
'type' => 'example_type',
|
||||
'status' => 'example_status',
|
||||
];
|
||||
```
|
||||
|
||||
### Q: 文件上传后 avatar 路径为空
|
||||
|
||||
A: 检查:
|
||||
1. `.env` 中 `FILE.UPLOAD_DIR` 是否配置
|
||||
2. 上传目录是否存在且有写入权限
|
||||
3. Logic 中是否配置了 `fileFields`
|
||||
|
||||
```php
|
||||
// ExampleLogic 中的配置
|
||||
protected array $fileFields = ['avatar'];
|
||||
protected string $fileSaveDir = 'example';
|
||||
```
|
||||
|
||||
### Q: 排序不生效
|
||||
|
||||
A: 检查 Logic 中 `pageOrderBy` 配置:
|
||||
|
||||
```php
|
||||
// ExampleLogic 中的配置
|
||||
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
|
||||
```
|
||||
|
||||
前端可通过 `orderField` 和 `orderType` 参数覆盖默认排序。
|
||||
|
||||
## 前端问题
|
||||
|
||||
### Q: 菜单不显示
|
||||
|
||||
A: 检查:
|
||||
1. 后端菜单管理中是否添加了对应菜单
|
||||
2. 菜单的组件路径是否正确(如 `tool/example/index`)
|
||||
3. 前端 `views` 目录下是否有对应的 `.vue` 文件
|
||||
4. 用户角色是否包含该菜单权限
|
||||
|
||||
### Q: API 请求 404
|
||||
|
||||
A: 检查:
|
||||
1. `route/app.php` 中是否注册了对应路由
|
||||
2. 路由路径是否与前端 API 模块中的 `url` 一致
|
||||
3. HTTP 方法是否正确(GET/POST/PUT/DELETE)
|
||||
|
||||
### Q: 表格数据不显示
|
||||
|
||||
A: 检查:
|
||||
1. API 返回的数据结构是否正确(应包含 `records` 和 `total`)
|
||||
2. 前端是否正确绑定了 `tableData`
|
||||
3. 浏览器控制台是否有报错
|
||||
|
||||
### Q: 导入失败
|
||||
|
||||
A: 检查:
|
||||
1. 文件格式是否为 `.xls` 或 `.xlsx`
|
||||
2. 表头是否与 `headerMap` 中定义的中文名一致
|
||||
3. 枚举字段的值是否在字典中存在
|
||||
@@ -0,0 +1,275 @@
|
||||
# 3.9 前端联调指引
|
||||
|
||||
:::tip 概述
|
||||
后端接口开发完成后,需要在前端创建对应的 API 模块和页面组件。本节以 example 模块为例,提供简要指引,详细内容参考[第 9 章](/zh/frontend/overview.html)。
|
||||
:::
|
||||
|
||||
## 联调流程
|
||||
|
||||
```text
|
||||
1. 创建 API 模块 → ui/src/api/tool/example.ts
|
||||
2. 创建页面组件 → ui/src/views/tool/example/index.vue
|
||||
3. 配置菜单组件路径 → 后端菜单管理中设置 component
|
||||
4. 测试联调
|
||||
```
|
||||
|
||||
## 步骤一:创建 API 模块
|
||||
|
||||
在 `ui/src/api/tool/` 下创建 `example.ts`(来自项目实际代码):
|
||||
|
||||
```typescript
|
||||
// ui/src/api/tool/example.ts
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
/**
|
||||
* 案例演示模块接口
|
||||
*
|
||||
* 提供案例演示的增删改查及导入导出接口调用。
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取案例演示分页列表
|
||||
*
|
||||
* @param params 查询参数,包含 pageNo、pageSize 及业务筛选条件
|
||||
* @returns 分页结果,含 records、total、size、current、pages
|
||||
*/
|
||||
export function getExamplePage(params?: Record<string, any>) {
|
||||
return http.request({
|
||||
url: '/example/page',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部案例演示列表
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @returns 案例演示列表
|
||||
*/
|
||||
export function getExampleList(params?: Record<string, any>) {
|
||||
return http.request({
|
||||
url: '/example/list',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取案例演示详情
|
||||
*
|
||||
* @param id 案例演示ID
|
||||
* @returns 案例演示详情数据
|
||||
*/
|
||||
export function getExampleDetail(id) {
|
||||
return http.request({
|
||||
url: '/example/detail/' + id,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加案例演示
|
||||
*
|
||||
* @param data 案例演示数据
|
||||
* @returns 新记录的 ID
|
||||
*/
|
||||
export function exampleAdd(data: any) {
|
||||
return http.request({
|
||||
url: '/example/add',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新案例演示
|
||||
*
|
||||
* @param data 案例演示数据,需包含主键 id
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function exampleUpdate(data: any) {
|
||||
return http.request({
|
||||
url: '/example/update',
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除案例演示
|
||||
*
|
||||
* @param id 案例演示ID
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function exampleDelete(id) {
|
||||
return http.request({
|
||||
url: '/example/delete/' + id,
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除案例演示
|
||||
*
|
||||
* @param data 含 ids 数组的对象,形如 { ids: [1, 2, 3] }
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function exampleBatchDelete(data: any) {
|
||||
return http.request({
|
||||
url: '/example/batchDelete',
|
||||
method: 'DELETE',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入案例演示
|
||||
*
|
||||
* @param data 文件数据(FormData)
|
||||
* @returns 导入结果,含成功数量与失败明细
|
||||
*/
|
||||
export function exampleImport(data) {
|
||||
return http.request({
|
||||
url: '/example/import',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出案例演示
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @returns 导出文件的 URL
|
||||
*/
|
||||
export function exampleExport(params) {
|
||||
return http.request({
|
||||
url: '/example/export',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 步骤二:创建页面组件
|
||||
|
||||
在 `ui/src/views/tool/example/` 下创建以下 5 个文件:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `index.vue` | 列表页(表格 + 搜索 + 分页) |
|
||||
| `edit.vue` | 新增/编辑弹窗表单 |
|
||||
| `detail.vue` | 详情查看页 |
|
||||
| `columns.ts` | 表格列定义 |
|
||||
| `querySchemas.ts` | 搜索表单字段定义 |
|
||||
|
||||
:::tip 关于以下示例
|
||||
以下代码使用原生 `el-table` / `el-form` 简化展示核心逻辑。实际项目中应使用封装好的 `BasicTable` + `useForm` 等组合式组件,详见[第 9 章组件文档](/zh/frontend/overview.html)。
|
||||
:::
|
||||
|
||||
`index.vue`(列表页)示例:
|
||||
|
||||
```vue
|
||||
<!-- ui/src/views/tool/example/index.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<!-- 搜索表单 -->
|
||||
<el-form :model="searchForm" inline>
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="searchForm.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="请选择" clearable>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-button type="primary" @click="handleAdd">新增</el-button>
|
||||
<el-button type="danger" :disabled="!selectedIds.length" @click="handleBatchDelete">
|
||||
批量删除
|
||||
</el-button>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="tableData" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="typeText" label="类型" width="100" />
|
||||
<el-table-column prop="statusText" label="状态" width="100" />
|
||||
<el-table-column prop="sort" label="排序" width="80" />
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="pageNo"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
|
||||
|
||||
const searchForm = ref({ name: '', status: '' });
|
||||
const tableData = ref([]);
|
||||
const pageNo = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const total = ref(0);
|
||||
const selectedIds = ref([]);
|
||||
|
||||
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 handleReset = () => { searchForm.value = { name: '', status: '' }; handleSearch(); };
|
||||
const handleDelete = async (id: number) => { await exampleDelete(id); fetchData(); };
|
||||
const handleBatchDelete = async () => { await exampleBatchDelete({ ids: selectedIds.value }); fetchData(); };
|
||||
const handleSelectionChange = (rows: any[]) => { selectedIds.value = rows.map((r) => r.id); };
|
||||
|
||||
onMounted(() => fetchData());
|
||||
</script>
|
||||
```
|
||||
|
||||
## 步骤三:配置菜单
|
||||
|
||||
在后端菜单管理中,将组件路径设置为:
|
||||
|
||||
```text
|
||||
tool/example/index
|
||||
```
|
||||
|
||||
前端路由系统会自动将此路径映射到 `ui/src/views/tool/example/index.vue`。
|
||||
|
||||
## 步骤四:测试联调
|
||||
|
||||
1. 确保后端服务运行:`php think run`
|
||||
2. 确保前端服务运行:`pnpm dev`
|
||||
3. 刷新前端页面,侧边栏应出现"案例演示"菜单
|
||||
4. 点击菜单,测试增删改查功能
|
||||
|
||||
详细前端开发内容请参考[第 9 章](/zh/frontend/overview.html)。
|
||||
@@ -0,0 +1,319 @@
|
||||
# 3.5 逻辑层开发
|
||||
|
||||
:::tip 概述
|
||||
逻辑层是业务核心,通过"属性配置 + 生命周期钩子"实现声明式 CRUD。子类只需配置属性和按需重写钩子,即可获得完整的增删改查能力。
|
||||
:::
|
||||
|
||||
## 逻辑层文件位置
|
||||
|
||||
```text
|
||||
app/logic/ExampleLogic.php
|
||||
```
|
||||
|
||||
## 完整代码(来自项目实际代码)
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\logic;
|
||||
|
||||
use app\BaseLogic; // 系统共享模块基类
|
||||
use app\model\Example; // 案例演示模型
|
||||
|
||||
/**
|
||||
* 案例演示业务逻辑类
|
||||
*
|
||||
* 继承 BaseLogic,通过属性配置声明式地定义案例演示模块的通用行为,
|
||||
* 并按需生成导入、导出方法。
|
||||
*
|
||||
* 配置覆盖:文件字段、查询条件、排序、唯一性校验、枚举显示名映射。
|
||||
*/
|
||||
class ExampleLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* 指定该逻辑层操作的数据模型为 Example。
|
||||
* BaseLogic 的所有 CRUD 方法都基于此模型。
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Example::class;
|
||||
|
||||
/**
|
||||
* 验证器类名
|
||||
*
|
||||
* 设置后,add/update 时自动调用对应验证器的 add/update 场景进行参数校验。
|
||||
* 校验失败抛出 ValidateException,由 ExceptionHandle 统一返回。
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $validateClass = \app\validate\ExampleValidate::class;
|
||||
|
||||
/**
|
||||
* 文件字段
|
||||
*
|
||||
* 新增/修改时迁移临时文件并去掉域名,
|
||||
* 查询时自动补全完整访问 URL。
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $fileFields = ['avatar'];
|
||||
|
||||
/**
|
||||
* 多文件字段(逗号分隔的多个URL)
|
||||
*
|
||||
* 与 fileFields 不同,这些字段的值是逗号分隔的多个文件URL,
|
||||
* 新增/修改时逐个迁移临时文件并去掉域名,
|
||||
* 查询时逐个补全完整访问 URL。
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $multiFileFields = ['images'];
|
||||
|
||||
/**
|
||||
* 文件保存子目录
|
||||
*
|
||||
* 文件字段上传后保存到该子目录下。
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $fileSaveDir = 'example';
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* 分页查询时,前端传入的 name 参数会自动使用 LIKE 查询。
|
||||
* 例如:GET /api/example/page?name=测试
|
||||
* 生成 SQL:WHERE name LIKE '%测试%'
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* 分页查询时,前端传入的 type 和 status 参数
|
||||
* 会自动使用 = 查询。
|
||||
* 例如:GET /api/example/page?type=1&status=1
|
||||
* 生成 SQL:WHERE type = 1 AND status = 1
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageEqFields = ['type', 'status'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* 支持两种格式:
|
||||
* - 单字段:['field' => 'sort', 'type' => 'asc']
|
||||
* - 多字段:[['field' => 'sort', 'type' => 'asc'], ['field' => 'id', 'type' => 'desc']]
|
||||
*
|
||||
* 前端可通过 orderField/orderType 参数覆盖。
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
|
||||
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* 查询数据时自动根据字典编码翻译值为描述文本,补 {字段名}Text 字段。
|
||||
* 例如:type=1 时,typeText='类型一'(从字典 example_type 读取)。
|
||||
* status=1 时,statusText='启用'(从字典 example_status 读取)。
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = [
|
||||
'type' => 'example_type',
|
||||
'status' => 'example_status',
|
||||
];
|
||||
// ... 其他属性配置
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 属性配置即全部
|
||||
子类只需声明以上属性,无需编写任何 CRUD 代码,即可获得完整的分页查询、详情、新增、修改、删除能力。这是 BaseLogic 模板方法模式的核心优势。
|
||||
:::
|
||||
|
||||
## 扩展方法(导入导出示例)
|
||||
|
||||
```php
|
||||
/**
|
||||
* 导入案例演示数据
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 按表单字段生成"中文表头 => 字段名"的映射,跳过图片与富文本字段;
|
||||
* 2. 调用 ExcelService::importWithMap 解析文件为关联数组;
|
||||
* 3. 枚举字段反向映射:把文字通过 DictService 还原为数值;
|
||||
* 4. 逐行调用 add 方法写入,成功计数,失败收集行号与原因;
|
||||
* 5. 返回成功数与错误列表。
|
||||
*
|
||||
* @param string $filePath 文件路径
|
||||
* @return array ['count' => 成功数, 'errors' => [失败行信息]]
|
||||
* @throws \Exception 文件解析失败时抛出
|
||||
*/
|
||||
public function import(string $filePath): array
|
||||
{
|
||||
// 定义表头映射:Excel 表头名 → 数据库字段名
|
||||
$headerMap = [
|
||||
'案例名称' => 'name',
|
||||
'案例类型' => 'type',
|
||||
'案例状态' => 'status',
|
||||
'案例排序' => 'sort',
|
||||
];
|
||||
|
||||
// 解析 Excel 文件为关联数组
|
||||
$data = \app\service\ExcelService::importWithMap($filePath, $headerMap);
|
||||
|
||||
// 枚举字段反向映射:文字转数字
|
||||
// 例如 Excel 中"启用" → 数据库中 1
|
||||
foreach ($data as &$row) {
|
||||
if (!empty($row['type']) && !is_numeric($row['type'])) {
|
||||
$row['type'] = \app\service\DictService::getValue('example_type', $row['type']);
|
||||
}
|
||||
if (!empty($row['status']) && !is_numeric($row['status'])) {
|
||||
$row['status'] = \app\service\DictService::getValue('example_status', $row['status']);
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
|
||||
// 逐行导入
|
||||
$count = 0;
|
||||
$errors = [];
|
||||
foreach ($data as $index => $row) {
|
||||
try {
|
||||
$this->add($row); // 调用 BaseLogic::add,触发 beforeAdd/afterAdd 钩子
|
||||
$count++;
|
||||
} catch (\Exception $e) {
|
||||
// 单行失败不影响其它行,收集错误信息
|
||||
$errors[] = [
|
||||
'row' => $index + 2, // +2 因为第1行是表头,索引从0开始
|
||||
'reason' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return ['count' => $count, 'errors' => $errors];
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出案例演示数据
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 按列表字段生成导出表头,跳过图片与富文本字段;
|
||||
* 带选项的字段使用 {字段名}Text,以导出显示名而非原始值;
|
||||
* 2. 追加创建时间列;
|
||||
* 3. 调用 ExcelService::chunkExport 分批导出,每批 500 条,
|
||||
* 避免一次性加载全部数据导致内存溢出。
|
||||
*
|
||||
* @param array $params 查询参数
|
||||
* @return string 导出文件的 URL
|
||||
* @throws \Exception 导出失败时抛出
|
||||
*/
|
||||
public function export(array $params = []): string
|
||||
{
|
||||
// 定义导出表头:字段名 → Excel 表头名
|
||||
// 带 Text 后缀的字段取枚举显示名(如 typeText='类型一')
|
||||
$headers = [
|
||||
'name' => '案例名称',
|
||||
'typeText' => '案例类型', // 枚举显示名
|
||||
'statusText' => '案例状态', // 枚举显示名
|
||||
'sort' => '案例排序',
|
||||
'create_time' => '创建时间',
|
||||
];
|
||||
|
||||
// 分批导出,每批 500 条
|
||||
return \app\service\ExcelService::chunkExport($headers, function (int $page) use ($params) {
|
||||
$chunk = [];
|
||||
// chunkList 是 BaseLogic 提供的分批查询方法
|
||||
$this->chunkList($params, function (array $data) use (&$chunk) {
|
||||
$chunk = $data;
|
||||
}, 500);
|
||||
return $chunk;
|
||||
}, '案例演示数据.xlsx', 500);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 属性配置速查
|
||||
|
||||
| 属性 | 类型 | 说明 | 本例配置 |
|
||||
|------|------|------|----------|
|
||||
| `modelClass` | string | 关联模型类名(必须) | `Example::class` |
|
||||
| `validateClass` | string | 验证器类名(自动参数校验) | `ExampleValidate::class` |
|
||||
| `fileFields` | array | 单文件字段列表 | `['avatar']` |
|
||||
| `multiFileFields` | array | 多文件字段列表(逗号分隔的多个URL) | `['images']` |
|
||||
| `fileSaveDir` | string | 文件保存子目录 | `'example'` |
|
||||
| `pageLikeFields` | array | LIKE 模糊匹配字段 | `['name']` |
|
||||
| `pageEqFields` | array | 精确匹配字段 | `['type', 'status']` |
|
||||
| `pageOrderBy` | array | 默认排序规则 | `['field'=>'sort','type'=>'asc']` |
|
||||
| `uniqueFields` | array | 唯一性校验字段 | `[]`(本例未配置) |
|
||||
| `serializeMaps` | array | 枚举显示名映射 | `['type'=>'example_type', 'status'=>'example_status']` |
|
||||
| `contentFields` | array | 富文本字段 | `[]`(本例未配置) |
|
||||
|
||||
## 生命周期钩子
|
||||
|
||||
| 钩子 | 时机 | 用途 |
|
||||
|------|------|------|
|
||||
| `beforeAdd($data)` | 新增前 | 修改数据、校验、抛异常拦截 |
|
||||
| `afterAdd($id, $data)` | 新增后 | 保存关联表、发送通知 |
|
||||
| `beforeUpdate($id, $data)` | 修改前 | 修改数据、校验 |
|
||||
| `afterUpdate($id, $data)` | 修改后 | 更新关联表 |
|
||||
| `beforeDelete($id)` | 删除前 | 检查是否有子级、抛异常拦截 |
|
||||
| `afterDelete($id)` | 删除后 | 清理关联数据 |
|
||||
| `afterDetail($id, &$data)` | 详情查询后 | 补充关联数据、格式化输出 |
|
||||
| `afterPageList(&$records, $params)` | 列表查询后 | 批量补充关联数据(分页和全量列表均触发) |
|
||||
|
||||
### afterPageList 示例
|
||||
|
||||
当列表需要补充关联统计数据时,`afterPageList` 比逐条 `afterDetail` 更高效,只需一次 SQL 查询:
|
||||
|
||||
```php
|
||||
/**
|
||||
* 列表查询后处理:批量补充评论数
|
||||
*
|
||||
* @param array &$records 记录列表(引用传递)
|
||||
* @param array $params 查询参数
|
||||
* @return void
|
||||
*/
|
||||
protected function afterPageList(array &$records, array $params): void
|
||||
{
|
||||
if (empty($records)) return;
|
||||
|
||||
// 一次查询获取所有记录的评论数,避免 N+1
|
||||
$ids = array_column($records, 'id');
|
||||
$counts = Comment::whereIn('article_id', $ids)
|
||||
->group('article_id')
|
||||
->column('count(*)', 'article_id');
|
||||
|
||||
foreach ($records as &$record) {
|
||||
$record['commentCount'] = $counts[$record['id']] ?? 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 与 afterDetail 的区别
|
||||
- `afterDetail`:单条详情查询后触发,适合补充单条记录的关联数据
|
||||
- `afterPageList`:列表查询后触发(分页和全量均适用),适合批量补充关联数据,避免 N+1 查询
|
||||
:::
|
||||
|
||||
## 事务处理
|
||||
|
||||
```php
|
||||
public function addWithRelated(array $data): int
|
||||
{
|
||||
$this->startTrans(); // 开启事务
|
||||
try {
|
||||
$id = $this->add($data);
|
||||
// 保存关联数据...
|
||||
$this->commit(); // 提交事务
|
||||
return $id;
|
||||
} catch (\Exception $e) {
|
||||
$this->rollback(); // 回滚事务
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,103 @@
|
||||
# 3.3 模型层开发
|
||||
|
||||
:::tip 概述
|
||||
模型层负责定义数据库表的映射关系,继承 BaseModel 后自动获得软删除和审计字段能力。
|
||||
:::
|
||||
|
||||
## 模型文件位置
|
||||
|
||||
```text
|
||||
app/model/Example.php
|
||||
```
|
||||
|
||||
## 完整代码(来自项目实际代码)
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use app\BaseModel;
|
||||
|
||||
/**
|
||||
* 案例演示模型
|
||||
*
|
||||
* 对应数据表 example,继承 BaseModel 获得通用模型能力
|
||||
* (软删除与审计字段自动写入)。
|
||||
*
|
||||
* 说明:本模型只有表映射,未定义关联或辅助方法;
|
||||
* 案例演示的查询过滤、排序、唯一校验等均由 ExampleLogic 配置。
|
||||
*/
|
||||
class Example extends BaseModel
|
||||
{
|
||||
/**
|
||||
* 数据表名
|
||||
*
|
||||
* 显式指定表名,覆盖默认的类名推导规则。
|
||||
* ThinkPHP 默认使用类名的小写下划线形式作为表名,
|
||||
* Example → think_example。如果表名与类名不对应,
|
||||
* 可通过 $name 属性显式指定。
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'example';
|
||||
}
|
||||
```
|
||||
|
||||
## 继承 BaseModel 后获得的能力
|
||||
|
||||
| 能力 | 说明 | 代码示例 |
|
||||
|------|------|----------|
|
||||
| 软删除 | 全局查询自动过滤 `is_delete=1` | `$list = Example::select();` 自动排除已删除 |
|
||||
| 软删除操作 | 设置 `is_delete=1` | `$model->softDelete();` |
|
||||
| 批量软删除 | 批量设置 `is_delete=1` | `Example::batchSoftDelete([1,2,3]);` |
|
||||
| 审计字段-创建 | 自动填充 `create_user` 和 `create_time` | `Example::create([...])` 时自动填充 |
|
||||
| 审计字段-更新 | 自动填充 `update_user` 和 `update_time` | `$model->save([...])` 时自动填充 |
|
||||
|
||||
## 查询已删除数据
|
||||
|
||||
```php
|
||||
// 查询单条(包括已删除)
|
||||
$example = Example::withoutGlobalScope(['soft_delete'])->find($id);
|
||||
|
||||
// 查询所有(包括已删除)
|
||||
$all = Example::withoutGlobalScope(['soft_delete'])->select();
|
||||
```
|
||||
|
||||
## 简单模型 vs 复杂模型
|
||||
|
||||
:::tip 模型层职责
|
||||
模型层只负责表映射和关联关系,不包含业务逻辑。所有查询条件、排序、唯一性校验等都由 Logic 层配置。
|
||||
:::
|
||||
|
||||
### 简单模型(如 Example)
|
||||
|
||||
只需声明表名,所有逻辑由 Logic 层配置:
|
||||
|
||||
```php
|
||||
class Example extends BaseModel
|
||||
{
|
||||
protected $name = 'example';
|
||||
}
|
||||
```
|
||||
|
||||
### 复杂模型(如 Role)
|
||||
|
||||
包含关联关系:
|
||||
|
||||
```php
|
||||
class Role extends BaseModel
|
||||
{
|
||||
protected $name = 'role';
|
||||
|
||||
/**
|
||||
* 关联菜单(多对多)
|
||||
* 通过 role_menu 中间表关联
|
||||
*/
|
||||
public function menus()
|
||||
{
|
||||
return $this->belongsToMany(Menu::class, 'role_menu', 'menu_id', 'role_id');
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,88 @@
|
||||
# 3.1 概述与目标
|
||||
|
||||
:::tip 概述
|
||||
本章以项目内置的 **example(案例演示)** 模块为例,从数据库建表到前端页面联调,手把手演示开发全流程。学完本章后,您将掌握新增业务模块的标准步骤。
|
||||
:::
|
||||
|
||||
## 开发流程总览
|
||||
|
||||
```text
|
||||
┌──────────┐
|
||||
│ 1. 建表 │ 创建数据库表(含公共字段)
|
||||
└────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ 2. Model │ 创建数据模型(继承 BaseModel)
|
||||
└────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ 3. Validate│ 创建验证器 + Logic 配置 validateClass
|
||||
└────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ 4. Logic │ 创建逻辑层(配置属性 + 钩子)
|
||||
└────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ 5. Controller│ 创建控制器(注解 + 异常处理)
|
||||
└────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ 6. Route │ 注册路由
|
||||
└────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ 7. Menu │ 配置菜单权限(可选)
|
||||
└────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ 8. Frontend│ 前端 API + 页面([详见第 9 章](/zh/frontend/overview.html))
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
## 开发约定
|
||||
|
||||
| 约定 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| 表名 | `think_` 前缀 + 小写下划线 | `think_example` |
|
||||
| 字段名 | 小写下划线 | `create_time`、`user_id` |
|
||||
| 主键 | 统一使用 `id`,INT 自增 | `id INT PRIMARY KEY AUTO_INCREMENT` |
|
||||
| Model | 继承 `BaseModel`,类名与表名对应 | `class Example extends BaseModel` |
|
||||
| Logic | 继承 `BaseLogic` 或 `BaseTenantLogic` | `class ExampleLogic extends BaseLogic` |
|
||||
| Controller | 继承 `BaseController` | `class ExampleController extends BaseController` |
|
||||
| 权限码 | `sys:模块名:操作` | `sys:example:add` |
|
||||
| 路由 | RESTful 风格 | `GET /api/example/page` |
|
||||
|
||||
## 本章示例
|
||||
|
||||
本章以项目内置的 **example(案例演示)** 模块为例,完整演示从建表到联调的全过程。该模块是代码生成器的参考模板,涵盖了标准 CRUD 的所有典型场景:
|
||||
|
||||
- 分页列表(支持名称模糊搜索、类型和状态精确过滤)
|
||||
- 全量列表
|
||||
- 详情查询
|
||||
- 新增(含头像上传)
|
||||
- 修改
|
||||
- 删除 / 批量删除
|
||||
- Excel 导入导出
|
||||
- 权限控制(#[Permission] 注解)
|
||||
- 操作日志(#[Log] 注解)
|
||||
- 枚举显示名翻译(type / status 字典映射)
|
||||
|
||||
## 涉及文件
|
||||
|
||||
```text
|
||||
app/model/Example.php # 数据模型
|
||||
app/validate/ExampleValidate.php # 验证器
|
||||
app/logic/ExampleLogic.php # 业务逻辑
|
||||
app/controller/ExampleController.php # 控制器
|
||||
route/app.php # 路由(第371行)
|
||||
ui/src/api/tool/example.ts # 前端 API 模块
|
||||
ui/src/views/tool/example/ # 前端页面
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
# 3.8 权限节点配置
|
||||
|
||||
:::tip 概述
|
||||
权限节点用于控制用户能访问哪些接口。通过菜单管理界面配置,格式为 `sys:{模块名}:{操作}`。
|
||||
:::
|
||||
|
||||
## Example 模块权限码
|
||||
|
||||
| 权限码 | 说明 | 对应接口 |
|
||||
|--------|------|----------|
|
||||
| `sys:example:page` | 案例演示分页列表 | `GET /api/example/page` |
|
||||
| `sys:example:add` | 添加案例演示 | `POST /api/example/add` |
|
||||
| `sys:example:update` | 修改案例演示 | `PUT /api/example/update` |
|
||||
| `sys:example:delete` | 删除案例演示 | `DELETE /api/example/delete/:id` |
|
||||
| `sys:example:import` | 导入案例演示 | `POST /api/example/import` |
|
||||
| `sys:example:export` | 导出案例演示 | `GET /api/example/export` |
|
||||
|
||||
## 配置步骤
|
||||
|
||||
### 1. 添加目录节点
|
||||
|
||||
在系统菜单管理中添加一级目录:
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|-----|
|
||||
| 菜单名称 | 工具管理 |
|
||||
| 菜单类型 | 目录(type=0) |
|
||||
| 路由地址 | /tool |
|
||||
| 排序 | 60 |
|
||||
|
||||
### 2. 添加菜单节点
|
||||
|
||||
在目录下添加二级菜单:
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|-----|
|
||||
| 菜单名称 | 案例演示 |
|
||||
| 菜单类型 | 菜单(type=1) |
|
||||
| 路由地址 | /example |
|
||||
| 组件路径 | tool/example/index |
|
||||
| 权限标识 | sys:example:page |
|
||||
| 排序 | 1 |
|
||||
|
||||
### 3. 添加按钮权限
|
||||
|
||||
在菜单下添加三级按钮权限:
|
||||
|
||||
| 字段 | 值 | 权限标识 |
|
||||
|------|-----|----------|
|
||||
| 添加案例演示 | type=2 | sys:example:add |
|
||||
| 修改案例演示 | type=2 | sys:example:update |
|
||||
| 删除案例演示 | type=2 | sys:example:delete |
|
||||
| 导入案例演示 | type=2 | sys:example:import |
|
||||
| 导出案例演示 | type=2 | sys:example:export |
|
||||
|
||||
### 4. 分配角色权限
|
||||
|
||||
在角色管理中,为角色勾选新增的权限节点。
|
||||
|
||||
## 菜单类型说明
|
||||
|
||||
| type | 说明 | 前端行为 |
|
||||
|------|------|----------|
|
||||
| 0 | 目录 | 渲染为侧边栏分组 |
|
||||
| 1 | 菜单 | 渲染为可点击的菜单项,对应一个页面 |
|
||||
| 2 | 按钮 | 不渲染,仅用于权限校验 |
|
||||
|
||||
## 权限校验流程
|
||||
|
||||
```text
|
||||
1. 用户登录 → 获取用户角色
|
||||
2. 角色关联菜单 → 获取权限节点列表
|
||||
3. 请求接口 → AuthMiddleware 读取 #[Permission] 注解
|
||||
4. 检查权限码是否在用户权限列表中
|
||||
5. 超级管理员(uid=1)跳过校验,直接放行
|
||||
```
|
||||
@@ -0,0 +1,92 @@
|
||||
# 3.7 路由注册
|
||||
|
||||
:::tip 概述
|
||||
路由定义在 `route/app.php` 中,按模块分组注册。所有需认证的接口统一绑定四个中间件。
|
||||
:::
|
||||
|
||||
## 路由文件位置
|
||||
|
||||
```text
|
||||
route/app.php
|
||||
```
|
||||
|
||||
## Example 模块路由(来自项目实际代码)
|
||||
|
||||
```php
|
||||
// route/app.php 第 370~383 行
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 自动生成 - example
|
||||
// ----------------------------------------------------------
|
||||
Route::group('example', function () {
|
||||
Route::get('page', 'ExampleController/page'); // 分页列表
|
||||
Route::get('list', 'ExampleController/list'); // 全量列表
|
||||
Route::get('detail/:id', 'ExampleController/detail'); // 详情(:id 为路径参数)
|
||||
Route::post('add', 'ExampleController/add'); // 新增
|
||||
Route::put('update', 'ExampleController/update'); // 修改
|
||||
Route::delete('delete/:id', 'ExampleController/delete'); // 删除
|
||||
Route::delete('batchDelete', 'ExampleController/batchDelete'); // 批量删除
|
||||
Route::post('import', 'ExampleController/import'); // 导入
|
||||
Route::get('export', 'ExampleController/export'); // 导出
|
||||
});
|
||||
```
|
||||
|
||||
## 路由格式说明
|
||||
|
||||
```php
|
||||
Route::HTTP方法('路径', '控制器/方法');
|
||||
```
|
||||
|
||||
| HTTP 方法 | 用途 | 路径格式 | 示例 |
|
||||
|-----------|------|----------|------|
|
||||
| GET | 查询 | `/module/page`、`/module/detail/:id` | `GET /api/example/page` |
|
||||
| POST | 新增/导入 | `/module/add`、`/module/import` | `POST /api/example/add` |
|
||||
| PUT | 修改 | `/module/update` | `PUT /api/example/update` |
|
||||
| DELETE | 删除 | `/module/delete/:id`、`/module/batchDelete` | `DELETE /api/example/delete/1` |
|
||||
|
||||
## 标准路由模板
|
||||
|
||||
每个业务模块通常包含以下路由:
|
||||
|
||||
```php
|
||||
Route::group('模块名', function () {
|
||||
Route::get('page', 'XxxController/page'); // 分页列表
|
||||
Route::get('list', 'XxxController/list'); // 全量列表(可选)
|
||||
Route::get('detail/:id', 'XxxController/detail'); // 详情
|
||||
Route::post('add', 'XxxController/add'); // 新增
|
||||
Route::put('update', 'XxxController/update'); // 修改
|
||||
Route::delete('delete/:id', 'XxxController/delete'); // 删除
|
||||
Route::delete('batchDelete', 'XxxController/batchDelete'); // 批量删除
|
||||
Route::post('import', 'XxxController/import'); // 导入(可选)
|
||||
Route::get('export', 'XxxController/export'); // 导出(可选)
|
||||
});
|
||||
```
|
||||
|
||||
:::tip 路由命名规范
|
||||
- 路径使用小写英文(如 `example`、`user`)
|
||||
- 详情/删除使用 `:id` 参数(如 `detail/:id`)
|
||||
- 批量删除使用 `batchDelete` 路径
|
||||
:::
|
||||
|
||||
## 中间件绑定
|
||||
|
||||
所有需认证的接口在 `route/app.php` 末尾统一绑定中间件:
|
||||
|
||||
```php
|
||||
})->middleware([
|
||||
\app\middleware\AuthMiddleware::class, // JWT认证 + 权限校验
|
||||
\app\middleware\TenantMiddleware::class, // 租户上下文注入
|
||||
\app\middleware\DemoMiddleware::class, // 演示环境拦截
|
||||
\app\middleware\LogMiddleware::class, // 操作日志记录
|
||||
]);
|
||||
```
|
||||
|
||||
## 验证路由注册
|
||||
|
||||
```bash
|
||||
# 查看所有已注册路由
|
||||
php think route:list
|
||||
|
||||
# 测试路由是否正确
|
||||
curl http://localhost:8000/api/example/page -H "Authorization: Bearer <token>"
|
||||
```
|
||||
@@ -0,0 +1,174 @@
|
||||
# 3.4 验证器开发
|
||||
|
||||
:::tip 概述
|
||||
验证器用于校验请求参数的合法性,继承 ThinkPHP 的 Validate 类。通过在 Logic 层配置 `$validateClass` 属性,`add`/`update` 时自动触发校验,无需手动调用。
|
||||
:::
|
||||
|
||||
## 验证器文件位置
|
||||
|
||||
```text
|
||||
app/validate/ExampleValidate.php
|
||||
```
|
||||
|
||||
## 完整代码(来自项目实际代码)
|
||||
|
||||
```php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* 案例演示验证器
|
||||
*
|
||||
* 负责案例演示模块的入参校验,继承 ThinkPHP 的 Validate。
|
||||
*
|
||||
* 结构说明:
|
||||
* - $rule:字段校验规则,由必填字段自动推导;
|
||||
* - $message:自定义错误提示,按"字段.规则"生成中文提示;
|
||||
* - $scene:场景化校验,add 与 update 使用相同的字段集。
|
||||
*
|
||||
* 说明:唯一性校验未写入本验证器,
|
||||
* 由 ExampleLogic 的 uniqueFields 在逻辑层完成。
|
||||
*/
|
||||
class ExampleValidate extends Validate
|
||||
{
|
||||
/**
|
||||
* 校验规则
|
||||
*
|
||||
* 由生成器按字段的必填属性自动推导,
|
||||
* 通常形如 '字段名' => 'require'。
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $rule = [
|
||||
'name' => 'require',
|
||||
];
|
||||
|
||||
/**
|
||||
* 自定义错误提示
|
||||
*
|
||||
* 键为"字段名.规则名",值为中文提示语,
|
||||
* 覆盖框架默认提示,提升用户可读性。
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $message = [
|
||||
'name.require' => '请输入案例名称',
|
||||
];
|
||||
|
||||
/**
|
||||
* 校验场景
|
||||
*
|
||||
* add 与 update 使用相同的字段集,
|
||||
* 与项目其它验证器的统一模式保持一致。
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $scene = [
|
||||
'add' => ['name'], // 新增场景
|
||||
'update' => ['name'], // 修改场景
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## 在 Logic 中配置自动验证(推荐)
|
||||
|
||||
在 Logic 子类中配置 `$validateClass` 属性,`add`/`update` 时自动调用对应验证器:
|
||||
|
||||
```php
|
||||
// app/logic/ExampleLogic.php
|
||||
class ExampleLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Example::class;
|
||||
|
||||
/**
|
||||
* 参数验证器类
|
||||
*
|
||||
* 配置后,add/update 自动校验
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $validateClass = \app\validate\ExampleValidate::class;
|
||||
|
||||
// ... 其他属性
|
||||
}
|
||||
```
|
||||
|
||||
**工作原理:**
|
||||
|
||||
1. `add()` 流程:`beforeAdd` → `validateData($data, 'add')` → 后续入库
|
||||
2. `update()` 流程:`beforeUpdate` → `validateData($data, 'update', $id)` → 后续入库
|
||||
3. 验证器按场景(`add`/`update`)校验,场景不存在时使用默认规则
|
||||
4. 校验失败抛出 `ValidateException`,由 `ExceptionHandle` 统一返回错误响应
|
||||
|
||||
**覆盖范围:** 无论是 Controller 通过 `_add`/`_edit` 调用,还是 `import` 等内部调用都会生效。
|
||||
|
||||
## 在 Controller 中手动调用(特殊场景)
|
||||
|
||||
对于不走 `BaseLogic.add()`/`update()` 的特殊接口(如登录),可在 Controller 中手动调用:
|
||||
|
||||
```php
|
||||
// app/controller/LoginController.php
|
||||
public function login(): Json
|
||||
{
|
||||
$params = $this->getParams();
|
||||
try {
|
||||
$this->validate($params, LoginValidate::class . '.login');
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage(), 422);
|
||||
}
|
||||
// ... 登录逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 常用验证规则
|
||||
|
||||
| 规则 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `require` | 必填 | `'name' => 'require'` |
|
||||
| `max:n` | 最大长度 | `'title' => 'max:200'` |
|
||||
| `min:n` | 最小长度 | `'password' => 'min:6'` |
|
||||
| `number` | 数字 | `'sort' => 'number'` |
|
||||
| `integer` | 整数 | `'age' => 'integer'` |
|
||||
| `in:a,b,c` | 枚举值 | `'status' => 'in:0,1'` |
|
||||
| `email` | 邮箱格式 | `'email' => 'email'` |
|
||||
| `mobile` | 手机号格式 | `'mobile' => 'mobile'` |
|
||||
| `unique:table` | 唯一性 | `'username' => 'unique:user'` |
|
||||
|
||||
## 场景(Scene)用法
|
||||
|
||||
场景允许在不同接口使用不同的验证规则。项目统一使用 `$scene` 属性定义:
|
||||
|
||||
```php
|
||||
class UserValidate extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'username' => 'require|unique:user',
|
||||
'realname' => 'require',
|
||||
'mobile' => 'mobile',
|
||||
'email' => 'email',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'username.require' => '用户名不能为空',
|
||||
'username.unique' => '用户名已存在',
|
||||
'realname.require' => '姓名不能为空',
|
||||
];
|
||||
|
||||
// 场景属性:指定每个场景校验哪些字段
|
||||
protected $scene = [
|
||||
'add' => ['username', 'realname', 'mobile', 'email'],
|
||||
'update' => ['realname', 'mobile', 'email'], // 修改时不校验 username
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
**自动场景匹配:** Logic 配置了 `$validateClass` 后,`add()` 自动使用 `add` 场景,`update()` 自动使用 `update` 场景,无需手动指定。
|
||||
Reference in New Issue
Block a user