签入版本
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
# 6.7 文章管理
|
||||
|
||||
:::tip 概述
|
||||
文章管理模块支持富文本内容编辑、封面图上传、分类关联。继承 BaseTenantLogic 实现租户隔离,并配置了数据权限(按创建人过滤)。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 文件字段 | cover(封面图),保存到 article 目录 |
|
||||
| 富文本字段 | content(文章内容),自动处理临时文件 |
|
||||
| 枚举映射 | status → article_status 字典(0=正常 1=下架) |
|
||||
| 数据权限 | dataScopeUserField='create_user',按创建人过滤 |
|
||||
| 模糊搜索 | title、author |
|
||||
| 精确匹配 | category_id、status |
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/article/page` | sys:article:page | 分页列表 |
|
||||
| GET | `/api/article/list` | - | 全量列表 |
|
||||
| GET | `/api/article/detail/:id` | sys:article:detail | 详情 |
|
||||
| POST | `/api/article/add` | sys:article:add | 新增 |
|
||||
| PUT | `/api/article/update` | sys:article:update | 修改 |
|
||||
| DELETE | `/api/article/delete/:id` | sys:article:delete | 删除 |
|
||||
| DELETE | `/api/article/batchDelete` | sys:article:batchDelete | 批量删除 |
|
||||
|
||||
## 核心代码
|
||||
|
||||
```php
|
||||
// app/logic/ArticleLogic.php
|
||||
|
||||
class ArticleLogic extends BaseTenantLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Article::class;
|
||||
|
||||
/**
|
||||
* 需要自动处理的文件上传字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $fileFields = ['cover'];
|
||||
|
||||
/**
|
||||
* 文件保存的子目录
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $fileSaveDir = 'article';
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['title', 'author'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageEqFields = ['category_id', 'status'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = [
|
||||
['field' => 'tenant_id', 'type' => 'asc'],
|
||||
['field' => 'sort', 'type' => 'asc'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = [
|
||||
'status' => 'article_status',
|
||||
];
|
||||
|
||||
/**
|
||||
* 富文本字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $contentFields = ['content'];
|
||||
|
||||
/**
|
||||
* 数据权限:归属字段名(用于按创建人过滤)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $dataScopeUserField = 'create_user';
|
||||
}
|
||||
```
|
||||
|
||||
### Controller 代码
|
||||
|
||||
```php
|
||||
// app/controller/ArticleController.php
|
||||
|
||||
class ArticleController extends BaseController
|
||||
{
|
||||
protected ArticleLogic $logic;
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->logic = new ArticleLogic();
|
||||
}
|
||||
|
||||
#[Log('文章管理-查询分页记录', Log::TYPE_QUERY)]
|
||||
#[Permission('sys:article:page', '文章分页')]
|
||||
public function page(): Json
|
||||
{
|
||||
return parent::_index($this->logic);
|
||||
}
|
||||
|
||||
#[Log('文章管理-新增记录', Log::TYPE_ADD, '新增文章:{title}')]
|
||||
#[Permission('sys:article:add', '添加文章')]
|
||||
public function add(): Json
|
||||
{
|
||||
try {
|
||||
$id = $this->logic->add($this->getJsonBody());
|
||||
return $this->success(['id' => $id], '添加成功');
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[Log('文章管理-修改记录', Log::TYPE_UPDATE, '修改文章ID:{id}')]
|
||||
#[Permission('sys:article:update', '修改文章')]
|
||||
public function update(): Json
|
||||
{
|
||||
$data = $this->getJsonBody();
|
||||
$id = $data['id'] ?? 0;
|
||||
if (empty($id)) return $this->fail('缺少id参数');
|
||||
try {
|
||||
$this->logic->update($id, $data);
|
||||
return $this->success(null, '修改成功');
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[Log('文章管理-删除记录', Log::TYPE_DELETE, '删除文章ID:{id}')]
|
||||
#[Permission('sys:article:delete', '删除文章')]
|
||||
public function delete(int $id): Json
|
||||
{
|
||||
return parent::_remove($this->logic, $id);
|
||||
}
|
||||
|
||||
#[Log('文章管理-批量删除', Log::TYPE_DELETE)]
|
||||
#[Permission('sys:article:delete', '删除文章')]
|
||||
public function batchDelete(): Json
|
||||
{
|
||||
$ids = $this->getJsonBody()['ids'] ?? [];
|
||||
return parent::_batchRemove($this->logic, $ids);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 数据权限
|
||||
文章模块配置了 `dataScopeUserField = 'create_user'`,普通用户只能看到自己创建的文章。管理员(data_scope=1)可看到全部文章。
|
||||
:::
|
||||
|
||||
:::warning 富文本处理
|
||||
content 字段配置为富文本字段后,新增/修改时会自动处理内容中的临时文件:解析 `<img>` 标签的 src,临时文件迁移到正式目录,替换为 `[IMG_URL]` 占位符。读取时自动替换为实际域名。
|
||||
:::
|
||||
@@ -0,0 +1,168 @@
|
||||
# 6.10 分类管理
|
||||
|
||||
:::tip 概述
|
||||
分类管理支持树形分类数据,可用于文章分类、商品分类等场景。继承 BaseTenantLogic 实现租户隔离。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 唯一校验 | category_name + parent_id 复合唯一(同一父级下名称不可重复) |
|
||||
| 删除校验 | beforeDelete 检查子分类和关联文章 |
|
||||
| 模糊搜索 | category_name |
|
||||
| 精确匹配 | parent_id |
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE `think_category` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`category_name` varchar(100) NOT NULL COMMENT '分类名称',
|
||||
`category_code` varchar(100) DEFAULT NULL COMMENT '分类编码',
|
||||
`parent_id` int DEFAULT 0 COMMENT '上级ID',
|
||||
`sort` int DEFAULT 0 COMMENT '排序',
|
||||
`note` varchar(100) DEFAULT NULL COMMENT '分类备注',
|
||||
`create_user` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT (now()) COMMENT '创建时间',
|
||||
`update_user` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT (now()) COMMENT '更新时间',
|
||||
`is_delete` int DEFAULT 0 COMMENT '删除标识:0-正常 1-已删除',
|
||||
PRIMARY KEY (`id`)
|
||||
) COMMENT = '文章分类表';
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/category/page` | sys:category:page | 分页列表 |
|
||||
| GET | `/api/category/list` | sys:category:list | 全量列表 |
|
||||
| GET | `/api/category/detail/:id` | sys:category:detail | 详情 |
|
||||
| POST | `/api/category/add` | sys:category:add | 新增 |
|
||||
| PUT | `/api/category/update` | sys:category:update | 修改 |
|
||||
| DELETE | `/api/category/delete/:id` | sys:category:delete | 删除 |
|
||||
| DELETE | `/api/category/batchDelete` | sys:category:batchDelete | 批量删除 |
|
||||
|
||||
## 核心代码
|
||||
|
||||
```php
|
||||
// app/logic/CategoryLogic.php
|
||||
|
||||
class CategoryLogic extends BaseTenantLogic
|
||||
{
|
||||
protected string $modelClass = Category::class;
|
||||
protected array $pageLikeFields = ['category_name'];
|
||||
protected array $pageEqFields = ['parent_id'];
|
||||
protected array $pageOrderBy = [
|
||||
['field' => 'tenant_id', 'type' => 'asc'],
|
||||
['field' => 'sort', 'type' => 'asc'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 唯一性校验字段
|
||||
*
|
||||
* 同一父级下分类名称不可重复
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $uniqueFields = [['category_name', 'parent_id']];
|
||||
|
||||
/**
|
||||
* 删除前处理:检查子分类和关联文章
|
||||
*
|
||||
* @param int $id 待删除的分类 ID
|
||||
* @return void
|
||||
* @throws \Exception 当存在子分类或关联文章时抛出
|
||||
*/
|
||||
protected function beforeDelete(int $id): void
|
||||
{
|
||||
$childCount = Category::where('parent_id', $id)->count();
|
||||
if ($childCount > 0) {
|
||||
throw new \Exception('该分类下存在子分类,不可删除');
|
||||
}
|
||||
|
||||
$articleCount = Article::where('category_id', $id)->count();
|
||||
if ($articleCount > 0) {
|
||||
throw new \Exception('该分类下存在文章,不可删除');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Controller 代码
|
||||
|
||||
```php
|
||||
// app/controller/CategoryController.php
|
||||
|
||||
class CategoryController extends BaseController
|
||||
{
|
||||
protected CategoryLogic $logic;
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->logic = new CategoryLogic();
|
||||
}
|
||||
|
||||
#[Log('分类管理-查询分页记录', Log::TYPE_QUERY)]
|
||||
#[Permission('sys:category:page', '分类分页')]
|
||||
public function page(): Json
|
||||
{
|
||||
return parent::_index($this->logic);
|
||||
}
|
||||
|
||||
#[Log('分类管理-查询树形列表', Log::TYPE_QUERY)]
|
||||
#[Permission('sys:category:list', '分类列表')]
|
||||
public function list(): Json
|
||||
{
|
||||
return parent::_list($this->logic);
|
||||
}
|
||||
|
||||
#[Log('分类管理-新增记录', Log::TYPE_ADD, '新增分类:{category_name}')]
|
||||
#[Permission('sys:category:add', '添加分类')]
|
||||
public function add(): Json
|
||||
{
|
||||
try {
|
||||
$id = $this->logic->add($this->getJsonBody());
|
||||
return $this->success(['id' => $id], '添加成功');
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[Log('分类管理-修改记录', Log::TYPE_UPDATE, '修改分类ID:{id}')]
|
||||
#[Permission('sys:category:update', '修改分类')]
|
||||
public function update(): Json
|
||||
{
|
||||
$data = $this->getJsonBody();
|
||||
$id = $data['id'] ?? 0;
|
||||
if (empty($id)) return $this->fail('缺少id参数');
|
||||
try {
|
||||
$this->logic->update($id, $data);
|
||||
return $this->success(null, '修改成功');
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[Log('分类管理-删除记录', Log::TYPE_DELETE, '删除分类ID:{id}')]
|
||||
#[Permission('sys:category:delete', '删除分类')]
|
||||
public function delete(int $id): Json
|
||||
{
|
||||
return parent::_remove($this->logic, $id);
|
||||
}
|
||||
|
||||
#[Log('分类管理-批量删除', Log::TYPE_DELETE)]
|
||||
#[Permission('sys:category:delete', '删除分类')]
|
||||
public function batchDelete(): Json
|
||||
{
|
||||
$ids = $this->getJsonBody()['ids'] ?? [];
|
||||
return parent::_batchRemove($this->logic, $ids);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::warning 复合唯一性
|
||||
分类的唯一性校验使用复合格式 `[['category_name', 'parent_id']]`,表示同一父级下分类名称不可重复,但不同父级下可以有同名分类。
|
||||
:::
|
||||
@@ -0,0 +1,160 @@
|
||||
# 6.9 行政区划管理
|
||||
|
||||
:::tip 概述
|
||||
行政区划管理维护全国省市区街道四级数据,支持按父编码级联查询。继承 BaseLogic(系统共享,不隔离)。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,不隔离) |
|
||||
| 层级结构 | id/pid + parent_code 双重层级标识 |
|
||||
| 特殊接口 | getCityList 按父编码查询、getParent 获取省份列表 |
|
||||
| 详情钩子 | afterDetail 补全父级名称 |
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE `think_city` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`city_code` varchar(6) NOT NULL COMMENT '城市区号',
|
||||
`area_code` varchar(20) NOT NULL COMMENT '行政编码',
|
||||
`parent_code` varchar(20) DEFAULT NULL COMMENT '上级行政编码',
|
||||
`zip_code` varchar(6) NOT NULL COMMENT '邮政编码',
|
||||
`level` int DEFAULT 0 COMMENT '城市级别:0-省份 1-城市 2-县区 3-街道',
|
||||
`pid` int DEFAULT 0 COMMENT '上级城市ID',
|
||||
`name` varchar(150) NOT NULL COMMENT '城市名称',
|
||||
`short_name` varchar(150) NOT NULL COMMENT '城市简称',
|
||||
`full_name` varchar(150) DEFAULT NULL COMMENT '城市全称',
|
||||
`pinyin` varchar(150) DEFAULT NULL COMMENT '城市拼音',
|
||||
`lng` varchar(150) DEFAULT NULL COMMENT '城市经度',
|
||||
`lat` varchar(150) DEFAULT NULL COMMENT '城市纬度',
|
||||
`create_user` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT (now()) COMMENT '创建时间',
|
||||
`update_user` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT (now()) COMMENT '更新时间',
|
||||
`is_delete` int DEFAULT 0 COMMENT '删除标识:0-正常 1-已删除',
|
||||
PRIMARY KEY (`id`)
|
||||
) COMMENT = '城市行政区划表';
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/city/list` | sys:city:page | 城市列表 |
|
||||
| GET | `/api/city/detail/:id` | sys:city:detail | 详情 |
|
||||
| POST | `/api/city/add` | sys:city:add | 新增 |
|
||||
| PUT | `/api/city/update` | sys:city:update | 修改 |
|
||||
| DELETE | `/api/city/delete/:id` | sys:city:delete | 删除 |
|
||||
| GET | `/api/city/getCityList/:parentCode` | - | 按父编码获取子级城市 |
|
||||
| GET | `/api/city/getParent` | - | 获取省份列表(level=0) |
|
||||
| GET | `/api/city/findByParentCode/:parentCode` | - | 根据父编码查询子级城市 |
|
||||
|
||||
## 核心代码
|
||||
|
||||
```php
|
||||
// app/logic/CityLogic.php
|
||||
|
||||
class CityLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = City::class;
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name', 'full_name', 'pinyin'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageEqFields = ['level', 'pid'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = ['field' => 'id', 'type' => 'asc'];
|
||||
|
||||
/**
|
||||
* 详情查询后处理:补全父级名称
|
||||
*
|
||||
* @param int $id 城市 ID
|
||||
* @param array &$data 详情数据(引用传递)
|
||||
* @return void
|
||||
*/
|
||||
protected function afterDetail(int $id, array &$data): void
|
||||
{
|
||||
$pid = $data['pid'] ?? 0;
|
||||
if ($pid > 0) {
|
||||
$parent = City::where('id', $pid)->find();
|
||||
$data['parentName'] = $parent ? $parent->name : '';
|
||||
} else {
|
||||
$data['parentName'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 按父级行政编码获取子级城市
|
||||
public function getCityListByParentCode(string $parentCode): array
|
||||
{
|
||||
return City::where('parent_code', $parentCode)
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
// 获取省份列表(level=0)
|
||||
public function getParentList(): array
|
||||
{
|
||||
return City::where('level', 0)
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 前端级联选择
|
||||
|
||||
```typescript
|
||||
// ui/src/api/data/city.ts
|
||||
export function getCityList(parentCode: string) {
|
||||
return http.request({
|
||||
url: '/city/getCityList/' + parentCode,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 省 → 市 → 区 → 街道 级联加载
|
||||
// 选择省份后加载城市,选择城市后加载区县...
|
||||
```
|
||||
|
||||
## 模型辅助方法
|
||||
|
||||
City 模型提供两个静态方法,供导入导出使用:
|
||||
|
||||
| 方法 | 用途 | 示例 |
|
||||
|------|------|------|
|
||||
| `resolveCityNames(codes)` | 编码→名称 | `['320000000000','320100000000']` → `'江苏省,南京市'` |
|
||||
| `resolveCityCodes(info)` | 名称→编码 | `'江苏省,南京市,建邺区'` → `['320000000000','320100000000','320105000000','']` |
|
||||
|
||||
```php
|
||||
// 导出时:城市编码拼为名称字符串
|
||||
$cityInfo = City::resolveCityNames(['320000000000', '320100000000', '320105000000']);
|
||||
// 结果: "江苏省,南京市,建邺区"
|
||||
|
||||
// 导入时:城市名称反查为编码数组(逐级匹配,避免同名歧义)
|
||||
$codes = City::resolveCityCodes('江苏省,南京市,建邺区');
|
||||
// 结果: ['320000000000', '320100000000', '320105000000', '']
|
||||
```
|
||||
@@ -0,0 +1,190 @@
|
||||
# 6.8 公告与链接管理
|
||||
|
||||
:::tip 概述
|
||||
本节涵盖通知公告和友情链接两个模块,均继承 BaseTenantLogic 实现租户隔离。
|
||||
|
||||
> **说明:** 本文件因历史原因命名为 `content.md`,实际文档内容为通知公告(Notice)和友情链接(Link)两个独立模块,并不存在 "Content" 模块。
|
||||
:::
|
||||
|
||||
## 通知公告
|
||||
|
||||
### 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 文件字段 | cover(封面图),保存到 notice 目录 |
|
||||
| 富文本字段 | content(公告内容) |
|
||||
| 枚举映射 | type → notice_type(1=通知 2=公告),status → notice_status |
|
||||
| 排序 | 按 id 倒序(最新在前) |
|
||||
|
||||
### 核心代码
|
||||
|
||||
```php
|
||||
// app/logic/NoticeLogic.php
|
||||
|
||||
class NoticeLogic extends BaseTenantLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Notice::class;
|
||||
|
||||
/**
|
||||
* 需要自动处理的文件上传字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $fileFields = ['cover'];
|
||||
|
||||
/**
|
||||
* 文件保存的子目录
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $fileSaveDir = 'notice';
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['title'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageEqFields = ['type', 'status'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = [
|
||||
['field' => 'tenant_id', 'type' => 'asc'],
|
||||
['field' => 'id', 'type' => 'desc'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = [
|
||||
'type' => 'notice_type',
|
||||
'status' => 'notice_status',
|
||||
];
|
||||
|
||||
/**
|
||||
* 富文本字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $contentFields = ['content'];
|
||||
}
|
||||
```
|
||||
|
||||
### API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/notice/page` | sys:notice:page | 分页列表 |
|
||||
| GET | `/api/notice/list` | - | 全量列表 |
|
||||
| GET | `/api/notice/detail/:id` | sys:notice:detail | 详情 |
|
||||
| POST | `/api/notice/add` | sys:notice:add | 新增 |
|
||||
| PUT | `/api/notice/update` | sys:notice:update | 修改 |
|
||||
| DELETE | `/api/notice/delete/:id` | sys:notice:delete | 删除 |
|
||||
| DELETE | `/api/notice/batchDelete` | sys:notice:batchDelete | 批量删除 |
|
||||
|
||||
---
|
||||
|
||||
## 友情链接
|
||||
|
||||
### 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 文件字段 | image(友链Logo),保存到 link 目录 |
|
||||
| 枚举映射 | type → link_type,form → link_form,status → link_status |
|
||||
|
||||
### 核心代码
|
||||
|
||||
```php
|
||||
// app/logic/LinkLogic.php
|
||||
|
||||
class LinkLogic extends BaseTenantLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Link::class;
|
||||
|
||||
/**
|
||||
* 需要自动处理的文件上传字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $fileFields = ['image'];
|
||||
|
||||
/**
|
||||
* 文件保存的子目录
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $fileSaveDir = 'link';
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageEqFields = ['type', 'status'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = [
|
||||
['field' => 'tenant_id', 'type' => 'asc'],
|
||||
['field' => 'sort', 'type' => 'asc'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = [
|
||||
'type' => 'link_type',
|
||||
'form' => 'link_form',
|
||||
'status' => 'link_status',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/link/page` | sys:link:page | 分页列表 |
|
||||
| GET | `/api/link/detail/:id` | sys:link:detail | 详情 |
|
||||
| POST | `/api/link/add` | sys:link:add | 新增 |
|
||||
| PUT | `/api/link/update` | sys:link:update | 修改 |
|
||||
| DELETE | `/api/link/delete/:id` | sys:link:delete | 删除 |
|
||||
| DELETE | `/api/link/batchDelete` | sys:link:batchDelete | 批量删除 |
|
||||
@@ -0,0 +1,145 @@
|
||||
# 6.4 部门管理
|
||||
|
||||
:::tip 概述
|
||||
部门管理维护组织架构数据,支持树形结构,是数据权限(按部门过滤)的基础。继承 BaseTenantLogic 实现租户隔离。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 树形结构 | treeParentField='parent_id' |
|
||||
| 唯一校验 | name 全局唯一 |
|
||||
| 删除校验 | beforeDelete 检查是否存在子部门 |
|
||||
| 数据权限 | 被 UserLogic 的 dataScopeDeptField='dept_id' 引用 |
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE `think_dept` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`name` varchar(150) NOT NULL COMMENT '部门名称',
|
||||
`type` int DEFAULT 0 COMMENT '部门类型:1-公司 2-子公司 3-部门 4-小组',
|
||||
`parent_id` int DEFAULT 0 COMMENT '上级部门ID',
|
||||
`sort` int DEFAULT 0 COMMENT '部门排序',
|
||||
`note` varchar(255) DEFAULT NULL COMMENT '备注',
|
||||
`create_user` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT (now()) COMMENT '创建时间',
|
||||
`update_user` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT (now()) COMMENT '更新时间',
|
||||
`is_delete` int DEFAULT 0 COMMENT '删除标识:0-正常 1-已删除',
|
||||
PRIMARY KEY (`id`)
|
||||
) COMMENT = '部门表';
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/dept/list` | sys:dept:page | 树形列表 |
|
||||
| GET | `/api/dept/detail/:id` | sys:dept:detail | 详情 |
|
||||
| POST | `/api/dept/add` | sys:dept:add | 新增 |
|
||||
| PUT | `/api/dept/update` | sys:dept:update | 修改 |
|
||||
| DELETE | `/api/dept/delete/:id` | sys:dept:delete | 删除 |
|
||||
|
||||
## 核心代码
|
||||
|
||||
### Logic 配置
|
||||
|
||||
```php
|
||||
// app/logic/DeptLogic.php
|
||||
|
||||
class DeptLogic extends BaseTenantLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Dept::class;
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageEqFields = ['type'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = [
|
||||
['field' => 'tenant_id', 'type' => 'asc'],
|
||||
['field' => 'sort', 'type' => 'asc'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 唯一性校验字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $uniqueFields = ['name'];
|
||||
|
||||
/**
|
||||
* 删除前处理:检查是否存在子部门
|
||||
*
|
||||
* @param int $id 待删除的部门 ID
|
||||
* @return void
|
||||
* @throws \Exception 当存在子部门时抛出
|
||||
*/
|
||||
protected function beforeDelete(int $id): void
|
||||
{
|
||||
$childCount = Dept::where('parent_id', $id)->count();
|
||||
if ($childCount > 0) {
|
||||
throw new \Exception('该部门下存在子部门,不可删除');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 树形接口
|
||||
部门使用 `_list()` 而非 `_index()`,返回全量数据后由前端或 Logic 构建树形结构。
|
||||
:::
|
||||
|
||||
### Controller 示例
|
||||
|
||||
```php
|
||||
// app/controller/DeptController.php
|
||||
|
||||
class DeptController extends BaseController
|
||||
{
|
||||
protected DeptLogic $logic;
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->logic = new DeptLogic();
|
||||
}
|
||||
|
||||
#[Log('部门管理-查询树形列表', Log::TYPE_QUERY)]
|
||||
#[Permission('sys:dept:page', '部门列表')]
|
||||
public function list(): Json
|
||||
{
|
||||
return parent::_list($this->logic);
|
||||
}
|
||||
|
||||
#[Log('部门管理-新增记录', Log::TYPE_ADD, '新增部门:{name}')]
|
||||
#[Permission('sys:dept:add', '添加部门')]
|
||||
public function add(): Json
|
||||
{
|
||||
return parent::_add($this->logic, $this->getJsonBody());
|
||||
}
|
||||
|
||||
// ... 其他 CRUD 方法
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
# 6.14 文件模板管理
|
||||
|
||||
:::tip 概述
|
||||
文件模板管理用于维护系统中使用的文件模板,支持按编码查询模板。继承 BaseLogic(系统共享,不隔离)。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,不隔离) |
|
||||
| 文件字段 | file_path(模板文件),保存到 file_template 目录 |
|
||||
| 唯一校验 | code 全局唯一 |
|
||||
| 特殊接口 | getTemplateByCode 按编码查询模板 |
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/file/template/page` | sys:fileTemplate:page | 分页列表 |
|
||||
| GET | `/api/file/template/detail/:id` | sys:fileTemplate:detail | 详情 |
|
||||
| POST | `/api/file/template/add` | sys:fileTemplate:add | 新增 |
|
||||
| PUT | `/api/file/template/update` | sys:fileTemplate:update | 修改 |
|
||||
| DELETE | `/api/file/template/delete/:id` | sys:fileTemplate:delete | 删除 |
|
||||
| DELETE | `/api/file/template/batchDelete` | sys:fileTemplate:batchDelete | 批量删除 |
|
||||
| GET | `/api/file/template/getTemplateByCode/:code` | - | 按编码查询模板 |
|
||||
|
||||
## 核心代码
|
||||
|
||||
```php
|
||||
// app/logic/FileTemplateLogic.php
|
||||
|
||||
class FileTemplateLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = FileTemplate::class;
|
||||
|
||||
/**
|
||||
* 需要自动处理的文件上传字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $fileFields = ['file_path'];
|
||||
|
||||
/**
|
||||
* 文件保存的子目录
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $fileSaveDir = 'file_template';
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name', 'code'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
|
||||
|
||||
/**
|
||||
* 唯一性校验字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $uniqueFields = ['code'];
|
||||
|
||||
// 按编码查询模板
|
||||
public function getByCode(string $code): ?array
|
||||
{
|
||||
$result = FileTemplate::where('code', $code)->find();
|
||||
return $result ? $result->toArray() : null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Controller 代码
|
||||
|
||||
```php
|
||||
// app/controller/FileTemplateController.php
|
||||
|
||||
class FileTemplateController extends BaseController
|
||||
{
|
||||
protected FileTemplateLogic $logic;
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->logic = new FileTemplateLogic();
|
||||
}
|
||||
|
||||
#[Log('文件模板-查询分页记录', Log::TYPE_QUERY)]
|
||||
#[Permission('sys:fileTemplate:page', '模板分页')]
|
||||
public function page(): Json
|
||||
{
|
||||
return parent::_index($this->logic);
|
||||
}
|
||||
|
||||
#[Log('文件模板-新增记录', Log::TYPE_ADD, '新增模板:{name}')]
|
||||
#[Permission('sys:fileTemplate:add', '添加模板')]
|
||||
public function add(): Json
|
||||
{
|
||||
return parent::_add($this->logic, $this->getJsonBody());
|
||||
}
|
||||
|
||||
#[Log('文件模板-修改记录', Log::TYPE_UPDATE, '修改模板ID:{id}')]
|
||||
#[Permission('sys:fileTemplate:update', '修改模板')]
|
||||
public function update(): Json
|
||||
{
|
||||
$data = $this->getJsonBody();
|
||||
$id = $data['id'] ?? 0;
|
||||
if (empty($id)) return $this->fail('缺少id参数');
|
||||
return parent::_edit($this->logic, $id, $data);
|
||||
}
|
||||
|
||||
#[Log('文件模板-删除记录', Log::TYPE_DELETE, '删除模板ID:{id}')]
|
||||
#[Permission('sys:fileTemplate:delete', '删除模板')]
|
||||
public function delete(int $id): Json
|
||||
{
|
||||
return parent::_remove($this->logic, $id);
|
||||
}
|
||||
|
||||
// 按编码查询(无权限注解,已登录即可访问)
|
||||
#[Log('文件模板-按编码查询', Log::TYPE_QUERY)]
|
||||
public function getTemplateByCode(string $code): Json
|
||||
{
|
||||
$result = $this->logic->getByCode($code);
|
||||
if (!$result) {
|
||||
return $this->fail('模板不存在', 404);
|
||||
}
|
||||
return $this->success($result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 按编码查询
|
||||
`getTemplateByCode` 接口未配置权限注解,通常用于前端按编码读取模板配置的场景。
|
||||
:::
|
||||
@@ -0,0 +1,211 @@
|
||||
# 6.15 首页与个人中心
|
||||
|
||||
:::tip 概述
|
||||
首页与个人中心模块负责当前登录用户的信息获取、菜单加载、密码修改和个人信息更新。该模块不继承 BaseLogic,而是专用业务逻辑。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | 无(直接操作 Model) |
|
||||
| 用途 | 当前登录用户的自身操作 |
|
||||
| 权限 | 无需配置 #[Permission],已登录即可访问 |
|
||||
| 超级管理员 | uid=1 拥有全部权限 `*:*:*` |
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/index/getUser` | 获取当前登录用户信息(含角色和权限) |
|
||||
| GET | `/api/index/getMenus` | 获取当前用户菜单(树形) |
|
||||
| PUT | `/api/index/updatePassword` | 修改密码 |
|
||||
| PUT | `/api/index/updateProfile` | 更新个人信息 |
|
||||
|
||||
## 核心代码
|
||||
|
||||
### 获取用户信息
|
||||
|
||||
```php
|
||||
// app/logic/IndexLogic.php
|
||||
|
||||
public function getUser(int $userId): array
|
||||
{
|
||||
$user = User::with(['dept'])->find($userId);
|
||||
if (!$user) {
|
||||
throw new \Exception('用户不存在');
|
||||
}
|
||||
|
||||
$data = $user->toArray();
|
||||
|
||||
// 头像补全域名前缀
|
||||
if (!empty($data['avatar'])) {
|
||||
$data['avatar'] = get_file_url($data['avatar']);
|
||||
}
|
||||
|
||||
// 组装城市编码数组(province_code, city_code, district_code, street_code)
|
||||
$data['city'] = [
|
||||
$data['province_code'] ?? '',
|
||||
$data['city_code'] ?? '',
|
||||
$data['district_code'] ?? '',
|
||||
$data['street_code'] ?? '',
|
||||
];
|
||||
|
||||
// 获取角色列表(完整角色对象)
|
||||
$roleIds = UserRole::where('user_id', $userId)->column('role_id');
|
||||
if (!empty($roleIds)) {
|
||||
$data['roles'] = Role::whereIn('id', $roleIds)->select()->toArray();
|
||||
} else {
|
||||
$data['roles'] = [];
|
||||
}
|
||||
|
||||
// 获取权限列表
|
||||
if ($userId === 1) {
|
||||
$data['permissions'] = ['*:*:*'];
|
||||
} else {
|
||||
if (!empty($roleIds)) {
|
||||
$data['permissions'] = RoleMenu::alias('rm')
|
||||
->join('think_menu m', 'rm.menu_id = m.id')
|
||||
->whereIn('rm.role_id', $roleIds)
|
||||
->where('m.is_delete', 0)
|
||||
->where('m.type', 1)
|
||||
->column('m.permission');
|
||||
} else {
|
||||
$data['permissions'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// authorities 字段(预留)
|
||||
$data['authorities'] = null;
|
||||
|
||||
// 隐藏敏感字段
|
||||
unset($data['password'], $data['salt']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
```
|
||||
|
||||
:::info 返回结构说明
|
||||
- `roles`:完整角色对象数组(含 id、name、code 等),非仅 ID
|
||||
- `permissions`:权限码数组,超级管理员为 `['*:*:*']`
|
||||
- `city`:4 层城市编码数组 `[省, 市, 区, 街道]`
|
||||
- `avatar`:自动补全域名前缀
|
||||
- 字段名由 `Result::success()` 自动转为 camelCase 返回前端
|
||||
:::
|
||||
|
||||
### 获取菜单
|
||||
|
||||
```php
|
||||
public function getMenus(int $userId): array
|
||||
{
|
||||
// 超级管理员:全部菜单
|
||||
// type=0 仅返回可导航菜单(目录+页面),type=1 为权限按钮不返回
|
||||
if ($userId === 1) {
|
||||
$menus = Menu::where('status', 0)
|
||||
->where('hide', 0)
|
||||
->where('type', 0)
|
||||
->where('is_delete', 0)
|
||||
->order('sort', 'asc')
|
||||
->select()->toArray();
|
||||
return $this->buildTree($menus);
|
||||
}
|
||||
|
||||
// 普通用户:通过角色关联获取菜单
|
||||
$roleIds = UserRole::where('user_id', $userId)->column('role_id');
|
||||
if (empty($roleIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$menuIds = RoleMenu::whereIn('role_id', $roleIds)->column('menu_id');
|
||||
$menuIds = array_unique($menuIds);
|
||||
|
||||
if (empty($menuIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$menus = Menu::whereIn('id', $menuIds)
|
||||
->where('status', 0)
|
||||
->where('hide', 0)
|
||||
->where('type', 0)
|
||||
->where('is_delete', 0)
|
||||
->order('sort', 'asc')
|
||||
->select()->toArray();
|
||||
|
||||
return $this->buildTree($menus);
|
||||
}
|
||||
```
|
||||
|
||||
:::info 菜单查询条件
|
||||
获取菜单(非节点)数据时保证四个条件:`type=0`(可导航菜单)、`status=0`(启用)、`hide=0`(未隐藏)、`is_delete=0`(未删除)。
|
||||
:::
|
||||
|
||||
:::info 菜单类型说明
|
||||
代码中 `where('type', 0)` 用于筛选可导航的菜单项(目录和页面)。在当前实现中,`type=0` 表示可导航的菜单项(包括目录和页面),`type=1` 表示仅用于权限控制的按钮节点,不会出现在菜单树中。
|
||||
:::
|
||||
|
||||
### 修改密码
|
||||
|
||||
```php
|
||||
public function updatePassword(int $userId, string $oldPassword, string $newPassword): bool
|
||||
{
|
||||
$user = User::find($userId);
|
||||
|
||||
// 验证旧密码
|
||||
if (!PasswordService::verify($oldPassword, $user->password, $user->salt)) {
|
||||
throw new \Exception('旧密码错误');
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
$encrypted = PasswordService::encrypt($newPassword);
|
||||
$user->password = $encrypted['password'];
|
||||
$user->salt = $encrypted['salt'];
|
||||
return $user->save() !== false;
|
||||
}
|
||||
```
|
||||
|
||||
### 更新个人信息
|
||||
|
||||
```php
|
||||
public function updateProfile(int $userId, array $data): bool
|
||||
{
|
||||
$user = User::find($userId);
|
||||
|
||||
// 处理城市字段
|
||||
if (isset($data['city']) && is_array($data['city'])) {
|
||||
$data['province_code'] = $data['city'][0] ?? '';
|
||||
$data['city_code'] = $data['city'][1] ?? '';
|
||||
$data['district_code'] = $data['city'][2] ?? '';
|
||||
$data['street_code'] = $data['city'][3] ?? '';
|
||||
$data['city_info'] = City::resolveCityNames($data['city']);
|
||||
unset($data['city']);
|
||||
}
|
||||
|
||||
// 排除敏感字段
|
||||
unset($data['password'], $data['salt'], $data['role_id'], $data['tenant_id'], $data['id']);
|
||||
|
||||
return $user->save($data) !== false;
|
||||
}
|
||||
```
|
||||
|
||||
:::warning 安全设计
|
||||
updateProfile 会排除 `password`、`salt`、`role_id`、`tenant_id` 等敏感字段,防止用户越权修改自身权限。
|
||||
:::
|
||||
|
||||
## 前端调用
|
||||
|
||||
```typescript
|
||||
// 获取当前用户信息
|
||||
const userInfo = await getUserInfo();
|
||||
// userInfo.permissions = ['sys:user:page', 'sys:article:add', ...]
|
||||
// userInfo.roles = [1]
|
||||
|
||||
// 获取菜单(用于动态路由生成)
|
||||
const menus = await getMenuList();
|
||||
// menus = [{id:1, name:'系统管理', path:'/system', children:[...]}]
|
||||
|
||||
// 修改密码
|
||||
await updatePassword({ oldPassword: '123456', newPassword: '654321' });
|
||||
|
||||
// 更新个人信息
|
||||
await updateProfile({ realname: '张三', mobile: '13800138000' });
|
||||
```
|
||||
@@ -0,0 +1,788 @@
|
||||
# 6.11 定时任务
|
||||
|
||||
:::tip 概述
|
||||
定时任务系统由三部分组成:JobLogic(调度逻辑)+ BaseTask(任务处理器)+ JobRunCommand(命令行入口)。支持三种触发方式:Linux Crontab、Windows 计划任务、Daemon 守护进程。
|
||||
:::
|
||||
|
||||
## 系统架构
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 触发层(三选一) │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Linux Crontab│ │Windows 计划 │ │ Daemon 守护 │ │
|
||||
│ │ │ │任务 │ │ 进程 │ │
|
||||
│ │ * * * * * │ │ schtasks │ │ php think │ │
|
||||
│ │ php think │ │ │ │ job:daemon │ │
|
||||
│ │ job:run │ │ │ │ │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||
│ │ │ │ │
|
||||
│ └─────────────────┼─────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ JobRunCommand │ │
|
||||
│ │ php think job:run │ │
|
||||
│ │ 或 php think job:daemon│ │
|
||||
│ └────────────┬────────────┘ │
|
||||
└───────────────────────────┼─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 调度层(JobLogic) │
|
||||
│ │
|
||||
│ runPending() shouldRun() │
|
||||
│ ├─ 查询 status=1 的任务 ├─ 解析 cron 表达式 │
|
||||
│ ├─ 逐个判断是否到执行时间 ├─ 分 时 日 月 周 逐段匹配 │
|
||||
│ └─ 到期则执行 └─ 全部匹配返回 true │
|
||||
│ │
|
||||
│ executeJob($job) │
|
||||
│ ├─ 检查执行策略(立即/排队/放弃) │
|
||||
│ ├─ 调用 doExecute() 实际执行 │
|
||||
│ ├─ 记录 JobLog 执行日志 │
|
||||
│ └─ 返回执行结果 │
|
||||
└───────────────────────────┼─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 执行层 │
|
||||
│ │
|
||||
│ doExecute($job) │
|
||||
│ ├─ 判断 URL 类型 │
|
||||
│ │ │
|
||||
│ ├─ http:// / https:// → executeHttp() │
|
||||
│ │ └─ curl 发起 HTTP GET/POST 请求 │
|
||||
│ │ │
|
||||
│ └─ 其它 → executeInternal() │
|
||||
│ └─ 映射到 app\task\{Name}Task::run() │
|
||||
│ │
|
||||
│ 示例: │
|
||||
│ url = "SendSms" → app\task\SendSmsTask::run() │
|
||||
│ url = "Order/Close" → app\task\Order\CloseTask::run() │
|
||||
│ url = "https://..." → HTTP GET/POST │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 状态说明
|
||||
|
||||
| 状态值 | 说明 | 可执行 | 可暂停 | 可启动 |
|
||||
|--------|------|--------|--------|--------|
|
||||
| 0 | 未发布 | ✗ | ✗ | ✓ |
|
||||
| 1 | 运行中 | ✓ | ✓ | ✗ |
|
||||
| 2 | 暂停 | ✗ | ✗ | ✓ |
|
||||
| 3 | 已删除 | ✗ | ✗ | ✗ |
|
||||
|
||||
## 执行策略
|
||||
|
||||
| 策略值 | 说明 | 行为 |
|
||||
|--------|------|------|
|
||||
| 1 | 立即执行 | 丢弃之前的执行,立即运行 |
|
||||
| 2 | 执行一次 | 等待上一次完成后执行 |
|
||||
| 3 | 放弃执行 | 如果上一次还在运行则跳过 |
|
||||
|
||||
:::tip 枚举文本自动翻译
|
||||
JobLogic 配置了 `serializeMaps`,API 响应中会自动包含翻译后的文本字段:
|
||||
|
||||
| 原始字段 | 文本字段 | 字典编码 | 说明 |
|
||||
|----------|----------|----------|------|
|
||||
| `status` | `statusText` | `job_status` | 任务状态(未发布/运行中/暂停/删除) |
|
||||
| `execute_policy` | `executePolicyText` | `job_execute_policy` | 执行策略(立即执行/执行一次/放弃执行) |
|
||||
|
||||
字典数据在代码生成器自动创建,也可在「系统管理 → 字典管理」中手动维护。
|
||||
:::
|
||||
|
||||
## Cron 表达式语法
|
||||
|
||||
标准 5 段格式:`分 时 日 月 周`
|
||||
|
||||
```text
|
||||
┌────────── 分钟(0-59)
|
||||
│ ┌──────── 小时(0-23)
|
||||
│ │ ┌────── 日(1-31)
|
||||
│ │ │ ┌──── 月(1-12)
|
||||
│ │ │ │ ┌── 周(0-6,0=周日)
|
||||
│ │ │ │ │
|
||||
* * * * *
|
||||
```
|
||||
|
||||
| 表达式 | 说明 |
|
||||
|--------|------|
|
||||
| `* * * * *` | 每分钟 |
|
||||
| `0 * * * *` | 每小时整点 |
|
||||
| `0 0 * * *` | 每天 0 点 |
|
||||
| `0 2 * * *` | 每天凌晨 2 点 |
|
||||
| `0 0 * * 1` | 每周一 0 点 |
|
||||
| `0 0 1 * *` | 每月 1 号 0 点 |
|
||||
| `*/5 * * * *` | 每 5 分钟 |
|
||||
| `0 */2 * * *` | 每 2 小时 |
|
||||
| `0 9-18 * * 1-5` | 工作日 9-18 点每小时 |
|
||||
| `0 2 1,15 * *` | 每月 1 号和 15 号凌晨 2 点 |
|
||||
|
||||
---
|
||||
|
||||
## 方式一:Linux Crontab(推荐)
|
||||
|
||||
:::tip 适用场景
|
||||
Linux / macOS 服务器,最常用的定时任务触发方式。系统级 cron 每分钟触发一次 PHP 命令。
|
||||
:::
|
||||
|
||||
### 配置步骤
|
||||
|
||||
#### 1. 编辑 crontab
|
||||
|
||||
```bash
|
||||
crontab -e
|
||||
```
|
||||
|
||||
#### 2. 添加定时任务
|
||||
|
||||
```bash
|
||||
# 每分钟执行一次(推荐)
|
||||
* * * * * cd /www/api && php think job:run >> /dev/null 2>&1
|
||||
|
||||
# 每分钟执行一次,日志写入文件(调试用)
|
||||
* * * * * cd /www/api && php think job:run >> /var/log/job.log 2>&1
|
||||
|
||||
# 每分钟执行一次,带环境变量
|
||||
* * * * * cd /www/api && /usr/bin/php think job:run >> /dev/null 2>&1
|
||||
```
|
||||
|
||||
:::warning 路径注意
|
||||
- `cd /www/api` 必须切换到项目根目录,否则 ThinkPHP 无法找到配置文件
|
||||
- `php` 建议使用绝对路径(如 `/usr/bin/php`),避免 cron 环境变量不同导致找不到 PHP
|
||||
- `>> /dev/null 2>&1` 表示丢弃输出,调试时可改为写入日志文件
|
||||
:::
|
||||
|
||||
#### 3. 保存并验证
|
||||
|
||||
```bash
|
||||
# 保存后 cron 自动生效
|
||||
|
||||
# 查看已配置的 cron
|
||||
crontab -l
|
||||
|
||||
# 查看 cron 日志(确认是否正常触发)
|
||||
tail -f /var/log/syslog | grep CRON
|
||||
|
||||
# 手动执行一次验证
|
||||
cd /www/api && php think job:run
|
||||
```
|
||||
|
||||
#### 4. 权限设置
|
||||
|
||||
```bash
|
||||
# 确保 PHP 有权限执行
|
||||
chmod +x /usr/bin/php
|
||||
|
||||
# 确保项目目录权限
|
||||
chmod -R 755 /www/api/runtime/
|
||||
```
|
||||
|
||||
### Crontab 常用命令
|
||||
|
||||
```bash
|
||||
crontab -e # 编辑当前用户的 cron
|
||||
crontab -l # 列出当前用户的 cron
|
||||
crontab -r # 删除当前用户的所有 cron(慎用)
|
||||
crontab -u username -l # 查看指定用户的 cron
|
||||
```
|
||||
|
||||
### 系统 cron 目录
|
||||
|
||||
```text
|
||||
/etc/crontab # 系统级 cron 配置
|
||||
/etc/cron.d/ # 额外的 cron 配置文件目录
|
||||
/etc/cron.daily/ # 每天执行的脚本
|
||||
/etc/cron.hourly/ # 每小时执行的脚本
|
||||
/etc/cron.weekly/ # 每周执行的脚本
|
||||
/etc/cron.monthly/ # 每月执行的脚本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 方式二:Windows 计划任务
|
||||
|
||||
:::tip 适用场景
|
||||
Windows Server 或本地开发环境,通过任务计划程序定时触发。
|
||||
:::
|
||||
|
||||
### 方式 A:图形界面配置
|
||||
|
||||
#### 1. 打开任务计划程序
|
||||
|
||||
```text
|
||||
方法1:Win + R → 输入 taskschd.msc → 回车
|
||||
方法2:控制面板 → 管理工具 → 任务计划程序
|
||||
方法3:开始菜单搜索"任务计划程序"
|
||||
```
|
||||
|
||||
#### 2. 创建基本任务
|
||||
|
||||
1. 右侧点击"创建基本任务"
|
||||
2. 名称:`RXThinkCMF 定时任务`
|
||||
3. 描述:`每分钟执行一次定时任务扫描`
|
||||
4. 触发器:选择"每天"
|
||||
5. 开始时间:设置当前时间
|
||||
6. 重复任务间隔:`1 分钟`
|
||||
7. 持续时间:`无限期`
|
||||
8. 操作:选择"启动程序"
|
||||
9. 程序或脚本:
|
||||
|
||||
```text
|
||||
C:\xampp\php\php.exe
|
||||
```
|
||||
|
||||
10. 添加参数:
|
||||
|
||||
```text
|
||||
think job:run
|
||||
```
|
||||
|
||||
11. 起始于:
|
||||
|
||||
```text
|
||||
D:\xampp\htdocs\v3\thinkphp6
|
||||
```
|
||||
|
||||
12. 勾选"不管用户是否登录都要运行"
|
||||
13. 点击完成
|
||||
|
||||
#### 3. 验证
|
||||
|
||||
```powershell
|
||||
# 手动运行测试
|
||||
cd D:\xampp\htdocs\v3\thinkphp6
|
||||
C:\xampp\php\php.exe think job:run
|
||||
|
||||
# 在任务计划程序中右键任务 → 运行
|
||||
# 查看"上次运行结果"是否为 0x0
|
||||
```
|
||||
|
||||
### 方式 B:命令行配置(schtasks)
|
||||
|
||||
```powershell
|
||||
# 创建每分钟执行的任务
|
||||
schtasks /create /tn "RXThinkCMF_Job" /tr "C:\xampp\php\php.exe think job:run" /sc minute /mo 1 /st 00:00 /f
|
||||
|
||||
# 参数说明:
|
||||
# /tn 任务名称
|
||||
# /tr 要执行的命令
|
||||
# /sc 频率:minute(分钟)、hourly(小时)、daily(天)
|
||||
# /mo 间隔:1 表示每 1 分钟
|
||||
# /st 开始时间
|
||||
# /f 强制覆盖同名任务
|
||||
|
||||
# 创建任务(指定工作目录)
|
||||
schtasks /create /tn "RXThinkCMF_Job" /tr "cmd /c cd /d D:\xampp\htdocs\v3\thinkphp6 && C:\xampp\php\php.exe think job:run" /sc minute /mo 1 /f
|
||||
|
||||
# 查询任务
|
||||
schtasks /query /tn "RXThinkCMF_Job"
|
||||
|
||||
# 手动运行
|
||||
schtasks /run /tn "RXThinkCMF_Job"
|
||||
|
||||
# 删除任务
|
||||
schtasks /delete /tn "RXThinkCMF_Job" /f
|
||||
|
||||
# 查看所有任务
|
||||
schtasks /query /fo table
|
||||
```
|
||||
|
||||
### Windows 注意事项
|
||||
|
||||
:::warning 常见问题
|
||||
1. **路径空格**:如果路径含空格,需要用引号包裹
|
||||
2. **PHP 路径**:使用 `php.exe` 的完整路径
|
||||
3. **工作目录**:必须设置为项目根目录
|
||||
4. **权限**:确保任务以有权限的用户身份运行
|
||||
5. **日志**:可在参数中添加 `>> log.txt 2>&1` 重定向输出
|
||||
:::
|
||||
|
||||
```powershell
|
||||
# 带日志输出的完整命令
|
||||
schtasks /create /tn "RXThinkCMF_Job" /tr "cmd /c cd /d D:\xampp\htdocs\v3\thinkphp6 && C:\xampp\php\php.exe think job:run >> runtime\job.log 2>&1" /sc minute /mo 1 /f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 方式三:Daemon 守护进程
|
||||
|
||||
:::tip 适用场景
|
||||
需要更精确的执行时机(秒级),或不想依赖系统 cron 的场景。进程常驻内存,自行调度。
|
||||
:::
|
||||
|
||||
### 原理
|
||||
|
||||
```text
|
||||
Daemon 进程启动
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ while (true) │ 无限循环
|
||||
│ │ │
|
||||
│ ├─ runPending() │ 扫描并执行到期任务
|
||||
│ ├─ sleep(60) │ 休眠 60 秒
|
||||
│ └─ 继续循环 │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### 实现方式
|
||||
|
||||
#### 方式 A:使用 Supervisor 管理(推荐生产环境)
|
||||
|
||||
创建命令文件:
|
||||
|
||||
```php
|
||||
// app/command/JobDaemonCommand.php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use app\logic\JobLogic;
|
||||
|
||||
/**
|
||||
* 定时任务守护进程命令
|
||||
*
|
||||
* 常驻内存运行,每分钟扫描并执行到期任务。
|
||||
* 建议通过 Supervisor 管理,确保进程异常退出后自动重启。
|
||||
*
|
||||
* 用法:php think job:daemon
|
||||
*/
|
||||
class JobDaemonCommand extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('job:daemon')
|
||||
->setDescription('定时任务守护进程(常驻内存)');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output): void
|
||||
{
|
||||
$output->writeln('[' . date('Y-m-d H:i:s') . '] Daemon 启动...');
|
||||
|
||||
$logic = new JobLogic();
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
$count = $logic->runPending();
|
||||
if ($count > 0) {
|
||||
$output->writeln('[' . date('Y-m-d H:i:s') . '] 执行了 ' . $count . ' 个任务');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('[' . date('Y-m-d H:i:s') . '] 异常: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// 休眠 60 秒
|
||||
sleep(60);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supervisor 配置:
|
||||
|
||||
```ini
|
||||
; /etc/supervisor/conf.d/rxthinkcmf-job.conf
|
||||
|
||||
[program:rxthinkcmf-job]
|
||||
command=php /www/api/think job:daemon
|
||||
directory=/www/api
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=www-data
|
||||
numprocs=1
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/rxthinkcmf-job.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stdout_logfile_backups=5
|
||||
```
|
||||
|
||||
启动:
|
||||
|
||||
```bash
|
||||
# 重新加载配置
|
||||
sudo supervisorctl reread
|
||||
sudo supervisorctl update
|
||||
|
||||
# 启动
|
||||
sudo supervisorctl start rxthinkcmf-job
|
||||
|
||||
# 查看状态
|
||||
sudo supervisorctl status rxthinkcmf-job
|
||||
|
||||
# 重启
|
||||
sudo supervisorctl restart rxthinkcmf-job
|
||||
|
||||
# 查看日志
|
||||
tail -f /var/log/rxthinkcmf-job.log
|
||||
```
|
||||
|
||||
#### 方式 B:Windows CMD 运行
|
||||
|
||||
在项目根目录打开 CMD,执行以下命令:
|
||||
|
||||
```cmd
|
||||
:: 启动 Daemon(前台运行,关闭窗口则停止)
|
||||
php think job:daemon
|
||||
|
||||
:: 停止 Daemon(另开一个 CMD 窗口执行)
|
||||
php think job:daemon --stop
|
||||
```
|
||||
|
||||
**启动成功输出:**
|
||||
|
||||
```text
|
||||
[2026-09-23 09:06:07] Daemon 已启动 (PID: 22720)
|
||||
[2026-09-23 09:06:07] 每分钟检查一次到期任务,按 Ctrl+C 优雅退出
|
||||
```
|
||||
|
||||
**运行过程中的日志输出:**
|
||||
|
||||
```text
|
||||
[2026-09-23 11:25:00] [执行] 定时任务演示 (cron: */5 * * * *)
|
||||
[2026-09-23 11:25:00] [执行] 定时任务案例 (cron: */5 * * * *)
|
||||
[2026-09-23 11:25:00] 本轮执行完成,共执行 2 个任务
|
||||
[2026-09-23 11:30:00] [执行] 定时任务演示 (cron: */5 * * * *)
|
||||
[2026-09-23 11:30:00] [跳过] 定时任务案例 - 当前分钟已执行过
|
||||
[2026-09-23 11:30:00] 本轮执行完成,共执行 1 个任务
|
||||
```
|
||||
|
||||
日志类型说明:
|
||||
|
||||
| 标记 | 含义 |
|
||||
|------|------|
|
||||
| `[执行]` | 任务已触发执行,括号内显示 cron 表达式 |
|
||||
| `[跳过]` | 同一分钟内该任务已执行过,自动跳过(防重复) |
|
||||
| `[失败]` | 任务执行异常,后跟错误信息 |
|
||||
| `本轮执行完成` | 当前分钟的所有任务扫描完毕,显示执行数量 |
|
||||
|
||||
:::tip 提示
|
||||
- 没有到期任务时,Daemon 不输出任何日行(静默等待),属于正常行为
|
||||
- `[跳过]` 是防重复机制,同一任务在同一分钟内只会执行一次
|
||||
- 出现 `[失败]` 时检查对应的任务处理器代码,单个任务失败不影响其他任务
|
||||
:::
|
||||
|
||||
**前端联动:** Daemon 启动后,后台管理系统的「定时任务」模块会自动检测到守护进程状态,页面顶部显示:
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ [新增] [删除] [🟢 守护进程运行中 (PID: 22720)] │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 绿色「守护进程运行中」按钮 — 表示 Daemon 正常工作,鼠标悬停可查看 PID
|
||||
- 灰色「守护进程未运行」— 表示 Daemon 未启动或已停止
|
||||
|
||||
:::tip 状态检测原理
|
||||
前端通过 `GET /api/job/daemonStatus` 接口轮询状态,后端读取 `runtime/job_daemon.lock` 锁文件中的 PID 并检查进程是否存活。因此即使 Daemon 非正常退出(如强制杀进程),只要锁文件残留,前端仍可能显示「运行中」,此时需手动删除 `runtime/job_daemon.lock` 或执行 `php think job:daemon --stop` 清理。
|
||||
:::
|
||||
|
||||
:::warning Windows 注意事项
|
||||
1. **无信号扩展**:Windows 不支持 `pcntl_signal`,`Ctrl+C` 会强制终止进程而非优雅退出
|
||||
2. **停止方式**:推荐使用 `php think job:daemon --stop`,或关闭 CMD 窗口
|
||||
3. **防重复启动**:命令内部通过文件锁(`runtime/job_daemon.lock`)防止重复启动,启动前会自动检测并提示 `Daemon 已在运行中 (PID: xxx)`
|
||||
4. **后台运行**:如需后台运行,可使用 `start /B php think job:daemon`,停止时用 `php think job:daemon --stop`
|
||||
5. **窗口关闭即停止**:前台运行时关闭 CMD 窗口会终止进程,生产环境建议配合 Windows 服务或计划任务守护
|
||||
:::
|
||||
|
||||
#### 方式 C:nohup 后台运行(简单场景)
|
||||
|
||||
```bash
|
||||
# 后台启动
|
||||
nohup php /www/api/think job:daemon >> /var/log/job.log 2>&1 &
|
||||
|
||||
# 查看进程
|
||||
ps aux | grep job:daemon
|
||||
|
||||
# 停止
|
||||
kill $(pgrep -f "job:daemon")
|
||||
```
|
||||
|
||||
#### 方式 D:systemd 服务(推荐)
|
||||
|
||||
```ini
|
||||
; /etc/systemd/system/rxthinkcmf-job.service
|
||||
|
||||
[Unit]
|
||||
Description=RXThinkCMF Job Daemon
|
||||
After=network.target mysql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
WorkingDirectory=/www/api
|
||||
ExecStart=/usr/bin/php /www/api/think job:daemon
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```bash
|
||||
# 启用并启动
|
||||
sudo systemctl enable rxthinkcmf-job
|
||||
sudo systemctl start rxthinkcmf-job
|
||||
|
||||
# 查看状态
|
||||
sudo systemctl status rxthinkcmf-job
|
||||
|
||||
# 查看日志
|
||||
sudo journalctl -u rxthinkcmf-job -f
|
||||
|
||||
# 重启
|
||||
sudo systemctl restart rxthinkcmf-job
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三种方式对比
|
||||
|
||||
| 特性 | Crontab | Windows 计划任务 | Daemon 守护进程 |
|
||||
|------|---------|-----------------|----------------|
|
||||
| 适用系统 | Linux / macOS | Windows | 全平台 |
|
||||
| 最小间隔 | 1 分钟 | 1 分钟 | 1 秒 |
|
||||
| 资源占用 | 低(按需启动) | 低(按需启动) | 常驻内存 |
|
||||
| 可靠性 | 高 | 高 | 高(配合 Supervisor) |
|
||||
| 配置复杂度 | 低 | 中 | 低 |
|
||||
| 日志追踪 | 需重定向 | 任务计划程序 | 自带输出 |
|
||||
| 推荐场景 | 生产环境 | Windows 生产 | 需要秒级精度 / 开发调试 |
|
||||
|
||||
:::tip 推荐选择
|
||||
- **Linux 生产环境**:Crontab(简单可靠)
|
||||
- **Windows 生产环境**:任务计划程序 或 Daemon 守护进程
|
||||
- **需要秒级精度**:Daemon + Supervisor
|
||||
- **Docker 环境**:Daemon(容器内运行)
|
||||
- **本地开发调试**:Daemon 前台运行(直接观察日志输出)
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 新建定时任务步骤
|
||||
|
||||
### 1. 创建任务处理器
|
||||
|
||||
```php
|
||||
<?php
|
||||
// app/task/CleanLogTask.php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\task;
|
||||
|
||||
/**
|
||||
* 清理过期日志任务
|
||||
*
|
||||
* 数据库 url 字段:CleanLog
|
||||
* 参数格式:{"days": 30}
|
||||
*/
|
||||
class CleanLogTask extends BaseTask
|
||||
{
|
||||
/**
|
||||
* 执行任务
|
||||
*
|
||||
* @param array $params 任务参数
|
||||
* @return string 执行结果消息
|
||||
* @throws \Exception 执行失败时抛出
|
||||
*/
|
||||
public function run(array $params = []): string
|
||||
{
|
||||
$days = $params['days'] ?? 30;
|
||||
$cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days"));
|
||||
|
||||
// 清理登录日志
|
||||
$loginCount = \app\model\LoginLog::where('create_time', '<', $cutoffDate)->delete();
|
||||
|
||||
// 清理操作日志
|
||||
$operCount = \app\model\OperationLog::where('create_time', '<', $cutoffDate)->delete();
|
||||
|
||||
// 清理任务日志
|
||||
$jobCount = \app\model\JobLog::where('start_time', '<', $cutoffDate)->delete();
|
||||
|
||||
return "已清理 {$days} 天前的日志:登录日志 {$loginCount} 条,操作日志 {$operCount} 条,任务日志 {$jobCount} 条";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 在数据库 job 表中配置
|
||||
|
||||
```sql
|
||||
INSERT INTO think_job (
|
||||
job_name, job_alias, job_group, job_trigger,
|
||||
url, params, cron_expression,
|
||||
execute_policy, status, is_sync,
|
||||
is_delete, create_user, create_time
|
||||
) VALUES (
|
||||
'清理过期日志', 'CleanLog', '系统维护', 'cron',
|
||||
'CleanLog', '{"days":30}', '0 2 * * *',
|
||||
2, 1, 0,
|
||||
0, 'admin', NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 3. 配置触发方式(三选一)
|
||||
|
||||
```bash
|
||||
# 方式一:Linux Crontab
|
||||
crontab -e
|
||||
# 添加:* * * * * cd /www/api && php think job:run >> /dev/null 2>&1
|
||||
|
||||
# 方式二:Windows 计划任务
|
||||
schtasks /create /tn "RXThinkCMF_Job" /tr "cmd /c cd /d D:\xampp\htdocs\v3\thinkphp6 && C:\xampp\php\php.exe think job:run" /sc minute /mo 1 /f
|
||||
|
||||
# 方式三:Daemon 守护进程
|
||||
php think job:daemon
|
||||
# 停止:php think job:daemon --stop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/job/page` | sys:job:page | 分页列表 |
|
||||
| GET | `/api/job/list` | sys:job:list | 全量列表 |
|
||||
| GET | `/api/job/detail/:id` | sys:job:detail | 详情 |
|
||||
| GET | `/api/job/runOnce/:id` | sys:job:execute | 手动执行一次 |
|
||||
| GET | `/api/job/pause/:id` | sys:job:pause | 暂停任务 |
|
||||
| GET | `/api/job/resume/:id` | sys:job:resume | 恢复任务 |
|
||||
| GET | `/api/job/daemonStatus` | - | 守护进程状态 |
|
||||
| POST | `/api/job/add` | sys:job:add | 新增任务 |
|
||||
| POST | `/api/job/status` | sys:job:update | 设置状态 |
|
||||
| PUT | `/api/job/update` | sys:job:update | 修改任务 |
|
||||
| DELETE | `/api/job/delete/:id` | sys:job:delete | 删除任务 |
|
||||
| DELETE | `/api/job/batchDelete` | sys:job:batchDelete | 批量删除 |
|
||||
|
||||
### 任务日志
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/job/log/page` | sys:job:log:page | 日志分页 |
|
||||
| GET | `/api/job/log/detail/:id` | sys:job:log:detail | 日志详情 |
|
||||
| DELETE | `/api/job/log/delete/:id` | sys:job:log:delete | 删除日志 |
|
||||
| DELETE | `/api/job/log/batchDelete` | sys:job:log:batchDelete | 批量删除 |
|
||||
|
||||
---
|
||||
|
||||
## 核心代码解析
|
||||
|
||||
### Cron 表达式匹配(JobLogic::matchCronField)
|
||||
|
||||
```php
|
||||
// app/logic/JobLogic.php
|
||||
|
||||
/**
|
||||
* 匹配 cron 字段
|
||||
*
|
||||
* 支持以下语法:
|
||||
* - *:匹配所有
|
||||
* - */n:每隔 n 个单位
|
||||
* - n-m:范围匹配
|
||||
* - n,m,...:逗号分隔的多值
|
||||
* - n:单个值精确匹配
|
||||
*/
|
||||
protected function matchCronField(string $cronField, int $current): bool
|
||||
{
|
||||
if ($cronField === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 处理 */n(每隔 n 分钟/小时等)
|
||||
if (preg_match('/^\*\/(\d+)$/', $cronField, $m)) {
|
||||
return $current % (int)$m[1] === 0;
|
||||
}
|
||||
|
||||
// 处理 n-m(范围)
|
||||
if (preg_match('/^(\d+)-(\d+)$/', $cronField, $m)) {
|
||||
return $current >= (int)$m[1] && $current <= (int)$m[2];
|
||||
}
|
||||
|
||||
// 处理逗号分隔的多个值
|
||||
if (strpos($cronField, ',') !== false) {
|
||||
$values = array_map('intval', explode(',', $cronField));
|
||||
return in_array($current, $values);
|
||||
}
|
||||
|
||||
// 单个值
|
||||
return (int)$cronField === $current;
|
||||
}
|
||||
```
|
||||
|
||||
### 防重复执行(JobLogic::hasRunThisMinute)
|
||||
|
||||
```php
|
||||
/**
|
||||
* 检查任务在当前分钟是否已执行过
|
||||
*
|
||||
* Daemon 模式下,防止同一分钟内重复执行。
|
||||
*/
|
||||
public function hasRunThisMinute(int $jobId): bool
|
||||
{
|
||||
$minuteStart = date('Y-m-d H:i') . ':00';
|
||||
$minuteEnd = date('Y-m-d H:i') . ':59';
|
||||
|
||||
return JobLog::where('job_id', $jobId)
|
||||
->where('start_time', '>=', $minuteStart)
|
||||
->where('start_time', '<=', $minuteEnd)
|
||||
->count() > 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 任务不执行?
|
||||
|
||||
A: 检查以下几点:
|
||||
1. 任务状态是否为 `1`(运行中)
|
||||
2. Cron 表达式格式是否正确(5 段)
|
||||
3. Crontab 是否已配置且 cron 服务已启动
|
||||
4. PHP 路径是否正确(建议使用绝对路径)
|
||||
5. 手动执行 `php think job:run` 是否正常
|
||||
|
||||
### Q: 任务重复执行?
|
||||
|
||||
A: Daemon 模式下检查 `hasRunThisMinute` 是否生效。Crontab 模式下确保只有一个 cron 在触发。
|
||||
|
||||
### Q: 任务执行超时?
|
||||
|
||||
A: 检查 PHP 的 `max_execution_time` 配置,长时间任务建议使用 Daemon 模式或设置 `set_time_limit(0)`。
|
||||
|
||||
### Q: 如何查看任务执行日志?
|
||||
|
||||
A: 两种方式:
|
||||
1. **控制台实时日志**:Daemon 前台运行时,`[执行]`、`[跳过]`、`[失败]` 等日志直接输出到终端
|
||||
2. **数据库持久化日志**:通过 API `/api/job/log/page` 查询,或直接查 `think_job_log` 表
|
||||
|
||||
### Q: Daemon 控制台没有新日志输出,是卡住了吗?
|
||||
|
||||
A: 不是。Daemon 每分钟检查一次,没有到期任务时静默等待,不输出任何日志。这是正常行为,说明当前没有任务需要执行。
|
||||
|
||||
### Q: Windows 下 Daemon 如何启动和停止?
|
||||
|
||||
A: 在项目根目录打开 CMD:
|
||||
|
||||
```cmd
|
||||
:: 启动
|
||||
php think job:daemon
|
||||
|
||||
:: 停止(另开一个 CMD 窗口)
|
||||
php think job:daemon --stop
|
||||
```
|
||||
|
||||
启动成功输出:
|
||||
|
||||
```text
|
||||
[2026-09-23 09:06:07] Daemon 已启动 (PID: 22720)
|
||||
[2026-09-23 09:06:07] 每分钟检查一次到期任务,按 Ctrl+C 优雅退出
|
||||
```
|
||||
|
||||
启动后,后台管理系统「定时任务」模块顶部会显示绿色「守护进程运行中」按钮(带 PID 提示),表示 Daemon 正常工作。
|
||||
|
||||
如提示 `Daemon 已在运行中 (PID: xxx)`,先执行 `php think job:daemon --stop` 停止旧进程,或手动删除 `runtime/job_daemon.lock` 文件。
|
||||
@@ -0,0 +1,148 @@
|
||||
# 6.5 职级管理
|
||||
|
||||
:::tip 概述
|
||||
职级管理维护用户的职级属性数据,支持 Excel 导入导出。继承 BaseTenantLogic 实现租户隔离。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 唯一校验 | name 全局唯一 |
|
||||
| 枚举映射 | status → level_status 字典(1=在用 2=停用) |
|
||||
| 导入导出 | 支持 Excel 导入导出 |
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/level/page` | sys:level:page | 分页列表 |
|
||||
| GET | `/api/level/list` | sys:level:list | 全量列表 |
|
||||
| GET | `/api/level/detail/:id` | sys:level:detail | 详情 |
|
||||
| POST | `/api/level/add` | sys:level:add | 新增 |
|
||||
| PUT | `/api/level/update` | sys:level:update | 修改 |
|
||||
| DELETE | `/api/level/delete/:id` | sys:level:delete | 删除 |
|
||||
| DELETE | `/api/level/batchDelete` | sys:level:batchDelete | 批量删除 |
|
||||
| GET | `/api/level/export` | sys:level:export | 导出 |
|
||||
| POST | `/api/level/import` | sys:level:import | 导入 |
|
||||
|
||||
## 核心代码
|
||||
|
||||
### Logic 配置
|
||||
|
||||
```php
|
||||
// app/logic/LevelLogic.php
|
||||
|
||||
class LevelLogic extends BaseTenantLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Level::class;
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageEqFields = ['status'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = [
|
||||
['field' => 'tenant_id', 'type' => 'asc'],
|
||||
['field' => 'sort', 'type' => 'asc'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 唯一性校验字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $uniqueFields = ['name'];
|
||||
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = [
|
||||
'status' => 'level_status',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### 导出方法
|
||||
|
||||
```php
|
||||
public function export(array $params = []): string
|
||||
{
|
||||
$list = $this->allList($params);
|
||||
|
||||
$headers = [
|
||||
'name' => '职级名称',
|
||||
'statusText' => '状态', // 枚举显示名
|
||||
'sort' => '排序',
|
||||
'create_user' => '创建人',
|
||||
'create_time' => '创建时间',
|
||||
];
|
||||
|
||||
return ExcelService::export($list, $headers, '职级数据.xlsx');
|
||||
}
|
||||
```
|
||||
|
||||
### 导入方法
|
||||
|
||||
```php
|
||||
public function import(string $filePath): array
|
||||
{
|
||||
$headerMap = [
|
||||
'职级名称' => 'name',
|
||||
'状态' => 'status',
|
||||
'排序' => 'sort',
|
||||
];
|
||||
|
||||
$data = ExcelService::importWithMap($filePath, $headerMap);
|
||||
|
||||
// 枚举字段反向映射:文字转数字
|
||||
foreach ($data as &$row) {
|
||||
if (!empty($row['status']) && !is_numeric($row['status'])) {
|
||||
$row['status'] = DictService::getValue('level_status', $row['status']);
|
||||
}
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$errors = [];
|
||||
foreach ($data as $index => $row) {
|
||||
try {
|
||||
$this->add($row);
|
||||
$count++;
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = [
|
||||
'row' => $index + 2,
|
||||
'name' => $row['name'] ?? '',
|
||||
'reason' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return ['count' => $count, 'errors' => $errors];
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 导入导出模式
|
||||
LevelLogic 是导入导出的标准实现示例。导出使用 `allList()` 获取全量数据,导入使用 `ExcelService::importWithMap()` 解析文件,枚举字段通过 `DictService::getValue()` 反向映射。
|
||||
:::
|
||||
@@ -0,0 +1,135 @@
|
||||
# 6.3 菜单管理
|
||||
|
||||
:::tip 概述
|
||||
菜单管理负责维护系统的菜单树和权限节点,是 RBAC 权限体系的基础数据。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,不隔离) |
|
||||
| 树形结构 | treeParentField='parent_id',支持树形列表 |
|
||||
| 菜单类型 | type: 0=目录/页面(可导航) 1=权限按钮(仅权限校验,不显示在菜单树) |
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/menu/list` | sys:menu:page | 树形列表 |
|
||||
| GET | `/api/menu/detail/:id` | sys:menu:detail | 详情 |
|
||||
| POST | `/api/menu/add` | sys:menu:add | 新增 |
|
||||
| PUT | `/api/menu/update` | sys:menu:update | 修改 |
|
||||
| DELETE | `/api/menu/delete/:id` | sys:menu:delete | 删除 |
|
||||
|
||||
## 菜单数据结构
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 100,
|
||||
"parentId": 0,
|
||||
"name": "系统管理",
|
||||
"type": 0,
|
||||
"path": "/system",
|
||||
"icon": "SettingOutlined",
|
||||
"sort": 10,
|
||||
"permission": "",
|
||||
"component": "",
|
||||
"target": 0,
|
||||
"hide": false,
|
||||
"children": [
|
||||
{
|
||||
"id": 101,
|
||||
"parentId": 100,
|
||||
"name": "用户管理",
|
||||
"type": 1,
|
||||
"path": "/user",
|
||||
"component": "system/user/index",
|
||||
"permission": "sys:user:page",
|
||||
"children": [
|
||||
{
|
||||
"id": 102,
|
||||
"parentId": 101,
|
||||
"name": "添加用户",
|
||||
"type": 2,
|
||||
"permission": "sys:user:add"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 字段说明
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `type` | 0=目录 1=菜单 2=按钮 |
|
||||
| `path` | 路由路径 |
|
||||
| `component` | 前端组件路径(type=1 时必填) |
|
||||
| `permission` | 权限标识(type=1,2 时填写) |
|
||||
| `target` | 打开方式:0=内部 1=iframe 2=外链 |
|
||||
| `hide` | 是否隐藏 |
|
||||
|
||||
## 核心代码
|
||||
|
||||
### Controller
|
||||
|
||||
```php
|
||||
// app/controller/MenuController.php
|
||||
|
||||
class MenuController extends BaseController
|
||||
{
|
||||
protected MenuLogic $logic;
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->logic = new MenuLogic();
|
||||
}
|
||||
|
||||
// 树形列表(支持 status 参数过滤)
|
||||
#[Log('菜单管理-查询树形列表', Log::TYPE_QUERY)]
|
||||
#[Permission('sys:menu:page', '菜单列表')]
|
||||
public function list(): Json
|
||||
{
|
||||
$where = [];
|
||||
$status = $this->getParam('status');
|
||||
if ($status !== null && $status !== '') {
|
||||
$where['status'] = $status;
|
||||
}
|
||||
return parent::_list($this->logic, $where);
|
||||
}
|
||||
|
||||
#[Log('菜单管理-新增记录', Log::TYPE_ADD, '新增菜单:{name}')]
|
||||
#[Permission('sys:menu:add', '添加菜单')]
|
||||
public function add(): Json
|
||||
{
|
||||
return parent::_add($this->logic, $this->getJsonBody());
|
||||
}
|
||||
|
||||
#[Log('菜单管理-修改记录', Log::TYPE_UPDATE, '修改菜单ID:{id}')]
|
||||
#[Permission('sys:menu:update', '修改菜单')]
|
||||
public function update(): Json
|
||||
{
|
||||
$data = $this->getJsonBody();
|
||||
$id = $data['id'] ?? 0;
|
||||
if (empty($id)) return $this->fail('缺少id参数');
|
||||
return parent::_edit($this->logic, $id, $data);
|
||||
}
|
||||
|
||||
#[Log('菜单管理-删除记录', Log::TYPE_DELETE, '删除菜单ID:{id}')]
|
||||
#[Permission('sys:menu:delete', '删除菜单')]
|
||||
public function delete(int $id): Json
|
||||
{
|
||||
return parent::_remove($this->logic, $id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 菜单类型
|
||||
菜单使用 `_list()` 而非 `_index()`,返回全量树形数据。`type` 字段区分:0=目录、1=菜单、2=按钮。前端只渲染 type=0 和 type=1 的节点。
|
||||
:::
|
||||
|
||||
:::warning 删除前校验
|
||||
删除菜单时,`MenuLogic::beforeDelete` 会检查是否存在子菜单(`think_menu` 中 `parent_id` 等于当前 ID 且未删除的记录),如有子菜单则抛出异常阻止删除。需先删除子菜单再删除父菜单。
|
||||
:::
|
||||
@@ -0,0 +1,115 @@
|
||||
# 6.12 参数管理
|
||||
|
||||
:::tip 概述
|
||||
参数管理维护系统级配置参数,如默认密码、系统名称等。继承 BaseLogic(系统共享,不隔离),通过 ParamService 读取。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,不隔离) |
|
||||
| 唯一校验 | code 全局唯一 |
|
||||
| 缓存 | ParamService 两级缓存(请求级 + 持久缓存,1小时过期) |
|
||||
| 模糊搜索 | name、code |
|
||||
| 精确匹配 | type、status |
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE `think_param` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`name` varchar(100) NOT NULL COMMENT '参数名称',
|
||||
`code` varchar(100) NOT NULL COMMENT '参数编码',
|
||||
`value` varchar(255) NOT NULL COMMENT '参数值',
|
||||
`type` int NOT NULL COMMENT '参数类型:0-系统 1-业务',
|
||||
`status` int NOT NULL COMMENT '参数状态:1-正常 2-禁用',
|
||||
`sort` int NOT NULL COMMENT '参数排序',
|
||||
`note` varchar(255) DEFAULT NULL COMMENT '参数备注',
|
||||
`create_user` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT (now()) COMMENT '创建时间',
|
||||
`update_user` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT (now()) COMMENT '更新时间',
|
||||
`is_delete` int DEFAULT 0 COMMENT '删除标识:0-正常 1-已删除',
|
||||
PRIMARY KEY (`id`)
|
||||
) COMMENT = '系统参数表';
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/param/page` | sys:param:page | 分页列表 |
|
||||
| GET | `/api/param/list` | sys:param:list | 全量列表 |
|
||||
| GET | `/api/param/detail/:id` | sys:param:detail | 详情 |
|
||||
| POST | `/api/param/add` | sys:param:add | 新增 |
|
||||
| PUT | `/api/param/update` | sys:param:update | 修改 |
|
||||
| DELETE | `/api/param/delete/:id` | sys:param:delete | 删除 |
|
||||
| DELETE | `/api/param/batchDelete` | sys:param:batchDelete | 批量删除 |
|
||||
|
||||
## 核心代码
|
||||
|
||||
```php
|
||||
// app/logic/ParamLogic.php
|
||||
|
||||
class ParamLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Param::class;
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name', 'code'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageEqFields = ['type', 'status'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = ['field' => 'sort', 'type' => 'asc'];
|
||||
|
||||
/**
|
||||
* 唯一性校验字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $uniqueFields = ['code'];
|
||||
}
|
||||
```
|
||||
|
||||
## 使用 ParamService 读取参数
|
||||
|
||||
```php
|
||||
use app\service\ParamService;
|
||||
|
||||
// 获取字符串参数
|
||||
$value = ParamService::getString('DEFAULT_PASSWORD', '123456');
|
||||
|
||||
// 获取整数参数
|
||||
$value = ParamService::getInt('MAX_LOGIN_ATTEMPTS', 5);
|
||||
|
||||
// 获取布尔参数
|
||||
$value = ParamService::getBool('ENABLE_SMS', false);
|
||||
|
||||
// 获取默认密码(从系统参数读取,默认 '123456')
|
||||
$password = ParamService::getDefaultPassword();
|
||||
```
|
||||
|
||||
:::tip 与配置管理的区别
|
||||
- **参数管理(Param)**:独立的键值对,适合系统级参数(默认密码、限流阈值)
|
||||
- **配置管理(Config)**:分组 + 配置项,适合网站配置(名称、Logo、SMTP)
|
||||
:::
|
||||
@@ -0,0 +1,170 @@
|
||||
# 6.6 岗位管理
|
||||
|
||||
:::tip 概述
|
||||
岗位管理维护用户的岗位属性数据。继承 BaseTenantLogic 实现租户隔离,是典型的零自定义方法模块——全部能力由属性配置自动装配。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 唯一校验 | name 全局唯一 |
|
||||
| 枚举映射 | status → position_status 字典(1=在用 2=停用) |
|
||||
| 自定义方法 | 无(全部由基类自动装配) |
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE `think_position` (
|
||||
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`name` varchar(255) NOT NULL COMMENT '岗位名称',
|
||||
`status` int DEFAULT 0 COMMENT '岗位状态:1-在用 2-停用',
|
||||
`sort` int DEFAULT 0 COMMENT '岗位排序',
|
||||
`create_user` varchar(50) DEFAULT NULL COMMENT '创建人',
|
||||
`create_time` datetime DEFAULT (now()) COMMENT '创建时间',
|
||||
`update_user` varchar(50) DEFAULT NULL COMMENT '更新人',
|
||||
`update_time` datetime DEFAULT (now()) COMMENT '更新时间',
|
||||
`is_delete` int DEFAULT 0 COMMENT '删除标识:0-正常 1-已删除',
|
||||
PRIMARY KEY (`id`)
|
||||
) COMMENT = '岗位表';
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/position/page` | sys:position:page | 分页列表 |
|
||||
| GET | `/api/position/list` | sys:position:list | 全量列表 |
|
||||
| GET | `/api/position/detail/:id` | sys:position:detail | 详情 |
|
||||
| POST | `/api/position/add` | sys:position:add | 新增 |
|
||||
| PUT | `/api/position/update` | sys:position:update | 修改 |
|
||||
| DELETE | `/api/position/delete/:id` | sys:position:delete | 删除 |
|
||||
| DELETE | `/api/position/batchDelete` | sys:position:batchDelete | 批量删除 |
|
||||
|
||||
## 核心代码
|
||||
|
||||
```php
|
||||
// app/logic/PositionLogic.php
|
||||
|
||||
class PositionLogic extends BaseTenantLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Position::class;
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageEqFields = ['status'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = [
|
||||
['field' => 'tenant_id', 'type' => 'asc'],
|
||||
['field' => 'sort', 'type' => 'asc'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 唯一性校验字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $uniqueFields = ['name'];
|
||||
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = [
|
||||
'status' => 'position_status',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Controller 代码
|
||||
|
||||
```php
|
||||
// app/controller/PositionController.php
|
||||
|
||||
class PositionController extends BaseController
|
||||
{
|
||||
protected PositionLogic $logic;
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->logic = new PositionLogic();
|
||||
}
|
||||
|
||||
#[Log('岗位管理-查询分页记录', Log::TYPE_QUERY)]
|
||||
#[Permission('sys:position:page', '岗位分页')]
|
||||
public function page(): Json
|
||||
{
|
||||
return parent::_index($this->logic);
|
||||
}
|
||||
|
||||
#[Log('岗位管理-查询全量列表', Log::TYPE_QUERY)]
|
||||
public function list(): Json
|
||||
{
|
||||
return parent::_list($this->logic);
|
||||
}
|
||||
|
||||
#[Log('岗位管理-查询详情', Log::TYPE_QUERY, '查询岗位ID:{id}')]
|
||||
public function detail(int $id): Json
|
||||
{
|
||||
return parent::_detail($this->logic, $id);
|
||||
}
|
||||
|
||||
#[Log('岗位管理-新增记录', Log::TYPE_ADD, '新增岗位:{name}')]
|
||||
#[Permission('sys:position:add', '添加岗位')]
|
||||
public function add(): Json
|
||||
{
|
||||
return parent::_add($this->logic, $this->getJsonBody());
|
||||
}
|
||||
|
||||
#[Log('岗位管理-修改记录', Log::TYPE_UPDATE, '修改岗位ID:{id}')]
|
||||
#[Permission('sys:position:update', '修改岗位')]
|
||||
public function update(): Json
|
||||
{
|
||||
$data = $this->getJsonBody();
|
||||
$id = $data['id'] ?? 0;
|
||||
if (empty($id)) return $this->fail('缺少id参数');
|
||||
return parent::_edit($this->logic, $id, $data);
|
||||
}
|
||||
|
||||
#[Log('岗位管理-删除记录', Log::TYPE_DELETE, '删除岗位ID:{id}')]
|
||||
#[Permission('sys:position:delete', '删除岗位')]
|
||||
public function delete(int $id): Json
|
||||
{
|
||||
return parent::_remove($this->logic, $id);
|
||||
}
|
||||
|
||||
#[Log('岗位管理-批量删除', Log::TYPE_DELETE)]
|
||||
#[Permission('sys:position:delete', '删除岗位')]
|
||||
public function batchDelete(): Json
|
||||
{
|
||||
$ids = $this->getJsonBody()['ids'] ?? [];
|
||||
return parent::_batchRemove($this->logic, $ids);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 极简 CRUD
|
||||
岗位模块是"零自定义方法"的典型示例——Logic 类只声明属性配置,Controller 调用父类通用方法,无需编写任何业务代码,即可获得完整的增删改查、分页、唯一性校验、枚举翻译能力。
|
||||
:::
|
||||
@@ -0,0 +1,183 @@
|
||||
# 6.2 角色管理
|
||||
|
||||
:::tip 概述
|
||||
角色管理是 RBAC 权限体系的核心,通过角色关联菜单实现权限分配。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,不隔离) |
|
||||
| 关联 | 与 Menu 多对多(通过 RoleMenu 中间表) |
|
||||
| 数据权限 | data_scope 字段控制(1=全部 2=本部门 3=仅本人) |
|
||||
|
||||
## 套餐化角色预置
|
||||
|
||||
系统内置三套租户角色模板,用于按套餐等级快速授权:
|
||||
|
||||
| 角色名称 | 定位 | 菜单范围 |
|
||||
|----------|------|----------|
|
||||
| 租户-标准版 | 基础功能 | 核心业务模块 |
|
||||
| 租户-专业版 | 进阶功能 | 核心 + 高级模块 |
|
||||
| 租户-旗舰版 | 全功能 | 全部菜单(最全) |
|
||||
|
||||
创建租户后,通过「角色管理 → 授权菜单」为对应角色分配菜单权限。详见 [租户管理](/zh/modules/tenant.html)。
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/role/page` | sys:role:page | 分页列表 |
|
||||
| GET | `/api/role/list` | sys:role:list | 全量列表 |
|
||||
| GET | `/api/role/detail/:id` | sys:role:detail | 详情 |
|
||||
| POST | `/api/role/add` | sys:role:add | 新增 |
|
||||
| PUT | `/api/role/update` | sys:role:update | 修改 |
|
||||
| DELETE | `/api/role/delete/:id` | sys:role:delete | 删除 |
|
||||
| DELETE | `/api/role/batchDelete` | sys:role:batchDelete | 批量删除 |
|
||||
| GET | `/api/role/menu/list/:id` | - | 获取角色菜单 |
|
||||
| POST | `/api/role/menu/save` | sys:role:authorize | 保存角色菜单 |
|
||||
|
||||
## 菜单权限分配
|
||||
|
||||
```php
|
||||
// app/logic/RoleLogic.php
|
||||
|
||||
/**
|
||||
* 保存角色菜单关联(事务保护)
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 开启事务
|
||||
* 2. 先删除该角色原有的全部菜单关联(全量覆盖式保存)
|
||||
* 3. 遍历新菜单 ID,组装关联记录
|
||||
* 4. 批量写入关联记录
|
||||
* 5. 提交事务
|
||||
*/
|
||||
public function saveMenus(int $roleId, array $menuIds): bool
|
||||
{
|
||||
Db::startTrans();
|
||||
try {
|
||||
RoleMenu::where('role_id', $roleId)->delete();
|
||||
|
||||
$data = [];
|
||||
foreach ($menuIds as $menuId) {
|
||||
$data[] = [
|
||||
'role_id' => $roleId,
|
||||
'menu_id' => $menuId,
|
||||
'create_user' => request()->userInfo->username ?? '',
|
||||
'create_time' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($data)) {
|
||||
(new RoleMenu())->saveAll($data);
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Controller 代码
|
||||
|
||||
```php
|
||||
// app/controller/RoleController.php
|
||||
|
||||
class RoleController extends BaseController
|
||||
{
|
||||
protected RoleLogic $logic;
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->logic = new RoleLogic();
|
||||
}
|
||||
|
||||
#[Log('角色管理-查询分页记录', Log::TYPE_QUERY)]
|
||||
#[Permission('sys:role:page', '角色分页')]
|
||||
public function page(): Json
|
||||
{
|
||||
return parent::_index($this->logic);
|
||||
}
|
||||
|
||||
#[Log('角色管理-查询全量列表', Log::TYPE_QUERY)]
|
||||
public function list(): Json
|
||||
{
|
||||
return parent::_list($this->logic);
|
||||
}
|
||||
|
||||
#[Log('角色管理-查询详情', Log::TYPE_QUERY, '查询角色ID:{id}')]
|
||||
public function detail(int $id): Json
|
||||
{
|
||||
return parent::_detail($this->logic, $id);
|
||||
}
|
||||
|
||||
#[Log('角色管理-新增记录', Log::TYPE_ADD, '新增角色:{name}')]
|
||||
#[Permission('sys:role:add', '添加角色')]
|
||||
public function add(): Json
|
||||
{
|
||||
return parent::_add($this->logic, $this->getJsonBody());
|
||||
}
|
||||
|
||||
#[Log('角色管理-修改记录', Log::TYPE_UPDATE, '修改角色ID:{id}')]
|
||||
#[Permission('sys:role:update', '修改角色')]
|
||||
public function update(): Json
|
||||
{
|
||||
$data = $this->getJsonBody();
|
||||
$id = $data['id'] ?? 0;
|
||||
if (empty($id)) {
|
||||
return $this->fail('缺少id参数');
|
||||
}
|
||||
return parent::_edit($this->logic, $id, $data);
|
||||
}
|
||||
|
||||
#[Log('角色管理-删除记录', Log::TYPE_DELETE, '删除角色ID:{id}')]
|
||||
#[Permission('sys:role:delete', '删除角色')]
|
||||
public function delete(int $id): Json
|
||||
{
|
||||
return parent::_remove($this->logic, $id);
|
||||
}
|
||||
|
||||
#[Log('角色管理-批量删除记录', Log::TYPE_DELETE)]
|
||||
#[Permission('sys:role:delete', '删除角色')]
|
||||
public function batchDelete(): Json
|
||||
{
|
||||
$ids = $this->getJsonBody()['ids'] ?? [];
|
||||
return parent::_batchRemove($this->logic, $ids);
|
||||
}
|
||||
|
||||
// 获取角色菜单(带选中状态)
|
||||
#[Log('角色管理-查询菜单权限', Log::TYPE_QUERY, '查询角色ID:{id}的菜单权限')]
|
||||
public function menuList(int $id): Json
|
||||
{
|
||||
$result = $this->logic->getMenuListWithChecked($id);
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
// 保存角色菜单
|
||||
#[Log('角色管理-保存菜单权限', Log::TYPE_AUTHORIZE, '保存角色ID:{roleId}的菜单权限')]
|
||||
#[Permission('sys:role:authorize', '授权角色菜单')]
|
||||
public function menuSave(): Json
|
||||
{
|
||||
$data = $this->getJsonBody();
|
||||
$roleId = $data['roleId'] ?? 0;
|
||||
$menuIds = $data['menuIds'] ?? [];
|
||||
if (empty($roleId)) {
|
||||
return $this->fail('缺少roleId参数');
|
||||
}
|
||||
try {
|
||||
$this->logic->saveMenus($roleId, $menuIds);
|
||||
return $this->success(null, '保存成功');
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::warning 权限生效
|
||||
权限分配后,拥有该角色的用户需重新登录才能获取最新权限。权限变更时相关缓存会自动失效。
|
||||
:::
|
||||
@@ -0,0 +1,214 @@
|
||||
# 6.13 租户管理
|
||||
|
||||
:::tip 概述
|
||||
租户管理是多租户体系的核心,支持租户的 CRUD、创建租户账号、查看租户用户。继承 BaseLogic(系统共享,租户表本身不隔离)。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,租户表不隔离) |
|
||||
| 特殊接口 | account(创建租户账号)、users(查看租户用户) |
|
||||
| 删除校验 | beforeDelete 检查用户数 + 保护默认租户 |
|
||||
| 编码生成 | afterAdd 自动生成租户编码(T+4位ID) |
|
||||
| 状态校验 | TenantMiddleware 校验租户状态和过期时间 |
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/tenant/page` | sys:tenant:page | 分页列表 |
|
||||
| GET | `/api/tenant/list` | sys:tenant:list | 全量列表 |
|
||||
| GET | `/api/tenant/detail/:id` | sys:tenant:detail | 详情 |
|
||||
| GET | `/api/tenant/users/:tenantId` | - | 租户用户列表 |
|
||||
| POST | `/api/tenant/add` | sys:tenant:add | 新增租户 |
|
||||
| POST | `/api/tenant/account` | sys:tenant:add | 创建租户账号 |
|
||||
| PUT | `/api/tenant/update` | sys:tenant:update | 修改租户 |
|
||||
| DELETE | `/api/tenant/delete/:id` | sys:tenant:delete | 删除租户 |
|
||||
| DELETE | `/api/tenant/batchDelete` | sys:tenant:batchDelete | 批量删除 |
|
||||
|
||||
## 核心代码
|
||||
|
||||
```php
|
||||
// app/logic/TenantLogic.php
|
||||
|
||||
class TenantLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Tenant::class;
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name', 'code', 'contact'];
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageEqFields = ['status'];
|
||||
|
||||
/**
|
||||
* 默认排序规则
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageOrderBy = ['field' => 'id', 'type' => 'desc'];
|
||||
|
||||
/**
|
||||
* 枚举显示名映射
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $serializeMaps = ['status' => 'tenant_status'];
|
||||
|
||||
/**
|
||||
* 修改前处理:禁止修改编码
|
||||
*
|
||||
* @param int $id 租户 ID
|
||||
* @param array $data 待更新的数据
|
||||
* @return array 过滤后的数据
|
||||
*/
|
||||
protected function beforeUpdate(int $id, array $data): array
|
||||
{
|
||||
// 租户编码由系统自动生成(T+4位ID),不允许手动修改
|
||||
unset($data['code']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增后处理:自动生成租户编码
|
||||
*
|
||||
* @param int $id 新增的租户 ID
|
||||
* @param array $data 新增时提交的原始数据
|
||||
* @return void
|
||||
*/
|
||||
protected function afterAdd(int $id, array $data): void
|
||||
{
|
||||
// 编码格式:T + 4位ID左补零,如 T0001、T0002;因为依赖自增ID,所以在新增完成后回写
|
||||
$code = 'T' . str_pad((string)$id, 4, '0', STR_PAD_LEFT);
|
||||
Tenant::where('id', $id)->update(['code' => $code]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除前处理:保护默认租户 + 检查用户数
|
||||
*
|
||||
* @param int $id 待删除的租户 ID
|
||||
* @return void
|
||||
* @throws \Exception 当为默认租户或存在用户时抛出
|
||||
*/
|
||||
protected function beforeDelete(int $id): void
|
||||
{
|
||||
// 默认租户不可删除
|
||||
if ($id == 1) {
|
||||
throw new \Exception('默认租户不可删除');
|
||||
}
|
||||
$userCount = User::where('tenant_id', $id)->where('is_delete', 0)->count();
|
||||
if ($userCount > 0) {
|
||||
throw new \Exception('该租户下存在用户,不可删除');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表查询后处理:批量补充租户用户数
|
||||
*
|
||||
* @param array &$records 记录列表(引用传递)
|
||||
* @param array $params 查询参数
|
||||
* @return void
|
||||
*/
|
||||
protected function afterPageList(array &$records, array $params): void
|
||||
{
|
||||
if (empty($records)) return;
|
||||
|
||||
// 一次性查询当前页所有租户的用户数量,避免 N+1
|
||||
$tenantIds = array_column($records, 'id');
|
||||
$counts = User::where('is_delete', 0)
|
||||
->whereIn('tenant_id', $tenantIds)
|
||||
->group('tenant_id')
|
||||
->column('count(*)', 'tenant_id');
|
||||
|
||||
foreach ($records as &$record) {
|
||||
$record['userCount'] = $counts[$record['id']] ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 列表返回 userCount
|
||||
|
||||
`/api/tenant/page` 和 `/api/tenant/list` 接口返回的每条记录中自动包含 `userCount` 字段,表示该租户下的用户数量。
|
||||
|
||||
:::tip 实现方式
|
||||
通过 `BaseLogic::afterPageList` 钩子实现,使用 `whereIn` + `group` 批量查询,避免 N+1 问题。
|
||||
:::
|
||||
|
||||
### 创建租户账号
|
||||
|
||||
```php
|
||||
// TenantLogic::createAccount()
|
||||
// 处理流程:
|
||||
// 1. 校验租户存在 + 用户数限制
|
||||
// 2. 注入租户ID,调用 UserLogic::add() 完成标准创建
|
||||
// (含编码自动生成、密码加密、头像处理、角色关联)
|
||||
```
|
||||
|
||||
:::tip 用户编码由 UserLogic 统一生成
|
||||
创建租户账号时不再由 TenantLogic 直接生成用户编码,
|
||||
而是通过 UserLogic::beforeAdd 统一生成,确保所有用户创建路径(直接添加、租户账号、导入)编码规则一致。
|
||||
:::
|
||||
|
||||
:::warning 默认租户保护
|
||||
默认租户(id=1)不可删除,编码不可修改。超级管理员(tenant_id=0)不受租户隔离限制,创建租户隔离数据时自动代入默认租户(tenant_id=1)。详见 [多租户隔离](/zh/core/tenant.html)。
|
||||
:::
|
||||
|
||||
## 套餐化角色预置
|
||||
|
||||
系统内置三套租户角色模板,分配不同的菜单权限,用于按套餐等级快速授权:
|
||||
|
||||
| 角色名称 | 定位 | 菜单范围 |
|
||||
|----------|------|----------|
|
||||
| 租户-标准版 | 基础功能 | 核心业务模块 |
|
||||
| 租户-专业版 | 进阶功能 | 核心 + 高级模块 |
|
||||
| 租户-旗舰版 | 全功能 | 全部菜单(最全) |
|
||||
|
||||
**使用方式:** 创建租户后,通过「角色管理 → 授权菜单」为对应角色分配菜单权限。创建租户管理员账号时,选择对应套餐角色即可完成授权。
|
||||
|
||||
:::tip 设计意图
|
||||
套餐角色解决了"每个租户都要手动配一遍菜单"的重复工作。管理员创建租户时只需选择套餐,角色权限即刻生效。
|
||||
:::
|
||||
|
||||
## 演示数据
|
||||
|
||||
系统内置一组演示数据,可直接登录体验多租户效果:
|
||||
|
||||
### 平台级
|
||||
|
||||
| 账号 | 密码 | 角色 | 说明 |
|
||||
|------|------|------|------|
|
||||
| admin | 123456 | 超级管理员 | 平台管理,可管理所有租户 |
|
||||
|
||||
### 租户级
|
||||
|
||||
| 账号 | 密码 | 所属租户 | 角色 | 说明 |
|
||||
|------|------|----------|------|------|
|
||||
| tenant001 | 123456 | 客户租户A | 租户管理员 | 可体验租户视角的功能与数据隔离 |
|
||||
|
||||
:::warning 演示环境注意
|
||||
以上账号仅用于开发和演示环境,生产环境部署后请立即修改密码或删除演示数据。
|
||||
:::
|
||||
|
||||
## 租户状态
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `status` | 0=禁用 1=启用 |
|
||||
| `expire_time` | 过期时间,过期后 TenantMiddleware 返回 403 |
|
||||
| `max_user_num` | 最大用户数限制 |
|
||||
@@ -0,0 +1,222 @@
|
||||
# 6.1 用户管理
|
||||
|
||||
:::tip 概述
|
||||
用户管理是系统核心模块,继承 BaseTenantLogic 实现租户隔离,支持 CRUD、重置密码、Excel 导入导出。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 租户隔离 | 继承 BaseTenantLogic,自动按 tenant_id 过滤 |
|
||||
| 数据权限 | 配置 dataScopeDeptField='dept_id',支持按部门过滤 |
|
||||
| 编码生成 | beforeAdd 自动生成用户编码(租户编码-6位序号,如 T0001-000020) |
|
||||
| 文件字段 | avatar(头像),保存到 user/avatar 目录 |
|
||||
| 唯一校验 | username、mobile 全局唯一 |
|
||||
| 枚举映射 | gender→gender 字典,status→user_status 字典 |
|
||||
| 密码加密 | bcrypt 双重加盐,新增时自动加密 |
|
||||
| 角色关联 | 通过 UserRole 中间表关联,支持多角色 |
|
||||
|
||||
## 文件清单
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `app/controller/UserController.php` | 控制器(9 个接口) |
|
||||
| `app/logic/UserLogic.php` | 逻辑层(含 import/export/resetPassword) |
|
||||
| `app/model/User.php` | 模型 |
|
||||
| `app/model/UserRole.php` | 用户角色关联模型 |
|
||||
| `app/validate/UserValidate.php` | 验证器 |
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| GET | `/api/user/page` | sys:user:page | 分页列表 |
|
||||
| GET | `/api/user/detail/:id` | sys:user:detail | 详情 |
|
||||
| POST | `/api/user/add` | sys:user:add | 新增 |
|
||||
| PUT | `/api/user/update` | sys:user:update | 修改 |
|
||||
| PUT | `/api/user/resetPwd` | sys:user:resetPwd | 重置密码 |
|
||||
| DELETE | `/api/user/delete/:id` | sys:user:delete | 删除 |
|
||||
| DELETE | `/api/user/batchDelete` | sys:user:batchDelete | 批量删除 |
|
||||
| POST | `/api/user/import` | sys:user:import | 导入 |
|
||||
| GET | `/api/user/export` | sys:user:export | 导出 |
|
||||
|
||||
## 特殊逻辑
|
||||
|
||||
### 重置密码
|
||||
|
||||
```php
|
||||
// app/controller/UserController.php
|
||||
|
||||
public function resetPwd(): Json
|
||||
{
|
||||
$data = $this->getJsonBody();
|
||||
$id = $data['userId'] ?? 0;
|
||||
|
||||
if (empty($id)) {
|
||||
return $this->fail('缺少id参数');
|
||||
}
|
||||
|
||||
$this->logic->resetPassword($id);
|
||||
return $this->success(null, '密码重置成功');
|
||||
}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `userId` | int | 是 | 用户ID |
|
||||
|
||||
:::tip 默认密码
|
||||
重置密码接口无需传递密码参数,系统自动使用 `PasswordService::getDefaultPasswordPlain()` 作为默认密码。
|
||||
:::
|
||||
|
||||
### 新增用户(beforeAdd 钩子)
|
||||
|
||||
```php
|
||||
// app/logic/UserLogic.php
|
||||
|
||||
/**
|
||||
* 新增前处理:生成编码、处理城市、加密密码
|
||||
*
|
||||
* @param array $data 待新增的数据
|
||||
* @return array 处理后的数据
|
||||
*/
|
||||
protected function beforeAdd(array $data): array
|
||||
{
|
||||
// 自动生成用户编码:租户编码 + 租户内序号(如 T0001-000020)
|
||||
$tenantId = $data['tenantId'] ?? request()->tenantId ?? 0;
|
||||
if ($tenantId == 0) {
|
||||
$tenantId = config('tenant.default_tenant_id', 1);
|
||||
}
|
||||
if ($tenantId > 0) {
|
||||
$tenant = Tenant::find($tenantId);
|
||||
$currentUserCount = User::where('tenant_id', $tenantId)->where('is_delete', 0)->count();
|
||||
$nextSeq = $currentUserCount + 1;
|
||||
$data['code'] = $tenant->code . '-' . str_pad((string)$nextSeq, 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
// 处理城市字段
|
||||
if (isset($data['city']) && is_array($data['city'])) {
|
||||
$data['province_code'] = $data['city'][0] ?? '';
|
||||
$data['city_code'] = $data['city'][1] ?? '';
|
||||
$data['district_code'] = $data['city'][2] ?? '';
|
||||
$data['street_code'] = $data['city'][3] ?? '';
|
||||
$data['city_info'] = City::resolveCityNames($data['city']);
|
||||
unset($data['city']);
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
$password = $data['password'] ?? PasswordService::getDefaultPasswordPlain();
|
||||
$encrypted = PasswordService::encrypt($password);
|
||||
$data['password'] = $encrypted['password'];
|
||||
$data['salt'] = $encrypted['salt'];
|
||||
|
||||
return $data;
|
||||
}
|
||||
```
|
||||
|
||||
:::tip 编码规则
|
||||
- 用户编码格式:`租户编码-6位序号`,如 `T0001-000020`
|
||||
- 序号 = 当前租户下未删除用户数 + 1
|
||||
- 超级管理员(tenantId=0)自动代入默认租户生成编码
|
||||
- 编码由系统自动生成,前端不传,修改时不可变更
|
||||
:::
|
||||
|
||||
### 新增后保存角色(afterAdd 钩子)
|
||||
|
||||
```php
|
||||
/**
|
||||
* 新增后处理:保存用户角色关联
|
||||
*
|
||||
* @param int $id 新增用户的主键 ID
|
||||
* @param array $data 新增时提交的原始数据
|
||||
* @return void
|
||||
*/
|
||||
protected function afterAdd(int $id, array $data): void
|
||||
{
|
||||
$roleIds = $data['roles'] ?? [];
|
||||
foreach ($roleIds as $roleId) {
|
||||
UserRole::create([
|
||||
'user_id' => $id,
|
||||
'role_id' => $roleId,
|
||||
'create_user' => request()->userInfo->username ?? '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::info 多角色支持
|
||||
前端传 `roles: [1, 2, 3]` 数组,`afterAdd` 遍历写入 `UserRole` 中间表。`afterUpdate` 同理,先删后插实现全量覆盖。
|
||||
:::
|
||||
|
||||
### 详情补全(afterDetail 钩子)
|
||||
|
||||
```php
|
||||
/**
|
||||
* 详情查询后处理:补充关联数据与格式化输出
|
||||
*
|
||||
* @param int $id 用户 ID
|
||||
* @param array &$data 详情数据(引用传递)
|
||||
* @return void
|
||||
*/
|
||||
protected function afterDetail(int $id, array &$data): void
|
||||
{
|
||||
// 补充角色信息
|
||||
$roleIds = UserRole::where('user_id', $id)->column('role_id');
|
||||
$data['roles'] = $roleIds;
|
||||
|
||||
// 补充关联名称
|
||||
$data['levelName'] = Level::getNameMap()[$data['level_id']] ?? '';
|
||||
$data['positionName'] = Position::getNameMap()[$data['position_id']] ?? '';
|
||||
$data['deptName'] = Dept::getNameMap()[$data['dept_id']] ?? '';
|
||||
|
||||
// 组装城市数组
|
||||
$data['city'] = [
|
||||
$data['province_code'] ?? '',
|
||||
$data['city_code'] ?? '',
|
||||
$data['district_code'] ?? '',
|
||||
$data['street_code'] ?? '',
|
||||
];
|
||||
|
||||
// 移除敏感字段
|
||||
unset($data['password'], $data['salt']);
|
||||
}
|
||||
```
|
||||
|
||||
### 导出(export)
|
||||
|
||||
导出为分批导出(chunkExport),每批 500 条,避免内存溢出。
|
||||
|
||||
| 导出列 | 字段来源 |
|
||||
|--------|----------|
|
||||
| 用户编码 | code(系统自动生成) |
|
||||
| 姓名 | realname |
|
||||
| 性别 | genderText(DictService 自动转换) |
|
||||
| 手机号 | mobile |
|
||||
| 邮箱 | email |
|
||||
| 部门 | deptName(Dept::getNameMap 映射) |
|
||||
| 职级 | levelName(Level::getNameMap 映射) |
|
||||
| 岗位 | positionName(Position::getNameMap 映射) |
|
||||
| 城市 | cityInfo(4个城市编码拼接为名称字符串) |
|
||||
| 地址 | address |
|
||||
| 用户名 | username |
|
||||
| 简介 | intro |
|
||||
| 状态 | statusText(DictService 自动转换) |
|
||||
| 备注 | note |
|
||||
|
||||
### 导入(import)
|
||||
|
||||
导入时需处理以下反向映射:
|
||||
|
||||
| 导入列 | 处理逻辑 |
|
||||
|--------|----------|
|
||||
| 性别/状态 | 非数字时 DictService::getValue 反查 |
|
||||
| 部门/职级/岗位 | 名称通过 array_flip(getNameMap) 反查 ID |
|
||||
| 城市 | cityInfo 通过 City::resolveCityCodes 拆为4个编码 |
|
||||
| 用户编码 | 忽略(由 beforeAdd 自动生成) |
|
||||
|
||||
:::warning 导入不处理 code
|
||||
导入时即使 Excel 中有「用户编码」列也会被忽略,编码由 `beforeAdd` 自动生成。
|
||||
:::
|
||||
Reference in New Issue
Block a user