Veitool V2.3.5 源码深度解读
Veitool V2.3.5 源码深度解读
项目地址:https://gitee.com/veitool/veitoolthink
技术栈:ThinkPHP 8.x + Layui 2.13.x + PHP 8.1+
项目定位:企业级后台管理快速开发框架,内置 RBAC 权限、插件系统、JWT API、数据字典、多云存储等
目录
- 项目总览
- 目录结构
- 技术栈与依赖
- 多应用架构与入口
- BaseController — 控制器基类
- AdminBase — 后台认证与权限拦截
- 登录流程精读
- RBAC 权限引擎
- JWT API 认证体系
- 数据加解密 — RSA + AES
- Model 基类与软删除
- 系统管理模块精读
- 插件系统架构
- AppInit 中间件 — 解密与插件路由
- Layui 前端架构
- 工具函数库 common.php
- 安全机制全链路
- 学习路线图
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. 目录结构
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.md3. 技术栈与依赖
composer.json 核心依赖
{ "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 压缩 }}自动加载机制
"autoload": { "psr-4": { "app\\": "app", // 应用代码 → app/ 目录 "addons\\": "addons" // 插件代码 → addons/ 目录 }, "psr-0": { "": "extend/" // extend/ 下按命名空间自动加载 }}关键点:extend/ 使用 PSR-0 自动加载,所以 extend/jwt/JwtToken.php 的命名空间是 jwt\JwtToken,extend/tool/Lock.php 的命名空间是 tool\Lock,使用时直接 use jwt\JwtToken 即可。
4. 多应用架构与入口
入口文件 public/index.php
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
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 自动按路径段映射到应用:
/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 是所有控制器的抽象父类,定义了请求处理、参数验证、响应输出的统一范式。
构造方法 — 三段式初始化
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 过滤:
protected final function only($name = [], $type = 'post', $filter = 'strip_sql', $bin = true)用法示例(来自 Menus 控制器):
// 菜单添加时的参数验证$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 语法解析:
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 等安全字段)响应输出方法
// 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 响应格式:
{ "code": 1, // 1=成功, 0=失败 "msg": "操作成功", "data": {...}, "count": 10, "token": "xxx" // CSRF Token(每次请求刷新)}6. AdminBase — 后台认证与权限拦截
app/admin/controller/AdminBase.php 继承 BaseController,覆盖 __auth() 实现后台完整的认证链。
认证链流程
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()
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()
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
$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()
private function isPower(){ // 超管直接通过;非超管检查 routeUri 是否在 actions 数组中 if($this->manUser['userid']>1 && !in_array($this->routeUri, $this->manUser['actions'])){ $this->exitMsg('抱歉,您没有该项权限请联系管理员!', ...); }}权限扩展机制 getRoleExt()
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()
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('帐号或密码错误!');}密码加密算法
function set_password($p, $s){ return md5((is_md5($p) ? md5($p) : md5(md5($p))).$s);}流程:
- 前端:
hex_md5(password)→ 将明文 MD5 后传输(防明文传输) - 后端:检测传入是否已是 MD5 →
md5(md5(password) + salt)→ 存储双重 MD5 + 盐值
锁定机制 tool\Lock
// 默认配置: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()
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 的权限系统基于 角色 → 菜单 → 操作 三级模型,通过缓存实现高性能权限校验。
数据模型
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()
// 缓存键: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()
// 缓存键:VMENUS_1_角色IDpublic 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;}权限校验流程图:
请求 /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()
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
'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()
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 载荷结构
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()
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()
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
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
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 实现了前后端数据加密传输,防止接口数据被中间人窃取。
加解密流程
前端 后端 │ │ ├── 生成随机 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
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 密钥配置
'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 中注入公钥:
var rsa_public = `{:config('veitool.rsa_pub_key')}`;11. Model 基类与软删除
app/model/Base.php 是所有模型的父类,封装了统一的查询和删除接口。
模型基类
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; 启用软删除:
class SystemRoles extends Base{ use \think\model\concern\SoftDelete; protected $pk = 'roleid'; // ...}软删除时,del_time 字段会被设为当前时间戳,查询时自动排除 del_time > 0 的记录。
模型查询范式
Veitool 的模型查询有两种模式:
// 方式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、批量添加、快速编辑、导入导出。
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 组件)。
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、个人中心、修改密码、重置密码、组织机构管理。
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() 函数读取,支持动态增删配置项。
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()
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() 函数
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 查询字典(从数据库表动态生成字典)。
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 四种存储引擎。
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
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 中间件中)
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
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('@插件名.键名') 读取:
// 读取插件配置$group = vconfig('@myaddon.'.'group', []);14. AppInit 中间件 — 解密与插件路由
app/AppInit.php 是全局中间件,在 app/middleware.php 中注册:
return [ \think\middleware\SessionInit::class, // Session 初始化 \app\AppInit::class // 应用初始化(解密 + 插件路由)];完整流程
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
<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 配置与初始化
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
// 登录处理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+ 工具函数。
全局常量
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 函数详解
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", '0x\\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( ['_','d','e','g','i','m','n','p','r','s','t','v','x'], ['_','d','e','g','i','m','n','p','r','s','t','v','x'], $s ); } }}原理:将 SQL 关键词的最后一个字符转换为 HTML 实体(如 select → select),使注入语句失效但不影响正常数据。
17. 安全机制全链路
Veitool 的安全机制贯穿请求的整个生命周期:
请求进入 │ ▼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 映射 |
| 控制器 | BaseController → AdminBase → 具体控制器,三层继承 |
| 模型 | 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 源码逐文件精读生成
所有代码引用均来自项目实际源码,行号和逻辑经过逐行核对
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!
樊笼










