项目地址: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. 目录结构
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", "topthink/think-orm": "^3.0|^4.0", "topthink/think-multi-app": "^1.1", "topthink/think-view": "^2.0", "topthink/think-captcha": "^3.0", "topthink/think-swoole": "^4.0", "topthink/think-worker": "^4.0", "topthink/think-queue": "^3.0", "topthink/think-image": "^1.0", "veitool/admin": "^3.0", "firebase/php-jwt": "^7.0", "aliyuncs/oss-sdk-php": "^2.7", "qcloud/cos-sdk-v5": "^2.6", "qiniu/php-sdk": "^7.14", "phpoffice/phpspreadsheet": "^3.8", "phpoffice/phpword": "^1.3", "nelexa/zip": "^4.0" } }
|
自动加载机制
1 2 3 4 5 6 7 8 9
| "autoload": { "psr-4": { "app\\": "app", "addons\\": "addons" }, "psr-0": { "": "extend/" } }
|
关键点:extend/ 使用 PSR-0 自动加载,所以 extend/jwt/JwtToken.php 的命名空间是 jwt\JwtToken,extend/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;
if(version_compare(PHP_VERSION,'8.1.0','<')) die('require PHP >= 8.1.0!');
if (is_dir(__DIR__ . '/install') && !is_file(__DIR__ . '/install/install.lock')) exit(header('Location:/install/'));
require __DIR__ . '/../vendor/autoload.php';
$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'], 'app_express' => true, 'deny_app_list' => ['model','event'], '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); $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
| 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, "msg": "操作成功", "data": {...}, "count": 10, "token": "xxx" }
|
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 = ''; protected $routeUri = '';
protected function __home(){}
protected final function __auth() { $this->appMap = VT_DIR . '/' . (array_search("admin", config('app.app_map')) ?: 'admin'); $this->isLogin(); $this->loadMenusRoles(); $this->routeUri = strtolower(...); $this->isPower(); } }
|
第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); } } }
|
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() { $us = Manager::one("username = '{$this->manUser['username']}' AND state > 0"); if($us && $this->manUser['password'] == $us['password'] && $this->manUser['passsalt'] == $us['passsalt']){
if(in_array(vconfig('ip_login',0),[2,3]) && $us['loginip'] != $this->request->ip()){ session(null); $this->exitMsg('您的帐号已在其他终端登录!', ...); }
$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 : '') );
|
第4步:权限验证 isPower()
1 2 3 4 5 6 7
| private function isPower() { 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; $d = explode('/', $this->routeUri); 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() { $ip = $this->request->ip(); if(Lock::check(['key'=>'LOGIN_'.$ip])) return $this->returnMsg(Lock::msg());
$d = $this->only(['username/*/u/管理帐号','password/*/p/登录密码','captcha']);
if(vconfig('admin_captcha',1) && !captcha_check($d['captcha'])) return $this->returnMsg('验证码错误!');
$rs = Manager::one(compact('username')); if(empty($rs)){ LoginLog::add($username, $password, '', '账号错误'); Lock::add(); return $this->returnMsg('帐号或密码错误!'); }
if($rs->state == 0) return $this->returnMsg('帐号已被停用!');
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); Lock::del();
return $this->returnMsg('登录成功!', 1, ['url'=>($this->appMap ?: '/')]); }
LoginLog::add($username, $password, $rs['passsalt'], '密码错误'); Lock::add(); return $this->returnMsg('帐号或密码错误!'); }
|
密码加密算法
1 2 3 4
| 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 + 盐值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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 (状态)
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
| 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){ if(strpos(",$ro[role_menuid],", ",$k,") !== false){ $str .= $v['role_url'] ? ','.$v['role_url'] : ''; } } }
$rs['actions'] = explode(',', trim($str, ','));
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); } 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']) ? 通过 : "没有该项权限"
|
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'].','; 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 双令牌认证。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 'jwt' => [ 'algorithms' => 'HS256', 'access_secret_key' => '86433bf7adb7...', 'refresh_secret_key' => '1713a24f7b11...', 'access_exp' => 7200, 'refresh_exp' => 604800, 'refresh_off' => false, 'iss' => 'veitool', 'nbf' => 0, 'leeway' => 60, 'single_device_on' => false, 'cache_token_ttl' => 604800, 'cache_token_a_pre' => 'JWT:TOKEN:', 'cache_token_r_pre' => 'JWT:REFRESH_TOKEN:', 'get_token_on' => false, '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); $secrets = self::getPrivateKey($config);
$token = [ 'token_type' => 'Bearer', 'expires_in' => $config['access_exp'], 'access_token' => self::makeToken($payload['accessPayload'], $secrets['accessKey'], $config['algorithms']) ];
if (!$config['refresh_off']) { $token['refresh_token'] = self::makeToken($payload['refreshPayload'], $secrets['refreshKey'], $config['algorithms']); }
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;
$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); $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);
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); $_extend = $extend['extend'];
$payload = self::getPayload($config, $extend['extend']); $secrets = self::getPrivateKey($config);
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(...); } 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 { 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)); }
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() {
$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 加密返回) ◄┘
|
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 { 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; }
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); }
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); }
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
| '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');
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
| $rs = Manager::one(compact('username')); $rs = Manager::all("state = 1"); Manager::del(['userid'=>1]);
$rs = (new R())->listQuery(); $rs = (new S())->listArray($where, 'name,value');
|
12. 系统管理模块精读
菜单管理支持分类管理、菜单 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 { 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 { 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']); $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'){ $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]; } $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 = 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;
$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; $data['fileid'] = UploadFile::insertGetId($data);
if($data['filesize']>300 && $engine == 'local'){ $pic = ROOT_PATH . 'public' . $data['fileurl']; if($data['fileext']=='jpg'){ $pics = Imagecreatefromjpeg($pic); Imagejpeg($pics, $pic, 70); }elseif($data['fileext']=='png'){ $pics = imagecreatefrompng($pic); imagepng($pics, $pic, 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]); }
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
| public function handle(Request $request, Closure $next): Response {
$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->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 = '') { $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
| return [ \think\middleware\SessionInit::class, \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 { if(($data = $request->post('encrypt_data') or $data = $request->get('encrypt_data')) && ($key = $request->header('VeitoolAdminxKeySecret'))){ try { $KeySecret = DataEncryptor::rsaDecrypt($key); $KeySecret = str_split($KeySecret, 32); $request->aes_key = $KeySecret[0]; $request->aes_iv = $KeySecret[1];
$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()}"); } }
$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, bins: { baseServer: '', pageTabs: true, cacheTab: false, maxTabNum: 12, token: '{:token($tokenName)}', } }).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), 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'); define('VT_MEMBER', 'V_MEMBER'); define('VT_VISITOR', 'V_VISITOR'); 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", '0x\\1', $s); $s = preg_replace_callback( "/(select|update|replace|delete|drop)([\s\S]*?)(".$p."|from)/i", 'strip_wd', $s ); $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 ); $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 的安全机制贯穿请求的整个生命周期:
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 映射 |
| 控制器 |
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 源码逐文件精读生成
所有代码引用均来自项目实际源码,行号和逻辑经过逐行核对