Veitool V2.3.5 源码深度解读

项目地址https://gitee.com/veitool/veitoolthink
技术栈:ThinkPHP 8.x + Layui 2.13.x + PHP 8.1+
项目定位:企业级后台管理快速开发框架,内置 RBAC 权限、插件系统、JWT API、数据字典、多云存储等


目录

  1. 项目总览
  2. 目录结构
  3. 技术栈与依赖
  4. 多应用架构与入口
  5. BaseController — 控制器基类
  6. AdminBase — 后台认证与权限拦截
  7. 登录流程精读
  8. RBAC 权限引擎
  9. JWT API 认证体系
  10. 数据加解密 — RSA + AES
  11. Model 基类与软删除
  12. 系统管理模块精读
  13. 插件系统架构
  14. AppInit 中间件 — 解密与插件路由
  15. Layui 前端架构
  16. 工具函数库 common.php
  17. 安全机制全链路
  18. 学习路线图

1. 项目总览

Veitool 是一个以 ThinkPHP 8 为后端框架、Layui 为前端 UI 的后台管理快速开发脚手架。与勾股OA 不同,Veitool 的定位不是某个具体业务系统,而是一个 通用后台开发框架 —— 它提供了权限、菜单、字典、配置、文件上传、数据库管理、插件系统等基础设施,开发者在此基础上快速搭建业务模块。

核心特征

特征 说明
多应用 admin(后台)、index(前台)、api(接口)三应用分离
RBAC 角色 → 菜单 → 操作三级权限,菜单缓存 + 角色权限缓存
JWT 基于 firebase/php-jwt,access_token + refresh_token 双令牌
加解密 前端 RSA 加密 AES 密钥 → 后端 RSA 解密 → AES 解密数据体
插件系统 addons/ 目录插件,动态路由注入、菜单注入、配置注入
数据字典 字典组 + 字典项 + SQL 查询字典,全量缓存
系统配置 数据库存储配置项,vconfig() 函数读取,支持分组/隐私/关联
多云存储 本地 + 七牛 + 阿里云 OSS + 腾讯云 COS
Swoole 内置 Swoole/Worker 支持,可常驻内存运行
安装向导 public/install/ 独立安装流程,检测环境 → 配置数据库 → 导入 SQL

2. 目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
veitoolthink/
├── app/ # 应用核心
│ ├── BaseController.php # 控制器抽象基类(所有控制器父类)
│ ├── AppInit.php # 全局中间件(RSA解密 + 插件路由)
│ ├── ExceptionHandle.php # 全局异常处理
│ ├── Request.php # 自定义请求类
│ ├── common.php # 全局公共函数(常量定义 + 30+ 工具函数)
│ ├── middleware.php # 全局中间件注册
│ ├── event.php # 事件定义
│ ├── v_msg.tpl # 消息提示模板
│ ├── v_err.tpl # 错误提示模板
│ │
│ ├── admin/ # 后台应用
│ │ ├── controller/
│ │ │ ├── AdminBase.php # 后台控制器基类(认证+权限)
│ │ │ ├── Index.php # 后台首页/主面板/菜单JSON
│ │ │ ├── Login.php # 登录/退出/解锁屏
│ │ │ ├── Addon.php # 插件管理
│ │ │ └── system/ # 系统管理子控制器
│ │ │ ├── Area.php # 地区管理
│ │ │ ├── Database.php # 数据库备份/恢复/优化
│ │ │ ├── Dict.php # 字典管理
│ │ │ ├── Filemanage.php# 文件管理
│ │ │ ├── Log.php # 日志管理
│ │ │ ├── Manager.php # 管理员管理
│ │ │ ├── Menus.php # 菜单管理
│ │ │ ├── Online.php # 在线统计
│ │ │ ├── Roles.php # 角色管理
│ │ │ ├── Sequence.php # 序列号/单据号
│ │ │ ├── Setting.php # 系统设置
│ │ │ ├── Sms.php # 短信配置
│ │ │ └── Upload.php # 文件上传
│ │ └── view/ # 后台视图模板
│ │
│ ├── api/ # API 接口应用
│ │ └── controller/
│ │ ├── Auth.php # JWT 登录/验证/刷新
│ │ └── Error.php # API 错误处理
│ │
│ ├── index/ # 前台应用
│ │ └── controller/
│ │ └── Index.php # 前台首页
│ │
│ ├── model/ # 数据模型
│ │ ├── Base.php # 模型基类(one/all/del/事务)
│ │ └── system/ # 系统模型(17个)
│ │ ├── SystemManager.php
│ │ ├── SystemRoles.php
│ │ ├── SystemMenus.php
│ │ ├── SystemSetting.php
│ │ ├── SystemDict.php
│ │ ├── SystemDictGroup.php
│ │ └── ...
│ │
│ └── event/ # 事件目录

├── config/ # 配置文件(20个)
│ ├── app.php # 应用配置
│ ├── database.php # 数据库配置
│ ├── veitool.php # ★ Veitool 自定义配置(RSA密钥 + JWT)
│ ├── middleware.php # 中间件别名
│ ├── cache.php # 缓存配置
│ ├── session.php # Session 配置
│ ├── swoole.php # Swoole 配置
│ └── ...

├── extend/ # 扩展类库(PSR-0 自动加载)
│ ├── jwt/
│ │ ├── JwtToken.php # JWT 令牌生成/验证/刷新
│ │ └── StoreToken.php # Token Redis 缓存(单设备登录)
│ └── tool/
│ ├── DataEncryptor.php # RSA/AES 加解密
│ ├── Lock.php # 登录锁定/频繁操作限制
│ ├── Menus.php # 插件菜单管理
│ ├── MysqlBackup.php # 数据库备份工具
│ └── SendSms.php # 短信发送

├── addons/ # 插件目录
├── public/ # Web 入口
│ ├── index.php # 入口文件
│ ├── install/ # 安装向导
│ │ ├── index.php
│ │ ├── install.lock # 安装锁
│ │ ├── data/
│ │ │ ├── install_data.sql # 初始数据库
│ │ │ └── area_data.sql # 地区数据
│ │ └── tpl/ # 安装步骤页面
│ └── static/ # 静态资源
│ ├── layui/ # Layui 框架
│ ├── admin/ # 后台自定义 CSS/JS
│ │ ├── module/ # JS 模块(admin.js, vtable.js 等)
│ │ └── page/ # 锁屏/主题模板
│ ├── tinymce/ # TinyMCE 编辑器
│ ├── ueditor/ # UEditor 编辑器
│ └── cherrymd/ # Cherry Markdown 编辑器

├── route/
│ └── swoole.php # Swoole 静态文件路由

├── .env # 环境变量(数据库/Redis/调试)
├── composer.json
├── think # 命令行入口
└── README.md

3. 技术栈与依赖

composer.json 核心依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"require": {
"php": ">=8.0.0",
"topthink/framework": "^8.0", // ThinkPHP 8 核心
"topthink/think-orm": "^3.0|^4.0", // ORM
"topthink/think-multi-app": "^1.1", // 多应用支持
"topthink/think-view": "^2.0", // 模板引擎
"topthink/think-captcha": "^3.0", // 验证码
"topthink/think-swoole": "^4.0", // Swoole 支持
"topthink/think-worker": "^4.0", // Worker 支持
"topthink/think-queue": "^3.0", // 队列
"topthink/think-image": "^1.0", // 图片处理
"veitool/admin": "^3.0", // ★ Veitool 官方组件(插件/代码生成/云存储)
"firebase/php-jwt": "^7.0", // JWT 认证
"aliyuncs/oss-sdk-php": "^2.7", // 阿里云 OSS
"qcloud/cos-sdk-v5": "^2.6", // 腾讯云 COS
"qiniu/php-sdk": "^7.14", // 七牛云
"phpoffice/phpspreadsheet": "^3.8", // Excel
"phpoffice/phpword": "^1.3", // Word
"nelexa/zip": "^4.0" // ZIP 压缩
}
}

自动加载机制

1
2
3
4
5
6
7
8
9
"autoload": {
"psr-4": {
"app\\": "app", // 应用代码 → app/ 目录
"addons\\": "addons" // 插件代码 → addons/ 目录
},
"psr-0": {
"": "extend/" // extend/ 下按命名空间自动加载
}
}

关键点extend/ 使用 PSR-0 自动加载,所以 extend/jwt/JwtToken.php 的命名空间是 jwt\JwtTokenextend/tool/Lock.php 的命名空间是 tool\Lock,使用时直接 use jwt\JwtToken 即可。


4. 多应用架构与入口

入口文件 public/index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace think;

// 1. PHP 版本检测
if(version_compare(PHP_VERSION,'8.1.0','<')) die('require PHP >= 8.1.0!');

// 2. 安装引导(首次访问跳转到 /install/)
if (is_dir(__DIR__ . '/install') && !is_file(__DIR__ . '/install/install.lock'))
exit(header('Location:/install/'));

// 3. 引入 Composer 自动加载
require __DIR__ . '/../vendor/autoload.php';

// 4. 启动 HTTP 应用
$http = (new App())->http;
$response = $http->run();
$response->send();
$http->end($response);

多应用配置 config/app.php

1
2
3
4
5
6
7
8
return [
'default_app' => 'index', // 默认前台应用
'app_map' => ['admin'=>'admin'],// URL 映射:/admin/* → admin 应用
'app_express' => true, // 开启应用快速访问
'deny_app_list' => ['model','event'], // 禁止 URL 直接访问的目录
'with_route' => true, // 启用路由
'with_event' => true, // 启用事件
];

URL 路由映射规则

得益于 think-multi-app 多应用扩展,URL 自动按路径段映射到应用:

1
2
3
4
/admin/login/check        → app\admin\controller\Login::check()
/admin/system/menus/index → app\admin\controller\system\Menus::index()
/api/auth/login → app\api\controller\Auth::login()
/index/index/index → app\index\controller\Index::index()(默认应用)

与勾股OA 的对比:勾股OA 有 13 个子应用(finance/contract/project 等),每个应用是独立业务模块;Veitool 只有 3 个应用(admin/api/index),admin 内部通过 system/ 子目录划分系统管理功能,业务扩展通过 插件系统 实现。


5. BaseController — 控制器基类

app/BaseController.php 是所有控制器的抽象父类,定义了请求处理、参数验证、响应输出的统一范式。

构造方法 — 三段式初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
abstract class BaseController
{
protected $app;
protected $request;
protected $token = '';
protected $tokenName = '__token__';
protected $msgTpl = '';
protected $memUser = []; // 前台会员信息

public function __construct(App $app)
{
$this->app = $app;
$this->request = $this->app->request;
$this->__home(); // 前台集中业务(站点开关、会员信息)
$this->__auth(); // 验证(登录、权限)—— 子类覆盖
$this->__init(); // 控制器初始化 —— 子类覆盖
}

protected function __home() {
// 站点关闭开关
if(vconfig('site_close'))
$this->exitMsg(vconfig('site_close_tip','系统升级维护中'), 400);
// 获取前台会员 Session
$this->memUser = (array)session(VT_MEMBER);
}

protected function __auth(){} // 空方法,等待子类覆盖
protected function __init(){} // 空方法,等待子类覆盖
}

设计模式:模板方法模式(Template Method)。父类定义骨架 __home → __auth → __init,子类按需覆盖。

核心方法:only() — 参数获取与验证

这是整个框架最精巧的方法之一,用一套 DSL 语法实现了参数获取、类型转换、格式验证、SQL 过滤:

1
protected final function only($name = [], $type = 'post', $filter = 'strip_sql', $bin = true)

用法示例(来自 Menus 控制器):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 菜单添加时的参数验证
$d = $this->only([
'@token'=>'', // CSRF Token 验证
'@catid/d', // catid 转整数,@表示不执行 strip_sql
'menu_name/*/{2,20}/菜单名称', // 必填,2-20位,默认字符集
'role_name/*/{2,20}/权限名称', // 必填,2-20
'link_url/u', // 网址净化
'menu_url/u',
'role_url/u',
'icon/u',
'@parent_id/d', // 转整数
'@listorder/d',
'@ismenu/d',
'@state/d'
]);

DSL 语法解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
key/验证符/规则/提示/字符集/允许字符

验证符:
* 必填+格式验证
? 非空时验证
$ 非空时验证,不规范则置空(不中断)
a 转数组
d 转整数
f 转浮点
b 转布尔
s 转字符串
u 网址净化(strip_html 低级)
h 全净化(strip_html 高级)
c 转 HTML 实体
r 转2位小数

规则(验证符为 * 时):
e 邮箱
m 手机
c 身份证
p 密码 {6,16}
u 帐号 {4,30}
n 姓名 {2,30}
i 数串 {1,30}
a 插件名 {3,20}
v 配置名 {2,20}
{2,20} 自定义位数范围

前缀 @:
表示跳过 strip_sql 过滤(用于数字、Token 等安全字段)

响应输出方法

1
2
3
4
5
6
7
8
// JSON 响应(标准格式:code/msg/data/count/token)
protected final function returnMsg($msg = '', $code = 0, $data = [], ...)

// 中断响应(400=页面提示, 401=Ajax未登录, 303=跳转登录, 其他=JSON
protected final function exitMsg($m, $c = 0, $d = [], $h = [])

// 模板渲染
protected final function fetch(string $tmp = '', string $tip = '', bool $logon = true)

标准 JSON 响应格式

1
2
3
4
5
6
7
{
"code": 1, // 1=成功, 0=失败
"msg": "操作成功",
"data": {...},
"count": 10,
"token": "xxx" // CSRF Token(每次请求刷新)
}

6. AdminBase — 后台认证与权限拦截

app/admin/controller/AdminBase.php 继承 BaseController,覆盖 __auth() 实现后台完整的认证链。

认证链流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class AdminBase extends BaseController
{
protected $manUser = []; // 后台管理员信息
protected $appMap = ''; // 映射路径(如 /admin)
protected $routeUri = ''; // 当前路由 URI

// 覆盖父类,后台不需要前台业务
protected function __home(){}

// 后台认证初始化
protected final function __auth()
{
$this->appMap = VT_DIR . '/' . (array_search("admin", config('app.app_map')) ?: 'admin');
$this->isLogin(); // 第1步:验证登录
$this->loadMenusRoles(); // 第2步:载入权限菜单
$this->routeUri = strtolower(...); // 第3步:构建当前路由URI
$this->isPower(); // 第4步:验证操作权限
}
}

第1步:登录验证 isLogin()

1
2
3
4
5
6
7
8
9
10
11
private function isLogin()
{
if(is_null($this->manUser = session(VT_MANAGER))){
$url = $this->appMap.'/login/index';
if($this->request->isAjax()){
$this->exitMsg('您还未登录或已过期,请先登录!', 401, ['url'=>$url]);
}else{
$this->redirect($url); // 直接跳转登录页
}
}
}

第2步:载入权限菜单 loadMenusRoles()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private function loadMenusRoles()
{
// 从数据库重新查询用户,验证 Session 中的密码和盐值是否匹配
$us = Manager::one("username = '{$this->manUser['username']}' AND state > 0");
if($us && $this->manUser['password'] == $us['password']
&& $this->manUser['passsalt'] == $us['passsalt']){

// ★ 单设备登录检测:配置 ip_login 为 2 或 3 时开启
if(in_array(vconfig('ip_login',0),[2,3]) && $us['loginip'] != $this->request->ip()){
session(null);
$this->exitMsg('您的帐号已在其他终端登录!', ...);
}

// 超级管理员(userid=1)直接通过,非超管载入角色权限
$this->manUser = $us['userid']>1
? array_merge($us, Roles::cache($us['roleid'])) // 合并角色权限
: $us;
}else{
session(null);
$this->exitMsg('您还未登录或已过期,请先登录!', ...);
}
}

第3步:构建路由 URI

1
2
3
4
5
6
7
$this->routeUri = strtolower(
$this->request->ADDON_APP // 插件前缀(如 "myplugin/"
. $this->request->controller() // 控制器名
. "/" . $this->request->action() // 方法名
. (($action = $this->request->get('action')) ? '/'.$action : '')
);
// 示例结果:system/menus/index 或 system/manager/index/info

第4步:权限验证 isPower()

1
2
3
4
5
6
7
private function isPower()
{
// 超管直接通过;非超管检查 routeUri 是否在 actions 数组中
if($this->manUser['userid']>1 && !in_array($this->routeUri, $this->manUser['actions'])){
$this->exitMsg('抱歉,您没有该项权限请联系管理员!', ...);
}
}

权限扩展机制 getRoleExt()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected function getRoleExt()
{
if($this->manUser['userid'] == 1) return 0; // 超管直接返回0
$d = explode('/', $this->routeUri);
// 优先级:addon/* > addon/controller/* > addon/controller/action
if(isset($this->manUser['role_ext'][$d[0].'/*'])){
return $this->manUser['role_ext'][$d[0].'/*'];
}elseif(...){
return $this->manUser['role_ext'][$d[0].'/'.$d[1].'/*'];
}elseif(...){
return $this->manUser['role_ext'][$d[0].'/'.$d[1].'/'.$d[2].'/*'];
}else{
return $this->manUser['role_ext'][$this->routeUri] ?? 0;
}
}

用途:在 Manager 控制器中,getRoleExt() 返回 1 表示当前用户只能管理自己,不能编辑其他用户。


7. 登录流程精读

app/admin/controller/Login.php 实现后台登录全流程。

登录验证 check()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public function check()
{
// 1. IP 锁定检测(默认5次失败锁定30分钟)
$ip = $this->request->ip();
if(Lock::check(['key'=>'LOGIN_'.$ip]))
return $this->returnMsg(Lock::msg());

// 2. 参数验证(用户名+密码+验证码)
$d = $this->only(['username/*/u/管理帐号','password/*/p/登录密码','captcha']);

// 3. 验证码校验(可在后台关闭)
if(vconfig('admin_captcha',1) && !captcha_check($d['captcha']))
return $this->returnMsg('验证码错误!');

// 4. 查询用户
$rs = Manager::one(compact('username'));
if(empty($rs)){
LoginLog::add($username, $password, '', '账号错误');
Lock::add(); // 记录失败次数
return $this->returnMsg('帐号或密码错误!');
}

// 5. 状态检测
if($rs->state == 0) return $this->returnMsg('帐号已被停用!');

// 6. 密码验证(前端 MD5 传输 → 后端再 MD5 + salt)
if($rs->password === set_password($password, $rs->passsalt)){
// 更新登录信息
$rs->logintime = time();
$rs->loginip = $ip;
$rs->logins ++;
$rs->save();

$rs = $rs->toArray();
$rs['uid'] = 'AM-'.uniqid(); // 生成会话编号

LoginLog::add($username, $password, $rs['passsalt']);
session(VT_MANAGER, $rs); // 写入 Session
Lock::del(); // 清除锁定

return $this->returnMsg('登录成功!', 1, ['url'=>($this->appMap ?: '/')]);
}

LoginLog::add($username, $password, $rs['passsalt'], '密码错误');
Lock::add();
return $this->returnMsg('帐号或密码错误!');
}

密码加密算法

1
2
3
4
// app/common.php
function set_password($p, $s){
return md5((is_md5($p) ? md5($p) : md5(md5($p))).$s);
}

流程

  1. 前端:hex_md5(password) → 将明文 MD5 后传输(防明文传输)
  2. 后端:检测传入是否已是 MD5 → md5(md5(password) + salt) → 存储双重 MD5 + 盐值

锁定机制 tool\Lock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 默认配置:5次失败,锁定1800秒(30分钟)
private static $config = [
'times' => 5, 'time' => 1800, 'tips' => '登录',
'key' => 'ToolLock', 'msg' => '', 'add' => false
];

// 检查是否已锁定
public static function check(array $c = [])
{
self::$config = array_merge(self::$config, $c);
$data = Cache::get(self::$config['key']);
if(isset($data['times']) && $data['times'] >= self::$config['times']
&& (time() - $data['time']) < self::$config['time']){
self::$config['msg'] = '多次登录失败,您已被锁定'.(intval(self::$config['time']/60)).'分钟';
return true; // 已锁定
}
return false;
}

锁屏解锁 unlock()

1
2
3
4
5
6
7
8
9
10
public function unlock()
{
if(is_null($us=session(VT_MANAGER))) return $this->returnMsg('还未登录');
$password = $this->request->post('password','');
if($us['password'] === set_password($password, $us['passsalt'])){
return $this->returnMsg('success', 1);
}else{
return $this->returnMsg('解锁密码错误');
}
}

锁屏在前端通过 Layui 弹出层实现,用户输入密码后 Ajax 请求 unlock 接口验证。


8. RBAC 权限引擎

Veitool 的权限系统基于 角色 → 菜单 → 操作 三级模型,通过缓存实现高性能权限校验。

数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
vt_system_manager (管理员)
├── userid (主键)
├── username
├── password + passsalt
├── roleid (当前角色ID)
├── roleids (多角色ID串,逗号分隔)
└── groupid (组织机构ID)

vt_system_roles (角色)
├── roleid (主键)
├── role_name
├── role_menuid (拥有的菜单ID串,逗号分隔)
└── role_ext (扩展权限,格式:controller/action=数字)

vt_system_menus (菜单)
├── menuid (主键)
├── menu_name (菜单名称)
├── role_name (权限名称)
├── menu_url (前端路由)
├── role_url (后端路由,如 system/menus/index)
├── parent_id (父菜单ID)
├── catid (菜单分类ID)
├── type (1=后台菜单, 2=会员菜单)
└── state (状态)

菜单缓存 SystemMenus::cache()

1
2
3
4
5
6
7
8
9
10
11
12
13
// 缓存键:VMENUS_1(后台菜单)、VMENUS_2(会员菜单)
public static function cache(int $s = 0, int $t = 1)
{
$k = 'VMENUS_'.$t;
$r = cache($k);
if(!$r || $s){
$r = self::where("type=$t")
->order(['parent_id'=>'asc','listorder'=>'asc','menuid'=>'asc'])
->column('menuid,menu_name,role_name,link_url,menu_url,role_url,icon,catid,parent_id,state', 'menuid');
cache($k, $r);
}
return $r;
}

角色权限缓存 SystemRoles::cache()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 缓存键:VMENUS_1_角色ID
public static function cache(int|array $role, int $reset = 0)
{
$roleid = is_array($role) ? $role['roleid'] : $role;
$key = 'VMENUS_1_'.$roleid;
$rs = cache($key);
if(!$rs || $reset){
$rs = [];
$str = '';
$ro = is_array($role) ? $role : self::where(['state'=>1,'roleid'=>$roleid])
->field('roleid,role_name,role_menuid,role_ext')->findOrEmpty()->toArray();

if(!empty($ro) && $ro['role_menuid']){
$ms = SystemMenus::cache(); // 获取全部后台菜单
foreach ($ms as $k=>$v){
// 检查菜单ID是否在角色的 menuid 串中
if(strpos(",$ro[role_menuid],", ",$k,") !== false){
$str .= $v['role_url'] ? ','.$v['role_url'] : '';
}
}
}

// 构建 actions 数组(用于权限校验)
$rs['actions'] = explode(',', trim($str, ','));

// 解析 role_ext(扩展权限)
if($ro['role_ext']){
$arr = explode("\n", $ro['role_ext']);
$ro['role_ext'] = [];
foreach($arr as $v){
$ks = explode('=', $v);
$ro['role_ext'][$ks[0]] = $ks[1] ?? 0;
}
}else{
$ro['role_ext'] = [];
}

$rs = array_merge($ro, $rs);
cache($key, $rs, 31536000); // 缓存1年
}
return $rs;
}

权限校验流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
请求 /admin/system/menus/index


AdminBase::__auth()

├── isLogin() → Session 中是否有 VT_MANAGER?
├── loadMenusRoles() → 从 DB 重新验证用户 → 载入角色权限缓存
│ │
│ ├── 超管(userid=1) → 直接通过,manUser 中无 actions 限制
│ └── 非超管 → manUser['actions'] = ['system/menus/index', ...]

├── 构建 routeUri = "system/menus/index"

└── isPower()

├── 超管 → 直接通过
└── 非超管 → in_array("system/menus/index", manUser['actions']) ?
通过 : "没有该项权限"

菜单树构建 SystemMenus::getMenus()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static function getMenus(array $data = [])
{
$arr = []; $str = ',';
if(isset($data['userid'])){
$tp = 1; $md = $data['role_menuid']; // 后台菜单
$userid = $data['userid'];
$data = self::cache(0, $tp); // 全部后台菜单

foreach($data as $k=>$v){
// 非超管且菜单不在权限范围内 → 跳过
$flag = (($userid>1 || $tp==2) && strpos(",$md,", ",$k,")===false);
if(!$flag && $v['role_url'])
$str .= $v['role_url'].','; // 收集权限URL
if(!$v['state'] || $flag) continue;

$arr[] = [
'id' => $v['menuid'],
'icon' => $v['icon'],
'name' => $v['menu_name'],
'catid' => $v['catid'],
'pid' => $v['parent_id'],
'url' => $v['menu_url'] ? '#/'.$v['menu_url'] : '',
'iframe' => $v['link_url']
];
}
}
return ['menus'=>$arr, 'roles'=>$str];
}

9. JWT API 认证体系

Veitool 在后台使用 Session 认证,在 API 接口使用 JWT 双令牌认证。

JWT 配置 config/veitool.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'jwt' => [
'algorithms' => 'HS256', // 签名算法
'access_secret_key' => '86433bf7adb7...', // access 令牌密钥
'refresh_secret_key' => '1713a24f7b11...', // refresh 令牌密钥
'access_exp' => 7200, // access 有效期 2小时
'refresh_exp' => 604800, // refresh 有效期 7天
'refresh_off' => false, // 是否禁用 refresh
'iss' => 'veitool', // 签发者
'nbf' => 0, // 生效延迟
'leeway' => 60, // 时钟偏差容忍
'single_device_on' => false, // 单设备登录开关
'cache_token_ttl' => 604800, // 缓存令牌TTL
'cache_token_a_pre' => 'JWT:TOKEN:', // access 缓存前缀
'cache_token_r_pre' => 'JWT:REFRESH_TOKEN:', // refresh 缓存前缀
'get_token_on' => false, // 是否支持 GET 传 token
'get_token_key' => 'authorization',
],

令牌生成 JwtToken::generateToken()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static function generateToken(array $extend): array
{
if (!isset($extend['id'])) {
throw new RuntimeException('扩展缺少关键参数:id');
}
$config = self::getConfig();
$config['access_exp'] = $extend['access_exp'] ?? $config['access_exp'];
$config['refresh_exp'] = $extend['refresh_exp'] ?? $config['refresh_exp'];

$payload = self::getPayload($config, $extend); // 构建 JWT 载荷
$secrets = self::getPrivateKey($config); // 获取签名密钥

// 生成 access_token
$token = [
'token_type' => 'Bearer',
'expires_in' => $config['access_exp'],
'access_token' => self::makeToken($payload['accessPayload'], $secrets['accessKey'], $config['algorithms'])
];

// 生成 refresh_token
if (!$config['refresh_off']) {
$token['refresh_token'] = self::makeToken($payload['refreshPayload'], $secrets['refreshKey'], $config['algorithms']);
}

// 单设备登录:缓存 token 到 Redis
if ($config['single_device_on']) {
self::saveStoreToken($config, $extend, $token);
}
return $token;
}

JWT 载荷结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static function getPayload(array $config, array $extend): array
{
$time = time();
$basePayload = [
'iss' => $config['iss'], // 签发者
'aud' => $config['iss'], // 接收方
'iat' => $time, // 签发时间
'nbf' => $time + ($config['nbf'] ?? 0), // 生效时间
'exp' => $time + $config['access_exp'], // 过期时间
'extend' => $extend // ★ 自定义扩展数据
];
$resPayLoad['accessPayload'] = $basePayload;

// refresh_token 使用更长的过期时间
$basePayload['exp'] = $time + $config['refresh_exp'];
$basePayload['extend']['access_exp'] = $config['refresh_exp'];
$resPayLoad['refreshPayload'] = $basePayload;

return $resPayLoad;
}

令牌验证 JwtToken::verify()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static function verify(int $tokenType = 1, string $token = '', array $config = []): array
{
$config = $config ?: self::getConfig();
$token = $token ?: self::getTokenFromHeaders($config); // 从 Header 获取
$pubKey = self::getPublicKey($config, $tokenType);

try {
JWT::$leeway = $config['leeway'];
$decoded = JWT::decode($token, new Key($pubKey, $config['algorithms']));
} catch (BeforeValidException $b) {
throw new RuntimeException('身份令牌未生效,请稍后再试', 401011);
} catch (ExpiredException $e) {
throw new RuntimeException('身份令牌已过期,请重新登录', 401012);
} catch (SignatureInvalidException $e) {
throw new RuntimeException('身份令牌无效', 401013);
} catch (Exception $e) {
throw new RuntimeException('令牌错误:'. $e->getMessage(), 401014);
}

// 验证扩展字段
if (!isset($decoded->extend) || empty($decoded->extend)) {
throw new RuntimeException('身份令牌扩展字段不存在', 401015);
}

$decodeToken = json_decode(json_encode($decoded), true);

// 单设备登录:检查 Redis 中的 token 是否匹配
if ($config['single_device_on']) {
$cacheTokenPre = $tokenType == 1 ? $config['cache_token_a_pre'] : $config['cache_token_r_pre'];
$client = $decodeToken['extend']['client'] ?? self::TOKEN_CLIENT_WEB;
StoreToken::verifyToken($cacheTokenPre, $client, (string)$decodeToken['extend']['id'], $token);
}
return $decodeToken;
}

令牌刷新 JwtToken::refreshToken()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static function refreshToken(array &$_extend = []): array
{
$config = self::getConfig();
$extend = self::verify(2, '', $config); // 先验证 refresh_token
$_extend = $extend['extend']; // 更新外部扩展指针

$payload = self::getPayload($config, $extend['extend']);
$secrets = self::getPrivateKey($config);

// 计算 refresh_token 剩余时长,保持不过期
if (($times = $extend['exp'] - time()) > 0) {
$payload['refreshPayload']['exp'] = $extend['exp'];
$payload['refreshPayload']['extend']['access_exp'] = $times;
$config['refresh_exp'] = $times;
}

$newToken['access_token'] = self::makeToken(...);
if (!$config['refresh_off']) {
$newToken['refresh_token'] = self::makeToken(...);
}
// 更新 Redis 缓存
if ($config['single_device_on']) {
self::saveStoreToken($config, $extend, $newToken);
}
return $newToken;
}

单设备登录 StoreToken

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class StoreToken
{
// 缓存 token:JWT:TOKEN:WEB:1 → access_token
public static function saveToken(string $pre, string $client, string $uid, int $ttl, string $token)
{
$cacheKey = self::generateKey($pre, $client, $uid);
Cache::store('redis')->set($cacheKey, $token, $ttl + (int)config('veitool.jwt.leeway', 0));
}

// 验证 token:如果 Redis 中的 token 与当前请求的不同 → 已在其他设备登录
public static function verifyToken(string $pre, string $client, string $uid, string $token): bool
{
$cacheKey = self::generateKey($pre, $client, $uid);
$ttl = Cache::store('redis')->handler()->ttl($cacheKey);
if ($ttl === -2 || $ttl === 0) {
throw new RuntimeException('该帐号身份已过期,请重新登录.', 401012);
} elseif (Cache::store('redis')->get($cacheKey) != $token) {
throw new RuntimeException('该帐号已在其他设备登录,强制下线.', 401016);
}
return true;
}

private static function generateKey(string $pre, string $client, string $uid): string
{
return sprintf('%s%s:%s', $pre, $client, $uid);
}
}

API 登录示例 app/api/controller/Auth.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function login()
{
// ... 验证账号密码(与后台登录逻辑一致)...

// 生成 JWT Token
$access_exp = config('veitool.jwt.access_exp', 7200);
$token = JwtToken::generateToken([
'access_exp' => $access_exp,
'username' => $rs->username,
'uid' => $rs->uid, // 会话编号
'id' => $rs->userid, // 用户ID(关键:单设备登录的依据)
'type' => $d['type'], // pc|h5|app
'plat' => 'veitool',
]);

return $this->returnMsg('登录成功!', 1, ['token' => $token, 'access_exp' => $access_exp]);
}

10. 数据加解密 — RSA + AES

Veitool 实现了前后端数据加密传输,防止接口数据被中间人窃取。

加解密流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
前端                              后端
│ │
├── 生成随机 AES key (32字节) │
├── 生成随机 AES iv (32字节) │
├── key+iv 拼接 → RSA 公钥加密 │
├── 数据 → AES-256-CBC 加密 │
├── POST: encrypt_data=密文 │
├── Header: VeitoolAdminxKeySecret=RSA密文
│ ▼ │
│ AppInit 中间件
│ │
│ ├── RSA 私钥解密 → 得到 key+iv
│ ├── 切分:aes_key = 前32字节, aes_iv = 后32字节
│ ├── AES 解密 encrypt_data → 得到明文 JSON
│ └── 合并到 $_POST / $_GET
│ │
└── 接收响应(可选 AES 加密返回) ◄┘

后端解密 extend/tool/DataEncryptor.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class DataEncryptor
{
// RSA 解密(用私钥解密前端用公钥加密的数据)
public static function rsaDecrypt(?string $data) : string
{
$private_key = config('veitool.rsa_pri_key');
$private_key = openssl_pkey_get_private($private_key);
openssl_private_decrypt(base64_decode($data), $decrypted, $private_key);
return $decrypted;
}

// AES-256-CBC 加密
public static function aesEncrypt($data, string $key, string $iv) : string
{
$data = json_encode($data);
$data = openssl_encrypt($data, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($data);
}

// AES-256-CBC 解密
public static function aesDecrypt(?string $data, string $key, string $iv) : array
{
$data = base64_decode($data);
$data = openssl_decrypt($data, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
$data = json_decode($data, true);
return self::formatJsonToArray($data);
}

// 处理带标号的同名键(如 items[0], items[1] → items 数组)
public static function formatJsonToArray(array $arr) : array
{
foreach($arr as $key => $value){
if(preg_match('/^(\w+)\[(\d+)\]$/', $key, $match)){
$arr[$match[1]][] = $value;
unset($arr[$key]);
}
}
return $arr;
}
}

RSA 密钥配置

1
2
3
4
5
6
7
8
9
10
11
// config/veitool.php
'rsa_pri_key' => <<<EOF
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDScXb6...
-----END PRIVATE KEY-----
EOF,
'rsa_pub_key' => <<<EOF
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0nF2+iIO06SQKBz1...
-----END PUBLIC KEY-----
EOF,

前端在 index.html 中注入公钥:

1
var rsa_public = `{:config('veitool.rsa_pub_key')}`;

11. Model 基类与软删除

app/model/Base.php 是所有模型的父类,封装了统一的查询和删除接口。

模型基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Base extends Model
{
protected $createTime = 'add_time'; // 创建时间字段
protected $updateTime = 'upd_time'; // 更新时间字段
protected $deleteTime = 'del_time'; // 删除时间字段
protected $hidden = ['del_time']; // 隐藏字段
protected $readonly = ['add_time']; // 只读字段
protected $defaultSoftDelete = 0; // 软删除默认值

// 获取单条数据
public static function one(string|array $where = '', string $field = '*', string|array $order = [])
{
return self::where($where)->field($field)->order($order)->find();
}

// 获取多条数据
public static function all(string|array $where = '', string $field = '*', string|array $order = [])
{
return self::where($where)->field($field)->order($order)->select();
}

// 删除数据(支持软删除和真删除)
public static function del(mixed $where, bool $force = false)
{
if(empty($where) && 0 !== $where) return false;

$query = (new static())->db();
if($force) $query->removeOption('soft_delete'); // 真删

// 支持多种 where 格式:字符串、键值对、闭包、主键
if((is_string($where) && strpos($where, ' ') !== false)
|| (is_array($where) && key($where) !== 0)){
$query->where($where);
$where = [];
}elseif($where instanceof \Closure){
call_user_func_array($where, [&$query]);
$where = [];
}

$result = $query->select((array)$where);
$i = 0;
foreach($result as $rs){
$i += $rs->force($force)->delete();
}
return $i;
}

// 事务支持
public static function beginTrans() { Db::startTrans(); }
public static function commitTrans() { Db::commit(); }
public static function rollbackTrans() { Db::rollback(); }
public static function checkTrans(bool|int $res = false) {
$res ? self::commitTrans() : self::rollbackTrans();
}
}

软删除使用

各系统模型通过 use \think\model\concern\SoftDelete; 启用软删除:

1
2
3
4
5
6
class SystemRoles extends Base
{
use \think\model\concern\SoftDelete;
protected $pk = 'roleid';
// ...
}

软删除时,del_time 字段会被设为当前时间戳,查询时自动排除 del_time > 0 的记录。

模型查询范式

Veitool 的模型查询有两种模式:

1
2
3
4
5
6
7
8
// 方式1:静态方法(Base 类提供)
$rs = Manager::one(compact('username')); // 单条
$rs = Manager::all("state = 1"); // 多条
Manager::del(['userid'=>1]); // 删除

// 方式2:实例方法(模型自定义的列表查询)
$rs = (new R())->listQuery(); // 分页列表
$rs = (new S())->listArray($where, 'name,value'); // 数组列表

12. 系统管理模块精读

12.1 菜单管理 system/Menus

菜单管理支持分类管理、菜单 CRUD、批量添加、快速编辑、导入导出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Menus extends AdminBase
{
// 菜单列表(异步JSON)
public function index(string $do = '')
{
if($do=='json'){
$catid = $this->request->get('catid/d',0);
return $this->returnMsg(
M::where("type=1 AND catid=$catid")
->order(['listorder'=>'asc','menuid'=>'asc'])->select()
);
}
$this->assign(['category' => json_encode(C::catList('01',0,'title,catid'))]);
return $this->fetch();
}

// 菜单添加
public function add()
{
$d = $this->only(['@token'=>'','@catid/d','menu_name/*/{2,20}/菜单名称',
'role_name/*/{2,20}/权限名称','link_url/u','menu_url/u','role_url/u',
'icon/u','@parent_id/d','@listorder/d','@ismenu/d','@state/d']);
$d['creator'] = $this->manUser['username'];
M::create($d);
M::cache(1); // 刷新菜单缓存
return $this->returnMsg("添加菜单成功", 1);
}

// 快速编辑(行内编辑)
public function edit(string $do = '')
{
if($do=='up'){
// 快编模式:只更新单个字段
$value = $d['av'];
$field = $d['af'];
if(!in_array($field,['menu_name','menu_url','role_url','link_url','listorder','ismenu','state']))
return $this->returnMsg("参数错误");
// ...
$Myobj->save([$field=>$value]);
M::cache(1);
}else{
// 完整编辑
$Myobj->save($d);
M::cache(1);
}
}
}

12.2 角色管理 system/Roles

角色管理核心是 菜单权限树 的分配(zTree 组件)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Roles extends AdminBase
{
// 角色权限树JSON(zTree 格式)
public function index(string $do = '')
{
if($do=='mjson'){
$roleid = $this->request->get('roleid/d');
$ids = '';
if($roleid){
$rs = R::one(['roleid'=>$roleid]);
$ids = empty($rs) ? '' : $rs['role_menuid'];
}
$rs = SystemMenus::cache();
$data = [];
foreach($rs as $v){
$flag = (strpos(",$ids,", ",$v[menuid],")!==false) ? true : false;
$data[] = [
'id'=>$v['menuid'], 'pId'=>$v['parent_id'],
'name'=>$v['role_name'].' '.$v['role_url'],
'checked'=>$flag, 'open'=>true
];
}
return $this->returnMsg($data);
}
// ...
}

// 角色添加
public function add()
{
$d = $this->only(['@token'=>'','role_name/*/{2,30}/角色名称',
'@listorder/d','@state/d','role_menuid','role_ext']);
// menuid 数组转逗号串
$d['role_menuid'] = is_array($d['role_menuid'])
? implode(',', array_map('intval', $d['role_menuid'])) : '';
$d['creator'] = $this->manUser['username'];
$obj = R::create($d);
// 立即缓存角色权限
R::cache(['roleid'=>$obj->roleid,'role_name'=>$d['role_name'],
'role_menuid'=>$d['role_menuid'],'role_ext'=>$d['role_ext']], 1);
return $this->returnMsg("添加成功", 1);
}
}

12.3 管理员管理 system/Manager

管理员管理包含用户 CRUD、个人中心、修改密码、重置密码、组织机构管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Manager extends AdminBase
{
// 管理员列表(带数据权限控制)
public function index(string $do = '', string $action = '')
{
if($action=='info'){ // 个人中心
// ...
}elseif($action=='role'){ // 角色切换
$roleid = $this->request->get('roleid/d');
if($roleid!=$this->manUser['roleid']
&& in_array($roleid, explode(',', $this->manUser['roleids']))){
M::update(['userid'=>$this->manUser['userid'],'roleid'=>$roleid]);
}
return $this->redirect($this->appMap);
}
if($do=='json'){
// ★ 数据权限:getRoleExt() 返回0表示全部可见,返回1表示只能看自己
$where = [[],[['username', '=', $this->manUser['username']]]];
return $this->returnMsg(
(new M())->listQuery($where[$this->getRoleExt()] ?? [], 'password,passsalt,token')
);
}
// ...
}

// 用户添加
public function add()
{
$d = $this->only(['@token'=>'','username/*/u/管理帐号','password/*/p/登录密码',
'@groupid/d/请选择所属机构','roleids/*/i/请选择所属角色',
'truename/?/n','mobile/?/m','email/?/e']);
if(M::one(['username'=>$d['username']])) return $this->returnMsg("该用户帐号已经存在");
$d["passsalt"] = random(8); // 生成随机盐
$d["password"] = set_password($d["password"],$d["passsalt"]);
$d["roleid"] = explode(",",$d['roleids'])[0]; // 多角色取第一个为当前角色
$d["creator"] = $this->manUser['username'];
M::create($d);
return $this->returnMsg("添加用户成功", 1);
}

// 重置密码
public function resetpwd()
{
$d = $this->only(['@token'=>'','@userid/d/参数错误','newPassword/*/p/新登录密码']);
if($d["userid"]==1) return $this->returnMsg('超级管理员禁止重置密码');
// 权限扩展检查
if($d["userid"] != $this->manUser['userid'] && $this->getRoleExt() == 1)
return $this->returnMsg("您没有重置其他用户密码的权限");
$d["passsalt"] = random(8);
$d["password"] = set_password($d["newPassword"],$d["passsalt"]);
unset($d["newPassword"]);
M::update($d);
return $this->returnMsg("重置密码成功", 1);
}
}

12.4 系统设置 system/Setting

系统设置是 Veitool 的特色功能:所有配置项存储在数据库中,通过 vconfig() 函数读取,支持动态增删配置项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Setting extends AdminBase
{
// 设置首页
public function index(string $do = '')
{
$groups = vconfig('sys_group',[]); // 配置分组
if($do=='json'){
$group = $this->request->get('group', key($groups));
$where[] = ['state','=',1];
$where[] = ['addon','=',''];
$where[] = ['group','=',strip_sql($group)];
$rs = (new S())->listArray($where, 'name,title,value,type,options,private,relation,tips');

// 根据配置类型预处理数据
foreach($rs as &$v){
if($v['type'] == 'images'){
$v['value'] = $v['value'] ? json_decode($v['value']) : [];
}elseif($v['type'] == 'upfile'){
$v['filetype'] = $v['options']; $v['options'] = '';
}elseif(in_array($v['type'], ['year','month','date','time','datetime'])){
$v['range'] = $v['options']; $v['options'] = '';
}elseif($v['private']){
$v['value'] = half_replace($v['value']); // 隐私字段脱敏
}
$v['placeholder'] = $v['tips'];
if($v['options']) $v['options'] = parse_attr($v['options']);
}
return $this->returnMsg($rs, 1);
}
// ...
}

// 设置更新
public function edit()
{
$d = $this->only(['@token'=>''], 'post', 'strip_sql', false);
$group = $d['__g'] ?? 'system';
// 查询该分组下的所有配置项
$rs = (new S())->listArray($where, 'name,type,private');
foreach($rs as $v){
$name = $v['name'];
if(in_array($name, ['sys_group','sys_type'])) continue; // 关键配置不可改
// 根据类型处理值
if($v['type'] == 'checkbox'){
$data['value'] = isset($d[$name]) ? implode(',', $d[$name]) : '';
}elseif($v['type'] == 'images'){
$data['value'] = isset($d[$name]) ? json_encode($d[$name]) : '';
}else{
$data['value'] = $d[$name] ?? 0;
if($v['private'] && strpos($data['value'], '***') !== false) continue; // 隐私含星号跳过
}
S::where("name='$name'")->update($data);
}
S::cache(1); // 刷新配置缓存
return $this->returnMsg("设置成功", 1);
}
}

配置缓存机制 SystemSetting::cache()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static function cache(int $reset = 0)
{
$key = 'VSETTING';
$rs = cache($key);
if(!$rs || $reset){
$rs = self::getSetting(); // 从数据库读取全部配置
cache($key, $rs);
}
return $rs;
}

public static function getSetting(string|null $name = null)
{
$configs = self::column('value,type,name,addon');
$result = [];
foreach($configs as $config){
// 根据类型解析值
if($config['type'] == 'array'){
$val = parse_attr($config['value']);
}elseif($config['type'] == 'checkbox'){
$val = $config['value']!='' ? explode(',', $config['value']) : [];
}else{
$val = $config['value'];
}
// 插件配置以 @插件名 为前缀
if($config['addon']){
$result['@'.$config['addon']][$config['name']] = $val;
}else{
$result[$config['name']] = $val;
}
}
return is_null($name) ? $result : $result[$name];
}

vconfig() 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function vconfig($name='', $default=''){
static $_VCF = [];
$_VCF = $_VCF ?: SystemSetting::cache(); // 首次调用加载缓存
if($name){
if(isset($_VCF[$name])){
return $_VCF[$name];
}
// 支持 域.子域 格式(如 sys_group.title)
$dt = explode('.', $name);
if(isset($_VCF[$dt[0]])){
$rs = $_VCF[$dt[0]];
if(isset($dt[1])){
$rs = $rs[$dt[1]] ?? $default;
}
return $rs;
}
// 插件配置:@插件名.键名
return $default;
}
return $_VCF; // 返回全部配置
}

12.5 数据字典 system/Dict

数据字典支持字典组层级管理和字典项 CRUD,还支持 SQL 查询字典(从数据库表动态生成字典)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class SystemDict extends Base
{
// 字典缓存
public static function cache(int $s = 0)
{
$k = 'DICTS_ARRAY';
$r = cache($k);
if(!$r || $s){
$r = [];
$g = SystemDictGroup::all("groupid > 0", 'id,code,sql');
$p = env('database.prefix', 'vt_');
foreach($g as $v){
if($v['sql']){
// ★ SQL 查询字典:将 SQL 中的 vt_ 替换为实际前缀
$sql = str_ireplace(
['update','replace','delete','drop','vt_'],
['@@','@@','@@','@@',$p], $v['sql']
);
if(strpos($sql,'@@') === false)
$r[$v['code']] = Db::query("{$sql}");
}else{
// 普通字典项
$r[$v['code']] = self::where("groupid = $v[id] AND state = 1")
->order(['parentid'=>'asc','listorder'=>'asc','id'=>'asc'])
->column('id,name,value,parentid as pid,arrparentid as pids');
}
}
cache($k, $r);
}
return $r;
}
}

12.6 文件上传 system/Upload

支持本地 + 七牛 + 阿里云 OSS + 腾讯云 COS 四种存储引擎。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Upload extends AdminBase
{
private $CF = [
'upload_engine'=>'local',
'upload_image_type'=> 'jpg,png,gif,jpeg',
'upload_image_size'=> 2,
// ... 七牛/阿里云/腾讯云配置
];

public function upfile(string $file = 'file', int $groupid = 0, string $action = '', string $thum = '')
{
$this->init();
$engine = $this->config['default']; // 存储引擎
$this->config['engine'][$engine]['type'] = $action;
$this->config['engine'][$engine]['thum'] = $thum;

// 实例化存储驱动(veitool/admin 包提供)
$StorageDriver = new StorageDriver($this->config);
$StorageDriver->setUploadFile($file);
if(!$StorageDriver->upload())
return $this->returnMsg('上传失败!'.$StorageDriver->getError());

// 保存到数据库
$data['storage'] = $engine;
$data['fileurl'] = VT_DIR.$domain.'/'.$fileName;
$data['filename'] = $fileInfo['oname'];
$data['filesize'] = round($fileInfo['size']/1024, 2);
$data['filetype'] = $action; // image/file/video/audio
$data['fileid'] = UploadFile::insertGetId($data);

// 本地图片压缩(>300KB 时)
if($data['filesize']>300 && $engine == 'local'){
$pic = ROOT_PATH . 'public' . $data['fileurl'];
if($data['fileext']=='jpg'){
$pics = Imagecreatefromjpeg($pic);
Imagejpeg($pics, $pic, 70); // 质量70
}elseif($data['fileext']=='png'){
$pics = imagecreatefrompng($pic);
imagepng($pics, $pic, 9); // 压缩级别9
}
}
return $this->returnMsg('上传成功!', 1, $data);
}
}

12.7 数据库管理 system/Database

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Database extends AdminBase
{
public function __init()
{
$this->db = new BK(['compress'=>1, 'level'=>5]); // MysqlBackup 工具
}

// 数据表列表
public function index()
{
$rs = $this->db->dataList();
// 计算每张表的数据大小、索引大小、碎片大小
foreach($rs as $k=>$v){
$rs[$k]['data_length'] = round($v['data_length']/1024/1024, 3);
$rs[$k]['index_length'] = round($v['index_length']/1024/1024, 3);
$rs[$k]['data_total'] = round($rs[$k]['data_length']+$rs[$k]['index_length'], 3);
}
// ...
}

// 备份(分卷)
public function backup()
{
$d = $this->only(['tables/a','sizes/a']);
return $this->returnMsg($this->db->doBack($d['tables'], $d['sizes']));
}

// 恢复(需要安全密码验证)
public function replace()
{
$d = $this->only(['files','old','new','safepass']);
if($this->manUser['password'] != set_password($d['safepass'], $this->manUser["passsalt"]))
return $this->returnMsg(['code'=>1,'p'=>0,'filenum'=>0,'msg'=>'安全密码错误']);
return $this->returnMsg($this->db->doReplace($d['files'], $d['old'], $d['new']));
}

// 优化表
public function youhua() { $this->db->optimize($table); }

// 修复表
public function xiufu() { $this->db->repair($table); }
}

13. 插件系统架构

Veitool 的插件系统通过 veitool/admin Composer 包提供核心服务,addons/ 目录存放插件代码。

插件路由处理(在 AppInit 中间件中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// app/AppInit.php
public function handle(Request $request, Closure $next): Response
{
// ... RSA 解密处理 ...

$url = ...; // 解析 URL
$arr = explode('/', $url);
$addon = $arr[0];

// 插件应用处理
if(in_array($addon, config('veitool.addons'))){
$App->config->set(['app_express'=>false], 'app');
$module = $addon;
$contr = $arr[1] ?? 'index';
$method = $arr[2] ?? 'index';

// 动态设置命名空间和应用路径
$App->setNamespace("addons\\" . $module);
$App->setAppPath($App->getRootPath() . 'addons' . VT_DS . $module . VT_DS);

// 加载插件路由文件
is_file($file = ADDON_PATH . $addon . VT_DS . 'data' . VT_DS . 'route.php')
&& require_once($file);

Route::rule($url, $contr . '/' . $method);
}

// 插件名传递给 Request(用于 AdminBase 权限兼容)
$request->ADDON_APP = $module;
return $next($request);
}

插件管理控制器 admin/controller/Addon.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Addon extends AdminBase
{
// 插件安装
public function install()
{
$d = $this->only(['name/*/a','@uid/d','token','version'=>'1.0.0']);
try{
$d['vversion'] = VT_VERSION;
Service::install($d['name'], config('veitool.force',1), $d);
}catch(AddonException $e){
return $this->returnMsg($e->getMessage().'_01', $e->getCode(), $e->getData());
}
return $this->returnMsg('安装成功', 1, ['addons' => Service::hasAddon()]);
}

// 插件卸载(可选删除数据表和配置)
public function uninstall()
{
$d = $this->only(['@token'=>'','name/*/a']);
try{
// 只有调试模式 + 超管才删除数据表和配置
$tables = config('veitool.ddata',1) && env('app_debug')
&& $this->manUser['userid']==1
? Service::getAddonTables($d['name']) : [];
Service::uninstall($d['name'], config('veitool.force',1));
if($tables){
$prefix = env('database.prefix', 'vt_');
foreach($tables as $table){
if(!preg_match("/^{$prefix}{$d['name']}/", $table)) continue;
Db::execute("DROP TABLE IF EXISTS `{$table}`");
}
Db::name('system_setting')->where('addon',$d['name'])->delete();
}
}catch(AddonException $e){
return $this->returnMsg($e->getMessage(), $e->getCode(), $e->getData());
}
return $this->returnMsg('卸载成功', 1, ['addons' => Service::hasAddon()]);
}

// 插件配置管理
public function setting(string $do = '', string $addon = '', string $group = '')
{
// 读取插件的配置项(addon 字段过滤)
$groups = (array) vconfig('@'.$addon.'.'.'group', []);
if($do=='json'){
$where[] = ['state','=',1];
$where[] = ['addon','=',$addon];
$rs = (new S())->listArray($where, 'name,title,value,type,options,private,relation,tips');
// ... 数据预处理 ...
return $this->returnMsg($rs, 1);
}
// ...
}
}

插件配置读取

插件的配置项存储在 vt_system_setting 表中,addon 字段标识所属插件。通过 vconfig('@插件名.键名') 读取:

1
2
// 读取插件配置
$group = vconfig('@myaddon.'.'group', []);

14. AppInit 中间件 — 解密与插件路由

app/AppInit.php 是全局中间件,在 app/middleware.php 中注册:

1
2
3
4
5
// app/middleware.php
return [
\think\middleware\SessionInit::class, // Session 初始化
\app\AppInit::class // 应用初始化(解密 + 插件路由)
];

完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class AppInit
{
public function handle(Request $request, Closure $next): Response
{
/* ========== 1. RSA+AES 前置解密 ========== */
if(($data = $request->post('encrypt_data') or $data = $request->get('encrypt_data'))
&& ($key = $request->header('VeitoolAdminxKeySecret'))){
try {
// RSA 解密得到 AES key + iv
$KeySecret = DataEncryptor::rsaDecrypt($key);
$KeySecret = str_split($KeySecret, 32);
$request->aes_key = $KeySecret[0];
$request->aes_iv = $KeySecret[1];

// AES 解密数据
$data = DataEncryptor::aesDecrypt((string)$data, $request->aes_key, $request->aes_iv);

// 合并解密数据到请求中
if($request->method(true) === 'GET'){
$request->withGet(array_merge($request->get(), $data));
}else{
$request->withPost(array_merge($request->post(), $data));
}
} catch (\Exception $e) {
throw new \Exception("数据解密失败:{$e->getMessage()}");
}
}

/* ========== 2. 插件路由处理 ========== */
$url = ...; // 解析当前 URL
$arr = explode('/', $url);
$addon = $arr[0];

if(in_array($addon, config('veitool.addons'))){
// 动态切换命名空间和应用路径
$App->setNamespace("addons\\" . $addon);
$App->setAppPath($App->getRootPath() . 'addons' . VT_DS . $addon . VT_DS);
// 加载插件路由文件
is_file($file = ADDON_PATH . $addon . VT_DS . 'data' . VT_DS . 'route.php')
&& require_once($file);
Route::rule($url, $contr . '/' . $method);
}

$request->ADDON_APP = $module;
return $next($request);
}
}

15. Layui 前端架构

后台主框架 admin/view/index/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class="layui-layout layui-layout-admin">
<!-- 顶部导航 -->
<div class="layui-header">
<div class="layui-logo">...</div>
<ul class="layui-nav layui-layout-left">
<li><a v-event="flexible">侧边伸缩</a></li>
<li><a v-event="refresh">刷新</a></li>
</ul>
<ul class="layui-nav layui-layout-right">
<li><a v-event="clearCache">缓存</a></li>
<li><a href="{PUBLIC__PATH}/" target="_blank">前台</a></li>
<li><a v-event="lockScreen">锁屏</a></li>
<li><a v-event="fullScreen">全屏</a></li>
<li>用户头像/姓名 → 个人中心/角色切换/退出</li>
<li><a v-event="popupRight">主题</a></li>
</ul>
</div>
<!-- 侧边菜单 -->
<div class="layui-side"><ul class="layui-nav layui-nav-tree"></ul></div>
<!-- 主体内容 -->
<div class="layui-body"></div>
<!-- 底部 -->
<div class="layui-footer">Version {:VT_VERSION}</div>
</div>

JS 配置与初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var $ = jQuery = layui.$, rsa_public = `{:config('veitool.rsa_pub_key')}`;
layui.config({
base: "{STATIC__PATH}admin/module/", // 模块目录
maps: "{$appMap}/", // 后台根路径
static: "{STATIC__PATH}", // 静态资源根
version: "{:VT_VERSION}",
rsa_public: rsa_public, // RSA 公钥(前端加密用)
bins: {
baseServer: '', // API 地址
pageTabs: true, // 多标签页
cacheTab: false, // 记忆 Tab
maxTabNum: 12, // 最多 Tab 数
token: '{:token($tokenName)}', // CSRF Token
}
}).extend({
Cropper: "Cropper/Cropper", // 图片裁剪
tagsInput: "tagsInput/tagsInput", // 标签输入
fileLibrary: "fileLibrary/fileLibrary", // 资源库
buildItems: "buildItems/buildItems", // 表单构建
cascader: "cascader/cascader", // 无限级联
orgCharts: "orgCharts/orgCharts", // 组织结构图
zTree: "zTree/zTree" // 树形结构
}).use(["index", "admin"], function(){
// 加载菜单和用户信息
layui.admin.req(layui.cache.maps + 'index/json', function(res){
layui.admin.putUser(res.user);
layui.index.buildLeftMenus(res.menus); // 构建左侧菜单
// 构建角色切换选项
$.each(res.user.rolem, function(k, val){
r_str += '<dd><a href="...?action=role&roleid='+ val.id +'">'+ val.name +'</a></dd>';
});
});
});

登录页 admin/view/login/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 登录处理
var login = function(){
var username = $('#username').val();
var password = $('#password').val();
// ... 表单验证 ...
$.ajax({
type: "POST",
url: "{$appMap}/login/check",
data: {
username: username,
password: hex_md5(password), // ★ 前端 MD5 加密
captcha: captcha
},
dataType: "json",
success: function(res){
if(res.code == '1'){
location.href = res.data.url;
}else{
if(captcha) getCaptcha(); // 刷新验证码
}
}
});
};

前端核心 JS 模块

模块 文件 功能
admin.js module/admin.js 核心模块:Ajax 封装、Tab 管理、菜单构建、锁屏、主题
vtable.js module/vtable.js 增强表格:搜索、排序、分页、行内编辑
formX.js module/formX.js 表单增强:验证、提交、联动
vinfo.js module/vinfo.js 信息页组件
buildItems.js module/buildItems/ 动态表单构建(根据配置类型生成表单控件)
iconPicker.js module/iconPicker.js 图标选择器
printer.js module/printer.js 打印组件

16. 工具函数库 common.php

app/common.php 定义了全局常量和 30+ 工具函数。

全局常量

1
2
3
4
5
6
7
8
9
10
11
define('VT_VERSION', '2.3.5');     // 版本号
define('VT_MANAGER', 'V_MANAGER'); // 后台管理员 Session 标识
define('VT_MEMBER', 'V_MEMBER'); // 前台会员 Session 标识
define('VT_VISITOR', 'V_VISITOR'); // 前台游客 Session 标识
define('VT_DS', DIRECTORY_SEPARATOR);
define('VT_DIR', ''); // 资源目录前缀
define('VT_STATIC', VT_DIR . '/static/');
define('ROOT_PATH', realpath(dirname(__DIR__)) . VT_DS);
define('VT_PUBLIC', ROOT_PATH . 'public' . VT_DS);
define('ADDON_PATH', ROOT_PATH . 'addons' . VT_DS);
define('RUNTIME_PATH', ROOT_PATH . 'runtime' . VT_DS);

核心函数分类

分类 函数 功能
验证 is_md5($w) 判断是否 MD5 字符串
is_date($date,$format) 日期格式验证(含闰年)
is_preg($s,$f,$t,$o) 正则验证(IP/手机/邮箱/身份证/自定义)
is_regex($str) 判断是否合法正则
安全 set_password($p,$s) 密码加密(MD5 + salt)
strip_sql($s,$t) SQL 注入过滤(核心安全函数)
strip_sql_extend($filter) SQL 过滤扩展初始化
strip_wd($m) ASCII 转码(防注入辅助)
strip_html($str,$low) HTML 标签过滤
vhtmlspecialchars($s) HTML 实体转换
half_replace($s,$n) 字符串脱敏(中间替换星号)
工具 random($l,$c) 生成随机字符串
vtrim($s) 去除换行/空格/制表符
dround($v,$p,$s) 数字格式化
file_ext($f) 获取文件扩展名
word_count($s) 字符串长度(支持中文)
set_order_id() 生成订单号
show_time($time) 人性化时间显示
配置 vconfig($name,$default) 获取站点配置(从数据库缓存)
parse_attr($value) 配置项解析(键值对/数组)
树形 get_subclass($pid,$box,$ikey,$pkey) 获取所有子类 ID
list_tree(...) 多级列表树构造
area_pos($areaid,$str,$deep,$start) 地区层级定位
文件 rmdirs($dirname,$self) 递归删除目录
copydirs($source,$dest) 递归复制目录
remove_empty_folder($dir) 移除空目录
idstoname($ids,$arr) ID 串转名称串
build_bill_no($code,$spr,$start) 生成单据号

strip_sql 函数详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function strip_sql($s, $t=1){
if(is_array($s)){
return array_map('strip_sql', $s); // 递归处理数组
}else{
if(empty($s)) return $s;
$s = trim($s);
if($t){
global $_VFILTER;
if($_VFILTER) $s = str_ireplace($_VFILTER, '', $s); // 自定义过滤词
if(strripos($s, ' ') == 0) return $s; // 无空格不过滤

$p = 'vt_';
$s = preg_replace("/\/\*([\s\S]*?)\*\//", "", $s); // 去注释
$s = preg_replace("/0x([a-f0-9]{2,})/i", '0&#120;\\1', $s); // 十六进制注入
$s = preg_replace_callback(
"/(select|update|replace|delete|drop)([\s\S]*?)(".$p."|from)/i",
'strip_wd', $s
); // SQL 关键词
$s = preg_replace_callback(
"/(load_file|substring|substr|reverse|trim|space|left|right|mid|lpad|concat|concat_ws|make_set|ascii|bin|oct|hex|ord|char|conv)([^a-z]?)\(/i",
'strip_wd', $s
); // SQL 函数
$s = preg_replace_callback(
"/(union|where|having|outfile|dumpfile|".$p.")/i",
'strip_wd', $s
); // 联合查询等
return $s;
}else{
// 解码模式
return str_replace(
['&#95;','&#100;','&#101;','&#103;','&#105;','&#109;','&#110;','&#112;','&#114;','&#115;','&#116;','&#118;','&#120;'],
['_','d','e','g','i','m','n','p','r','s','t','v','x'],
$s
);
}
}
}

原理:将 SQL 关键词的最后一个字符转换为 HTML 实体(如 selectselec&#116;),使注入语句失效但不影响正常数据。


17. 安全机制全链路

Veitool 的安全机制贯穿请求的整个生命周期:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
请求进入


AppInit 中间件
├── RSA + AES 解密(防止数据窃听)
└── 插件路由处理


BaseController::__construct()
├── __home() → 站点开关检测
├── __auth() → 认证链
│ ├── isLogin() → Session 登录验证
│ ├── loadMenusRoles() → 密码+盐值二次验证
│ │ └── ip_login → 单设备登录检测(IP比对)
│ └── isPower() → RBAC 操作权限校验
└── __init() → 控制器初始化


控制器方法
├── only() → 参数验证 + strip_sql 过滤
│ ├── 类型验证(邮箱/手机/身份证/密码/帐号...)
│ ├── 格式验证(位数/字符集/正则...)
│ └── SQL 注入过滤(关键词转 HTML 实体)
├── @token → CSRF Token 验证
└── Lock::check() → 频繁操作/暴力破解检测


Model 层
├── 软删除(del_time 字段)
├── 只读字段(add_time 不可修改)
└── 事务支持


响应输出
├── returnMsg() → JSON 响应(含刷新的 CSRF Token)
└── exitMsg() → 中断响应

安全配置项

配置项 作用 默认值
admin_captcha 后台登录验证码 1(开启)
ip_login 单设备登录(0关闭 1后台 2会员 3全部) 0
admin_log 后台操作日志 0
online_on 在线统计 0
sys_filter 自定义 SQL 过滤词 -
jwt.single_device_on API 单设备登录 false
jwt.leeway JWT 时钟偏差容忍 60秒

18. 学习路线图

推荐阅读顺序

步骤 文件 学习重点
1 public/index.php 入口文件、安装引导流程
2 composer.json 依赖关系、自动加载机制
3 config/app.php 多应用配置、URL 映射
4 config/veitool.php RSA 密钥、JWT 配置
5 app/middleware.php 全局中间件注册
6 app/AppInit.php RSA+AES 解密、插件路由处理
7 app/BaseController.php 三段式初始化、only() 参数验证 DSL、响应输出
8 app/admin/controller/AdminBase.php 认证链四步:登录→权限菜单→路由URI→权限校验
9 app/admin/controller/Login.php 登录全流程、密码加密、IP锁定
10 app/model/Base.php 模型基类、软删除、事务
11 app/model/system/SystemMenus.php 菜单缓存、菜单树构建
12 app/model/system/SystemRoles.php 角色权限缓存机制
13 app/model/system/SystemSetting.php 配置缓存、vconfig() 数据源
14 app/common.php 常量定义、set_password()strip_sql()vconfig()
15 extend/jwt/JwtToken.php JWT 生成/验证/刷新
16 extend/tool/DataEncryptor.php RSA/AES 加解密
17 extend/tool/Lock.php 登录锁定机制
18 app/admin/controller/system/Menus.php 菜单 CRUD 范式
19 app/admin/controller/system/Roles.php 角色权限分配(zTree)
20 app/admin/controller/system/Manager.php 管理员管理、数据权限
21 app/admin/controller/system/Setting.php 系统配置动态管理
22 app/admin/controller/system/Upload.php 多云存储上传
23 app/admin/controller/system/Database.php 数据库备份/恢复
24 app/admin/controller/Addon.php 插件安装/卸载/配置
25 app/admin/view/index/index.html Layui 后台框架
26 app/admin/view/login/index.html 登录页前端逻辑

ThinkPHP 8 核心知识点对照

ThinkPHP 概念 Veitool 中的体现
多应用 admin/api/index 三应用,app_map URL 映射
控制器 BaseControllerAdminBase → 具体控制器,三层继承
模型 Base 模型封装 one/all/del,各模型 use SoftDelete
中间件 AppInit 全局中间件处理解密和插件路由
Session VT_MANAGER/VT_MEMBER 常量标识,后台认证依赖 Session
缓存 cache() 函数,菜单/角色/配置/字典全量缓存
路由 think-multi-app 自动路由 + 插件动态路由注入
事件 app/event.php 定义,app/event/ 目录存放事件类
验证 only() 方法自研 DSL,非 ThinkPHP 内置 Validate
视图 Think 模板引擎,{:vconfig()} / {$appMap} 模板变量

Layui 核心知识点对照

Layui 概念 Veitool 中的体现
布局 layui-layout-admin 后台经典布局
导航 layui-nav 顶部导航 + layui-nav-tree 侧边菜单
表格 vtable.js 增强表格(搜索/排序/分页/行内编辑)
表单 formX.js 表单增强 + buildItems.js 动态表单
弹层 layer.msg() / layer.tips() 消息提示
Tab 多标签页管理(pageTabs: true
扩展 layui.extend() 注册 Cropper/zTree/cascader 等组件
事件 v-event 自定义事件绑定(flexible/refresh/lockScreen 等)

与勾股OA 对比总结

维度 Veitool 勾股OA
定位 通用后台开发框架 企业OA 业务系统
应用数 3个(admin/api/index) 13个业务模块
业务扩展 插件系统(addons/) 新增 app 子应用
认证 Session(后台)+ JWT(API) Session(统一)
权限 RBAC:角色→菜单→操作 RBAC:岗位→角色组→规则
加密 RSA+AES 前后端加密传输 无前端加密
配置 数据库存储 + vconfig() 数据库存储 + vconfig()
字典 字典组+字典项+SQL字典 无独立字典系统
审批流 自研工作流引擎
云存储 本地+七牛+阿里云+腾讯云
插件 完整插件系统(安装/卸载/配置)
安装 独立安装向导 手动导入 SQL
Swoole 内置支持 不支持

文档版本:基于 Veitool V2.3.5 源码逐文件精读生成
所有代码引用均来自项目实际源码,行号和逻辑经过逐行核对