签入版本
This commit is contained in:
@@ -1,179 +1,34 @@
|
||||
# 6.7 文章管理
|
||||
# 6.8 文章管理
|
||||
|
||||
:::tip 概述
|
||||
文章管理模块支持富文本内容编辑、封面图上传、分类关联。继承 BaseTenantLogic 实现租户隔离,并配置了数据权限(按创建人过滤)。
|
||||
文章(gin_article,租户级)是内容管理的核心表,承载富文本正文与单图封面——完整示范 ModuleSpec 的**内容字段(ContentFields)与文件字段(FileFields)**组合。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
## 功能清单
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 文件字段 | cover(封面图),保存到 article 目录 |
|
||||
| 富文本字段 | content(文章内容),自动处理临时文件 |
|
||||
| 枚举映射 | status → article_status 字典(0=正常 1=下架) |
|
||||
| 数据权限 | dataScopeUserField='create_user',按创建人过滤 |
|
||||
| 模糊搜索 | title、author |
|
||||
| 精确匹配 | category_id、status |
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 标准查询(分类/状态/关键字过滤) |
|
||||
| Add / Update | 维护(正文占位符化 + 封面转正) |
|
||||
| Status | 上下架单列更新 |
|
||||
| Delete / BatchDelete | 软删除 |
|
||||
|
||||
## 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 | 批量删除 |
|
||||
- **富文本契约**:正文内本站文件统一存 `[IMG_URL]/{相对路径}` 占位符(写侧 ContentFields 处理),读侧经 ExpandContentFields 唯一还原出口补全域名——防止正文里散落不可迁移的绝对 URL
|
||||
- **封面单图**:FileFields 声明,写侧 temp 转正到文章目录,读侧补访问域名
|
||||
- **分类关联**:category_id 关联 gin_category,列表读侧回填分类名
|
||||
- **租户隔离**:写补 tenant_id、读自动过滤
|
||||
|
||||
## 核心代码
|
||||
## 内容字段的意义
|
||||
|
||||
```php
|
||||
// app/logic/ArticleLogic.php
|
||||
正文存占位符而非完整 URL,使整库搬迁/换域名时只需改 `file.domainUrl` 配置——富文本资产随配置迁移,这是 [7.3 富文本处理](/zh/tools/rich-text.html) 的设计动机。
|
||||
|
||||
class ArticleLogic extends BaseTenantLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Article::class;
|
||||
## 管理界面
|
||||
|
||||
/**
|
||||
* 需要自动处理的文件上传字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $fileFields = ['cover'];
|
||||
`content/article`:列表 + 富文本编辑器(Editor 组件)+ 封面上传(Upload 组件)。
|
||||
|
||||
/**
|
||||
* 文件保存的子目录
|
||||
*
|
||||
* @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]` 占位符。读取时自动替换为实际域名。
|
||||
:::
|
||||
- [7.3 富文本处理](/zh/tools/rich-text.html)
|
||||
- [6.9 分类管理](/zh/modules/category.html)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# 6.19 缓存监控
|
||||
|
||||
:::tip 概述
|
||||
缓存监控页展示 Redis 运行状态:连接、内存、命中率、键空间规模。数据经 cache 包原始操作(PING / Info / DBSize)实时采集。
|
||||
:::
|
||||
|
||||
## 功能清单
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| GET /api/cache/info | Redis 视图聚合(Info 分区:服务器/内存/客户端/键空间;PING 探测;DBSize 键规模),权限码 sys:cache:info |
|
||||
|
||||
## 指标关注点
|
||||
|
||||
| 指标 | 关注点 |
|
||||
|---|---|
|
||||
| used_memory | 增长趋势;配合 maxmemory 策略 |
|
||||
| keyspace_hits/misses | 命中率下降 → 键设计或 TTL 复查 |
|
||||
| connected_clients | 连接数异常 → 连接池配置 |
|
||||
| DBSize | 键总量;黑名单/验证码类 TTL 键应动态平衡 |
|
||||
|
||||
## 降级模式下的表现
|
||||
|
||||
Redis 不可用时应用已自动切内存模式(启动告警可查),监控页反映内存实现的对应视图——**看到的不是真 Redis**。生产应把"启动告警出现降级"纳入告警规则。
|
||||
|
||||
## 键规范回顾
|
||||
|
||||
全部键带 `easygoadmin:` 前缀并由 `consts/cache_key.go` 函数生成(token 黑名单 / 在线会话 / 验证码 / 字典 / 限流计数)。
|
||||
|
||||
## 管理界面
|
||||
|
||||
`monitor/cache`:信息面板 + 键空间分布。
|
||||
|
||||
## 相关
|
||||
|
||||
- [4.9 缓存架构设计](/zh/architecture/cache.html)
|
||||
@@ -1,168 +1,25 @@
|
||||
# 6.10 分类管理
|
||||
# 6.9 分类管理
|
||||
|
||||
:::tip 概述
|
||||
分类管理支持树形分类数据,可用于文章分类、商品分类等场景。继承 BaseTenantLogic 实现租户隔离。
|
||||
分类(gin_category,租户级)为内容提供归类维度:文章按分类组织,分类支持树形层级。标准 CRUD + 树形输出。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
## 功能清单
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 唯一校验 | category_name + parent_id 复合唯一(同一父级下名称不可重复) |
|
||||
| 删除校验 | beforeDelete 检查子分类和关联文章 |
|
||||
| 模糊搜索 | category_name |
|
||||
| 精确匹配 | parent_id |
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail / Add / Update / Delete / BatchDelete | 标准维护 |
|
||||
|
||||
## 数据库表结构
|
||||
## 结构特点
|
||||
|
||||
```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 = '文章分类表';
|
||||
```
|
||||
- **树形**:parent_id 自关联,ModuleSpec 树形声明(IsTree + ParentOf)组树输出
|
||||
- **租户级**:每租户独立的分类树
|
||||
- **被引用**:文章经 category_id 引用;删除前应确认无文章引用(应用层校验)
|
||||
|
||||
## 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 | 批量删除 |
|
||||
`content/category`:树形表格 + 新增子分类。
|
||||
|
||||
## 核心代码
|
||||
## 相关
|
||||
|
||||
```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']]`,表示同一父级下分类名称不可重复,但不同父级下可以有同名分类。
|
||||
:::
|
||||
- [6.8 文章管理](/zh/modules/article.html)
|
||||
|
||||
@@ -1,160 +1,29 @@
|
||||
# 6.9 行政区划管理
|
||||
# 6.15 行政区划管理
|
||||
|
||||
:::tip 概述
|
||||
行政区划管理维护全国省市区街道四级数据,支持按父编码级联查询。继承 BaseLogic(系统共享,不隔离)。
|
||||
行政区划(gin_city)维护省市区三级数据,为地址选择、属地统计提供基础数据。树形结构 + 按父级取子级的级联查询。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
## 功能清单
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,不隔离) |
|
||||
| 层级结构 | id/pid + parent_code 双重层级标识 |
|
||||
| 特殊接口 | getCityList 按父编码查询、getParent 获取省份列表 |
|
||||
| 详情钩子 | afterDetail 补全父级名称 |
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 标准查询 |
|
||||
| Add / Update / Delete / BatchDelete | 维护(平台级数据) |
|
||||
| ByParent | 按父级 ID 取下级(级联选择器数据源) |
|
||||
| GetCityList | 区划列表聚合 |
|
||||
| AreaProvince | 省份数据入口 |
|
||||
|
||||
## 数据库表结构
|
||||
## 结构特点
|
||||
|
||||
```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 = '城市行政区划表';
|
||||
```
|
||||
- parent_id 三级树(省 → 市 → 区/县)
|
||||
- 平台级表(无租户隔离——区划是公共基础数据)
|
||||
- 前端配套 ChinaArea / Region 组件消费级联数据
|
||||
|
||||
## 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` | - | 根据父编码查询子级城市 |
|
||||
`data/city`:树形表格 + 级联维护。
|
||||
|
||||
## 核心代码
|
||||
## 相关
|
||||
|
||||
```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', '']
|
||||
```
|
||||
- [9.10 数据录入组件](/zh/frontend/form-components.html)(ChinaArea/Region 组件)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# 6.14 配置管理
|
||||
|
||||
:::tip 概述
|
||||
配置管理维护 gin_config(分组)+ gin_config_item(配置项)的分组树,承载站点级与业务级运营配置。另有前台站点配置的专用读写入口(WebIndex / WebSave)。
|
||||
:::
|
||||
|
||||
## 功能清单
|
||||
|
||||
### 配置分组(/api/config/*)
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail / Add / Update / Delete / BatchDelete | 分组维护 |
|
||||
| GET /api/config/web/index | 网站配置聚合读取(权限码 sys:configWeb:data) |
|
||||
| POST /api/config/web/save | 网站配置聚合保存(权限码 sys:configWeb:save) |
|
||||
|
||||
### 配置项(/api/config/item/*,权限码 sys:configitem:*)
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail / Add / Update / Delete / BatchDelete | 配置项维护(归属分组) |
|
||||
|
||||
## 分组语义
|
||||
|
||||
```text
|
||||
gin_config(分组,如"站点配置")
|
||||
└── gin_config_item(项:site_name=易购后台 / site_logo=... / icp=...)
|
||||
```
|
||||
|
||||
WebIndex/WebSave 为管理前台设置页的聚合契约——避免前端逐项调用。
|
||||
|
||||
## 界面双入口
|
||||
|
||||
| 入口 | 面向 | 说明 |
|
||||
|---|---|---|
|
||||
| setting/configWeb | 站点设置页 | 经 WebIndex/WebSave 聚合读写 |
|
||||
| data/config | 系统配置管理 | 分组/配置项的后台维护 |
|
||||
|
||||
## 管理界面
|
||||
|
||||
`data/config`:分组列表 + 配置项明细面板。
|
||||
|
||||
## 相关
|
||||
|
||||
- [5.12 系统配置管理](/zh/core/config.html)(机制与两层配置分工)
|
||||
@@ -1,190 +0,0 @@
|
||||
# 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 | 批量删除 |
|
||||
@@ -1,145 +1,39 @@
|
||||
# 6.4 部门管理
|
||||
|
||||
:::tip 概述
|
||||
部门管理维护组织架构数据,支持树形结构,是数据权限(按部门过滤)的基础。继承 BaseTenantLogic 实现租户隔离。
|
||||
部门(gin_dept,租户级)是组织树(parent_id 自关联),同时是**数据权限的锚点**——用户挂部门、角色数据范围按部门收敛、"本部门及以下"档位依赖树形子孙计算。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
## 功能清单
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 树形结构 | treeParentField='parent_id' |
|
||||
| 唯一校验 | name 全局唯一 |
|
||||
| 删除校验 | beforeDelete 检查是否存在子部门 |
|
||||
| 数据权限 | 被 UserLogic 的 dataScopeDeptField='dept_id' 引用 |
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail / Add / Update / Delete / BatchDelete | 标准维护(树形) |
|
||||
|
||||
## 数据库表结构
|
||||
## 树形声明
|
||||
|
||||
```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 = '部门表';
|
||||
deptSpec 以 ModuleSpec 树形能力声明(IsTree + ParentOf),List 平铺结果自动组树返回——前端树选择器/表格展开直接消费。
|
||||
|
||||
## 在数据权限中的角色
|
||||
|
||||
```text
|
||||
用户.dept_id → 部门树 → 数据范围档位展开:
|
||||
2 本部门及以下 = dept_id IN(自身部门 + 全部子孙部门)
|
||||
3 本部门 = dept_id = 自身部门
|
||||
```
|
||||
|
||||
## 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 | 删除 |
|
||||
## 约束
|
||||
|
||||
## 核心代码
|
||||
- 删除部门前确认无用户与子部门引用(应用层校验,无外键)
|
||||
- 部门名在租户内建议唯一(UniqueFields 预检可配)
|
||||
|
||||
### Logic 配置
|
||||
## 管理界面
|
||||
|
||||
```php
|
||||
// app/logic/DeptLogic.php
|
||||
`system/dept`:树形表格 + 新增子部门 + 拖拽调整层级。
|
||||
|
||||
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 方法
|
||||
}
|
||||
```
|
||||
## 相关
|
||||
|
||||
- [5.10 数据权限](/zh/core/data-scope.html)
|
||||
- [6.1 用户管理](/zh/modules/user.html)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# 6.12 字典管理
|
||||
|
||||
:::tip 概述
|
||||
字典管理页维护 gin_dict + gin_dict_item 两表:字典定义与字典项。它是字典机制([5.11 字典模块](/zh/core/dict.html))的数据维护入口,变更自动联动缓存失效。
|
||||
:::
|
||||
|
||||
## 功能清单
|
||||
|
||||
### 字典(dict)
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / Detail / Add / Update / Delete / BatchDelete | 字典定义维护 |
|
||||
| RefreshCache | 手动刷新字典缓存(运维兜底) |
|
||||
| AliasList / AliasInfo | 按别名批量/单个取字典项(业务侧常用入口) |
|
||||
|
||||
### 字典项(dict_item)
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / Detail / Add / Update / Delete / BatchDelete | 字典项维护 |
|
||||
| QuickList | 按字典编码快速取全部字典项(前端下拉数据源) |
|
||||
|
||||
## 缓存联动
|
||||
|
||||
字典项变更(Add/Update/Delete/BatchDelete)自动删除对应 `dict:{code}` 缓存键,下次读取回源重建——管理页保存即生效,无需重启。RefreshCache 用于缓存异常时的手动兜底。
|
||||
|
||||
## 字典项变更的影响面
|
||||
|
||||
字典值改动会即时影响:
|
||||
|
||||
- 后端 SerializeMaps 回填的 `{field}Text` 显示名
|
||||
- 前端下拉/标签的选项文案
|
||||
- 导出文件中的翻译列
|
||||
|
||||
改字典前评估正在使用的模块(尤其对外导出格式)。
|
||||
|
||||
## 管理界面
|
||||
|
||||
`data/dict`:字典列表 + 明细面板(字典项行内维护 + 排序 + 启停)。
|
||||
|
||||
## 相关
|
||||
|
||||
- [5.11 字典模块](/zh/core/dict.html)(机制)
|
||||
- [8.6 字典数据接口](/zh/api-spec/dict-api.html)(契约)
|
||||
@@ -0,0 +1,42 @@
|
||||
# 6.22 示例模块(example)
|
||||
|
||||
:::tip 概述
|
||||
`gin_example` 是全项目的**规约参考实现**:标准 CRUD + 单图转正 + 字典回填 + 导入导出 + 状态更新,一个模块覆盖 ModuleSpec 常用能力组合。第 3 章以其贯穿讲解。
|
||||
:::
|
||||
|
||||
## 功能清单
|
||||
|
||||
| 接口 | 权限码 | 说明 |
|
||||
|---|---|---|
|
||||
| Page / List / Detail | sys:example:* | 标准查询 |
|
||||
| Add / Update | sys:example:* | 维护(avatar 转正) |
|
||||
| Status | sys:example:status | 单列状态更新(手写) |
|
||||
| Delete / BatchDelete | sys:example:* | 软删除 |
|
||||
| Import | sys:example:import | Excel 导入(UnmarshalBody + 逐行校验) |
|
||||
| Export | sys:example:export | Excel 导出(applyFilters 复用 + 字典翻译) |
|
||||
|
||||
## 能力示范对照
|
||||
|
||||
| 能力 | 声明/实现 |
|
||||
|---|---|
|
||||
| 模糊 + 等值过滤 | LikeFields(name) / EqFields(type,status) |
|
||||
| 字典显示名 | SerializeMaps: status → example_status |
|
||||
| 单图转正 + 读侧补域名 | FileFields(avatar) + FileSaveDir |
|
||||
| 全量表单语义 | exampleToData + 全局钩子 |
|
||||
| 手写逻辑 | Status / Import / Export |
|
||||
| 逃生舱接口 | ImportReq.UnmarshalBody(multipart) |
|
||||
| 提示语定制 | ImportRes.ResMsg(MsgProvider) |
|
||||
|
||||
## 非租户级说明
|
||||
|
||||
gin_example 无 tenant_id 列、不在租户白名单——它是平台级示例表。做租户级模块开发时对照 [6.1 用户管理](/zh/modules/user.html) 等租户级模块。
|
||||
|
||||
## 学习路径
|
||||
|
||||
1. [3.1–3.11 模块开发实战](/zh/guide/overview.html) 逐文件精读
|
||||
2. 对照 [3.11 完整代码清单](/zh/guide/code-list.html) 自检
|
||||
3. 用生成器重新生成一遍比对产物
|
||||
|
||||
## 管理界面
|
||||
|
||||
`tool/example`:列表 + 编辑 + 导入导出(页面本身也是生成器模板的产物示范)。
|
||||
@@ -0,0 +1,33 @@
|
||||
# 6.21 文件模板管理
|
||||
|
||||
:::tip 概述
|
||||
文件模板(gin_file_template)管理可复用的文件模板(导入模板、导出表头模板等):登记模板元信息、提供按编码获取的入口,配套文件字段的上传转正。
|
||||
:::
|
||||
|
||||
## 功能清单
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 标准查询 |
|
||||
| Add / Update | 维护(文件字段经 FileFields 转正) |
|
||||
| Delete / BatchDelete | 软删除 |
|
||||
| ByCode | 按模板编码获取(业务侧下载导入模板的标准入口) |
|
||||
|
||||
## 典型用法
|
||||
|
||||
```text
|
||||
导入功能配套模板:
|
||||
管理页上传标准导入模板(xlsx)→ 登记编码(如 user_import)
|
||||
业务页"下载模板" → ByCode(user_import) → 返回文件 URL → 前端下载
|
||||
```
|
||||
|
||||
用户下载的模板与导入解析的列头保持同源,避免文档漂移。
|
||||
|
||||
## 管理界面
|
||||
|
||||
`file/fileTemplate`:列表 + 上传 + 编码管理。
|
||||
|
||||
## 相关
|
||||
|
||||
- [7.1 文件上传全流程](/zh/tools/upload.html)
|
||||
- [7.4 数据导入导出](/zh/tools/excel.html)
|
||||
@@ -1,146 +0,0 @@
|
||||
# 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` 接口未配置权限注解,通常用于前端按编码读取模板配置的场景。
|
||||
:::
|
||||
@@ -1,211 +1,46 @@
|
||||
# 6.15 首页与个人中心
|
||||
# 6.23 首页与个人中心
|
||||
|
||||
:::tip 概述
|
||||
首页与个人中心模块负责当前登录用户的信息获取、菜单加载、密码修改和个人信息更新。该模块不继承 BaseLogic,而是专用业务逻辑。
|
||||
index 模块面向登录后的当前用户:用户信息、菜单下发、个人资料与密码维护。它是前端登录后的第一站(仪表盘与个人中心共用其接口)。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
## 功能清单
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | 无(直接操作 Model) |
|
||||
| 用途 | 当前登录用户的自身操作 |
|
||||
| 权限 | 无需配置 #[Permission],已登录即可访问 |
|
||||
| 超级管理员 | uid=1 拥有全部权限 `*:*:*` |
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| GetUser | 当前登录用户信息(身份、角色、部门等) |
|
||||
| GetMenus | 当前用户菜单树(按角色过滤,前端动态路由数据源) |
|
||||
| GetMenusById | 按 ID 取菜单(管理场景) |
|
||||
| UpdatePassword | 修改个人密码(校验旧密码 → crypto 哈希落库) |
|
||||
| UpdateProfile | 更新个人资料(昵称、头像等) |
|
||||
|
||||
## 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;
|
||||
}
|
||||
```text
|
||||
登录成功 → GetMenus(菜单树)→ asyncRoute store 生成路由 → addRoute
|
||||
→ GetUser(用户信息)→ user store → 布局渲染(头像/名称/面包屑)
|
||||
```
|
||||
|
||||
:::info 返回结构说明
|
||||
- `roles`:完整角色对象数组(含 id、name、code 等),非仅 ID
|
||||
- `permissions`:权限码数组,超级管理员为 `['*:*:*']`
|
||||
- `city`:4 层城市编码数组 `[省, 市, 区, 街道]`
|
||||
- `avatar`:自动补全域名前缀
|
||||
- 字段名由 `Result::success()` 自动转为 camelCase 返回前端
|
||||
菜单接口的返回结构决定侧边栏形态与前端路由表——它是动态菜单机制的后端一半(前端一半见 [9.5 路由与动态菜单](/zh/frontend/router.html))。
|
||||
|
||||
## 密码修改安全点
|
||||
|
||||
- 旧密码必验(防会话被盗用改密)
|
||||
- 新密码经外部 salt + bcrypt 哈希
|
||||
- 修改成功后其他会话可经在线用户页强制下线
|
||||
|
||||
## 界面入口
|
||||
|
||||
| 界面 | 内容 |
|
||||
|---|---|
|
||||
| dashboard/console | 首页仪表盘(CountTo 统计卡片 + echarts 图表) |
|
||||
| setting/profile | 个人中心(资料编辑 + 修改密码 + 头像裁剪) |
|
||||
|
||||
:::warning 仪表盘数据现状
|
||||
`ui/src/api/dashboard/console.ts` 的 `getConsoleInfo()` 目前返回**前端内置的模拟数据**(源码注释"模拟控制台数据"),没有对应后端统计接口——访问量/销售额等卡片为演示效果。接入真实统计时需新增后端 dashboard 接口并替换该函数;运行状态类真实数据请用监控中心接口(serverMonitor/onlineUser 等)。
|
||||
:::
|
||||
|
||||
### 获取菜单
|
||||
## 相关
|
||||
|
||||
```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' });
|
||||
```
|
||||
- [9.8 登录流程](/zh/frontend/login.html)
|
||||
- [5.2 认证与授权流程](/zh/core/auth-flow.html)
|
||||
|
||||
@@ -1,788 +1,49 @@
|
||||
# 6.11 定时任务
|
||||
# 6.16 定时任务管理
|
||||
|
||||
:::tip 概述
|
||||
定时任务系统由三部分组成:JobLogic(调度逻辑)+ BaseTask(任务处理器)+ JobRunCommand(命令行入口)。支持三种触发方式:Linux Crontab、Windows 计划任务、Daemon 守护进程。
|
||||
任务管理页(gin_job + gin_job_log)管理 cron 任务的定义与执行:新建、启停、手动执行、执行日志。调度机制详见 [5.19 定时任务调度机制](/zh/core/job-scheduler.html)。
|
||||
:::
|
||||
|
||||
## 系统架构
|
||||
## 功能清单
|
||||
|
||||
```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 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
### 任务(job)
|
||||
|
||||
## 状态说明
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 标准查询 |
|
||||
| Add / Update / Delete / BatchDelete | 任务定义维护(url 字段 = 处理器注册名) |
|
||||
| RunOnce | GET /api/job/runOnce/:id 立即执行一次(经执行锁防并发,权限码 sys:job:execute) |
|
||||
| Pause / Resume | GET /api/job/pause/:id、resume/:id 暂停/恢复(cron 摘除/挂载) |
|
||||
| Status | POST /api/job/status 设置任务状态 |
|
||||
| DaemonStatus | GET /api/job/daemonStatus 调度器守护状态(登录即可,无需权限码) |
|
||||
|
||||
| 状态值 | 说明 | 可执行 | 可暂停 | 可启动 |
|
||||
|--------|------|--------|--------|--------|
|
||||
| 0 | 未发布 | ✗ | ✗ | ✓ |
|
||||
| 1 | 运行中 | ✓ | ✓ | ✗ |
|
||||
| 2 | 暂停 | ✗ | ✗ | ✓ |
|
||||
| 3 | 已删除 | ✗ | ✗ | ✗ |
|
||||
### 执行日志(job_log)
|
||||
|
||||
## 执行策略
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 执行记录查询 |
|
||||
|
||||
| 策略值 | 说明 | 行为 |
|
||||
|--------|------|------|
|
||||
| 1 | 立即执行 | 丢弃之前的执行,立即运行 |
|
||||
| 2 | 执行一次 | 等待上一次完成后执行 |
|
||||
| 3 | 放弃执行 | 如果上一次还在运行则跳过 |
|
||||
## 任务记录关键字段
|
||||
|
||||
:::tip 枚举文本自动翻译
|
||||
JobLogic 配置了 `serializeMaps`,API 响应中会自动包含翻译后的文本字段:
|
||||
| 字段 | 语义 |
|
||||
|---|---|
|
||||
| url | 处理器注册名(与 internal/task 的 RegisterJobHandler 名一致,**数据驱动查找**) |
|
||||
| cron_expression | cron 表达式(或 @every 描述符) |
|
||||
| execute_policy | 执行策略:1-立即执行 2-执行一次 3-放弃执行(字典 job_execute_policy 回填显示名) |
|
||||
| is_sync | 是否同步执行:0-否 1-是(字典 job_is_sync) |
|
||||
| params | 任务参数(JSON 字符串,传给处理器) |
|
||||
| status | 启停状态(字典 job_status) |
|
||||
|
||||
| 原始字段 | 文本字段 | 字典编码 | 说明 |
|
||||
|----------|----------|----------|------|
|
||||
| `status` | `statusText` | `job_status` | 任务状态(未发布/运行中/暂停/删除) |
|
||||
| `execute_policy` | `executePolicyText` | `job_execute_policy` | 执行策略(立即执行/执行一次/放弃执行) |
|
||||
url 与处理器注册名不匹配时任务无法执行——新建任务前确认处理器已注册(内置:LogClean / SendSms / HelloWorld)。
|
||||
|
||||
字典数据在代码生成器自动创建,也可在「系统管理 → 字典管理」中手动维护。
|
||||
:::
|
||||
## 执行可靠性
|
||||
|
||||
## Cron 表达式语法
|
||||
执行锁(`job.lockTTLSeconds`,执行期自动续期)+ 任务日志(开始/结束/耗时/结果摘要)。
|
||||
|
||||
标准 5 段格式:`分 时 日 月 周`
|
||||
## 管理界面
|
||||
|
||||
```text
|
||||
┌────────── 分钟(0-59)
|
||||
│ ┌──────── 小时(0-23)
|
||||
│ │ ┌────── 日(1-31)
|
||||
│ │ │ ┌──── 月(1-12)
|
||||
│ │ │ │ ┌── 周(0-6,0=周日)
|
||||
│ │ │ │ │
|
||||
* * * * *
|
||||
```
|
||||
`monitor/job`:任务列表 + 执行日志页签 + 手动执行/启停按钮。
|
||||
|
||||
| 表达式 | 说明 |
|
||||
|--------|------|
|
||||
| `* * * * *` | 每分钟 |
|
||||
| `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:\xampp8.2\htdocs\RXThinkCMF_ThinkPhp8_EleVue
|
||||
```
|
||||
|
||||
12. 勾选"不管用户是否登录都要运行"
|
||||
13. 点击完成
|
||||
|
||||
#### 3. 验证
|
||||
|
||||
```powershell
|
||||
# 手动运行测试
|
||||
cd D:\xampp8.2\htdocs\RXThinkCMF_ThinkPhp8_EleVue
|
||||
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:\xampp8.2\htdocs\RXThinkCMF_ThinkPhp8_EleVue && 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:\xampp8.2\htdocs\RXThinkCMF_ThinkPhp8_EleVue && 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:\xampp8.2\htdocs\RXThinkCMF_ThinkPhp8_EleVue && 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` 文件。
|
||||
- [5.19 定时任务调度机制](/zh/core/job-scheduler.html)
|
||||
|
||||
@@ -1,148 +1,32 @@
|
||||
# 6.5 职级管理
|
||||
|
||||
:::tip 概述
|
||||
职级管理维护用户的职级属性数据,支持 Excel 导入导出。继承 BaseTenantLogic 实现租户隔离。
|
||||
职级(gin_level,租户级)是组织人事的基础字典型模块:标准 CRUD + 启停状态 + 导入导出,适合作为理解"简单模块完整形态"的参照。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
## 功能清单
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 唯一校验 | name 全局唯一 |
|
||||
| 枚举映射 | status → level_status 字典(1=在用 2=停用) |
|
||||
| 导入导出 | 支持 Excel 导入导出 |
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 标准查询 |
|
||||
| Add / Update | 标准维护 |
|
||||
| Status | 启停单列更新(手写) |
|
||||
| Delete / BatchDelete | 软删除 |
|
||||
| Import / Export | 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 | 导入 |
|
||||
- **无关联表**:职级是扁平定义表,不挂用户/角色
|
||||
- **导入导出**:与 example 模块同款契约(ReadSheet 逐行校验 / WriteSheet 输出 URL)
|
||||
- **租户级**:写侧补 tenant_id,读侧隔离
|
||||
|
||||
## 核心代码
|
||||
职级与用户的关联由用户表承载(如需),本模块只管定义与排序。
|
||||
|
||||
### Logic 配置
|
||||
## 管理界面
|
||||
|
||||
```php
|
||||
// app/logic/LevelLogic.php
|
||||
`system/level`:列表 + 状态开关 + 导入导出按钮。
|
||||
|
||||
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()` 反向映射。
|
||||
:::
|
||||
- [6.6 岗位管理](/zh/modules/position.html)(同形态模块)
|
||||
- [7.4 数据导入导出](/zh/tools/excel.html)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# 6.11 友情链接管理
|
||||
|
||||
:::tip 概述
|
||||
友情链接(gin_link,租户级)管理外部站点互链:名称、URL、图标、排序。模块刻意保持**最小接口集**——无列表接口,仅供前台按需取用。
|
||||
:::
|
||||
|
||||
## 功能清单
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page | 分页查询 |
|
||||
| Detail / Add / Update | 标准维护 |
|
||||
| Delete / BatchDelete | 软删除 |
|
||||
|
||||
无 List/Status——友链场景分页足够,状态可用软删除表达。
|
||||
|
||||
## 字段形态
|
||||
|
||||
- 名称、URL、图标(单图,FileFields 转正 + 读侧补域名)
|
||||
- 排序字段承载展示顺序
|
||||
|
||||
## 管理界面
|
||||
|
||||
`content/link`:列表 + 编辑(URL 合法性前端校验)。
|
||||
|
||||
## 相关
|
||||
|
||||
- [3.3 契约层开发(dto)](/zh/guide/dto.html)(最小接口集的 dto 形态)
|
||||
@@ -0,0 +1,49 @@
|
||||
# 6.20 日志管理
|
||||
|
||||
:::tip 概述
|
||||
日志管理涵盖两类审计表:**操作日志**(gin_operation_log,接口调用审计)与**登录日志**(gin_login_log,登录行为审计)。均由系统自动写入,管理页只读检索 + 清理。
|
||||
:::
|
||||
|
||||
## 功能清单
|
||||
|
||||
两类日志路径前缀与模块名不同(注意拼写):
|
||||
|
||||
### 操作日志(/api/oper/log/*,权限码 sys:operLog:*)
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 检索(操作人/类型/时间/IP) |
|
||||
| Delete / BatchDelete | 手动清理(软删除) |
|
||||
|
||||
### 登录日志(/api/login/log/*,权限码 sys:loginLog:*)
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 检索(用户/结果/时间/地域) |
|
||||
| Delete / BatchDelete | 手动清理 |
|
||||
|
||||
## 记录来源
|
||||
|
||||
- 操作日志:OperationLog 中间件按 RouteSpec 声明异步落库([5.14 操作日志](/zh/core/operation-log.html))
|
||||
- 登录日志:auth 模块在登录链路内写入([5.15 登录日志](/zh/core/login-log.html))
|
||||
|
||||
## 保留策略
|
||||
|
||||
自动保留:`job.logRetentionDays`(默认 90 天),LogClean 定时任务超期**物理清理**(经 WithPhysicalDelete 放行)。管理页的手动删除是软删除,两者不冲突。
|
||||
|
||||
## 检索建议
|
||||
|
||||
| 场景 | 检索方式 |
|
||||
|---|---|
|
||||
| 追查单次操作 | 响应头 X-Request-Id → 日志文件精确定位(表内按人/时间缩小范围) |
|
||||
| 安全审计 | 登录日志按 status=1 聚合看失败来源 |
|
||||
| 合规导出 | 操作日志按时间范围导出 |
|
||||
|
||||
## 管理界面
|
||||
|
||||
`system/logger`:操作日志与登录日志两个页签。
|
||||
|
||||
## 相关
|
||||
|
||||
- [5.14 操作日志](/zh/core/operation-log.html)
|
||||
- [5.15 登录日志](/zh/core/login-log.html)
|
||||
@@ -1,135 +1,45 @@
|
||||
# 6.3 菜单管理
|
||||
|
||||
:::tip 概述
|
||||
菜单管理负责维护系统的菜单树和权限节点,是 RBAC 权限体系的基础数据。
|
||||
菜单(gin_menu,平台级)一张表同时承担三件事:**前端路由定义**(path/component)、**侧边栏展示**(树形/图标/排序)、**权限节点承载**(permission 列)。后端菜单管理即前端路由管理。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
## 功能清单
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,不隔离) |
|
||||
| 树形结构 | treeParentField='parent_id',支持树形列表 |
|
||||
| 菜单类型 | type: 0=目录/页面(可导航) 1=权限按钮(仅权限校验,不显示在菜单树) |
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail / Add / Update / Delete / BatchDelete | 标准维护(树形数据) |
|
||||
|
||||
## 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 | 删除 |
|
||||
| 字段 | 语义 |
|
||||
|---|---|
|
||||
| parent_id | 树形层级(目录 → 菜单/节点) |
|
||||
| path / component | 前端路由路径 / `views` 下组件相对路径 |
|
||||
| permission | 权限节点码(`sys:user:add`),type=1 节点专用此列 |
|
||||
| type | 0-菜单(页面)1-节点(纯权限点) |
|
||||
| target | 0-组件 1-内链 2-外链 |
|
||||
| status / hide | 停用 / 不可见(路由保留但侧边栏隐藏) |
|
||||
|
||||
## 菜单数据结构
|
||||
## 动态菜单联动
|
||||
|
||||
```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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```text
|
||||
用户登录 → /api/index/getMenus 返回按角色过滤的菜单树
|
||||
→ 前端 asyncRoute store 生成路由表
|
||||
→ addRoute 注入 + 侧边栏渲染
|
||||
```
|
||||
|
||||
## 字段说明
|
||||
菜单行的 component 路径必须真实存在于 `ui/src/views` 下,否则路由懒加载失败。
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `type` | 0=菜单/目录(可导航) 1=权限节点 |
|
||||
| `path` | 路由路径 |
|
||||
| `component` | 前端组件路径(type=1 时必填) |
|
||||
| `permission` | 权限标识(type=1,2 时填写) |
|
||||
| `target` | 打开方式:0=组件 1=内链 2=外链 |
|
||||
| `hide` | 是否隐藏 |
|
||||
## 代码生成器的落点
|
||||
|
||||
## 核心代码
|
||||
生成器为新模块幂等创建菜单行(页面菜单 + 权限节点),挂载位置由 `generator.parentMenuId` 指定——菜单/权限/路由三者一致性由工具保证。
|
||||
|
||||
### Controller
|
||||
## 管理界面
|
||||
|
||||
```php
|
||||
// app/controller/MenuController.php
|
||||
`system/menu`:树形表格 + 拖拽排序 + 新增/编辑(类型、图标、组件路径、权限码)。
|
||||
|
||||
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 且未删除的记录),如有子菜单则抛出异常阻止删除。需先删除子菜单再删除父菜单。
|
||||
:::
|
||||
- [3.9 权限节点配置](/zh/guide/permission.html)
|
||||
- [9.5 路由与动态菜单](/zh/frontend/router.html)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# 6.10 公告管理
|
||||
|
||||
:::tip 概述
|
||||
公告(gin_notice,租户级)承载站点通知/公告的发布管理:标准 CRUD + 上下架状态。字段轻量,适合短文本通知场景。
|
||||
:::
|
||||
|
||||
## 功能清单
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 标准查询 |
|
||||
| Add / Update | 维护 |
|
||||
| Status | 发布/下架单列更新 |
|
||||
| Delete / BatchDelete | 软删除 |
|
||||
|
||||
## 与文章的选型
|
||||
|
||||
| | 公告(notice) | 文章(article) |
|
||||
|---|---|---|
|
||||
| 内容形态 | 短文本 | 富文本长文 |
|
||||
| 富文本 | 无 | 有(ContentFields 契约) |
|
||||
| 分类 | 无 | 有 |
|
||||
| 典型场景 | 系统通知、维护公告 | 内容运营 |
|
||||
|
||||
## 管理界面
|
||||
|
||||
`data/notice`:列表 + 编辑 + 上下架开关。
|
||||
|
||||
## 相关
|
||||
|
||||
- [6.8 文章管理](/zh/modules/article.html)
|
||||
@@ -0,0 +1,34 @@
|
||||
# 6.17 在线用户
|
||||
|
||||
:::tip 概述
|
||||
在线用户页展示当前活跃的 JWT 会话(缓存登记,不落库),支持查看明细与强制下线。多端登录并存,每个会话独立管理。
|
||||
:::
|
||||
|
||||
## 功能清单
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page | 分页查询在线会话(按租户隔离) |
|
||||
| Detail | 会话明细 |
|
||||
| ForceLogout | 按会话(jti)强制下线 |
|
||||
| ForceLogoutUser | 按用户强制下线(其全部会话) |
|
||||
|
||||
## 数据来源
|
||||
|
||||
在线登记在缓存:`online:jti:set`(有序集合,score=访问令牌过期时间)+ `online:info:{jti}`(明细 JSON:用户、IP、UA、登录时间、令牌过期)。登录时 MarkOnline、登出/强退时 MarkOffline。
|
||||
|
||||
## 强制下线语义
|
||||
|
||||
拉黑该会话的**访问令牌与刷新令牌**(防登出后凭刷新令牌复活)+ 移除在线登记。只影响目标会话,用户其他端不受影响(ForceLogoutUser 则清全部)。
|
||||
|
||||
## 过期清理
|
||||
|
||||
集合按 score(过期时间)可范围清理——令牌过期后在线记录随查询自然过滤/清理,无需专门任务。
|
||||
|
||||
## 管理界面
|
||||
|
||||
`monitor/online`:会话列表 + 强制下线按钮(需相应权限码)。
|
||||
|
||||
## 相关
|
||||
|
||||
- [5.1 用户认证(JWT)](/zh/core/jwt.html)
|
||||
@@ -1,115 +1,29 @@
|
||||
# 6.12 参数管理
|
||||
# 6.13 参数管理
|
||||
|
||||
:::tip 概述
|
||||
参数管理维护系统级配置参数,如默认密码、系统名称等。继承 BaseLogic(系统共享,不隔离),通过 ParamService 读取。
|
||||
参数(gin_param)是扁平键值表,管理独立系统参数(协议版本、联系方式、提示文案等)。标准 CRUD,按 code 定位。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
## 功能清单
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,不隔离) |
|
||||
| 唯一校验 | code 全局唯一 |
|
||||
| 缓存 | ParamService 两级缓存(请求级 + 持久缓存,1小时过期) |
|
||||
| 模糊搜索 | name、code |
|
||||
| 精确匹配 | type、status |
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 标准查询(code/名称搜索) |
|
||||
| Add / Update | 维护(code 全局唯一) |
|
||||
| Delete / BatchDelete | 软删除 |
|
||||
|
||||
## 数据库表结构
|
||||
## 使用规约
|
||||
|
||||
```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 = '系统参数表';
|
||||
```
|
||||
- code 小写下划线、尽量带模块前缀
|
||||
- value 一律字符串(数字/布尔消费方转换)
|
||||
- 禁止放运维级配置(端口/密钥走 config.yaml)
|
||||
|
||||
## API 接口
|
||||
机制与选型见 [5.13 系统参数管理](/zh/core/param.html)。
|
||||
|
||||
| 方法 | 路径 | 权限码 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| 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 | 批量删除 |
|
||||
## 管理界面
|
||||
|
||||
## 核心代码
|
||||
`data/param`:列表 + 编辑 + 备注说明。
|
||||
|
||||
```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)
|
||||
:::
|
||||
- [5.12 系统配置管理](/zh/core/config.html)(分组形态的姊妹模块)
|
||||
|
||||
@@ -1,170 +1,31 @@
|
||||
# 6.6 岗位管理
|
||||
|
||||
:::tip 概述
|
||||
岗位管理维护用户的岗位属性数据。继承 BaseTenantLogic 实现租户隔离,是典型的零自定义方法模块——全部能力由属性配置自动装配。
|
||||
岗位(gin_position,租户级)与职级同形态:标准 CRUD + 启停。两者共同构成组织人事的定义层,用户与岗位的关联由用户侧承载。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
## 功能清单
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseTenantLogic(租户隔离) |
|
||||
| 唯一校验 | name 全局唯一 |
|
||||
| 枚举映射 | status → position_status 字典(1=在用 2=停用) |
|
||||
| 自定义方法 | 无(全部由基类自动装配) |
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 标准查询 |
|
||||
| Add / Update | 标准维护 |
|
||||
| Status | 启停单列更新(手写) |
|
||||
| Delete / BatchDelete | 软删除 |
|
||||
|
||||
## 数据库表结构
|
||||
## 与职级的区别
|
||||
|
||||
```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 = '岗位表';
|
||||
```
|
||||
| | 职级(level) | 岗位(position) |
|
||||
|---|---|---|
|
||||
| 语义 | 层级(P5/P6/主管) | 角色位置(运维/客服/财务) |
|
||||
| 导入导出 | 有 | 无(本版本) |
|
||||
|
||||
## 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 | 批量删除 |
|
||||
## 管理界面
|
||||
|
||||
## 核心代码
|
||||
`system/position`:列表 + 状态开关。
|
||||
|
||||
```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 调用父类通用方法,无需编写任何业务代码,即可获得完整的增删改查、分页、唯一性校验、枚举翻译能力。
|
||||
:::
|
||||
- [6.5 职级管理](/zh/modules/level.html)
|
||||
|
||||
@@ -1,183 +1,35 @@
|
||||
# 6.2 角色管理
|
||||
|
||||
:::tip 概述
|
||||
角色管理是 RBAC 权限体系的核心,通过角色关联菜单实现权限分配。
|
||||
角色(gin_role,租户级)是权限与数据范围的载体:向上关联用户(gin_user_role),向下关联菜单权限节点(gin_role_menu)。角色上配置的数据范围档位决定行级可见性。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
## 功能清单
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,不隔离) |
|
||||
| 关联 | 与 Menu 多对多(通过 RoleMenu 中间表) |
|
||||
| 数据权限 | data_scope 字段控制(1=全部 2=本部门 3=仅本人) |
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 标准查询 |
|
||||
| Add / Update / Delete / BatchDelete | 标准维护 |
|
||||
| MenuList | 获取菜单树(供权限勾选界面渲染) |
|
||||
| SavePermission | 保存角色的菜单权限勾选(事务内重建 gin_role_menu) |
|
||||
|
||||
## 套餐化角色预置
|
||||
## 特色机制
|
||||
|
||||
系统内置三套租户角色模板,用于按套餐等级快速授权:
|
||||
- **数据范围档位**(data_scope):1 全部 / 2 本部门及以下 / 3 本部门 / 4 仅本人;多角色用户取最宽松
|
||||
- **权限保存**:SavePermission 为典型手写方法——事务内重建关联表,非标准五方法能表达
|
||||
- **授权缓存联动**:权限变更后,用户权限缓存(300 秒)到期或重新登录后生效
|
||||
|
||||
| 角色名称 | 定位 | 菜单范围 |
|
||||
|----------|------|----------|
|
||||
| 租户-标准版 | 基础功能 | 核心业务模块 |
|
||||
| 租户-专业版 | 进阶功能 | 核心 + 高级模块 |
|
||||
| 租户-旗舰版 | 全功能 | 全部菜单(最全) |
|
||||
## 权限链路中的位置
|
||||
|
||||
创建租户后,通过「角色管理 → 授权菜单」为对应角色分配菜单权限。详见 [租户管理](/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;
|
||||
}
|
||||
}
|
||||
```text
|
||||
角色勾选菜单节点 → gin_role_menu → LoadIdentity 装配权限集合 → Authz 校验
|
||||
```
|
||||
|
||||
### Controller 代码
|
||||
## 管理界面
|
||||
|
||||
```php
|
||||
// app/controller/RoleController.php
|
||||
`system/role`:列表 + 编辑 + 权限树勾选(MenuList 渲染)+ 数据范围选择。
|
||||
|
||||
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 权限生效
|
||||
权限分配后,拥有该角色的用户需重新登录才能获取最新权限。权限变更时相关缓存会自动失效。
|
||||
:::
|
||||
- [5.4 权限管理(RBAC)](/zh/core/rbac.html)
|
||||
- [6.3 菜单管理](/zh/modules/menu.html)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# 6.18 服务监控
|
||||
|
||||
:::tip 概述
|
||||
服务监控页展示主机运行指标:CPU、内存、磁盘、goroutine 等。采集实现按平台拆分(Windows/Linux/其他),上层契约一致。
|
||||
:::
|
||||
|
||||
## 功能清单
|
||||
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| GET /api/server/info | 主机资源快照(CPU/内存/磁盘/goroutine 等),权限码 sys:server:info |
|
||||
|
||||
## 平台实现
|
||||
|
||||
```text
|
||||
server_monitor.go 采集接口与公共逻辑
|
||||
server_monitor_windows.go Windows 实现
|
||||
server_monitor_other.go 其他平台(Linux 等)兜底实现
|
||||
```
|
||||
|
||||
平台差异封装在实现内,新增平台加一个文件即可。
|
||||
|
||||
## 指标解读
|
||||
|
||||
| 指标 | 关注点 |
|
||||
|---|---|
|
||||
| CPU / 内存 | 持续高位 → 性能分析([11.7 性能调优](/zh/deploy/performance.html)) |
|
||||
| 磁盘 | 上传目录/日志目录增长监控 |
|
||||
| goroutine | 突增 → 泄漏或下游堆积排查 |
|
||||
|
||||
## 管理界面
|
||||
|
||||
`monitor/server`:图表化实时展示。
|
||||
|
||||
## 相关
|
||||
|
||||
- [5.18 服务与缓存监控](/zh/core/server-monitor.html)(机制)
|
||||
- Prometheus /metrics 端点(外部监控体系接入)
|
||||
@@ -1,214 +1,37 @@
|
||||
# 6.13 租户管理
|
||||
# 6.7 租户管理
|
||||
|
||||
:::tip 概述
|
||||
租户管理是多租户体系的核心,支持租户的 CRUD、创建租户账号、查看租户用户。继承 BaseLogic(系统共享,租户表本身不隔离)。
|
||||
租户(gin_tenant,平台级表)管理 SaaS 侧的租户生命周期:开户、启停、有效期、租户管理员账号与成员视图。租户状态在登录与请求链路两处被校验。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
## 功能清单
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 基类 | BaseLogic(系统共享,租户表不隔离) |
|
||||
| 特殊接口 | account(创建租户账号)、users(查看租户用户) |
|
||||
| 删除校验 | beforeDelete 检查用户数 + 保护默认租户 |
|
||||
| 编码生成 | afterAdd 自动生成租户编码(T+4位ID) |
|
||||
| 状态校验 | TenantMiddleware 校验租户状态和过期时间 |
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 标准查询 |
|
||||
| Add | 开户(手写:建租户 + 初始化归属,超管操作) |
|
||||
| Update | 修改租户信息 |
|
||||
| Status | 启停租户(禁用后该租户全部用户无法登录/请求被拒) |
|
||||
| Delete / BatchDelete | 软删除(平台级操作,慎用) |
|
||||
| Account | 租户管理员账号管理 |
|
||||
| Users | 租户成员列表(按租户过滤) |
|
||||
|
||||
## 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 | 批量删除 |
|
||||
| 执行点 | 行为 |
|
||||
|---|---|
|
||||
| 登录(Login 第 4 步) | 租户禁用/过期 → 拒绝登录(摘要缓存 60 秒) |
|
||||
| 请求(Tenant 中间件) | 进行中被禁用/过期 → 请求被拒 |
|
||||
|
||||
## 核心代码
|
||||
## 数据归属
|
||||
|
||||
```php
|
||||
// app/logic/TenantLogic.php
|
||||
- 租户级 9 张表写侧自动补 tenant_id;超管新增未显式指定时取 `tenant.defaultId`
|
||||
- 平台级(tenantID=0)超管可见全部租户数据,用于管理与稽核
|
||||
|
||||
class TenantLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* 关联的模型类
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $modelClass = Tenant::class;
|
||||
## 管理界面
|
||||
|
||||
/**
|
||||
* LIKE 模糊匹配字段
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $pageLikeFields = ['name', 'code', 'contact'];
|
||||
`system/tenant`:列表 + 开户 + 启停 + 成员查看 + 管理员账号维护。
|
||||
|
||||
/**
|
||||
* 精确匹配字段
|
||||
*
|
||||
* @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` | 最大用户数限制 |
|
||||
- [5.9 多租户隔离](/zh/core/tenant.html)
|
||||
|
||||
@@ -1,222 +1,41 @@
|
||||
# 6.1 用户管理
|
||||
|
||||
:::tip 概述
|
||||
用户管理是系统核心模块,继承 BaseTenantLogic 实现租户隔离,支持 CRUD、重置密码、Excel 导入导出。
|
||||
用户是组织权限体系的核心表(gin_user,租户级)。除标准 CRUD 外,承载角色分配、密码重置、导入导出与(租户场景的)开户逻辑,并挂接数据权限过滤。
|
||||
:::
|
||||
|
||||
## 模块特点
|
||||
## 功能清单
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 租户隔离 | 继承 BaseTenantLogic,自动按 tenant_id 过滤 |
|
||||
| 数据权限 | 配置 dataScopeDeptField='dept_id',支持按部门过滤 |
|
||||
| 编码生成 | beforeAdd 自动生成用户编码(租户编码-6位序号,如 T0001-000020) |
|
||||
| 文件字段 | avatar(头像),保存到 user/avatar 目录 |
|
||||
| 唯一校验 | username、mobile 全局唯一 |
|
||||
| 枚举映射 | gender→gender 字典,status→user_status 字典 |
|
||||
| 密码加密 | bcrypt 双重加盐,新增时自动加密 |
|
||||
| 角色关联 | 通过 UserRole 中间表关联,支持多角色 |
|
||||
| 接口 | 说明 |
|
||||
|---|---|
|
||||
| Page / List / Detail | 标准查询(分页支持关键字/部门/状态过滤) |
|
||||
| Add | 新增(租户场景经 addWithTenant 归属租户) |
|
||||
| Update | 修改(全量表单语义) |
|
||||
| Status | 启停单列更新 |
|
||||
| ResetPwd | 重置密码(手写逻辑,管理员操作留痕) |
|
||||
| Delete / BatchDelete | 软删除 |
|
||||
| Import / Export | Excel 导入导出 |
|
||||
| Document | 用户打印信息(打印视图数据) |
|
||||
|
||||
## 文件清单
|
||||
## 特色机制
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `app/controller/UserController.php` | 控制器(9 个接口) |
|
||||
| `app/logic/UserLogic.php` | 逻辑层(含 import/export/resetPassword) |
|
||||
| `app/model/User.php` | 模型 |
|
||||
| `app/model/UserRole.php` | 用户角色关联模型 |
|
||||
| `app/validate/UserValidate.php` | 验证器 |
|
||||
- **角色随表单写入**:Add/Update 请求带 `roles []int64`(json 名 `roles`),事务内 rewriteUserRoles 先清后写 gin_user_role——没有独立的"分配角色"接口;service 层 AssignRoles 是事务复用的内部方法
|
||||
- **数据权限挂接**:userSpec 声明 `DataScope: ApplyDataScope("dept_id")`(user.go:77 注释:角色数据权限(Page/List/导出生效))——本模块是数据权限的典型示范(行级部门过滤)
|
||||
- **租户级表**:写侧自动补 tenant_id,读侧自动隔离
|
||||
- **密码安全**:重置与新增均走 crypto 哈希(外部 salt + bcrypt),永不落明文
|
||||
- **导入导出**:标准 Excel 契约(列头、逐行校验、结果汇总)
|
||||
|
||||
## 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 | 导出 |
|
||||
- 用户名在租户内唯一(UniqueFields 预检 + 数据库 uk 兜底)
|
||||
- 部门列 dept_id 与 gin_dept 关联,数据权限与组织展示都依赖它
|
||||
- 状态非 1 视为禁用(登录校验同口径)
|
||||
|
||||
## 特殊逻辑
|
||||
## 管理界面
|
||||
|
||||
### 重置密码
|
||||
`system/user`:列表 + 搜索 + 新增/编辑弹窗 + 分配角色 + 重置密码 + 导入导出。
|
||||
|
||||
```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` 自动生成。
|
||||
:::
|
||||
- 机制:[5.4 权限管理](/zh/core/rbac.html)、[5.10 数据权限](/zh/core/data-scope.html)
|
||||
- 开发:[第 3 章 模块开发实战](/zh/guide/overview.html)
|
||||
|
||||
Reference in New Issue
Block a user