Veitool V2.3.5 源码深度解读

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、数据字典、多云存储等


目录

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

1. 项目总览

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

核心特征

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

2. 目录结构

text
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 核心依赖

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 压缩
    }
}

自动加载机制

json
"autoload": {
    "psr-4": {
        "app\\": "app",          // 应用代码 → app/ 目录
        "addons\\": "addons"     // 插件代码 → addons/ 目录
    },
    "psr-0": {
        "": "extend/"            // extend/ 下按命名空间自动加载
    }
}

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


4. 多应用架构与入口

入口文件 public/index.php

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

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 自动按路径段映射到应用:

text
/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 是所有控制器的抽象父类,定义了请求处理、参数验证、响应输出的统一范式。

构造方法 — 三段式初始化

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 过滤:

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

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

php
// 菜单添加时的参数验证
$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 语法解析

text
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 等安全字段)

响应输出方法

php
// 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 响应格式

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

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

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

认证链流程

php
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()

php
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()

php
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

php
$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()

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

权限扩展机制 getRoleExt()

php
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()

php
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('帐号或密码错误!');
}

密码加密算法

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

流程

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

锁定机制 tool\Lock

php
// 默认配置: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()

php
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 的权限系统基于 角色 → 菜单 → 操作 三级模型,通过缓存实现高性能权限校验。

数据模型

text
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()

php
// 缓存键: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()

php
// 缓存键:VMENUS_1_角色ID
public static function cache(int|array $role, int $reset = 0)
{
    $roleid = is_array($role) ? $role['roleid'] : $role;
    $key = 'VMENUS_1_'.$roleid;
    $rs = cache($key);
    if(!$rs || $reset){
        $rs = [];
        $str = '';
        $ro = is_array($role) ? $role : self::where(['state'=>1,'roleid'=>$roleid])
            ->field('roleid,role_name,role_menuid,role_ext')->findOrEmpty()->toArray();

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

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

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

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

权限校验流程图

text
请求 /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()

php
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

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()

php
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 载荷结构

php
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()

php
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()

php
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

php
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

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 实现了前后端数据加密传输,防止接口数据被中间人窃取。

加解密流程

text
前端                              后端
 │                                 │
 ├── 生成随机 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

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 密钥配置

php
// config/veitool.php
'rsa_pri_key' => <<<EOF
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDScXb6...
-----END PRIVATE KEY-----
EOF,
'rsa_pub_key' => <<<EOF
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0nF2+iIO06SQKBz1...
-----END PUBLIC KEY-----
EOF,

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

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

11. Model 基类与软删除

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

模型基类

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; 启用软删除:

php
class SystemRoles extends Base
{
    use \think\model\concern\SoftDelete;
    protected $pk = 'roleid';
    // ...
}

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

模型查询范式

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

php
// 方式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、批量添加、快速编辑、导入导出。

php
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 组件)。

php
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、个人中心、修改密码、重置密码、组织机构管理。

php
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() 函数读取,支持动态增删配置项。

php
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()

php
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() 函数

php
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 查询字典(从数据库表动态生成字典)。

php
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 四种存储引擎。

php
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

php
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 中间件中)

php
// app/AppInit.php
public function handle(Request $request, Closure $next): Response
{
    // ... RSA 解密处理 ...

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

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

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

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

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

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

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

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('@插件名.键名') 读取:

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

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

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

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

完整流程

php
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

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 配置与初始化

javascript
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

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

16. 工具函数库 common.php

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

全局常量

php
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 函数详解

php
function strip_sql($s, $t=1){
    if(is_array($s)){
        return array_map('strip_sql', $s);  // 递归处理数组
    }else{
        if(empty($s)) return $s;
        $s = trim($s);
        if($t){
            global $_VFILTER;
            if($_VFILTER) $s = str_ireplace($_VFILTER, '', $s);  // 自定义过滤词
            if(strripos($s, ' ') == 0) return $s;  // 无空格不过滤

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

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


17. 安全机制全链路

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

text
请求进入
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_onAPI 单设备登录false
jwt.leewayJWT 时钟偏差容忍60秒

18. 学习路线图

推荐阅读顺序

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

ThinkPHP 8 核心知识点对照

ThinkPHP 概念Veitool 中的体现
多应用admin/api/index 三应用,app_map URL 映射
控制器BaseControllerAdminBase → 具体控制器,三层继承
模型Base 模型封装 one/all/del,各模型 use SoftDelete
中间件AppInit 全局中间件处理解密和插件路由
SessionVT_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 源码逐文件精读生成
所有代码引用均来自项目实际源码,行号和逻辑经过逐行核对