初始化-安装依赖

This commit is contained in:
2026-03-03 10:06:12 +08:00
parent 3f349a35a4
commit ec8cac4221
187 changed files with 26292 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace plugin\saiadmin\app\cache;
use plugin\saiadmin\app\logic\system\SystemConfigLogic;
use support\think\Cache;
/**
* 配置缓存
*/
class ConfigCache
{
/**
* 读取缓存配置
* @return array
*/
public static function cacheConfig(): array
{
return config('plugin.saiadmin.saithink.config_cache', [
'expire' => 60 * 60 * 24 * 365,
'prefix' => 'saiadmin:config_cache:config_',
'tag' => 'saiadmin:config_cache'
]);
}
/**
* 获取配置信息
*/
public static function getConfig(string $code = ''): array
{
if (empty($code)) {
return [];
}
$cache = static::cacheConfig();
// 直接从缓存获取
$config = Cache::get($cache['prefix'] . md5($code));
if ($config) {
return $config;
}
// 设置配置并获取
$config = static::setConfig($code);
if ($config) {
return $config;
}
return [];
}
/**
* 设置配置数据
*/
public static function setConfig(string $code): array
{
$cache = static::cacheConfig();
$data = (new SystemConfigLogic())->getData($code);
if (empty($data)) {
return [];
}
$tag = [];
$tag[] = $cache['tag'];
// 保存到缓存
Cache::tag($tag)->set($cache['prefix'] . md5($code), $data, $cache['expire']);
return $data;
}
/**
* 清理单个配置缓存
*/
public static function clearConfig(string $code): bool
{
$cache = static::cacheConfig();
return Cache::delete($cache['prefix'] . md5($code));
}
/**
* 清理全部配置缓存
* @return bool
*/
public static function clear(): bool
{
$cache = static::cacheConfig();
return Cache::tag($cache['tag'])->clear();
}
}

View File

@@ -0,0 +1,86 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace plugin\saiadmin\app\cache;
use plugin\saiadmin\app\logic\system\SystemDictTypeLogic;
use plugin\saiadmin\app\model\system\SystemDictType;
use support\think\Cache;
/**
* 字典信息缓存
*/
class DictCache
{
/**
* 读取缓存配置
* @return array
*/
public static function cacheConfig(): array
{
return config('plugin.saiadmin.saithink.dict_cache', [
'expire' => 60 * 60 * 24 * 365,
'tag' => 'saiadmin:dict_cache',
]);
}
/**
* 获取全部字典
*/
public static function getDictAll(): array
{
$cache = static::cacheConfig();
// 直接从缓存获取
$data = Cache::get($cache['tag']);
if ($data) {
return $data;
}
// 获取信息并返回
$data = static::setDictAll();
if ($data) {
return $data;
}
return [];
}
/**
* 获取单个字典
*/
public static function getDict($code): array
{
$data = static::getDictAll();
if (isset($data[$code])) {
return $data[$code];
} else {
return [];
}
}
/**
* 设置全部字典
*/
public static function setDictAll(): array
{
$cache = static::cacheConfig();
$data = (new SystemDictTypeLogic)->getDictAll();
Cache::set($cache['tag'], $data, $cache['expire']);
return $data;
}
/**
* 清除全部字典信息
*/
public static function clear(): bool
{
$cache = static::cacheConfig();
return Cache::delete($cache['tag']);
}
}

View File

@@ -0,0 +1,104 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace plugin\saiadmin\app\cache;
use ReflectionClass;
use ReflectionMethod;
use plugin\saiadmin\service\Permission;
use support\think\Cache;
/**
* 反射文件缓存
*/
class ReflectionCache
{
/**
* 读取缓存配置
* @return array
*/
public static function cacheConfig(): array
{
return config('plugin.saiadmin.saithink.reflection_cache', [
'tag' => 'saiadmin:reflection',
'expire' => 60 * 60 * 24 * 365,
'no_need' => 'saiadmin:reflection_cache:no_need_',
'attr' => 'saiadmin:reflection_cache:attr_',
]);
}
/**
* 获取控制器中无需登录的方法列表
*/
public static function getNoNeedLogin(string $controller): array
{
$cache = static::cacheConfig();
$tag = [];
$tag[] = $cache['tag'];
$key = $cache['no_need'] . md5($controller);
$data = Cache::get($key);
if ($data !== null) {
return $data;
}
// 反射逻辑
if (class_exists($controller)) {
$ref = new ReflectionClass($controller);
$data = $ref->getDefaultProperties()['noNeedLogin'] ?? [];
} else {
$data = [];
}
Cache::tag($tag)->set($key, $data, $cache['expire']);
return $data;
}
/**
* 获取方法上的权限属性
*/
public static function getPermissionAttributes(string $controller, string $action): array
{
$cache = static::cacheConfig();
$tag = [];
$tag[] = $cache['tag'];
$key = $cache['attr'] . md5($controller . '::' . $action);
$data = Cache::get($key);
if ($data) {
return $data;
}
$data = [];
if (method_exists($controller, $action)) {
$refMethod = new ReflectionMethod($controller, $action);
$attributes = $refMethod->getAttributes(Permission::class);
if (!empty($attributes)) {
$attr = $attributes[0]->newInstance();
$data = [
'title' => $attr->getTitle(),
'slug' => $attr->getSlug(),
];
}
}
Cache::tag($tag)->set($key, $data, $cache['expire']);
return $data;
}
/**
* 清理所有反射缓存
* @return bool
*/
public static function clear(): bool
{
$cache = static::cacheConfig();
return Cache::tag($cache['tag'])->clear();
}
}

View File

@@ -0,0 +1,143 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace plugin\saiadmin\app\cache;
use plugin\saiadmin\app\logic\system\SystemMenuLogic;
use plugin\saiadmin\app\model\system\SystemUserRole;
use support\think\Cache;
/**
* 用户权限缓存
*/
class UserAuthCache
{
/**
* 读取缓存配置
* @return array
*/
public static function cacheConfig(): array
{
return config('plugin.saiadmin.saithink.button_cache', [
'prefix' => 'saiadmin:button_cache:user_',
'expire' => 60 * 60 * 2,
'all' => 'saiadmin:button_cache:all',
'role' => 'saiadmin:button_cache:role_',
'tag' => 'saiadmin:button_cache',
]);
}
/**
* 获取用户的权限
*/
public static function getUserAuth($uid): array
{
if (empty($uid)) {
return [];
}
$cache = static::cacheConfig();
// 直接从缓存获取
$auth = Cache::get($cache['prefix'] . $uid);
if ($auth) {
return $auth;
}
// 设置权限并返回
$auth = static::setUserAuth($uid);
if ($auth) {
return $auth;
}
return [];
}
/**
* 设置用户的权限
*/
public static function setUserAuth($uid): array
{
// 从缓存获取,直接返回
$roleIds = SystemUserRole::getRoleIds($uid);
// 获取角色关联的菜单权限
$data = (new SystemMenuLogic())->getAuthByRole($roleIds);
if (empty($data)) {
return [];
}
$cache = static::cacheConfig();
$tag = [];
$tag[] = $cache['tag'];
if (!empty($roleIds)) {
foreach ($roleIds as $role) {
$tag[] = $cache['role'] . $role;
}
}
// 保存到缓存
Cache::tag($tag)->set($cache['prefix'] . $uid, $data, $cache['expire']);
return $data;
}
/**
* 获取全部权限
*/
public static function getAllAuth(): array
{
$cache = static::cacheConfig();
// 直接从缓存获取
$auth = Cache::get($cache['all']);
if ($auth) {
return $auth;
}
$all = (new SystemMenuLogic())->getAllAuth();
// 设置权限并返回
Cache::tag($cache['tag'])->set($cache['all'], $all, $cache['expire']);
return $all;
}
/**
* 清理缓存
*/
public static function clearUserAuth($uid): bool
{
$cache = static::cacheConfig();
return Cache::delete($cache['prefix'] . $uid);
}
/**
* 清理角色缓存
*/
public static function clearUserAuthByRoleId($role_id): bool
{
$cache = static::cacheConfig();
if (is_array($role_id)) {
$tags = [];
foreach ($role_id as $id) {
$tags[] = $cache['role'] . $id;
}
} else {
$tags = $cache['role'] . $role_id;
}
return Cache::tag($tags)->clear();
}
/**
* 清理所有用户缓存
* @return bool
*/
public static function clear(): bool
{
$cache = static::cacheConfig();
return Cache::tag($cache['tag'])->clear();
}
}

View File

@@ -0,0 +1,145 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace plugin\saiadmin\app\cache;
use plugin\saiadmin\app\logic\system\SystemUserLogic;
use support\think\Cache;
/**
* 用户信息缓存
*/
class UserInfoCache
{
/**
* 读取缓存配置
* @return array
*/
public static function cacheConfig(): array
{
return config('plugin.saiadmin.saithink.user_cache', [
'prefix' => 'saiadmin:user_cache:info_',
'expire' => 60 * 60 * 4,
'dept' => 'saiadmin:user_cache:dept_',
'role' => 'saiadmin:user_cache:role_',
'post' => 'saiadmin:user_cache:post_',
]);
}
/**
* 通过id获取缓存管理员信息
*/
public static function getUserInfo($uid): array
{
if (empty($uid)) {
return [];
}
$cache = static::cacheConfig();
// 直接从缓存获取
$adminInfo = Cache::get($cache['prefix'] . $uid);
if ($adminInfo) {
return $adminInfo;
}
// 获取缓存信息并返回
$adminInfo = static::setUserInfo($uid);
if ($adminInfo) {
return $adminInfo;
}
return [];
}
/**
* 设置管理员信息
*/
public static function setUserInfo($uid): array
{
$data = (new SystemUserLogic())->getUser($uid);
$cache = static::cacheConfig();
$tags = [];
if (!empty($data['deptList'])) {
$tags[] = $cache['dept'] . $data['deptList']['id'];
}
if (!empty($data['roleList'])) {
foreach ($data['roleList'] as $role) {
$tags[] = $cache['role'] . $role['id'];
}
}
if (!empty($data['postList'])) {
foreach ($data['postList'] as $post) {
$tags[] = $cache['post'] . $post['id'];
}
}
Cache::tag($tags)->set($cache['prefix'] . $uid, $data, $cache['expire']);
return $data;
}
/**
* 清理管理员信息缓存
*/
public static function clearUserInfo($uid): bool
{
$cache = static::cacheConfig();
return Cache::delete($cache['prefix'] . $uid);
}
/**
* 清理部门下所有用户缓存
*/
public static function clearUserInfoByDeptId($dept_id): bool
{
$cache = static::cacheConfig();
if (is_array($dept_id)) {
$tags = [];
foreach ($dept_id as $id) {
$tags[] = $cache['dept'] . $id;
}
} else {
$tags = $cache['dept'] . $dept_id;
}
return Cache::tag($tags)->clear();
}
/**
* 清理角色下所有用户缓存
*/
public static function clearUserInfoByRoleId($role_id): bool
{
$cache = static::cacheConfig();
if (is_array($role_id)) {
$tags = [];
foreach ($role_id as $id) {
$tags[] = $cache['role'] . $id;
}
} else {
$tags = $cache['role'] . $role_id;
}
return Cache::tag($tags)->clear();
}
/**
* 清理岗位下所有用户缓存
*/
public static function clearUserInfoByPostId($post_id): bool
{
$cache = static::cacheConfig();
if (is_array($post_id)) {
$tags = [];
foreach ($post_id as $id) {
$tags[] = $cache['post'] . $id;
}
} else {
$tags = $cache['post'] . $post_id;
}
return Cache::tag($tags)->clear();
}
}

View File

@@ -0,0 +1,100 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace plugin\saiadmin\app\cache;
use plugin\saiadmin\app\logic\system\SystemMenuLogic;
use plugin\saiadmin\app\model\system\SystemUserRole;
use support\think\Cache;
/**
* 用户菜单缓存
*/
class UserMenuCache
{
/**
* 读取缓存配置
* @return array
*/
public static function cacheConfig(): array
{
return config('plugin.saiadmin.saithink.menu_cache', [
'prefix' => 'saiadmin:menu_cache:user_',
'expire' => 60 * 60 * 24 * 7,
'tag' => 'saiadmin:menu_cache',
]);
}
/**
* 获取用户的菜单
*/
public static function getUserMenu($uid): array
{
if (empty($uid)) {
return [];
}
$cache = static::cacheConfig();
// 直接从缓存获取
$menu = Cache::get($cache['prefix'] . $uid);
if ($menu) {
return $menu;
}
// 设置用户菜单并获取
$menu = static::setUserMenu($uid);
if ($menu) {
return $menu;
}
return [];
}
/**
* 设置用户菜单
*/
public static function setUserMenu($uid): array
{
$cache = static::cacheConfig();
$tag = [];
$tag[] = $cache['tag'];
$logic = new SystemMenuLogic();
if ($uid == 1) {
$data = $logic->getAllMenus();
} else {
$roleIds = SystemUserRole::getRoleIds($uid);
$data = $logic->getMenuByRole($roleIds);
if (empty($data)) {
return [];
}
}
// 保存到缓存
Cache::tag($tag)->set($cache['prefix'] . $uid, $data, $cache['expire']);
return $data;
}
/**
* 清理用户缓存
*/
public static function clearUserMenu($uid): bool
{
$cache = static::cacheConfig();
return Cache::delete($cache['prefix'] . $uid);
}
/**
* 清理所有菜单缓存
* @return bool
*/
public static function clearMenuCache(): bool
{
$cache = static::cacheConfig();
return Cache::tag($cache['tag'])->clear();
}
}

View File

@@ -0,0 +1,363 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller;
use Throwable;
use support\Request;
use support\Response;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\basic\OpenController;
/**
* 安装控制器
*/
class InstallController extends OpenController
{
/**
* 不需要登录的方法
*/
protected array $noNeedLogin = ['index', 'install'];
/**
* 应用名称
* @var string
*/
protected string $app = 'saiadmin';
protected string $version = '6.0.0';
/**
* 安装首页
*/
public function index()
{
$data['app'] = $this->app;
$data['version'] = config('plugin.saiadmin.app.version', $this->version);
$env = base_path() . DIRECTORY_SEPARATOR . '.env';
clearstatcache();
if (is_file($env)) {
$data['error'] = '程序已经安装';
return view('install/error', $data);
}
if (!is_writable(base_path() . DIRECTORY_SEPARATOR . 'config')) {
$data['error'] = '权限认证失败';
return view('install/error', $data);
}
return view('install/index', $data);
}
/**
* 执行安装
*/
public function install(Request $request)
{
$env = base_path() . DIRECTORY_SEPARATOR . '.env';
clearstatcache();
if (is_file($env)) {
return $this->fail('管理后台已经安装如需重新安装请删除根目录env配置文件并重启');
}
$user = $request->post('username');
$password = $request->post('password');
$database = $request->post('database');
$host = $request->post('host');
$port = (int) $request->post('port') ?: 3306;
$dataType = $request->post('dataType', 'demo');
try {
$db = $this->getPdo($host, $user, $password, $port);
$smt = $db->query("show databases like '$database'");
if (empty($smt->fetchAll())) {
$db->exec("create database `$database` CHARSET utf8mb4 COLLATE utf8mb4_general_ci");
}
} catch (\Throwable $e) {
$message = $e->getMessage();
if (stripos($message, 'Access denied for user')) {
return $this->fail('数据库用户名或密码错误');
}
if (stripos($message, 'Connection refused')) {
return $this->fail('Connection refused. 请确认数据库IP端口是否正确数据库已经启动');
}
if (stripos($message, 'timed out')) {
return $this->fail('数据库连接超时请确认数据库IP端口是否正确安全组及防火墙已经放行端口');
}
throw $e;
}
$db->exec("use `$database`");
$smt = $db->query("show tables like 'sa_system_menu';");
$tables = $smt->fetchAll();
if (count($tables) > 0) {
return $this->fail('数据库已经安装,请勿重复安装');
}
if ($dataType == 'demo') {
$sql_file = base_path() . '/plugin/saiadmin/db/saiadmin-6.0.sql';
} else {
$sql_file = base_path() . '/plugin/saiadmin/db/saiadmin-pure.sql';
}
if (!is_file($sql_file)) {
return $this->fail('数据库SQL文件不存在');
}
$sql_query = file_get_contents($sql_file);
$db->exec($sql_query);
$this->generateConfig();
$env_config = <<<EOF
# 数据库配置
DB_TYPE = mysql
DB_HOST = $host
DB_PORT = $port
DB_NAME = $database
DB_USER = $user
DB_PASSWORD = $password
DB_PREFIX =
# 缓存方式
CACHE_MODE = file
# Redis配置
REDIS_HOST = 127.0.0.1
REDIS_PORT = 6379
REDIS_PASSWORD = ''
REDIS_DB = 0
# 验证码配置
CAPTCHA_MODE = cache
#前端目录
FRONTEND_DIR = saiadmin-artd
EOF;
file_put_contents(base_path() . DIRECTORY_SEPARATOR . '.env', $env_config);
// 尝试reload
if (function_exists('posix_kill')) {
set_error_handler(function () {});
posix_kill(posix_getppid(), SIGUSR1);
restore_error_handler();
}
return $this->success('安装成功');
}
/**
* 生成配置文件
*/
protected function generateConfig()
{
// 1、think-orm配置文件
$think_orm_config = <<<EOF
<?php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
// 数据库类型
'type' => env('DB_TYPE', 'mysql'),
// 服务器地址
'hostname' => env('DB_HOST', '127.0.0.1'),
// 数据库名
'database' => env('DB_NAME', 'saiadmin'),
// 数据库用户名
'username' => env('DB_USER', 'root'),
// 数据库密码
'password' => env('DB_PASSWORD', '123456'),
// 数据库连接端口
'hostport' => env('DB_PORT', 3306),
// 数据库连接参数
'params' => [
// 连接超时3秒
\PDO::ATTR_TIMEOUT => 3,
],
// 数据库编码默认采用utf8
'charset' => 'utf8',
// 数据库表前缀
'prefix' => env('DB_PREFIX', ''),
// 断线重连
'break_reconnect' => true,
// 自定义分页类
'bootstrap' => '',
// 连接池配置
'pool' => [
'max_connections' => 5, // 最大连接数
'min_connections' => 1, // 最小连接数
'wait_timeout' => 3, // 从连接池获取连接等待超时时间
'idle_timeout' => 60, // 连接最大空闲时间,超过该时间会被回收
'heartbeat_interval' => 50, // 心跳检测间隔需要小于60秒
],
],
],
];
EOF;
file_put_contents(base_path() . '/config/think-orm.php', $think_orm_config);
// 2、chache配置文件
$cache_config = <<<EOF
<?php
return [
'default' => env('CACHE_MODE', 'file'),
'stores' => [
'file' => [
'driver' => 'file',
'path' => runtime_path('cache')
],
'redis' => [
'driver' => 'redis',
'connection' => 'default'
],
'array' => [
'driver' => 'array'
]
]
];
EOF;
file_put_contents(base_path() . '/config/cache.php', $cache_config);
// 3、redis配置文件
$redis_config = <<<EOF
<?php
return [
'default' => [
'password' => env('REDIS_PASSWORD', ''),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DB', 0),
'pool' => [
'max_connections' => 5,
'min_connections' => 1,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
]
];
EOF;
file_put_contents(base_path() . '/config/redis.php', $redis_config);
// 4、think-cache配置文件
$think_cache_config = <<<EOF
<?php
return [
// 默认缓存驱动
'default' => env('CACHE_MODE', 'file'),
// 缓存连接方式配置
'stores' => [
// redis缓存
'redis' => [
// 驱动方式
'type' => 'redis',
// 服务器地址
'host' => env('REDIS_HOST', '127.0.0.1'),
// 服务器端口
'port' => env('REDIS_PORT', 6379),
// 服务器密码
'password' => env('REDIS_PASSWORD', ''),
// 数据库
'select' => env('REDIS_DB', 0),
// 缓存前缀
'prefix' => 'cache:',
// 默认缓存有效期 0表示永久缓存
'expire' => 0,
// Thinkphp官方没有这个参数由于生成的tag键默认不过期如果tag键数量很大避免长时间占用内存可以设置一个超过其他缓存的过期时间0为不设置
'tag_expire' => 86400 * 30,
// 缓存标签前缀
'tag_prefix' => 'tag:',
// 连接池配置
'pool' => [
'max_connections' => 5, // 最大连接数
'min_connections' => 1, // 最小连接数
'wait_timeout' => 3, // 从连接池获取连接等待超时时间
'idle_timeout' => 60, // 连接最大空闲时间,超过该时间会被回收
'heartbeat_interval' => 50, // 心跳检测间隔需要小于60秒
],
],
// 文件缓存
'file' => [
// 驱动方式
'type' => 'file',
// 设置不同的缓存保存目录
'path' => runtime_path() . '/file/',
],
],
];
EOF;
file_put_contents(base_path() . '/config/think-cache.php', $think_cache_config);
// 5、database配置文件
$database = <<<EOF
<?php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
'driver' => env('DB_TYPE', 'mysql'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_NAME', 'saiadmin'),
'username' => env('DB_USER', 'root'),
'password' => env('DB_PASSWORD', '123456'),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_general_ci'),
'prefix' => env('DB_PREFIX', ''),
'strict' => true,
'engine' => null,
'options' => [
PDO::ATTR_EMULATE_PREPARES => false, // Must be false for Swoole and Swow drivers.
],
'pool' => [
'max_connections' => 5,
'min_connections' => 1,
'wait_timeout' => 3,
'idle_timeout' => 60,
'heartbeat_interval' => 50,
],
],
],
];
EOF;
file_put_contents(base_path() . '/config/database.php', $database);
}
/**
* 获取pdo连接
* @param $host
* @param $username
* @param $password
* @param $port
* @param $database
* @return \PDO
*/
protected function getPdo($host, $username, $password, $port, $database = null): \PDO
{
$dsn = "mysql:host=$host;port=$port;";
if ($database) {
$dsn .= "dbname=$database";
}
$params = [
\PDO::MYSQL_ATTR_INIT_COMMAND => "set names utf8mb4",
\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
\PDO::ATTR_EMULATE_PREPARES => false,
\PDO::ATTR_TIMEOUT => 5,
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
];
return new \PDO($dsn, $username, $password, $params);
}
}

View File

@@ -0,0 +1,61 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller;
use support\Request;
use support\Response;
use plugin\saiadmin\utils\Captcha;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemUserLogic;
/**
* 登录控制器
*/
class LoginController extends BaseController
{
/**
* 不需要登录的方法
*/
protected array $noNeedLogin = ['captcha', 'login'];
/**
* 获取验证码
*/
public function captcha() : Response
{
$captcha = new Captcha();
$result = $captcha->imageCaptcha();
if ($result['result'] !== 1) {
return $this->fail($result['message']);
}
return $this->success($result);
}
/**
* 登录
* @param Request $request
* @return Response
*/
public function login(Request $request): Response
{
$username = $request->post('username', '');
$password = $request->post('password', '');
$type = $request->post('type', 'pc');
$code = $request->post('code', '');
$uuid = $request->post('uuid', '');
$captcha = new Captcha();
if (!$captcha->checkCaptcha($uuid, $code)) {
return $this->fail('验证码错误');
}
$logic = new SystemUserLogic();
$data = $logic->login($username, $password, $type);
return $this->success($data);
}
}

View File

@@ -0,0 +1,263 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller;
use plugin\saiadmin\app\cache\DictCache;
use plugin\saiadmin\app\cache\UserAuthCache;
use plugin\saiadmin\app\cache\UserInfoCache;
use plugin\saiadmin\app\cache\UserMenuCache;
use plugin\saiadmin\app\logic\system\SystemCategoryLogic;
use plugin\saiadmin\app\logic\system\SystemLoginLogLogic;
use plugin\saiadmin\app\logic\system\SystemOperLogLogic;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemUserLogic;
use plugin\saiadmin\app\logic\system\SystemAttachmentLogic;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
use plugin\saiadmin\utils\Arr;
use Tinywan\Storage\Storage;
/**
* 系统控制器
*/
class SystemController extends BaseController
{
/**
* 用户信息
*/
public function userInfo(): Response
{
$info['user'] = $this->adminInfo;
$info = [];
$info['id'] = $this->adminInfo['id'];
$info['username'] = $this->adminInfo['username'];
$info['dashboard'] = $this->adminInfo['dashboard'];
$info['avatar'] = $this->adminInfo['avatar'];
$info['email'] = $this->adminInfo['email'];
$info['phone'] = $this->adminInfo['phone'];
$info['gender'] = $this->adminInfo['gender'];
$info['signed'] = $this->adminInfo['signed'];
$info['realname'] = $this->adminInfo['realname'];
$info['department'] = $this->adminInfo['deptList'];
if ($this->adminInfo['id'] === 1) {
$info['buttons'] = ['*'];
$info['roles'] = ['super_admin'];
} else {
$info['buttons'] = UserAuthCache::getUserAuth($this->adminInfo['id']);
$info['roles'] = Arr::getArrayColumn($this->adminInfo['roleList'], 'code');
}
return $this->success($info);
}
/**
* 全部字典数据
*/
public function dictAll(): Response
{
$dict = DictCache::getDictAll();
return $this->success($dict);
}
/**
* 菜单数据
* @return Response
*/
public function menu(): Response
{
$data = UserMenuCache::getUserMenu($this->adminInfo['id']);
return $this->success($data);
}
/**
* 获取资源列表
* @param Request $request
* @return Response
*/
#[Permission('附件列表读取', 'core:system:resource')]
public function getResourceCategory(Request $request): Response
{
$logic = new SystemCategoryLogic();
$data = $logic->tree([]);
return $this->success($data);
}
/**
* 获取资源列表
* @param Request $request
* @return Response
*/
#[Permission('附件列表读取', 'core:system:resource')]
public function getResourceList(Request $request): Response
{
$logic = new SystemAttachmentLogic();
$where = $request->more([
['origin_name', ''],
['category_id', ''],
]);
$query = $logic->search($where);
$query->whereIn('mime_type', ['image/jpeg', 'image/png', 'image/gif', 'image/webp']);
$data = $logic->getList($query);
return $this->success($data);
}
/**
* 获取用户列表
* @param Request $request
* @return Response
*/
#[Permission('用户列表读取', 'core:system:user')]
public function getUserList(Request $request): Response
{
$logic = new SystemUserLogic();
$where = $request->more([
['keyword', ''],
['dept_id', ''],
]);
$data = $logic->openUserList($where);
return $this->success($data);
}
/**
* 下载网络图片
*/
#[Permission('上传网络图片', 'core:system:uploadImage')]
public function saveNetworkImage(Request $request): Response
{
$url = $request->input('url', '');
$config = Storage::getConfig('local');
$logic = new SystemAttachmentLogic();
$data = $logic->saveNetworkImage($url, $config);
return $this->success($data, '操作成功');
}
/**
* 上传图片
*/
#[Permission('上传图片', 'core:system:uploadImage')]
public function uploadImage(Request $request): Response
{
$logic = new SystemAttachmentLogic();
$type = $request->input('mode', 'system');
if ($type == 'local') {
return $this->success($logic->uploadBase('image', true));
}
return $this->success($logic->uploadBase('image'));
}
/**
* 上传文件
*/
#[Permission('上传文件', 'core:system:uploadFile')]
public function uploadFile(Request $request): Response
{
$logic = new SystemAttachmentLogic();
$type = $request->input('mode', 'system');
if ($type == 'local') {
return $this->success($logic->uploadBase('file', true));
}
return $this->success($logic->uploadBase('file'));
}
/**
* 切片上传
*/
#[Permission('上传文件', 'core:system:chunkUpload')]
public function chunkUpload(Request $request): Response
{
$logic = new SystemAttachmentLogic();
$data = $request->post();
$result = $logic->chunkUpload($data);
return $this->success($result);
}
/**
* 获取登录日志
* @return Response
*/
public function getLoginLogList(): Response
{
$logic = new SystemLoginLogLogic();
$logic->init($this->adminInfo);
$query = $logic->search(['username' => $this->adminName]);
$data = $logic->getList($query);
return $this->success($data);
}
/**
* 获取操作日志
* @return Response
*/
public function getOperationLogList(): Response
{
$logic = new SystemOperLogLogic();
$logic->init($this->adminInfo);
$data = $logic->getOwnOperLogList(['username' => $this->adminName]);
return $this->success($data);
}
/**
* 清除缓存
* @return Response
*/
public function clearAllCache(): Response
{
UserInfoCache::clearUserInfo($this->adminId);
UserAuthCache::clearUserAuth($this->adminId);
UserMenuCache::clearUserMenu($this->adminId);
return $this->success([], '清除缓存成功!');
}
/**
* 基本统计
* @return Response
*/
#[Permission('工作台数据统计', 'core:console:list')]
public function statistics(): Response
{
$userLogic = new SystemUserLogic();
$userCount = $userLogic->count('id');
$uploadLogic = new SystemAttachmentLogic();
$attachCount = $uploadLogic->count('id');
$loginLogic = new SystemLoginLogLogic();
$loginCount = $loginLogic->count('id');
$operLogic = new SystemOperLogLogic();
$operCount = $operLogic->count('id');
return $this->success([
'user' => $userCount,
'attach' => $attachCount,
'login' => $loginCount,
'operate' => $operCount,
]);
}
/**
* 登录统计曲线图
* @return Response
*/
#[Permission('工作台数据统计', 'core:console:list')]
public function loginChart(): Response
{
$logic = new SystemLoginLogLogic();
$data = $logic->loginChart();
return $this->success($data);
}
/**
* 登录统计柱状图
* @return Response
*/
#[Permission('工作台数据统计', 'core:console:list')]
public function loginBarChart(): Response
{
$logic = new SystemLoginLogLogic();
$data = $logic->loginBarChart();
return $this->success($data);
}
}

View File

@@ -0,0 +1,147 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\app\logic\system\DatabaseLogic;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 数据表维护控制器
*/
class DataBaseController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new DatabaseLogic();
parent::__construct();
}
/**
* 数据源列表
* @return Response
*/
public function source(): Response
{
$data = $this->logic->getDbSource();
return $this->success($data);
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('数据表列表', 'core:database:index')]
public function index(Request $request): Response
{
$where = $request->more([
['name', ''],
['source', ''],
]);
$data = $this->logic->getList($where);
return $this->success($data);
}
/**
* 回收站数据
* @param Request $request
* @return Response
*/
#[Permission('回收站数据', 'core:recycle:index')]
public function recycle(Request $request): Response
{
$table = $request->input('table', '');
$data = $this->logic->recycleData($table);
return $this->success($data);
}
/**
* 销毁数据
* @param Request $request
* @return Response
*/
#[Permission('回收站销毁', 'core:recycle:edit')]
public function delete(Request $request): Response
{
$table = $request->input('table', '');
$ids = $request->input('ids', '');
if (!empty($ids)) {
$result = $this->logic->delete($table, $ids);
if (!$result) {
return $this->fail('操作失败');
}
return $this->success('操作成功');
} else {
return $this->fail('参数错误,请检查');
}
}
/**
* 恢复数据
* @param Request $request
* @return Response
*/
#[Permission('回收站恢复', 'core:recycle:edit')]
public function recovery(Request $request): Response
{
$table = $request->input('table', '');
$ids = $request->input('ids', '');
if (!empty($ids)) {
$result = $this->logic->recovery($table, $ids);
if (!$result) {
return $this->fail('操作失败');
}
return $this->success('操作成功');
} else {
return $this->fail('参数错误,请检查');
}
}
/**
* 获取表字段信息
* @param Request $request
* @return Response
*/
#[Permission('数据表字段', 'core:database:index')]
public function detailed(Request $request): Response
{
$table = $request->input('table', '');
$data = $this->logic->getColumnList($table, '');
return $this->success($data);
}
/**
* 优化表
* @param Request $request
* @return Response
*/
#[Permission('数据表优化表', 'core:database:edit')]
public function optimize(Request $request): Response
{
$tables = $request->input('tables', []);
$this->logic->optimizeTable($tables);
return $this->success('优化成功');
}
/**
* 清理表碎片
*/
#[Permission('数据表清理碎片', 'core:database:edit')]
public function fragment(Request $request): Response
{
$tables = $request->input('tables', []);
$this->logic->fragmentTable($tables);
return $this->success('清理成功');
}
}

View File

@@ -0,0 +1,107 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemAttachmentLogic;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 附件管理控制器
*/
class SystemAttachmentController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new SystemAttachmentLogic();
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('附件数据列表', 'core:attachment:index')]
public function index(Request $request) : Response
{
$where = $request->more([
['origin_name', ''],
['category_id', ''],
['storage_mode', ''],
['mime_type', ''],
['create_time', ''],
]);
$query = $this->logic->search($where);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('附件数据修改', 'core:attachment:edit')]
public function update(Request $request): Response
{
$data = $request->post();
$result = $this->logic->edit($data['id'], ['origin_name' => $data['origin_name']]);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('附件数据删除', 'core:attachment:edit')]
public function destroy(Request $request) : Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
/**
* 移动分类
* @param Request $request
* @return Response
*/
#[Permission('附件移动分类', 'core:attachment:edit')]
public function move(Request $request) : Response
{
$category_id = $request->post('category_id', '');
$ids = $request->post('ids', '');
if (empty($ids) || empty($category_id)) {
return $this->fail('参数错误,请检查参数');
}
$result = $this->logic->move($category_id, $ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
}

View File

@@ -0,0 +1,120 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\app\validate\system\SystemCategoryValidate;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemCategoryLogic;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 附件分类控制器
*/
class SystemCategoryController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new SystemCategoryLogic();
$this->validate = new SystemCategoryValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('附件分类列表', 'core:attachment:index')]
public function index(Request $request) : Response
{
$where = $request->more([
['category_name', ''],
]);
$data = $this->logic->tree($where);
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('附件分类读取', 'core:attachment:index')]
public function read(Request $request) : Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('附件分类添加', 'core:attachment:edit')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('附件分类修改', 'core:attachment:edit')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('附件分类删除', 'core:attachment:edit')]
public function destroy(Request $request) : Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
}

View File

@@ -0,0 +1,126 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\app\cache\ConfigCache;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemConfigLogic;
use plugin\saiadmin\app\logic\system\SystemConfigGroupLogic;
use plugin\saiadmin\app\validate\system\SystemConfigValidate;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 配置项数据控制器
*/
class SystemConfigController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new SystemConfigLogic();
$this->validate = new SystemConfigValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('系统设置列表', 'core:config:index')]
public function index(Request $request): Response
{
$where = $request->more([
['group_id', ''],
['name', ''],
['key', ''],
]);
$this->logic->setOrderField('sort');
$this->logic->setOrderType('desc');
$query = $this->logic->search($where);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('系统设置管理', 'core:config:edit')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('系统设置管理', 'core:config:edit')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('系统设置管理', 'core:config:edit')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
/**
* 修改配置内容
* @param Request $request
* @return Response
*/
#[Permission('系统设置修改', 'core:config:update')]
public function batchUpdate(Request $request): Response
{
$group_id = $request->post('group_id');
$config = $request->post('config');
if (empty($group_id) || empty($config)) {
return $this->fail('参数错误');
}
$this->logic->batchUpdate($group_id, $config);
return $this->success('操作成功');
}
}

View File

@@ -0,0 +1,154 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\app\cache\ConfigCache;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemConfigGroupLogic;
use plugin\saiadmin\app\validate\system\SystemConfigGroupValidate;
use plugin\saiadmin\service\Permission;
use plugin\saiadmin\utils\Arr;
use support\Request;
use support\Response;
use plugin\saiadmin\service\EmailService;
use plugin\saiadmin\app\model\system\SystemMail;
/**
* 配置控制器
*/
class SystemConfigGroupController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new SystemConfigGroupLogic();
$this->validate = new SystemConfigGroupValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('系统设置列表', 'core:config:index')]
public function index(Request $request) : Response
{
$where = $request->more([
['name', ''],
['code', ''],
]);
$query = $this->logic->search($where);
$data = $this->logic->getAll($query);
return $this->success($data);
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('系统设置管理', 'core:config:edit')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('系统设置管理', 'core:config:edit')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
ConfigCache::clearConfig($data['code']);
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('系统设置管理', 'core:config:edit')]
public function destroy(Request $request) : Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
/**
* 邮件测试
* @param Request $request
* @return Response
*/
#[Permission('系统设置修改', 'core:config:update')]
public function email(Request $request) : Response
{
$email = $request->input('email', '');
if (empty($email)) {
return $this->fail('请输入邮箱');
}
$subject = "测试邮件";
$code = "9527";
$content = "<h1>验证码:{code}</h1><p>这是一封测试邮件,请忽略</p>";
$template = [
'code' => $code
];
$config = EmailService::getConfig();
$model = SystemMail::create([
'gateway' => Arr::getConfigValue($config,'Host'),
'from' => Arr::getConfigValue($config,'From'),
'email' => $email,
'code' => $code,
]);
try {
$result = EmailService::sendByTemplate($email, $subject, $content, $template);
if (!empty($result)) {
$model->status = 'failure';
$model->response = $result;
$model->save();
return $this->fail('发送失败,请查看日志');
} else {
$model->status = 'success';
$model->save();
return $this->success([], '发送成功');
}
} catch (\Exception $e) {
$model->status = 'failure';
$model->response = $e->getMessage();
$model->save();
return $this->fail($e->getMessage());
}
}
}

View File

@@ -0,0 +1,134 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\app\validate\system\SystemDeptValidate;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemDeptLogic;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 部门控制器
*/
class SystemDeptController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new SystemDeptLogic();
$this->validate = new SystemDeptValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('部门数据列表', 'core:dept:index')]
public function index(Request $request) : Response
{
$where = $request->more([
['name', ''],
['code', ''],
['status', ''],
]);
$data = $this->logic->tree($where);
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('部门数据读取', 'core:dept:read')]
public function read(Request $request) : Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('部门数据添加', 'core:dept:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('部门数据修改','core:dept:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('部门数据删除','core:dept:destroy')]
public function destroy(Request $request) : Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
/**
* 可操作部门
* @param Request $request
* @return Response
*/
public function accessDept(Request $request) : Response
{
$where = ['status' => 1];
$data = $this->logic->accessDept($where);
return $this->success($data);
}
}

View File

@@ -0,0 +1,112 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\app\cache\DictCache;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemDictDataLogic;
use plugin\saiadmin\app\validate\system\SystemDictDataValidate;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 字典数据控制器
*/
class SystemDictDataController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new SystemDictDataLogic();
$this->validate = new SystemDictDataValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('数据字典列表', 'core:dict:index')]
public function index(Request $request): Response
{
$where = $request->more([
['label', ''],
['value', ''],
['type_id', ''],
['status', ''],
]);
$this->logic->setOrderField('sort');
$this->logic->setOrderType('desc');
$query = $this->logic->search($where);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('数据字典管理', 'core:dict:edit')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
DictCache::clear();
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('数据字典管理', 'core:dict:edit')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
DictCache::clear();
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('数据字典管理', 'core:dict:edit')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
DictCache::clear();
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
}

View File

@@ -0,0 +1,110 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\app\cache\DictCache;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemDictTypeLogic;
use plugin\saiadmin\app\validate\system\SystemDictTypeValidate;
use plugin\saiadmin\service\Permission;
use support\Cache;
use support\Request;
use support\Response;
/**
* 字典类型控制器
*/
class SystemDictTypeController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new SystemDictTypeLogic();
$this->validate = new SystemDictTypeValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('数据字典列表', 'core:dict:index')]
public function index(Request $request) : Response
{
$where = $request->more([
['name', ''],
['code', ''],
['status', ''],
]);
$query = $this->logic->search($where);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('数据字典管理', 'core:dict:edit')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
DictCache::clear();
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('数据字典管理', 'core:dict:edit')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
DictCache::clear();
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('数据字典管理', 'core:dict:edit')]
public function destroy(Request $request) : Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
DictCache::clear();
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
}

View File

@@ -0,0 +1,100 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemLoginLogLogic;
use plugin\saiadmin\app\logic\system\SystemOperLogLogic;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 日志控制器
*/
class SystemLogController extends BaseController
{
/**
* 登录日志列表
* @param Request $request
* @return Response
*/
#[Permission('登录日志列表', 'core:logs:login')]
public function getLoginLogPageList(Request $request) : Response
{
$where = $request->more([
['login_time', ''],
['username', ''],
['status', ''],
['ip', ''],
]);
$logic = new SystemLoginLogLogic();
$query = $logic->search($where);
$data = $logic->getList($query);
return $this->success($data);
}
/**
* 删除登录日志
* @param Request $request
* @return Response
*/
#[Permission('登录日志删除', 'core:logs:deleteLogin')]
public function deleteLoginLog(Request $request) : Response
{
$ids = $request->input('ids', '');
$logic = new SystemLoginLogLogic();
if (!empty($ids)) {
$logic->destroy($ids);
return $this->success('删除成功');
} else {
return $this->fail('参数错误,请检查');
}
}
/**
* 操作日志列表
* @param Request $request
* @return Response
*/
#[Permission('操作日志列表', 'core:logs:Oper')]
public function getOperLogPageList(Request $request) : Response
{
$where = $request->more([
['create_time', ''],
['username', ''],
['service_name', ''],
['router', ''],
['ip', ''],
]);
$logic = new SystemOperLogLogic();
$logic->init($this->adminInfo);
$query = $logic->search($where);
$data = $logic->getList($query);
return $this->success($data);
}
/**
* 删除操作日志
* @param Request $request
* @return Response
*/
#[Permission('操作日志删除', 'core:logs:deleteOper')]
public function deleteOperLog(Request $request) : Response
{
$ids = $request->input('ids', '');
$logic = new SystemOperLogLogic();
if (!empty($ids)) {
$logic->destroy($ids);
return $this->success('删除成功');
} else {
return $this->fail('参数错误,请检查');
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\service\Permission;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemMailLogic;
use plugin\saiadmin\app\validate\system\SystemMailValidate;
use support\Request;
use support\Response;
/**
* 邮件记录控制器
*/
class SystemMailController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new SystemMailLogic();
$this->validate = new SystemMailValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('邮件日志列表', 'core:email:index')]
public function index(Request $request): Response
{
$where = $request->more([
['gateway', ''],
['from', ''],
['code', ''],
['email', ''],
['status', ''],
['create_time', ''],
]);
$query = $this->logic->search($where);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('邮件日志删除', 'core:email:destroy')]
public function destroy(Request $request) : Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
}

View File

@@ -0,0 +1,143 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\app\cache\UserMenuCache;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemMenuLogic;
use plugin\saiadmin\app\validate\system\SystemMenuValidate;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 菜单控制器
*/
class SystemMenuController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new SystemMenuLogic();
$this->validate = new SystemMenuValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('菜单数据列表', 'core:menu:index')]
public function index(Request $request): Response
{
$where = $request->more([
['name', ''],
['path', ''],
['menu', ''],
['status', ''],
]);
$data = $this->logic->tree($where);
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('菜单数据读取', 'core:menu:read')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('菜单数据添加', 'core:menu:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
UserMenuCache::clearMenuCache();
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('菜单数据修改', 'core:menu:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
UserMenuCache::clearMenuCache();
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('菜单数据删除', 'core:menu:destroy')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
UserMenuCache::clearMenuCache();
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
/**
* 可操作菜单
* @param Request $request
* @return Response
*/
public function accessMenu(Request $request): Response
{
$where = [];
if ($this->adminId > 1) {
$data = $this->logic->auth();
} else {
$data = $this->logic->tree($where);
}
return $this->success($data);
}
}

View File

@@ -0,0 +1,177 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\system\SystemPostLogic;
use plugin\saiadmin\app\validate\system\SystemPostValidate;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 岗位信息控制器
*/
class SystemPostController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new SystemPostLogic();
$this->validate = new SystemPostValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('岗位数据列表', 'core:post:index')]
public function index(Request $request): Response
{
$where = $request->more([
['name', ''],
['code', ''],
['status', ''],
]);
$query = $this->logic->search($where);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('岗位数据读取', 'core:post:read')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('岗位数据添加', 'core:post:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('岗位数据修改', 'core:post:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('岗位数据删除', 'core:post:destroy')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
/**
* 导入数据
* @param Request $request
* @return Response
*/
#[Permission('岗位数据导入', 'core:post:import')]
public function import(Request $request): Response
{
$file = current($request->file());
if (!$file || !$file->isValid()) {
return $this->fail('未找到上传文件');
}
$this->logic->import($file);
return $this->success('导入成功');
}
/**
* 导出数据
* @param Request $request
* @return Response
*/
#[Permission('岗位数据导出', 'core:post:export')]
public function export(Request $request): Response
{
$where = $request->more([
['name', ''],
['code', ''],
['status', ''],
]);
return $this->logic->export($where);
}
/**
* 下载导入模板
* @return Response
*/
public function downloadTemplate(): Response
{
$file_name = "template.xlsx";
return downloadFile($file_name);
}
/**
* 可操作岗位
* @param Request $request
* @return Response
*/
public function accessPost(Request $request): Response
{
$where = ['status' => 1];
$data = $this->logic->accessPost($where);
return $this->success($data);
}
}

View File

@@ -0,0 +1,168 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\app\model\system\SystemUserRole;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\cache\UserInfoCache;
use plugin\saiadmin\app\model\system\SystemUser;
use plugin\saiadmin\app\validate\system\SystemRoleValidate;
use plugin\saiadmin\app\logic\system\SystemRoleLogic;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 角色控制器
*/
class SystemRoleController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new SystemRoleLogic();
$this->validate = new SystemRoleValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('角色数据列表', 'core:role:index')]
public function index(Request $request): Response
{
$where = $request->more([
['name', ''],
['code', ''],
['status', ''],
]);
$query = $this->logic->search($where);
$levelArr = array_column($this->adminInfo['roleList'], 'level');
$maxLevel = max($levelArr);
$query->where('level', '<', $maxLevel);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('角色数据读取', 'core:role:read')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('角色数据添加', 'core:role:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('角色数据修改', 'core:role:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('角色数据删除', 'core:role:destroy')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
/**
* 根据角色获取菜单
* @param Request $request
* @return Response
*/
#[Permission('角色数据列表', 'core:role:index')]
public function getMenuByRole(Request $request): Response
{
$id = $request->get('id');
$data = $this->logic->getMenuByRole($id);
return $this->success($data);
}
/**
* 菜单权限
* @param Request $request
* @return Response
*/
#[Permission('角色菜单权限', 'core:role:menu')]
public function menuPermission(Request $request): Response
{
$id = $request->post('id');
$menu_ids = $request->post('menu_ids');
$this->logic->saveMenuPermission($id, $menu_ids);
return $this->success('操作成功');
}
/**
* 可操作角色
* @param Request $request
* @return Response
*/
public function accessRole(Request $request): Response
{
$where = ['status' => 1];
$data = $this->logic->accessRole($where);
return $this->success($data);
}
}

View File

@@ -0,0 +1,85 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\service\Permission;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\utils\ServerMonitor;
use support\think\Cache;
use support\Request;
use support\Response;
/**
* 邮件记录控制器
*/
class SystemServerController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('服务监控', 'core:server:monitor')]
public function monitor(Request $request): Response
{
$service = new ServerMonitor();
return $this->success([
'memory' => $service->getMemoryInfo(),
'disk' => $service->getDiskInfo(),
'phpEnv' => $service->getPhpAndEnvInfo(),
]);
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('缓存信息', 'core:server:cache')]
public function cache(Request $request): Response
{
$menu_cache = config('plugin.saiadmin.saithink.menu_cache', []);
$button_cache = config('plugin.saiadmin.saithink.button_cache', []);
$config_cache = config('plugin.saiadmin.saithink.config_cache', []);
$dict_cache = config('plugin.saiadmin.saithink.dict_cache', []);
$reflection_cache = config('plugin.saiadmin.saithink.reflection_cache', []);
return $this->success([
'menu_cache' => $menu_cache,
'button_cache' => $button_cache,
'config_cache' => $config_cache,
'dict_cache' => $dict_cache,
'reflection_cache' => $reflection_cache
]);
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('缓存数据清理', 'core:server:clear')]
public function clear(Request $request) : Response
{
$tag = $request->input('tag', '');
if (empty($tag)) {
return $this->fail('请选择要删除的缓存');
}
Cache::tag($tag)->clear();
Cache::delete($tag);
return $this->success('删除成功');
}
}

View File

@@ -0,0 +1,210 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\system;
use plugin\saiadmin\app\cache\UserAuthCache;
use plugin\saiadmin\app\cache\UserMenuCache;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\cache\UserInfoCache;
use plugin\saiadmin\app\logic\system\SystemUserLogic;
use plugin\saiadmin\app\validate\system\SystemUserValidate;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 用户信息控制器
*/
class SystemUserController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new SystemUserLogic();
$this->validate = new SystemUserValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('用户数据列表', 'core:user:index')]
public function index(Request $request): Response
{
$where = $request->more([
['username', ''],
['phone', ''],
['email', ''],
['status', ''],
['dept_id', ''],
['create_time', ''],
]);
$data = $this->logic->indexList($where);
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('用户数据读取', 'core:user:read')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('用户数据保存', 'core:user:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('用户数据更新', 'core:user:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('用户数据删除', 'core:user:destroy')]
public function destroy(Request $request): Response
{
$ids = $request->input('ids', '');
if (!empty($ids)) {
$this->logic->destroy($ids);
return $this->success('操作成功');
} else {
return $this->fail('参数错误,请检查');
}
}
/**
* 清理用户缓存
* @param Request $request
* @return Response
*/
#[Permission('清理用户缓存', 'core:user:cache')]
public function clearCache(Request $request): Response
{
$id = $request->post('id', '');
UserInfoCache::clearUserInfo($id);
UserAuthCache::clearUserAuth($id);
UserMenuCache::clearUserMenu($id);
return $this->success('操作成功');
}
/**
* 修改用户密码
* @param Request $request
* @return Response
*/
#[Permission('修改用户密码', 'core:user:password')]
public function initUserPassword(Request $request): Response
{
$id = $request->post('id', '');
$password = $request->post('password', '');
if ($id == 1) {
return $this->fail('超级管理员不允许重置密码');
}
$data = ['password' => password_hash($password, PASSWORD_DEFAULT)];
$this->logic->authEdit($id, $data);
UserInfoCache::clearUserInfo($id);
return $this->success('操作成功');
}
/**
* 设置用户首页
* @param Request $request
* @return Response
*/
#[Permission('设置用户首页', 'core:user:home')]
public function setHomePage(Request $request): Response
{
$id = $request->post('id', '');
$dashboard = $request->post('dashboard', '');
$data = ['dashboard' => $dashboard];
$this->logic->authEdit($id, $data);
UserInfoCache::clearUserInfo($id);
return $this->success('操作成功');
}
/**
* 更新资料
* @param Request $request
* @return Response
*/
#[Permission('用户修改资料')]
public function updateInfo(Request $request): Response
{
$data = $request->post();
unset($data['deptList']);
unset($data['postList']);
unset($data['roleList']);
$result = $this->logic->updateInfo($this->adminId, $data);
if ($result) {
UserInfoCache::clearUserInfo($this->adminId);
return $this->success('操作成功');
} else {
return $this->fail('操作失败');
}
}
/**
* 修改密码
* @param Request $request
* @return Response
*/
#[Permission('用户修改密码')]
public function modifyPassword(Request $request): Response
{
$oldPassword = $request->input('oldPassword');
$newPassword = $request->input('newPassword');
$this->logic->modifyPassword($this->adminId, $oldPassword, $newPassword);
UserInfoCache::clearUserInfo($this->adminId);
return $this->success('修改成功');
}
}

View File

@@ -0,0 +1,181 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\tool;
use plugin\saiadmin\app\logic\tool\CrontabLogic;
use plugin\saiadmin\app\logic\tool\CrontabLogLogic;
use plugin\saiadmin\app\validate\tool\CrontabValidate;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\service\Permission;
use Webman\Channel\Client;
use support\Request;
use support\Response;
/**
* 定时任务控制器
*/
class CrontabController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new CrontabLogic();
$this->validate = new CrontabValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('定时任务列表', 'tool:crontab:index')]
public function index(Request $request) : Response
{
$where = $request->more([
['name', ''],
['type', ''],
['status', ''],
['create_time', ''],
]);
$query = $this->logic->search($where);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('定时任务添加', 'tool:crontab:edit')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('定时任务修改', 'tool:crontab:edit')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('定时任务删除', 'tool:crontab:edit')]
public function destroy(Request $request) : Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
/**
* 修改状态
* @param Request $request
* @return Response
*/
#[Permission('定时任务状态修改', 'tool:crontab:edit')]
public function changeStatus(Request $request) : Response
{
$id = $request->input('id', '');
$status = $request->input('status', 1);
if (empty($id)) {
return $this->fail('参数错误,请检查');
}
$result = $this->logic->changeStatus($id, $status);
if ($result) {
return $this->success('操作成功');
} else {
return $this->fail('操作失败');
}
}
/**
* 执行定时任务
* @param Request $request
* @return Response
*/
#[Permission('定时任务执行', 'tool:crontab:run')]
public function run(Request $request) : Response
{
$id = $request->input('id', '');
$result = $this->logic->run($id);
if ($result) {
return $this->success('执行成功');
} else {
return $this->fail('执行失败');
}
}
/**
* 定时任务日志
* @param Request $request
* @return Response
*/
#[Permission('定时任务日志', 'tool:crontab:index')]
public function logPageList(Request $request) : Response
{
$where = $request->more([
['crontab_id', ''],
['create_time', []]
]);
$logic = new CrontabLogLogic();
$query = $logic->search($where);
$data = $logic->getList($query);
return $this->success($data);
}
/**
* 定时任务日志删除
* @param Request $request
* @return Response
*/
#[Permission('定时任务日志删除', 'tool:crontab:edit')]
public function deleteCrontabLog(Request $request) : Response
{
$ids = $request->input('ids', '');
if (!empty($ids)) {
$logic = new CrontabLogLogic();
$logic->destroy($ids);
return $this->success('操作成功');
} else {
return $this->fail('参数错误,请检查');
}
}
}

View File

@@ -0,0 +1,178 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\controller\tool;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\app\logic\tool\GenerateTablesLogic;
use plugin\saiadmin\app\validate\tool\GenerateTablesValidate;
use plugin\saiadmin\app\cache\UserMenuCache;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* 代码生成控制器
*/
class GenerateTablesController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
$this->logic = new GenerateTablesLogic();
$this->validate = new GenerateTablesValidate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('代码生成列表', 'tool:code:index')]
public function index(Request $request): Response
{
$where = $request->more([
['table_name', ''],
]);
$query = $this->logic->search($where);
$data = $this->logic->getList($query);
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('代码生成列表', 'tool:code:index')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 修改数据
* @param Request $request
* @return Response
*/
#[Permission('代码生成修改', 'tool:code:edit')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('代码生成删除', 'tool:code:edit')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
/**
* 装载数据表
* @param Request $request
* @return Response
*/
#[Permission('代码生成装载', 'tool:code:edit')]
public function loadTable(Request $request): Response
{
$names = $request->input('names', []);
$source = $request->input('source', '');
$this->logic->loadTable($names, $source);
return $this->success('操作成功');
}
/**
* 同步数据表字段信息
* @param Request $request
* @return Response
*/
#[Permission('代码生成同步表结构', 'tool:code:edit')]
public function sync(Request $request): Response
{
$id = $request->input('id', '');
$this->logic->sync($id);
return $this->success('操作成功');
}
/**
* 代码预览
*/
#[Permission('代码生成预览', 'tool:code:edit')]
public function preview(Request $request): Response
{
$id = $request->input('id', '');
$data = $this->logic->preview($id);
return $this->success($data);
}
/**
* 代码生成
*/
#[Permission('代码生成文件', 'tool:code:edit')]
public function generate(Request $request): Response
{
$ids = $request->input('ids', '');
$data = $this->logic->generate($ids);
return response()->download($data['download'], $data['filename']);
}
/**
* 生成到模块
*/
#[Permission('代码生成到模块', 'tool:code:edit')]
public function generateFile(Request $request): Response
{
$id = $request->input('id', '');
$this->logic->generateFile($id);
UserMenuCache::clearMenuCache();
return $this->success('操作成功');
}
/**
* 获取数据表字段信息
* @param Request $request
* @return Response
*/
#[Permission('代码生成读取表字段', 'tool:code:index')]
public function getTableColumns(Request $request): Response
{
$table_id = $request->input('table_id', '');
$data = $this->logic->getTableColumns($table_id);
return $this->success($data);
}
}

View File

@@ -0,0 +1,162 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\event;
use plugin\saiadmin\app\cache\ReflectionCache;
use plugin\saiadmin\app\model\system\SystemLoginLog;
use plugin\saiadmin\app\model\system\SystemOperLog;
class SystemUser
{
/**
* 登录日志
* @param $item
*/
public function login($item)
{
$request = request();
$ip = $request ? $request->getRealIp() : '127.0.0.1';
$http_user_agent = $request ? $request->header('user-agent') : '';
$data['username'] = $item['username'];
$data['ip'] = $ip;
$data['ip_location'] = self::getIpLocation($ip);
$data['os'] = self::getOs($http_user_agent);
$data['browser'] = self::getBrowser($http_user_agent);
$data['status'] = $item['status'];
$data['message'] = $item['message'];
$data['login_time'] = date('Y-m-d H:i:s');
if (isset($item['admin_id'])) {
$data['created_by'] = $item['admin_id'];
$data['updated_by'] = $item['admin_id'];
}
SystemLoginLog::create($data);
}
/**
* 记录操作日志
*/
public function operateLog(): bool
{
$request = request();
if (!$request) {
return false;
}
if ($request->method() === 'GET') {
return false;
}
$info = getCurrentInfo();
$ip = $request->getRealIp();
$module = $request->plugin;
$rule = trim($request->uri());
$data['username'] = $info['username'];
$data['method'] = $request->method();
$data['router'] = $rule;
$data['service_name'] = self::getServiceName();
$data['app'] = $module;
$data['ip'] = $ip;
$data['ip_location'] = self::getIpLocation($ip);
$data['request_data'] = $this->filterParams($request->all());
SystemOperLog::create($data);
return true;
}
/**
* 获取业务名称
*/
protected function getServiceName(): string
{
$request = request();
if (!$request) {
return '未命名业务';
}
$permissions = ReflectionCache::getPermissionAttributes($request->controller, $request->action);
if (!empty($permissions)) {
return $permissions['title'] ?? '未命名业务';
} else {
return '未命名业务';
}
}
/**
* 过滤字段
*/
protected function filterParams($params): string
{
$blackList = ['password', 'oldPassword', 'newPassword'];
foreach ($params as $key => $value) {
if (in_array($key, $blackList)) {
$params[$key] = '******';
}
}
return json_encode($params, JSON_UNESCAPED_UNICODE);
}
/**
* 获取IP地理位置
*/
protected function getIpLocation($ip): string
{
$ip2region = new \Ip2Region();
try {
$region = $ip2region->memorySearch($ip);
} catch (\Exception $e) {
return '未知';
}
list($country, $province, $city, $network) = explode('|', $region['region']);
if ($network === '内网IP') {
return $network;
}
if ($country == '中国') {
return $province . '-' . $city . ':' . $network;
} else if ($country == '0') {
return '未知';
} else {
return $country;
}
}
/**
* 获取浏览器信息
*/
protected function getBrowser($user_agent): string
{
$br = 'Unknown';
if (preg_match('/MSIE/i', $user_agent)) {
$br = 'MSIE';
} elseif (preg_match('/Firefox/i', $user_agent)) {
$br = 'Firefox';
} elseif (preg_match('/Chrome/i', $user_agent)) {
$br = 'Chrome';
} elseif (preg_match('/Safari/i', $user_agent)) {
$br = 'Safari';
} elseif (preg_match('/Opera/i', $user_agent)) {
$br = 'Opera';
} else {
$br = 'Other';
}
return $br;
}
/**
* 获取操作系统信息
*/
protected function getOs($user_agent): string
{
$os = 'Unknown';
if (preg_match('/win/i', $user_agent)) {
$os = 'Win';
} elseif (preg_match('/mac/i', $user_agent)) {
$os = 'Mac';
} elseif (preg_match('/linux/i', $user_agent)) {
$os = 'Linux';
} else {
$os = 'Other';
}
return $os;
}
}

View File

@@ -0,0 +1,68 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\exception;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\Exception\ExceptionHandler;
use plugin\saiadmin\exception\ApiException;
/**
* 异常处理类
*/
class Handler extends ExceptionHandler
{
public $dontReport = [
ApiException::class,
];
public function report(Throwable $exception)
{
if ($this->shouldntReport($exception)) {
return;
}
$logs = '';
if ($request = \request()) {
$user = getCurrentInfo();
$logs .= $request->method() . ' ' . $request->uri();
$logs .= PHP_EOL . '[request_param]: ' . json_encode($request->all());
$logs .= PHP_EOL . '[timestamp]: ' . date('Y-m-d H:i:s');
$logs .= PHP_EOL . '[client_ip]: ' . $request->getRealIp();
$logs .= PHP_EOL . '[action_user]: ' . var_export($user, true);
$logs .= PHP_EOL . '[exception_handle]: ' . get_class($exception);
$logs .= PHP_EOL . '[exception_info]: ' . PHP_EOL . $exception;
}
$this->logger->error($logs);
}
public function render(Request $request, Throwable $exception): Response
{
$debug = config('app.debug', true);
$code = $exception->getCode();
$json = [
'code' => $code ? $code : 500,
'message' => $code !== 500 ? $exception->getMessage() : 'Server internal error',
'type' => 'failed'
];
if ($debug) {
$json['request_url'] = $request->method() . ' ' . $request->uri();
$json['timestamp'] = date('Y-m-d H:i:s');
$json['client_ip'] = $request->getRealIp();
$json['request_param'] = $request->all();
$json['exception_handle'] = get_class($exception);
$json['exception_info'] = [
'code' => $exception->getCode(),
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => explode("\n", $exception->getTraceAsString())
];
}
return new Response(200, ['Content-Type' => 'application/json;charset=utf-8'], json_encode($json));
}
}

View File

@@ -0,0 +1,114 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
use Webman\Route;
use support\Response;
use Tinywan\Jwt\JwtToken;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\app\cache\ConfigCache;
use plugin\saiadmin\app\cache\DictCache;
if (!function_exists('getCurrentInfo')) {
/**
* 获取当前登录用户
*/
function getCurrentInfo(): bool|array
{
if (!request()) {
return false;
}
try {
$token = JwtToken::getExtend();
} catch (\Throwable $e) {
return false;
}
return $token;
}
}
if (!function_exists('fastRoute')) {
/**
* 快速注册路由[index|save|update|read|destroy|import|export]
* @param string $name
* @param string $controller
* @return void
*/
function fastRoute(string $name, string $controller): void
{
$name = trim($name, '/');
if (method_exists($controller, 'index'))
Route::get("/$name/index", [$controller, 'index']);
if (method_exists($controller, 'save'))
Route::post("/$name/save", [$controller, 'save']);
if (method_exists($controller, 'update'))
Route::put("/$name/update", [$controller, 'update']);
if (method_exists($controller, 'read'))
Route::get("/$name/read", [$controller, 'read']);
if (method_exists($controller, 'destroy'))
Route::delete("/$name/destroy", [$controller, 'destroy']);
if (method_exists($controller, 'import'))
Route::post("/$name/import", [$controller, 'import']);
if (method_exists($controller, 'export'))
Route::post("/$name/export", [$controller, 'export']);
}
}
if (!function_exists('downloadFile')) {
/**
* 下载模板
* @param $file_name
* @return Response
*/
function downloadFile($file_name): Response
{
$base_dir = config('plugin.saiadmin.saithink.template', base_path() . '/public/template');
if (file_exists($base_dir . DIRECTORY_SEPARATOR . $file_name)) {
return response()->download($base_dir . DIRECTORY_SEPARATOR . $file_name, urlencode($file_name));
} else {
throw new ApiException('模板不存在');
}
}
}
if (!function_exists('formatBytes')) {
/**
* 根据字节计算大小
* @param $bytes
* @return string
*/
function formatBytes($bytes): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024; $i++) {
$bytes /= 1024;
}
return round($bytes, 2) . ' ' . $units[$i];
}
}
if (!function_exists('getConfigGroup')) {
/**
* 读取配置组
* @param $group
* @return array
*/
function getConfigGroup($group): array
{
return ConfigCache::getConfig($group);
}
}
if (!function_exists('dictDataList')) {
/**
* 根据字典编码获取字典列表
* @param string $code 字典编码
* @return array
*/
function dictDataList(string $code): array
{
return DictCache::getDict($code);
}
}

View File

@@ -0,0 +1,209 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use support\think\Db;
/**
* 数据表维护逻辑层
*/
class DatabaseLogic extends BaseLogic
{
/**
* 获取数据源
* @return array
*/
public function getDbSource(): array
{
$data = config('think-orm.connections');
$list = [];
foreach ($data as $k => $v) {
$list[] = $k;
}
return $list;
}
/**
* 数据列表
* @param $query
* @return mixed
*/
public function getList($query): mixed
{
$request = request();
$page = $request ? ($request->input('page') ?: 1) : 1;
$limit = $request ? ($request->input('limit') ?: 10) : 10;
return self::getTableList($query, $page, $limit);
}
/**
* 获取数据库表数据
*/
public function getTableList($query, $current_page = 1, $per_page = 10): array
{
if (!empty($query['source'])) {
if (!empty($query['name'])) {
$sql = 'show table status where name=:name ';
$list = Db::connect($query['source'])->query($sql, ['name' => $query['name']]);
} else {
$list = Db::connect($query['source'])->query('show table status');
}
} else {
if (!empty($query['name'])) {
$sql = 'show table status where name=:name ';
$list = Db::query($sql, ['name' => $query['name']]);
} else {
$list = Db::query('show table status');
}
}
$data = [];
foreach ($list as $item) {
$data[] = [
'name' => $item['Name'],
'engine' => $item['Engine'],
'rows' => $item['Rows'],
'data_free' => $item['Data_free'],
'data_length' => $item['Data_length'],
'index_length' => $item['Index_length'],
'collation' => $item['Collation'],
'create_time' => $item['Create_time'],
'update_time' => $item['Update_time'],
'comment' => $item['Comment'],
];
}
$total = count($data);
$last_page = ceil($total / $per_page);
$startIndex = ($current_page - 1) * $per_page;
$pageData = array_slice($data, $startIndex, $per_page);
return [
'data' => $pageData,
'total' => $total,
'current_page' => $current_page,
'per_page' => $per_page,
'last_page' => $last_page,
];
}
/**
* 获取列信息
*/
public function getColumnList($table, $source): array
{
$columnList = [];
if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) {
if (!empty($source)) {
$list = Db::connect($source)->query('SHOW FULL COLUMNS FROM `' . $table . '`');
} else {
$list = Db::query('SHOW FULL COLUMNS FROM `' . $table . '`');
}
foreach ($list as $column) {
preg_match('/^\w+/', $column['Type'], $matches);
$columnList[] = [
'column_key' => $column['Key'],
'column_name' => $column['Field'],
'column_type' => $matches[0],
'column_comment' => trim(preg_replace("/\([^()]*\)/", "", $column['Comment'])),
'extra' => $column['Extra'],
'default_value' => $column['Default'],
'is_nullable' => $column['Null'],
];
}
}
return $columnList;
}
/**
* 优化表
*/
public function optimizeTable($tables)
{
foreach ($tables as $table) {
if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) {
Db::execute('OPTIMIZE TABLE `' . $table . '`');
}
}
}
/**
* 清理表碎片
*/
public function fragmentTable($tables)
{
foreach ($tables as $table) {
if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) {
Db::execute('ANALYZE TABLE `' . $table . '`');
}
}
}
/**
* 获取回收站数据
*/
public function recycleData($table)
{
if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) {
// 查询表字段
$sql = 'SHOW COLUMNS FROM `' . $table . '` where Field = "delete_time"';
$columns = Db::query($sql);
$isDeleteTime = false;
if (count($columns) > 0) {
$isDeleteTime = true;
}
if (!$isDeleteTime) {
throw new ApiException('当前表不支持回收站功能');
}
// 查询软删除数据
$request = request();
$limit = $request ? ($request->input('limit') ?: 10) : 10;
return Db::table($table)->whereNotNull('delete_time')
->order('delete_time', 'desc')
->paginate($limit)
->toArray();
} else {
return [];
}
}
/**
* 删除数据
* @param $table
* @param $ids
* @return bool
*/
public function delete($table, $ids)
{
if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) {
$count = Db::table($table)->whereIn('id', $ids)->delete($ids);
return $count > 0;
} else {
return false;
}
}
/**
* 恢复数据
* @param $table
* @param $ids
* @return bool
*/
public function recovery($table, $ids)
{
if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) {
$count = Db::table($table)
->where('id', 'in', $ids)
->update(['delete_time' => null]);
return $count > 0;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,199 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use Exception;
use plugin\saiadmin\app\model\system\SystemAttachment;
use plugin\saiadmin\app\model\system\SystemCategory;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\service\storage\ChunkUploadService;
use plugin\saiadmin\service\storage\UploadService;
use plugin\saiadmin\utils\Arr;
use plugin\saiadmin\utils\Helper;
/**
* 附件逻辑层
*/
class SystemAttachmentLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemAttachment();
}
/**
* @param $category_id
* @param $ids
* @return mixed
*/
public function move($category_id, $ids): mixed
{
$category = SystemCategory::where('id', $category_id)->findOrEmpty();
if ($category->isEmpty()) {
throw new ApiException('目标分类不存在');
}
return $this->model->whereIn('id', $ids)->update(['category_id' => $category_id]);
}
/**
* 保存网络图片
* @param $url
* @param $config
* @return array
* @throws ApiException|Exception
*/
public function saveNetworkImage($url, $config): array
{
$image_data = file_get_contents($url);
if ($image_data === false) {
throw new ApiException('获取文件资源失败');
}
$image_resource = imagecreatefromstring($image_data);
if (!$image_resource) {
throw new ApiException('创建图片资源失败');
}
$filename = basename($url);
$file_extension = pathinfo($filename, PATHINFO_EXTENSION);
$full_dir = runtime_path() . '/resource/';
if (!is_dir($full_dir)) {
mkdir($full_dir, 0777, true);
}
$save_path = $full_dir . $filename;
$mime_type = 'image/';
switch ($file_extension) {
case 'jpg':
case 'jpeg':
$mime_type = 'image/jpeg';
$result = imagejpeg($image_resource, $save_path);
break;
case 'png':
$mime_type = 'image/png';
$result = imagepng($image_resource, $save_path);
break;
case 'gif':
$mime_type = 'image/gif';
$result = imagegif($image_resource, $save_path);
break;
default:
imagedestroy($image_resource);
throw new ApiException('文件格式错误');
}
imagedestroy($image_resource);
if (!$result) {
throw new ApiException('文件保存失败');
}
$hash = md5_file($save_path);
$size = filesize($save_path);
$model = $this->model->where('hash', $hash)->find();
if ($model) {
unlink($save_path);
return $model->toArray();
} else {
$logic = new SystemConfigLogic();
$uploadConfig = $logic->getGroup('upload_config');
$root = Arr::getConfigValue($uploadConfig, 'local_root');
$folder = date('Ymd');
$full_dir = base_path() . DIRECTORY_SEPARATOR . $root . $folder . DIRECTORY_SEPARATOR;
if (!is_dir($full_dir)) {
mkdir($full_dir, 0777, true);
}
$object_name = bin2hex(pack('Nn', time(), random_int(1, 65535))) . ".$file_extension";
$newPath = $full_dir . $object_name;
copy($save_path, $newPath);
unlink($save_path);
$domain = Arr::getConfigValue($uploadConfig, 'local_domain');
$uri = Arr::getConfigValue($uploadConfig, 'local_uri');
$baseUrl = $domain . $uri . $folder . '/';
$info['storage_mode'] = 1;
$info['category_id'] = request()->input('category_id', 1);
$info['origin_name'] = $filename;
$info['object_name'] = $object_name;
$info['hash'] = $hash;
$info['mime_type'] = $mime_type;
$info['storage_path'] = $root . $folder . '/' . $object_name;
$info['suffix'] = $file_extension;
$info['size_byte'] = $size;
$info['size_info'] = formatBytes($size);
$info['url'] = $baseUrl . $object_name;
$this->model->save($info);
return $info;
}
}
/**
* 文件上传
* @param string $upload
* @param bool $local
* @return array
*/
public function uploadBase(string $upload = 'image', bool $local = false): array
{
$logic = new SystemConfigLogic();
$uploadConfig = $logic->getGroup('upload_config');
$type = Arr::getConfigValue($uploadConfig, 'upload_mode');
if ($local === true) {
$type = 1;
}
$result = UploadService::disk($type, $upload)->uploadFile();
$data = $result[0];
$hash = $data['unique_id'];
$hash_check = config('plugin.saiadmin.saithink.file_hash', false);
if ($hash_check) {
$model = $this->model->where('hash', $hash)->findOrEmpty();
if (!$model->isEmpty()) {
return $model->toArray();
}
}
$url = str_replace('\\', '/', $data['url']);
$savePath = str_replace('\\', '/', $data['save_path']);
$info['storage_mode'] = $type;
$info['category_id'] = request()->input('category_id', 1);
$info['origin_name'] = $data['origin_name'];
$info['object_name'] = $data['save_name'];
$info['hash'] = $data['unique_id'];
$info['mime_type'] = $data['mime_type'];
$info['storage_path'] = $savePath;
$info['suffix'] = $data['extension'];
$info['size_byte'] = $data['size'];
$info['size_info'] = formatBytes($data['size']);
$info['url'] = $url;
$this->model->save($info);
return $info;
}
/**
* 切片上传
* @param $data
* @return array
*/
public function chunkUpload($data): array
{
$chunkService = new ChunkUploadService();
if ($data['index'] == 0) {
$model = $this->model->where('hash', $data['hash'])->findOrEmpty();
if (!$model->isEmpty()) {
return $model->toArray();
} else {
return $chunkService->checkChunk($data);
}
} else {
return $chunkService->uploadChunk($data);
}
}
}

View File

@@ -0,0 +1,101 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\app\model\system\SystemCategory;
use plugin\saiadmin\utils\Helper;
use plugin\saiadmin\utils\Arr;
/**
* 附件分类逻辑层
*/
class SystemCategoryLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemCategory();
}
/**
* 添加数据
*/
public function add($data): bool
{
$data = $this->handleData($data);
return $this->model->save($data);
}
/**
* 修改数据
*/
public function edit($id, $data): bool
{
$data = $this->handleData($data);
if ($data['parent_id'] == $id) {
throw new ApiException('上级分类和当前分类不能相同');
}
if (in_array($id, explode(',', $data['level']))) {
throw new ApiException('不能将上级分类设置为当前分类的子分类');
}
$model = $this->model->findOrEmpty($id);
if ($model->isEmpty()) {
throw new ApiException('数据不存在');
}
return $model->save($data);
}
/**
* 数据删除
*/
public function destroy($ids): bool
{
$num = $this->model->where('parent_id', 'in', $ids)->count();
if ($num > 0) {
throw new ApiException('该部门下存在子分类,请先删除子分类');
} else {
return $this->model->destroy($ids);
}
}
/**
* 数据处理
*/
protected function handleData($data)
{
if (empty($data['parent_id']) || $data['parent_id'] == 0) {
$data['level'] = '0';
$data['parent_id'] = 0;
} else {
$parentMenu = SystemCategory::findOrEmpty($data['parent_id']);
$data['level'] = $parentMenu['level'] . $parentMenu['id'] . ',';
}
return $data;
}
/**
* 数据树形化
* @param array $where
* @return array
*/
public function tree(array $where = []): array
{
$query = $this->search($where);
$request = request();
if ($request && $request->input('tree', 'false') === 'true') {
$query->field('id, id as value, category_name as label, parent_id, category_name, sort');
}
$query->order('sort', 'desc');
$data = $this->getAll($query);
return Helper::makeTree($data);
}
}

View File

@@ -0,0 +1,57 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\app\cache\ConfigCache;
use plugin\saiadmin\app\model\system\SystemConfigGroup;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\app\model\system\SystemConfig;
use support\think\Db;
/**
* 参数配置分组逻辑层
*/
class SystemConfigGroupLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemConfigGroup();
}
/**
* 删除配置信息
*/
public function destroy($ids): bool
{
$id = $ids[0];
$model = $this->model->where('id', $id)->findOrEmpty();
if ($model->isEmpty()) {
throw new ApiException('配置数据未找到');
}
if (in_array(intval($id), [1, 2, 3])) {
throw new ApiException('系统默认分组,无法删除');
}
Db::startTrans();
try {
// 删除配置组
$model->delete();
// 删除配置组数据
$typeIds = SystemConfig::where('group_id', $id)->column('id');
SystemConfig::destroy($typeIds);
ConfigCache::clearConfig($model->code);
Db::commit();
return true;
} catch (\Exception $e) {
Db::rollback();
throw new ApiException('删除数据异常,请检查');
}
}
}

View File

@@ -0,0 +1,107 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\app\cache\ConfigCache;
use plugin\saiadmin\app\model\system\SystemConfig;
use plugin\saiadmin\app\model\system\SystemConfigGroup;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Helper;
/**
* 参数配置逻辑层
*/
class SystemConfigLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemConfig();
}
/**
* 添加数据
* @param mixed $data
* @return mixed
*/
public function add($data): mixed
{
$result = $this->model->create($data);
$group = SystemConfigGroup::find($data['group_id']);
ConfigCache::clearConfig($group->code);
return $result;
}
/**
* 编辑数据
* @param mixed $id
* @param mixed $data
* @return bool
*/
public function edit($id, $data): bool
{
$result = parent::edit($id, $data);
$group = SystemConfigGroup::find($data['group_id']);
ConfigCache::clearConfig($group->code);
return $result;
}
/**
* 批量更新
* @param mixed $group_id
* @param mixed $config
* @return bool
*/
public function batchUpdate($group_id, $config): bool
{
$group = SystemConfigGroup::find($group_id);
if (!$group) {
throw new ApiException('配置组未找到');
}
$saveData = [];
foreach ($config as $key => $value) {
$saveData[] = [
'id' => $value['id'],
'group_id' => $group_id,
'name' => $value['name'],
'key' => $value['key'],
'value' => $value['value']
];
}
// upsert: 根据 id 更新,如果不存在则插入
$this->model->saveAll($saveData);
ConfigCache::clearConfig($group->code);
return true;
}
/**
* 获取配置数据
* @param mixed $code
* @return array
*/
public function getData($code): array
{
$group = SystemConfigGroup::where('code', $code)->findOrEmpty();
if (empty($group)) {
return [];
}
$config = SystemConfig::where('group_id', $group['id'])->select()->toArray();
return $config;
}
/**
* 获取配置组
*/
public function getGroup($config): array
{
return ConfigCache::getConfig($config);
}
}

View File

@@ -0,0 +1,127 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\app\model\system\SystemDept;
use plugin\saiadmin\app\model\system\SystemUser;
use plugin\saiadmin\utils\Helper;
use plugin\saiadmin\utils\Arr;
/**
* 部门逻辑层
*/
class SystemDeptLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemDept();
}
/**
* 添加数据
*/
public function add($data): mixed
{
$data = $this->handleData($data);
$this->model->save($data);
return $this->model->getKey();
}
/**
* 修改数据
*/
public function edit($id, $data): mixed
{
$oldLevel = $data['level'] . $id . ',';
$data = $this->handleData($data);
if ($data['parent_id'] == $id) {
throw new ApiException('上级部门和当前部门不能相同');
}
if (in_array($id, explode(',', $data['level']))) {
throw new ApiException('不能将上级部门设置为当前部门的子部门');
}
$newLevel = $data['level'] . $id . ',';
$deptIds = $this->model->where('level', 'like', $oldLevel . '%')->column('id');
return $this->transaction(function () use ($deptIds, $oldLevel, $newLevel, $data, $id) {
$this->model->whereIn('id', $deptIds)->exp('level', "REPLACE(level, '$oldLevel', '$newLevel')")->update([]);
return $this->model->update($data, ['id' => $id]);
});
}
/**
* 数据删除
*/
public function destroy($ids): bool
{
$num = $this->model->where('parent_id', 'in', $ids)->count();
if ($num > 0) {
throw new ApiException('该部门下存在子部门,请先删除子部门');
} else {
$count = SystemUser::where('dept_id', 'in', $ids)->count();
if ($count > 0) {
throw new ApiException('该部门下存在用户,请先删除或者转移用户');
}
return $this->model->destroy($ids);
}
}
/**
* 数据处理
*/
protected function handleData($data)
{
// 处理上级部门
if (empty($data['parent_id']) || $data['parent_id'] == 0) {
$data['level'] = '0';
$data['parent_id'] = 0;
} else {
$parentMenu = SystemDept::findOrEmpty($data['parent_id']);
$data['level'] = $parentMenu['level'] . $parentMenu['id'] . ',';
}
return $data;
}
/**
* 数据树形化
* @param array $where
* @return array
*/
public function tree(array $where = []): array
{
$query = $this->search($where);
$request = request();
if ($request && $request->input('tree', 'false') === 'true') {
$query->field('id, id as value, name as label, parent_id');
}
$query->order('sort', 'desc');
$query->with(['leader']);
$data = $this->getAll($query);
return Helper::makeTree($data);
}
/**
* 可操作部门
* @param array $where
* @return array
*/
public function accessDept(array $where = []): array
{
$query = $this->search($where);
$query->auth($this->adminInfo['deptList']);
$query->field('id, id as value, name as label, parent_id');
$query->order('sort', 'desc');
$data = $this->getAll($query);
return Helper::makeTree($data);
}
}

View File

@@ -0,0 +1,46 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\app\model\system\SystemDictData;
use plugin\saiadmin\app\model\system\SystemDictType;
use plugin\saiadmin\app\cache\DictCache;
use plugin\saiadmin\utils\Helper;
/**
* 字典类型逻辑层
*/
class SystemDictDataLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemDictData();
}
/**
* 添加数据
* @param $data
* @return mixed
*/
public function add($data): mixed
{
$type = SystemDictType::where('id', $data['type_id'])->findOrEmpty();
if ($type->isEmpty()) {
throw new ApiException('字典类型不存在');
}
$data['code'] = $type->code;
$model = $this->model->create($data);
DictCache::clear();
return $model->getKey();
}
}

View File

@@ -0,0 +1,116 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\app\model\system\SystemDictType;
use plugin\saiadmin\app\model\system\SystemDictData;
use support\think\Db;
/**
* 字典类型逻辑层
*/
class SystemDictTypeLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemDictType();
}
/**
* 添加数据
*/
public function add($data): mixed
{
$model = $this->model->where('code', $data['code'])->findOrEmpty();
if (!$model->isEmpty()) {
throw new ApiException('该字典标识已存在');
}
return $this->model->save($data);
}
/**
* 数据更新
*/
public function edit($id, $data): mixed
{
Db::startTrans();
try {
// 修改数据字典类型
$result = $this->model->update($data, ['id' => $id]);
// 更新数据字典数据
SystemDictData::update(['code' => $data['code']], ['type_id' => $id]);
Db::commit();
return $result;
} catch (\Exception $e) {
Db::rollback();
throw new ApiException('修改数据异常,请检查');
}
}
/**
* 数据删除
*/
public function destroy($ids): bool
{
Db::startTrans();
try {
// 删除数据字典类型
$result = $this->model->destroy($ids);
// 删除数据字典数据
$typeIds = SystemDictData::where('type_id', 'in', $ids)->column('id');
SystemDictData::destroy($typeIds);
Db::commit();
return $result;
} catch (\Exception $e) {
Db::rollback();
throw new ApiException('删除数据异常,请检查');
}
}
/**
* 获取全部字典
* @return array
*/
public function getDictAll(): array
{
$data = $this->model->where('status', 1)->field('id, name, code, remark')
->with([
'dicts' => function ($query) {
$query->where('status', 1)->field('id, type_id, label, value, color, code, sort')->order('sort', 'desc');
}
])->select()->toArray();
return $this->packageDict($data, 'code');
}
/**
* 组合数据
* @param $array
* @param $field
* @return array
*/
private function packageDict($array, $field): array
{
$result = [];
foreach ($array as $item) {
if (isset($item[$field])) {
if (isset($result[$item[$field]])) {
$result[$item[$field]] = [($result[$item[$field]])];
$result[$item[$field]][] = $item['dicts'];
} else {
$result[$item[$field]] = $item['dicts'];
}
}
}
return $result;
}
}

View File

@@ -0,0 +1,88 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\app\model\system\SystemLoginLog;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\utils\Helper;
use support\think\Db;
/**
* 登录日志逻辑层
*/
class SystemLoginLogLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemLoginLog();
}
/**
* 登录统计图表
* @return array
*/
public function loginChart(): array
{
$sql = "
SELECT
d.date AS login_date,
COUNT(l.login_time) AS login_count
FROM
(SELECT CURDATE() - INTERVAL (a.N) DAY AS date
FROM (SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a
) d
LEFT JOIN sa_system_login_log l
ON DATE(l.login_time) = d.date
GROUP BY d.date
ORDER BY d.date ASC;
";
$data = Db::query($sql);
return [
'login_count' => array_column($data, 'login_count'),
'login_date' => array_column($data, 'login_date'),
];
}
/**
* 登录统计图表
* @return array
*/
public function loginBarChart(): array
{
$sql = "
SELECT
-- 拼接成 YYYY-MM 格式,例如 2023-01
CONCAT(LPAD(m.month_num, 2, '0'), '月') AS login_month,
COUNT(l.login_time) AS login_count
FROM
-- 生成 1 到 12 的月份数字
(SELECT 1 AS month_num UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12) m
LEFT JOIN sa_system_login_log l
-- 关联条件:年份等于今年 且 月份等于生成的数字
ON YEAR(l.login_time) = YEAR(CURDATE())
AND MONTH(l.login_time) = m.month_num
GROUP BY
m.month_num
ORDER BY
m.month_num ASC;
";
$data = Db::query($sql);
return [
'login_count' => array_column($data, 'login_count'),
'login_month' => array_column($data, 'login_month'),
];
}
}

View File

@@ -0,0 +1,26 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\app\model\system\SystemMail;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\utils\Helper;
/**
* 邮件模型逻辑层
*/
class SystemMailLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemMail();
}
}

View File

@@ -0,0 +1,189 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\app\model\system\SystemMenu;
use plugin\saiadmin\app\model\system\SystemRoleMenu;
use plugin\saiadmin\app\model\system\SystemUserRole;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Arr;
use plugin\saiadmin\utils\Helper;
/**
* 菜单逻辑层
*/
class SystemMenuLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemMenu();
}
/**
* 数据添加
*/
public function add($data): mixed
{
$data = $this->handleData($data);
return $this->model->save($data);
}
/**
* 数据修改
*/
public function edit($id, $data): mixed
{
$data = $this->handleData($data);
if ($data['parent_id'] == $id) {
throw new ApiException('不能设置父级为自身');
}
return $this->model->update($data, ['id' => $id]);
}
/**
* 数据删除
*/
public function destroy($ids): bool
{
$num = $this->model->where('parent_id', 'in', $ids)->count();
if ($num > 0) {
throw new ApiException('该菜单下存在子菜单,请先删除子菜单');
} else {
return $this->model->destroy($ids);
}
}
/**
* 数据处理
*/
protected function handleData($data)
{
// 处理上级菜单
if (empty($data['parent_id']) || $data['parent_id'] == 0) {
$data['level'] = '0';
$data['parent_id'] = 0;
} else {
$parentMenu = $this->model->findOrEmpty($data['parent_id']);
$data['level'] = $parentMenu['level'] . $parentMenu['id'] . ',';
}
return $data;
}
/**
* 数据树形化
* @param $where
* @return array
*/
public function tree($where = []): array
{
$query = $this->search($where);
$request = request();
if ($request && $request->input('tree', 'false') === 'true') {
$query->field('id, id as value, name as label, parent_id, type');
}
$query->order('sort', 'desc');
$data = $this->getAll($query);
return Helper::makeTree($data);
}
/**
* 权限菜单
* @return array
*/
public function auth(): array
{
$roleLogic = new SystemRoleLogic();
$role_ids = Arr::getArrayColumn($this->adminInfo['roleList'], 'id');
$roles = $roleLogic->getMenuIdsByRoleIds($role_ids);
$ids = $this->filterMenuIds($roles);
$query = $this->model
->field('id, id as value, name as label, parent_id, type')
->where('status', 1)
->where('id', 'in', $ids)
->order('sort', 'desc');
$data = $this->getAll($query);
return Helper::makeTree($data);
}
/**
* 获取全部菜单
*/
public function getAllMenus(): array
{
$query = $this->search(['status' => 1, 'type' => [1, 2, 4]])->order('sort', 'desc');
$data = $this->getAll($query);
return Helper::makeArtdMenus($data);
}
/**
* 获取全部权限
* @return array
*/
public function getAllAuth(): array
{
return SystemMenu::where('type', 3)
->where('status', 1)
->column('slug');
}
/**
* 根据角色获取权限
* @param $roleIds
* @return array
*/
public function getAuthByRole($roleIds): array
{
$menuId = SystemRoleMenu::whereIn('role_id', $roleIds)->column('menu_id');
return SystemMenu::distinct(true)
->where('type', 3)
->where('status', 1)
->where('id', 'in', array_unique($menuId))
->column('slug');
}
/**
* 根据角色获取菜单
* @param $roleIds
* @return array
*/
public function getMenuByRole($roleIds): array
{
$menuId = SystemRoleMenu::whereIn('role_id', $roleIds)->column('menu_id');
$data = SystemMenu::distinct(true)
->where('status', 1)
->where('type', 'in', [1, 2, 4])
->where('id', 'in', array_unique($menuId))
->order('sort', 'desc')
->select()
->toArray();
return Helper::makeArtdMenus($data);
}
/**
* 过滤通过角色查询出来的菜单id列表并去重
* @param array $roleData
* @return array
*/
public function filterMenuIds(array &$roleData): array
{
$ids = [];
foreach ($roleData as $val) {
foreach ($val['menus'] as $menu) {
$ids[] = $menu['id'];
}
}
unset($roleData);
return array_unique($ids);
}
}

View File

@@ -0,0 +1,38 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\app\model\system\SystemOperLog;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\utils\Helper;
/**
* 操作日志逻辑层
*/
class SystemOperLogLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemOperLog();
}
/**
* 获取自己的操作日志
* @param mixed $where
* @return array
*/
public function getOwnOperLogList($where): array
{
$query = $this->search($where);
$query->field('id, username, method, router, service_name, ip, ip_location, create_time');
return $this->getList($query);
}
}

View File

@@ -0,0 +1,95 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\app\model\system\SystemPost;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\service\OpenSpoutWriter;
use OpenSpout\Reader\XLSX\Reader;
/**
* 岗位管理逻辑层
*/
class SystemPostLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemPost();
}
/**
* 可操作岗位
* @param array $where
* @return array
*/
public function accessPost(array $where = []): array
{
$query = $this->search($where);
$query->field('id, id as value, name as label, name, code');
return $this->getAll($query);
}
/**
* 导入数据
*/
public function import($file)
{
$path = $this->getImport($file);
$reader = new Reader();
try {
$reader->open($path);
$data = [];
foreach ($reader->getSheetIterator() as $sheet) {
$isHeader = true;
foreach ($sheet->getRowIterator() as $row) {
if ($isHeader) {
$isHeader = false;
continue;
}
$cells = $row->getCells();
$data[] = [
'name' => $cells[0]->getValue(),
'code' => $cells[1]->getValue(),
'sort' => $cells[2]->getValue(),
'status' => $cells[3]->getValue(),
];
}
}
$this->saveAll($data);
} catch (\Exception $e) {
throw new ApiException('导入文件错误请上传正确的文件格式xlsx');
}
}
/**
* 导出数据
*/
public function export($where = [])
{
$query = $this->search($where)->field('id,name,code,sort,status,create_time');
$data = $this->getAll($query);
$file_name = '岗位数据.xlsx';
$header = ['编号', '岗位名称', '岗位标识', '排序', '状态', '创建时间'];
$filter = [
'status' => [
['value' => 1, 'label' => '正常'],
['value' => 2, 'label' => '禁用']
]
];
$writer = new OpenSpoutWriter($file_name);
$writer->setWidth([15, 15, 20, 15, 15, 25]);
$writer->setHeader($header);
$writer->setData($data, null, $filter);
$file_path = $writer->returnFile();
return response()->download($file_path, urlencode($file_name));
}
}

View File

@@ -0,0 +1,156 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\app\cache\UserMenuCache;
use plugin\saiadmin\app\model\system\SystemRole;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Helper;
use support\think\Cache;
use support\think\Db;
/**
* 角色逻辑层
*/
class SystemRoleLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemRole();
}
/**
* 添加数据
*/
public function add($data): bool
{
$data = $this->handleData($data);
return $this->model->save($data);
}
/**
* 修改数据
*/
public function edit($id, $data): bool
{
$model = $this->model->findOrEmpty($id);
if ($model->isEmpty()) {
throw new ApiException('数据不存在');
}
$data = $this->handleData($data);
return $model->save($data);
}
/**
* 删除数据
*/
public function destroy($ids): bool
{
// 越权保护
$levelArr = array_column($this->adminInfo['roleList'], 'level');
$maxLevel = max($levelArr);
$num = SystemRole::where('level', '>=', $maxLevel)->whereIn('id', $ids)->count();
if ($num > 0) {
throw new ApiException('不能操作比当前账户职级高的角色');
} else {
return $this->model->destroy($ids);
}
}
/**
* 数据处理
*/
protected function handleData($data)
{
// 越权保护
$levelArr = array_column($this->adminInfo['roleList'], 'level');
$maxLevel = max($levelArr);
if ($data['level'] >= $maxLevel) {
throw new ApiException('不能操作比当前账户职级高的角色');
}
return $data;
}
/**
* 可操作角色
* @param array $where
* @return array
*/
public function accessRole(array $where = []): array
{
$query = $this->search($where);
// 越权保护
$levelArr = array_column($this->adminInfo['roleList'], 'level');
$maxLevel = max($levelArr);
$query->where('level', '<', $maxLevel);
$query->order('sort', 'desc');
return $this->getAll($query);
}
/**
* 根据角色数组获取菜单
* @param $ids
* @return array
*/
public function getMenuIdsByRoleIds($ids): array
{
if (empty($ids))
return [];
return $this->model->where('id', 'in', $ids)->with([
'menus' => function ($query) {
$query->where('status', 1)->order('sort', 'desc');
}
])->select()->toArray();
}
/**
* 根据角色获取菜单
* @param $id
* @return array
*/
public function getMenuByRole($id): array
{
$role = $this->model->findOrEmpty($id);
$menus = $role->menus ?: [];
return [
'id' => $id,
'menus' => $menus
];
}
/**
* 保存菜单权限
* @param $id
* @param $menu_ids
* @return mixed
*/
public function saveMenuPermission($id, $menu_ids): mixed
{
return $this->transaction(function () use ($id, $menu_ids) {
$role = $this->model->findOrEmpty($id);
if ($role) {
$role->menus()->detach();
$data = array_map(function ($menu_id) use ($id) {
return ['menu_id' => $menu_id, 'role_id' => $id];
}, $menu_ids);
Db::name('sa_system_role_menu')->limit(100)->insertAll($data);
}
$cache = config('plugin.saiadmin.saithink.button_cache');
$tag = $cache['role'] . $id;
Cache::tag($tag)->clear(); // 清理权限缓存-角色TAG
UserMenuCache::clearMenuCache(); // 清理菜单缓存
return true;
});
}
}

View File

@@ -0,0 +1,336 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\system;
use plugin\saiadmin\app\cache\UserAuthCache;
use plugin\saiadmin\app\cache\UserInfoCache;
use plugin\saiadmin\app\cache\UserMenuCache;
use plugin\saiadmin\app\model\system\SystemDept;
use plugin\saiadmin\app\model\system\SystemRole;
use plugin\saiadmin\app\model\system\SystemUser;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\basic\think\BaseLogic;
use Webman\Event\Event;
use Tinywan\Jwt\JwtToken;
/**
* 用户信息逻辑层
*/
class SystemUserLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new SystemUser();
}
/**
* 分页数据列表
* @param mixed $where
* @return array
*/
public function indexList($where): array
{
$query = $this->search($where);
$query->with(['depts']);
$query->auth($this->adminInfo['deptList']);
return $this->getList($query);
}
/**
* 用户列表数据
* @param mixed $where
* @return array
*/
public function openUserList($where): array
{
$query = $this->search($where);
$query->field('id, username, realname, avatar, phone, email');
return $this->getList($query);
}
/**
* 读取用户信息
* @param mixed $id
* @return array
*/
public function getUser($id): array
{
$admin = $this->model->findOrEmpty($id);
$data = $admin->hidden(['password'])->toArray();
$data['roleList'] = $admin->roles->toArray() ?: [];
$data['postList'] = $admin->posts->toArray() ?: [];
$data['deptList'] = $admin->depts ? $admin->depts->toArray() : [];
return $data;
}
/**
* 读取数据
* @param $id
* @return array
*/
public function read($id): array
{
$data = $this->getUser($id);
if ($this->adminInfo['id'] > 1) {
// 部门保护
if (!$this->deptProtect($this->adminInfo['deptList'], $data['dept_id'])) {
throw new ApiException('没有权限操作该部门数据');
}
}
return $data;
}
/**
* 添加数据
* @param $data
* @return mixed
*/
public function add($data): mixed
{
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
return $this->transaction(function () use ($data) {
$role_ids = $data['role_ids'] ?? [];
$post_ids = $data['post_ids'] ?? [];
if ($this->adminInfo['id'] > 1) {
// 部门保护
if (!$this->deptProtect($this->adminInfo['deptList'], $data['dept_id'])) {
throw new ApiException('没有权限操作该部门数据');
}
// 越权保护
if (!$this->roleProtect($this->adminInfo['roleList'], $role_ids)) {
throw new ApiException('没有权限操作该角色数据');
}
}
$user = SystemUser::create($data);
$user->roles()->detach();
$user->posts()->detach();
$user->roles()->saveAll($role_ids);
if (!empty($post_ids)) {
$user->posts()->save($post_ids);
}
return $user;
});
}
/**
* 修改数据
* @param $id
* @param $data
* @return mixed
*/
public function edit($id, $data): mixed
{
unset($data['password']);
return $this->transaction(function () use ($data, $id) {
$role_ids = $data['role_ids'] ?? [];
$post_ids = $data['post_ids'] ?? [];
// 仅可修改当前部门和子部门的用户
$query = $this->model->where('id', $id);
$query->auth($this->adminInfo['deptList']);
$user = $query->findOrEmpty();
if ($user->isEmpty()) {
throw new ApiException('没有权限操作该数据');
}
if ($this->adminInfo['id'] > 1) {
// 部门保护
if (!$this->deptProtect($this->adminInfo['deptList'], $data['dept_id'])) {
throw new ApiException('没有权限操作该部门数据');
}
// 越权保护
if (!$this->roleProtect($this->adminInfo['roleList'], $role_ids)) {
throw new ApiException('没有权限操作该角色数据');
}
}
$result = parent::edit($id, $data);
if ($result) {
$user->roles()->detach();
$user->posts()->detach();
$user->roles()->saveAll($role_ids);
if (!empty($post_ids)) {
$user->posts()->save($post_ids);
}
UserInfoCache::clearUserInfo($id);
UserAuthCache::clearUserAuth($id);
UserMenuCache::clearUserMenu($id);
}
return $result;
});
}
/**
* 删除数据
* @param $ids
* @return bool
*/
public function destroy($ids): bool
{
if (is_array($ids)) {
if (count($ids) > 1) {
throw new ApiException('禁止批量删除操作');
}
$ids = $ids[0];
}
if ($ids == 1) {
throw new ApiException('超级管理员禁止删除');
}
$query = $this->model->where('id', $ids);
$query->auth($this->adminInfo['deptList']);
$user = $query->findOrEmpty();
if ($user->isEmpty()) {
throw new ApiException('没有权限操作该数据');
}
if ($this->adminInfo['id'] > 1) {
$role_ids = $user->roles->toArray() ?: [];
if (!empty($role_ids)) {
// 越权保护
if (!$this->roleProtect($this->adminInfo['roleList'], array_column($role_ids, 'id'))) {
throw new ApiException('没有权限操作该角色数据');
}
}
}
UserInfoCache::clearUserInfo($ids);
UserAuthCache::clearUserAuth($ids);
UserMenuCache::clearUserMenu($ids);
return parent::destroy($ids);
}
/**
* 用户登录
* @param string $username
* @param string $password
* @param string $type
* @return array
*/
public function login(string $username, string $password, string $type): array
{
$adminInfo = $this->model->where('username', $username)->findOrEmpty();
$status = 1;
$message = '登录成功';
if ($adminInfo->isEmpty()) {
$message = '账号或密码错误,请重新输入!';
throw new ApiException($message);
}
if ($adminInfo->status === 2) {
$status = 0;
$message = '您已被禁止登录!';
}
if (!password_verify($password, $adminInfo->password)) {
$status = 0;
$message = '账号或密码错误,请重新输入!';
}
if ($status === 0) {
// 登录事件
Event::emit('user.login', compact('username', 'status', 'message'));
throw new ApiException($message);
}
$adminInfo->login_time = date('Y-m-d H:i:s');
$adminInfo->login_ip = request()->getRealIp();
$adminInfo->save();
$access_exp = config('plugin.saiadmin.saithink.access_exp', 3 * 3600);
$token = JwtToken::generateToken([
'access_exp' => $access_exp,
'id' => $adminInfo->id,
'username' => $adminInfo->username,
'type' => $type,
'plat' => 'saiadmin',
]);
// 登录事件
$admin_id = $adminInfo->id;
Event::emit('user.login', compact('username', 'status', 'message', 'admin_id'));
return $token;
}
/**
* 更新资料
* @param mixed $id
* @param mixed $data
* @return bool
*/
public function updateInfo($id, $data): bool
{
$this->model->update($data, ['id' => $id], ['realname', 'gender', 'phone', 'email', 'avatar', 'signed']);
return true;
}
/**
* 密码修改
* @param $adminId
* @param $oldPassword
* @param $newPassword
* @return bool
*/
public function modifyPassword($adminId, $oldPassword, $newPassword): bool
{
$model = $this->model->findOrEmpty($adminId);
if (password_verify($oldPassword, $model->password)) {
$model->password = password_hash($newPassword, PASSWORD_DEFAULT);
return $model->save();
} else {
throw new ApiException('原密码错误');
}
}
/**
* 修改数据
*/
public function authEdit($id, $data)
{
if ($this->adminInfo['id'] > 1) {
// 判断用户是否可以操作
$query = SystemUser::where('id', $id);
$query->auth($this->adminInfo['deptList']);
$user = $query->findOrEmpty();
if ($user->isEmpty()) {
throw new ApiException('没有权限操作该数据');
}
}
parent::edit($id, $data);
}
/**
* 部门保护
* @param $dept
* @param $dept_id
* @return bool
*/
public function deptProtect($dept, $dept_id): bool
{
// 部门保护
$deptIds = [$dept['id']];
$deptLevel = $dept['level'] . $dept['id'] . ',';
$dept_ids = SystemDept::whereLike('level', $deptLevel . '%')->column('id');
$deptIds = array_merge($deptIds, $dept_ids);
if (!in_array($dept_id, $deptIds)) {
return false;
}
return true;
}
/**
* 越权保护
* @param $roleList
* @param $role_ids
* @return bool
*/
public function roleProtect($roleList, $role_ids): bool
{
// 越权保护
$levelArr = array_column($roleList, 'level');
$maxLevel = max($levelArr);
$currentLevel = SystemRole::whereIn('id', $role_ids)->max('level');
if ($currentLevel >= $maxLevel) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,25 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\tool;
use plugin\saiadmin\app\model\tool\CrontabLog;
use plugin\saiadmin\basic\think\BaseLogic;
/**
* 定时任务日志逻辑层
*/
class CrontabLogLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new CrontabLog();
}
}

View File

@@ -0,0 +1,247 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\tool;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Webman\Channel\Client as ChannelClient;
use plugin\saiadmin\app\model\tool\Crontab;
use plugin\saiadmin\app\model\tool\CrontabLog;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
/**
* 定时任务逻辑层
*/
class CrontabLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new Crontab();
}
/**
* 添加任务
*/
public function add($data): bool
{
$second = $data['second'];
$minute = $data['minute'];
$hour = $data['hour'];
$week = $data['week'];
$day = $data['day'];
$month = $data['month'];
// 规则处理
$rule = match ($data['task_style']) {
1 => "0 {$minute} {$hour} * * *",
2 => "0 {$minute} * * * *",
3 => "0 {$minute} */{$hour} * * *",
4 => "0 */{$minute} * * * *",
5 => "*/{$second} * * * * *",
6 => "0 {$minute} {$hour} * * {$week}",
7 => "0 {$minute} {$hour} {$day} * *",
8 => "0 {$minute} {$hour} {$day} {$month} *",
default => throw new ApiException("任务类型异常"),
};
// 定时任务模型新增
$model = Crontab::create([
'name' => $data['name'],
'type' => $data['type'],
'task_style' => $data['task_style'],
'rule' => $rule,
'target' => $data['target'],
'parameter' => $data['parameter'],
'status' => $data['status'],
'remark' => $data['remark'],
]);
$id = $model->getKey();
// 连接到Channel服务
ChannelClient::connect();
ChannelClient::publish('crontab', ['args' => $id]);
return true;
}
/**
* 修改任务
*/
public function edit($id, $data): bool
{
$second = $data['second'];
$minute = $data['minute'];
$hour = $data['hour'];
$week = $data['week'];
$day = $data['day'];
$month = $data['month'];
// 规则处理
$rule = match ($data['task_style']) {
1 => "0 {$minute} {$hour} * * *",
2 => "0 {$minute} * * * *",
3 => "0 {$minute} */{$hour} * * *",
4 => "0 */{$minute} * * * *",
5 => "*/{$second} * * * * *",
6 => "0 {$minute} {$hour} * * {$week}",
7 => "0 {$minute} {$hour} {$day} * *",
8 => "0 {$minute} {$hour} {$day} {$month} *",
default => throw new ApiException("任务类型异常"),
};
// 查询任务数据
$model = $this->model->findOrEmpty($id);
if ($model->isEmpty()) {
throw new ApiException('数据不存在');
}
$result = $model->save([
'name' => $data['name'],
'type' => $data['type'],
'task_style' => $data['task_style'],
'rule' => $rule,
'target' => $data['target'],
'parameter' => $data['parameter'],
'status' => $data['status'],
'remark' => $data['remark'],
]);
if ($result) {
// 连接到Channel服务
ChannelClient::connect();
ChannelClient::publish('crontab', ['args' => $id]);
}
// 修改任务数据
return $result;
}
/**
* 删除定时任务
* @param $ids
* @return bool
* @throws Exception
*/
public function destroy($ids): bool
{
if (is_array($ids)) {
if (count($ids) > 1) {
throw new ApiException('禁止批量删除操作');
}
$ids = $ids[0];
}
$result = parent::destroy($ids);
if ($result) {
// 连接到Channel服务
ChannelClient::connect();
ChannelClient::publish('crontab', ['args' => $ids]);
}
return $result;
}
/**
* 修改状态
* @param $id
* @param $status
* @return bool
*/
public function changeStatus($id, $status): bool
{
$model = $this->model->findOrEmpty($id);
if ($model->isEmpty()) {
throw new ApiException('数据不存在');
}
$result = $model->save(['status' => $status]);
if ($result) {
// 连接到Channel服务
ChannelClient::connect();
ChannelClient::publish('crontab', ['args' => $id]);
}
return $result;
}
/**
* 执行定时任务
* @param $id
* @return bool
*/
public function run($id): bool
{
$info = $this->model->where('status', 1)->findOrEmpty($id);
if ($info->isEmpty()) {
return false;
}
$data['crontab_id'] = $info->id;
$data['name'] = $info->name;
$data['target'] = $info->target;
$data['parameter'] = $info->parameter;
switch ($info->type) {
case 1:
// URL任务GET
$httpClient = new Client([
'timeout' => 5,
'verify' => false,
]);
try {
$httpClient->request('GET', $info->target);
$data['status'] = 1;
CrontabLog::create($data);
return true;
} catch (GuzzleException $e) {
$data['status'] = 2;
$data['exception_info'] = $e->getMessage();
CrontabLog::create($data);
return false;
}
case 2:
// URL任务POST
$httpClient = new Client([
'timeout' => 5,
'verify' => false,
]);
try {
$res = $httpClient->request('POST', $info->target, [
'form_params' => json_decode($info->parameter ?? '', true)
]);
$data['status'] = 1;
$data['exception_info'] = $res->getBody();
CrontabLog::create($data);
return true;
} catch (GuzzleException $e) {
$data['status'] = 2;
$data['exception_info'] = $e->getMessage();
CrontabLog::create($data);
return false;
}
case 3:
// 类任务
$class_name = $info->target;
$method_name = 'run';
$class = new $class_name;
if (method_exists($class, $method_name)) {
$return = $class->$method_name($info->parameter);
$data['status'] = 1;
$data['exception_info'] = $return;
CrontabLog::create($data);
return true;
} else {
$data['status'] = 2;
$data['exception_info'] = '类:' . $class_name . ',方法:run,未找到';
CrontabLog::create($data);
return false;
}
default:
return false;
}
}
}

View File

@@ -0,0 +1,213 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\tool;
use plugin\saiadmin\app\model\tool\GenerateColumns;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\utils\Helper;
/**
* 代码生成业务字段逻辑层
*/
class GenerateColumnsLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new GenerateColumns();
}
public function saveExtra($data)
{
$default_column = ['create_time', 'update_time', 'created_by', 'updated_by', 'delete_time', 'remark'];
// 组装数据
foreach ($data as $k => $item) {
if ($item['column_name'] == 'delete_time') {
continue;
}
$column = [
'table_id' => $item['table_id'],
'column_name' => $item['column_name'],
'column_comment' => $item['column_comment'],
'column_type' => $item['column_type'],
'default_value' => $item['default_value'],
'is_pk' => ($item['column_key'] == 'PRI') ? 2 : 1,
'is_required' => $item['is_nullable'] == 'NO' ? 2 : 1,
'query_type' => 'eq',
'view_type' => 'input',
'sort' => count($data) - $k,
'options' => $item['options'] ?? null
];
// 设置默认选项
if (!in_array($item['column_name'], $default_column) && empty($item['column_key'])) {
$column = array_merge(
$column,
[
'is_insert' => 2,
'is_edit' => 2,
'is_list' => 2,
'is_query' => 1,
'is_sort' => 1,
]
);
}
$keyList = [
'column_comment',
'column_type',
'default_value',
'is_pk',
'is_required',
'is_insert',
'is_edit',
'is_list',
'is_query',
'is_sort',
'query_type',
'view_type',
'dict_type',
'options',
'sort',
'is_cover'
];
foreach ($keyList as $key) {
if (isset($item[$key]))
$column[$key] = $item[$key];
}
GenerateColumns::create($this->fieldDispose($column));
}
}
public function update($data, $where)
{
$data['is_insert'] = $data['is_insert'] ? 2 : 1;
$data['is_edit'] = $data['is_edit'] ? 2 : 1;
$data['is_list'] = $data['is_list'] ? 2 : 1;
$data['is_query'] = $data['is_query'] ? 2 : 1;
$data['is_sort'] = $data['is_sort'] ? 2 : 1;
$data['is_required'] = $data['is_required'] ? 2 : 1;
$this->model->update($data, $where);
}
private function fieldDispose(array $column): array
{
$object = new class {
public function viewTypeDispose(&$column): void
{
switch ($column['column_type']) {
case 'varchar':
$column['view_type'] = 'input';
break;
// 富文本
case 'text':
case 'longtext':
$column['is_list'] = 1;
$column['is_query'] = 1;
$column['view_type'] = 'editor';
$options = [
'height' => 400,
];
$column['options'] = $options;
break;
// 日期字段
case 'datetime':
$column['view_type'] = 'date';
$options = [
'mode' => 'datetime'
];
$column['options'] = $options;
$column['query_type'] = 'between';
break;
case 'date':
$column['view_type'] = 'date';
$options = [
'mode' => 'date'
];
$column['options'] = $options;
$column['query_type'] = 'between';
break;
}
}
public function columnName(&$column): void
{
if (stristr($column['column_name'], 'name')) {
$column['is_query'] = 2;
$column['is_required'] = 2;
$column['query_type'] = 'like';
}
if (stristr($column['column_name'], 'title')) {
$column['is_query'] = 2;
$column['is_required'] = 2;
$column['query_type'] = 'like';
}
if (stristr($column['column_name'], 'type')) {
$column['is_query'] = 2;
$column['is_required'] = 2;
$column['query_type'] = 'eq';
}
if (stristr($column['column_name'], 'image')) {
$column['is_query'] = 1;
$column['view_type'] = 'uploadImage';
$options = [
'multiple' => false,
'limit' => 1,
];
$column['options'] = $options;
}
if (stristr($column['column_name'], 'file')) {
$column['is_query'] = 1;
$column['view_type'] = 'uploadFile';
$options = [
'multiple' => false,
'limit' => 1,
];
$column['options'] = $options;
}
if (stristr($column['column_name'], 'attach')) {
$column['is_query'] = 1;
$column['view_type'] = 'uploadFile';
$options = [
'multiple' => false,
'limit' => 1,
];
$column['options'] = $options;
}
if ($column['column_name'] === 'sort') {
$column['view_type'] = 'inputNumber';
}
if ($column['column_name'] === 'status') {
$column['view_type'] = 'radio';
$column['dict_type'] = 'data_status';
}
if (stristr($column['column_name'], 'is_')) {
$column['view_type'] = 'radio';
$column['dict_type'] = 'yes_or_no';
}
}
};
if (!$column['is_cover']) {
$object->viewTypeDispose($column);
$object->columnName($column);
}
$column['options'] = json_encode($column['options'], JSON_UNESCAPED_UNICODE);
return $column;
}
}

View File

@@ -0,0 +1,478 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\logic\tool;
use plugin\saiadmin\app\cache\UserMenuCache;
use plugin\saiadmin\app\logic\system\DatabaseLogic;
use plugin\saiadmin\app\model\system\SystemMenu;
use plugin\saiadmin\app\model\tool\GenerateTables;
use plugin\saiadmin\app\model\tool\GenerateColumns;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\utils\Helper;
use plugin\saiadmin\utils\code\CodeZip;
use plugin\saiadmin\utils\code\CodeEngine;
/**
* 代码生成业务逻辑层
*/
class GenerateTablesLogic extends BaseLogic
{
protected $columnLogic = null;
protected $dataLogic = null;
/**
* 构造函数
*/
public function __construct()
{
$this->model = new GenerateTables();
$this->columnLogic = new GenerateColumnsLogic();
$this->dataLogic = new DatabaseLogic();
}
/**
* 删除表和字段信息
* @param $ids
* @return bool
*/
public function destroy($ids): bool
{
return $this->transaction(function () use ($ids) {
parent::destroy($ids);
GenerateColumns::destroy(function ($query) use ($ids) {
$query->where('table_id', 'in', $ids);
});
return true;
});
}
/**
* 装载表信息
* @param $names
* @param $source
* @return void
*/
public function loadTable($names, $source): void
{
$data = config('think-orm.connections');
$config = $data[$source];
if (!$config) {
throw new ApiException('数据库配置读取失败');
}
$prefix = $config['prefix'] ?? '';
foreach ($names as $item) {
$class_name = $item['name'];
if (!empty($prefix)) {
$class_name = Helper::str_replace_once($prefix, '', $class_name);
}
$class_name = Helper::camel($class_name);
$tableInfo = [
'table_name' => $item['name'],
'table_comment' => $item['comment'],
'class_name' => $class_name,
'business_name' => Helper::get_business($item['name']),
'belong_menu_id' => 80,
'menu_name' => $item['comment'],
'tpl_category' => 'single',
'template' => 'app',
'stub' => 'think',
'namespace' => '',
'package_name' => '',
'source' => $source,
'generate_menus' => 'index,save,update,read,destroy',
];
$model = GenerateTables::create($tableInfo);
$columns = $this->dataLogic->getColumnList($item['name'], $source);
foreach ($columns as &$column) {
$column['table_id'] = $model->id;
$column['is_cover'] = false;
}
$this->columnLogic->saveExtra($columns);
}
}
/**
* 同步表字段信息
* @param $id
* @return void
*/
public function sync($id)
{
$model = $this->model->findOrEmpty($id);
// 拉取已有数据表信息
$queryModel = $this->columnLogic->model->where('table_id', $id);
$columnLogicData = $this->columnLogic->getAll($queryModel);
$columnLogicList = [];
foreach ($columnLogicData as $item) {
$columnLogicList[$item['column_name']] = $item;
}
GenerateColumns::destroy(function ($query) use ($id) {
$query->where('table_id', $id);
});
$columns = $this->dataLogic->getColumnList($model->table_name, $model->source ?? '');
foreach ($columns as &$column) {
$column['table_id'] = $model->id;
$column['is_cover'] = false;
if (isset($columnLogicList[$column['column_name']])) {
// 存在历史信息的情况
$getcolumnLogicItem = $columnLogicList[$column['column_name']];
if ($getcolumnLogicItem['column_type'] == $column['column_type']) {
$column['is_cover'] = true;
foreach ($getcolumnLogicItem as $key => $item) {
$array = [
'column_comment',
'column_type',
'default_value',
'is_pk',
'is_required',
'is_insert',
'is_edit',
'is_list',
'is_query',
'is_sort',
'query_type',
'view_type',
'dict_type',
'options',
'sort',
'is_cover'
];
if (in_array($key, $array)) {
$column[$key] = $item;
}
}
}
}
}
$this->columnLogic->saveExtra($columns);
}
/**
* 代码预览
* @param $id
* @return array
*/
public function preview($id): array
{
$data = $this->renderData($id);
$codeEngine = new CodeEngine($data);
$controllerContent = $codeEngine->renderContent('php', 'controller.stub');
$logicContent = $codeEngine->renderContent('php', 'logic.stub');
$modelContent = $codeEngine->renderContent('php', 'model.stub');
$validateContent = $codeEngine->renderContent('php', 'validate.stub');
$sqlContent = $codeEngine->renderContent('sql', 'sql.stub');
$indexContent = $codeEngine->renderContent('vue', 'index.stub');
$editContent = $codeEngine->renderContent('vue', 'edit-dialog.stub');
$searchContent = $codeEngine->renderContent('vue', 'table-search.stub');
$apiContent = $codeEngine->renderContent('ts', 'api.stub');
// 返回生成内容
return [
[
'tab_name' => 'controller.php',
'name' => 'controller',
'lang' => 'php',
'code' => $controllerContent
],
[
'tab_name' => 'logic.php',
'name' => 'logic',
'lang' => 'php',
'code' => $logicContent
],
[
'tab_name' => 'model.php',
'name' => 'model',
'lang' => 'php',
'code' => $modelContent
],
[
'tab_name' => 'validate.php',
'name' => 'validate',
'lang' => 'php',
'code' => $validateContent
],
[
'tab_name' => 'sql.sql',
'name' => 'sql',
'lang' => 'sql',
'code' => $sqlContent
],
[
'tab_name' => 'index.vue',
'name' => 'index',
'lang' => 'html',
'code' => $indexContent
],
[
'tab_name' => 'edit-dialog.vue',
'name' => 'edit-dialog',
'lang' => 'html',
'code' => $editContent
],
[
'tab_name' => 'table-search.vue',
'name' => 'table-search',
'lang' => 'html',
'code' => $searchContent
],
[
'tab_name' => 'api.ts',
'name' => 'api',
'lang' => 'javascript',
'code' => $apiContent
]
];
}
/**
* 生成到模块
* @param $id
*/
public function genModule($id)
{
$data = $this->renderData($id);
// 生成文件到模块
$codeEngine = new CodeEngine($data);
$codeEngine->generateBackend('controller', $codeEngine->renderContent('php', 'controller.stub'));
$codeEngine->generateBackend('logic', $codeEngine->renderContent('php', 'logic.stub'));
$codeEngine->generateBackend('model', $codeEngine->renderContent('php', 'model.stub'));
$codeEngine->generateBackend('validate', $codeEngine->renderContent('php', 'validate.stub'));
$codeEngine->generateFrontend('index', $codeEngine->renderContent('vue', 'index.stub'));
$codeEngine->generateFrontend('edit-dialog', $codeEngine->renderContent('vue', 'edit-dialog.stub'));
$codeEngine->generateFrontend('table-search', $codeEngine->renderContent('vue', 'table-search.stub'));
$codeEngine->generateFrontend('api', $codeEngine->renderContent('ts', 'api.stub'));
}
/**
* 处理数据
* @param $id
* @return array
*/
protected function renderData($id): array
{
$table = $this->model->findOrEmpty($id);
if (!in_array($table['template'], ["plugin", "app"])) {
throw new ApiException('应用类型必须为plugin或者app');
}
if (empty($table['namespace'])) {
throw new ApiException('请先设置应用名称');
}
$columns = $this->columnLogic->where('table_id', $id)
->order('sort', 'desc')
->select()
->toArray();
$pk = 'id';
foreach ($columns as &$column) {
if ($column['is_pk'] == 2) {
$pk = $column['column_name'];
}
if ($column['column_name'] == 'delete_time') {
unset($column['column_name']);
}
}
// 处理特殊变量
if ($table['template'] == 'plugin') {
$namespace_start = "plugin\\" . $table['namespace'] . "\\app\\admin\\";
$namespace_start_model = "plugin\\" . $table['namespace'] . "\\app\\";
$namespace_end = "\\" . $table['package_name'];
$url_path = 'app/' . $table['namespace'] . '/admin/' . $table['package_name'] . '/' . $table['class_name'];
$route = 'app/';
} else {
$namespace_start = "app\\" . $table['namespace'] . "\\";
$namespace_start_model = "app\\" . $table['namespace'] . "\\";
$namespace_end = "\\" . $table['package_name'];
$url_path = $table['namespace'] . '/' . $table['package_name'] . '/' . $table['class_name'];
$route = '';
}
$config = config('think-orm');
$data = $table->toArray();
$data['pk'] = $pk;
$data['namespace_start'] = $namespace_start;
$data['namespace_start_model'] = $namespace_start_model;
$data['namespace_end'] = $namespace_end;
$data['url_path'] = $url_path;
$data['route'] = $route;
$data['tables'] = [$data];
$data['columns'] = $columns;
$data['db_source'] = $config['default'] ?? 'mysql';
return $data;
}
/**
* 生成到模块
*/
public function generateFile($id)
{
$table = $this->model->where('id', $id)->findOrEmpty();
if ($table->isEmpty()) {
throw new ApiException('请选择要生成的表');
}
$debug = config('app.debug', true);
if (!$debug) {
throw new ApiException('非调试模式下,不允许生成文件');
}
$this->updateMenu($table);
$this->genModule($id);
UserMenuCache::clearMenuCache();
}
/**
* 代码生成下载
*/
public function generate($idsArr): array
{
$zip = new CodeZip();
$tables = $this->model->where('id', 'in', $idsArr)->select()->toArray();
foreach ($idsArr as $table_id) {
$data = $this->renderData($table_id);
$data['tables'] = $tables;
$codeEngine = new CodeEngine($data);
$codeEngine->generateTemp();
}
$filename = 'saiadmin.zip';
$download = $zip->compress();
return compact('filename', 'download');
}
/**
* 处理菜单列表
* @param $tables
*/
public function updateMenu($tables)
{
/*不存在的情况下进行新建操作*/
$url_path = $tables['namespace'] . ":" . $tables['package_name'] . ':' . $tables['business_name'];
$code = $tables['namespace'] . "/" . $tables['package_name'] . '/' . $tables['business_name'];
$path = $tables['package_name'] . '/' . $tables['business_name'];
$component = $tables['namespace'] . "/" . $tables['package_name'] . '/' . $tables['business_name'];
/*先获取一下已有的路由中是否包含当前ID的路由的核心信息*/
$model = new SystemMenu();
$tableMenu = $model->where('generate_id', $tables['id'])->findOrEmpty();
$fistMenu = [
'parent_id' => $tables['belong_menu_id'],
'name' => $tables['menu_name'],
'code' => $code,
'path' => $path,
'icon' => 'ri:home-2-line',
'component' => "/plugin/$component/index",
'type' => 2,
'sort' => 100,
'is_iframe' => 2,
'is_keep_alive' => 2,
'is_hidden' => 2,
'is_fixed_tab' => 2,
'is_full_page' => 2,
'generate_id' => $tables['id']
];
if ($tableMenu->isEmpty()) {
$temp = SystemMenu::create($fistMenu);
$fistMenuId = $temp->id;
} else {
$fistMenu['id'] = $tableMenu['id'];
$tableMenu->save($fistMenu);
$fistMenuId = $tableMenu['id'];
}
/*开始进行子权限的判定操作*/
$childNodes = [
['name' => '列表', 'key' => 'index'],
['name' => '保存', 'key' => 'save'],
['name' => '更新', 'key' => 'update'],
['name' => '读取', 'key' => 'read'],
['name' => '删除', 'key' => 'destroy'],
];
foreach ($childNodes as $node) {
$nodeData = $model->where('parent_id', $fistMenuId)->where('generate_key', $node['key'])->findOrEmpty();
$childNodeData = [
'parent_id' => $fistMenuId,
'name' => $node['name'],
'slug' => "$url_path:{$node['key']}",
'type' => 3,
'sort' => 100,
'is_iframe' => 2,
'is_keep_alive' => 2,
'is_hidden' => 2,
'is_fixed_tab' => 2,
'is_full_page' => 2,
'generate_key' => $node['key']
];
if (!empty($nodeData)) {
$childNodeData['id'] = $nodeData['id'];
$nodeData->save($childNodeData);
} else {
$menuModel = new SystemMenu();
$menuModel->save($childNodeData);
}
}
}
/**
* 获取数据表字段信息
* @param $table_id
* @return mixed
*/
public function getTableColumns($table_id): mixed
{
$query = $this->columnLogic->where('table_id', $table_id);
return $this->columnLogic->getAll($query);
}
/**
* 编辑数据
* @param $id
* @param $data
* @return mixed
*/
public function edit($id, $data): mixed
{
$columns = $data['columns'];
unset($data['columns']);
if (!empty($data['belong_menu_id'])) {
$data['belong_menu_id'] = is_array($data['belong_menu_id']) ? array_pop($data['belong_menu_id']) : $data['belong_menu_id'];
} else {
$data['belong_menu_id'] = 0;
}
$data['generate_menus'] = implode(',', $data['generate_menus']);
if (empty($data['options'])) {
unset($data['options']);
}
$data['options'] = json_encode($data['options'], JSON_UNESCAPED_UNICODE);
// 更新业务表
$this->update($data, ['id' => $id]);
// 更新业务字段表
foreach ($columns as $column) {
if ($column['options']) {
$column['options'] = json_encode($column['options'], JSON_NUMERIC_CHECK);
}
$this->columnLogic->update($column, ['id' => $column['id']]);
}
return true;
}
}

View File

@@ -0,0 +1,70 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\middleware;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
use plugin\saiadmin\app\cache\UserAuthCache;
use plugin\saiadmin\app\cache\ReflectionCache;
use plugin\saiadmin\exception\SystemException;
/**
* 权限检查中间件
*/
class CheckAuth implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$controller = $request->controller;
$action = $request->action;
// 通过反射获取控制器哪些方法不需要登录
$noNeedLogin = ReflectionCache::getNoNeedLogin($controller);
// 不登录访问,无需权限验证
if (in_array($action, $noNeedLogin)) {
return $handler($request);
}
// 登录信息
$token = getCurrentInfo();
if ($token === false) {
throw new SystemException('用户信息读取失败,无法访问或操作');
}
// 系统默认超级管理员,无需权限验证
if ($token['id'] === 1) {
return $handler($request);
}
// 2. 获取接口权限属性 (使用缓存类)
$permissions = ReflectionCache::getPermissionAttributes($controller, $action);
if (!empty($permissions) && !empty($permissions['slug'])) {
// 用户权限缓存
$auth = UserAuthCache::getUserAuth($token['id']);
if (!$this->checkPermissions($permissions, $auth)) {
throw new SystemException('权限不足,无法访问或操作');
}
}
return $handler($request);
}
/**
* 检查权限
*/
private function checkPermissions(array $attr, array $userPermissions): bool
{
// 直接对比 slug
return in_array($attr['slug'], $userPermissions);
}
}

View File

@@ -0,0 +1,40 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\middleware;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
use Tinywan\Jwt\JwtToken;
use plugin\saiadmin\app\cache\ReflectionCache;
use plugin\saiadmin\exception\ApiException;
/**
* 登录检查中间件
*/
class CheckLogin implements MiddlewareInterface
{
public function process(Request $request, callable $handler): Response
{
// 通过反射获取控制器哪些方法不需要登录
$noNeedLogin = ReflectionCache::getNoNeedLogin($request->controller);
// 访问的方法需要登录
if (!in_array($request->action, $noNeedLogin)) {
try {
$token = JwtToken::getExtend();
} catch (\Throwable $e) {
throw new ApiException('您的登录凭证错误或者已过期,请重新登录', 401);
}
if ($token['plat'] !== 'saiadmin') {
throw new ApiException('登录凭证校验失败');
}
$request->setHeader('check_login', true);
$request->setHeader('check_admin', $token);
}
return $handler($request);
}
}

View File

@@ -0,0 +1,33 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\middleware;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
/**
* 跨域中间件
*/
class CrossDomain implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
// 如果是options请求则返回一个空响应否则继续向洋葱芯穿越并得到一个响应
$response = $request->method() == 'OPTIONS' ? response('') : $handler($request);
// 给响应添加跨域相关的http头
$response->withHeaders([
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Allow-Origin' => $request->header('origin', '*'),
'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
]);
return $response;
}
}

View File

@@ -0,0 +1,38 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\middleware;
use Webman\Event\Event;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\app\cache\ReflectionCache;
class SystemLog implements MiddlewareInterface
{
/**
* @param Request $request
* @param callable $handler
* @return Response
*/
public function process(Request $request, callable $handler): Response
{
// 通过反射获取控制器哪些方法不需要登录
$noNeedLogin = ReflectionCache::getNoNeedLogin($request->controller);
// 访问的方法需要登录
if (!in_array($request->action, $noNeedLogin)) {
try {
// 记录日志
Event::emit('user.operateLog', true);
} catch (\Throwable $e) {
throw new ApiException('登录凭获取失败,请检查');
}
}
return $handler($request);
}
}

View File

@@ -0,0 +1,60 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 附件模型
*
* sa_system_attachment 附件信息表
*
* @property $id 主键
* @property $category_id 文件分类
* @property $storage_mode 存储模式
* @property $origin_name 原文件名
* @property $object_name 新文件名
* @property $hash 文件hash
* @property $mime_type 资源类型
* @property $storage_path 存储目录
* @property $suffix 文件后缀
* @property $size_byte 字节数
* @property $size_info 文件大小
* @property $url url地址
* @property $remark 备注
* @property $created_by 创建者
* @property $updated_by 更新者
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class SystemAttachment extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_system_attachment';
/**
* 原文件名搜索
*/
public function searchOriginNameAttr($query, $value)
{
$query->where('origin_name', 'like', '%' . $value . '%');
}
/**
* 文件类型搜索
*/
public function searchMimeTypeAttr($query, $value)
{
$query->where('mime_type', 'like', $value . '/%');
}
}

View File

@@ -0,0 +1,46 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 附件分类模型
*
* sa_system_category 附件分类表
*
* @property $id 分类ID
* @property $parent_id 父id
* @property $level 组集关系
* @property $category_name 分类名称
* @property $sort 排序
* @property $status 状态
* @property $remark 备注
* @property $created_by 创建者
* @property $updated_by 更新者
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class SystemCategory extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_system_category';
/**
* 分类名称搜索
*/
public function searchCategoryNameAttr($query, $value)
{
$query->where('category_name', 'like', '%' . $value . '%');
}
}

View File

@@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 参数配置模型
*
* sa_system_config 参数配置信息表
*
* @property $id 编号
* @property $group_id 组id
* @property $key 配置键名
* @property $value 配置值
* @property $name 配置名称
* @property $input_type 数据输入类型
* @property $config_select_data 配置选项数据
* @property $sort 排序
* @property $remark 备注
* @property $created_by 创建人
* @property $updated_by 更新人
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class SystemConfig extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_system_config';
public function getConfigSelectDataAttr($value)
{
return json_decode($value ?? '', true);
}
}

View File

@@ -0,0 +1,59 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 参数配置分组模型
*
* sa_system_config_group 参数配置分组表
*
* @property $id 主键
* @property $name 字典名称
* @property $code 字典标示
* @property $remark 备注
* @property $created_by 创建人
* @property $updated_by 更新人
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class SystemConfigGroup extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_system_config_group';
/**
* 关联配置列表
*/
public function configs()
{
return $this->hasMany(SystemConfig::class, 'group_id', 'id');
}
/**
* 名称搜索
*/
public function searchNameAttr($query, $value)
{
return $query->where('name', 'like', '%' . $value . '%');
}
/**
* 编码搜索
*/
public function searchCodeAttr($query, $value)
{
return $query->where('code', 'like', '%' . $value . '%');
}
}

View File

@@ -0,0 +1,62 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 部门模型
*
* sa_system_dept 部门表
*
* @property $id 编号
* @property $parent_id 父级ID0为根节点
* @property $name 部门名称
* @property $code 部门编码
* @property $leader_id 部门负责人ID
* @property $level 祖级列表,格式: 0,1,5,
* @property $sort 排序,数字越小越靠前
* @property $status 状态: 1启用, 0禁用
* @property $remark 备注
* @property $created_by 创建者
* @property $updated_by 更新者
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class SystemDept extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_system_dept';
/**
* 权限范围
*/
public function scopeAuth($query, $value)
{
if (!empty($value)) {
$deptIds = [$value['id']];
$deptLevel = $value['level'] . $value['id'] . ',';
$ids = static::whereLike('level', $deptLevel . '%')->column('id');
$deptIds = array_merge($deptIds, $ids);
$query->whereIn('id', $deptIds);
}
}
/**
* 部门领导
*/
public function leader()
{
return $this->hasOne(SystemUser::class, 'id', 'leader_id');
}
}

View File

@@ -0,0 +1,47 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 字典数据模型
*
* sa_system_dict_data 字典数据表
*
* @property $id 主键
* @property $type_id 字典类型ID
* @property $label 字典标签
* @property $value 字典值
* @property $color 字典颜色
* @property $code 字典标示
* @property $sort 排序
* @property $status 状态
* @property $remark 备注
* @property $created_by 创建者
* @property $updated_by 更新者
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class SystemDictData extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_system_dict_data';
/**
* 关键字搜索
*/
public function searchKeywordsAttr($query, $value)
{
$query->where('label|code', 'LIKE', "%$value%");
}
}

View File

@@ -0,0 +1,44 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 字典类型模型
*
* sa_system_dict_type 字典类型表
*
* @property $id 主键
* @property $name 字典名称
* @property $code 字典标示
* @property $status 状态
* @property $remark 备注
* @property $created_by 创建者
* @property $updated_by 更新者
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class SystemDictType extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_system_dict_type';
/**
* 关联字典数据
*/
public function dicts()
{
return $this->hasMany(SystemDictData::class, 'type_id', 'id');
}
}

View File

@@ -0,0 +1,50 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 登录日志模型
*
* sa_system_login_log 登录日志表
*
* @property $id 主键
* @property $username 用户名
* @property $ip 登录IP地址
* @property $ip_location IP所属地
* @property $os 操作系统
* @property $browser 浏览器
* @property $status 登录状态
* @property $message 提示消息
* @property $login_time 登录时间
* @property $remark 备注
* @property $created_by 创建者
* @property $updated_by 更新者
* @property $create_time 创建时间
* @property $update_time 更新时间
*/
class SystemLoginLog extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_system_login_log';
/**
* 时间范围搜索
*/
public function searchLoginTimeAttr($query, $value)
{
$query->whereTime('login_time', 'between', $value);
}
}

View File

@@ -0,0 +1,52 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 邮件记录模型
*
* sa_system_mail 邮件记录
*
* @property $id 编号
* @property $gateway 网关
* @property $from 发送人
* @property $email 接收人
* @property $code 验证码
* @property $content 邮箱内容
* @property $status 发送状态
* @property $response 返回结果
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class SystemMail extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_system_mail';
/**
* 发送人搜索
*/
public function searchFromAttr($query, $value)
{
$query->where('from', 'like', '%' . $value . '%');
}
/**
* 接收人搜索
*/
public function searchEmailAttr($query, $value)
{
$query->where('email', 'like', '%' . $value . '%');
}
}

View File

@@ -0,0 +1,86 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 菜单模型
*
* sa_system_menu 菜单权限表
*
* @property $id
* @property $parent_id 父级ID
* @property $name 菜单名称
* @property $code 组件名称
* @property $slug 权限标识,如 user:list, user:add
* @property $type 类型: 1目录, 2菜单, 3按钮/API
* @property $path 路由地址或API路径
* @property $component 前端组件路径,如 layout/User
* @property $method 请求方式
* @property $icon 图标
* @property $sort 排序
* @property $link_url 外部链接
* @property $is_iframe 是否iframe
* @property $is_keep_alive 是否缓存
* @property $is_hidden 是否隐藏
* @property $is_fixed_tab 是否固定标签页
* @property $is_full_page 是否全屏
* @property $generate_id 生成id
* @property $generate_key 生成key
* @property $status 状态
* @property $remark
* @property $created_by 创建者
* @property $updated_by 更新者
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class SystemMenu extends BaseModel
{
// 完整数据库表名称
protected $table = 'sa_system_menu';
// 主键
protected $pk = 'id';
/**
* Id搜索
*/
public function searchIdAttr($query, $value)
{
$query->whereIn('id', $value);
}
public function searchNameAttr($query, $value)
{
$query->where('name', 'like', '%' . $value . '%');
}
public function searchPathAttr($query, $value)
{
$query->where('path', 'like', '%' . $value . '%');
}
public function searchMenuAttr($query, $value)
{
if (!empty($value)) {
$query->whereIn('type', [1, 2]);
}
}
/**
* Type搜索
*/
public function searchTypeAttr($query, $value)
{
if (is_array($value)) {
$query->whereIn('type', $value);
} else {
$query->where('type', $value);
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 操作日志模型
*
* sa_system_oper_log 操作日志表
*
* @property $id 主键
* @property $username 用户名
* @property $app 应用名称
* @property $method 请求方式
* @property $router 请求路由
* @property $service_name 业务名称
* @property $ip 请求IP地址
* @property $ip_location IP所属地
* @property $request_data 请求数据
* @property $remark 备注
* @property $created_by 创建者
* @property $updated_by 更新者
* @property $create_time 创建时间
* @property $update_time 更新时间
*/
class SystemOperLog extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_system_oper_log';
}

View File

@@ -0,0 +1,37 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 岗位模型
*
* sa_system_post 岗位信息表
*
* @property $id 主键
* @property $name 岗位名称
* @property $code 岗位代码
* @property $sort 排序
* @property $status 状态
* @property $remark 备注
* @property $created_by 创建者
* @property $updated_by 更新者
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class SystemPost extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_system_post';
}

View File

@@ -0,0 +1,78 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 角色模型
*
* sa_system_role 角色表
*
* @property $id
* @property $name 角色名称
* @property $code 角色标识,如: hr_manager
* @property $level 角色级别:用于行政控制,不可操作级别大于自己的角色
* @property $data_scope 数据范围: 1全部, 2本部门及下属, 3本部门, 4仅本人, 5自定义
* @property $remark 备注
* @property $sort
* @property $status 状态: 1启用, 0禁用
* @property $created_by 创建者
* @property $updated_by 更新者
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class SystemRole extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
/**
* 数据表完整名称
* @var string
*/
protected $table = 'sa_system_role';
/**
* 权限范围
*/
public function scopeAuth($query, $value)
{
$id = $value['id'];
$roles = $value['roles'];
if ($id > 1) {
$ids = [];
foreach ($roles as $item) {
$ids[] = $item['id'];
$temp = static::whereRaw('FIND_IN_SET("' . $item['id'] . '", level) > 0')->column('id');
$ids = array_merge($ids, $temp);
}
$query->where('id', 'in', array_unique($ids));
}
}
/**
* 通过中间表获取菜单
*/
public function menus()
{
return $this->belongsToMany(SystemMenu::class, SystemRoleMenu::class, 'menu_id', 'role_id');
}
/**
* 通过中间表获取部门
*/
public function depts()
{
return $this->belongsToMany(SystemDept::class, SystemRoleDept::class, 'dept_id', 'role_id');
}
}

View File

@@ -0,0 +1,25 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use think\model\Pivot;
/**
* 角色部门关联模型
*
* sa_system_role_dept 角色-自定义数据权限关联
*
* @property $id
* @property $role_id
* @property $dept_id
*/
class SystemRoleDept extends Pivot
{
protected $pk = 'id';
protected $table = 'sa_system_role_dept';
}

View File

@@ -0,0 +1,25 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use think\model\Pivot;
/**
* 角色菜单关联模型
*
* sa_system_role_menu 角色权限关联
*
* @property $id
* @property $role_id
* @property $menu_id
*/
class SystemRoleMenu extends Pivot
{
protected $pk = 'id';
protected $table = 'sa_system_role_menu';
}

View File

@@ -0,0 +1,96 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 用户信息模型
*
* sa_system_user 用户表
*
* @property $id
* @property $username 登录账号
* @property $password 加密密码
* @property $realname 真实姓名
* @property $gender 性别
* @property $avatar 头像
* @property $email 邮箱
* @property $phone 手机号
* @property $signed 个性签名
* @property $dashboard 工作台
* @property $dept_id 主归属部门
* @property $is_super 是否超级管理员: 1是
* @property $status 状态: 1启用, 2禁用
* @property $remark 备注
* @property $login_time 最后登录时间
* @property $login_ip 最后登录IP
* @property $created_by 创建者
* @property $updated_by 更新者
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class SystemUser extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
/**
* 数据表完整名称
* @var string
*/
protected $table = 'sa_system_user';
public function searchKeywordAttr($query, $value)
{
if ($value) {
$query->where('username|realname|phone', 'like', '%' . $value . '%');
}
}
/**
* 权限范围 - 过滤部门用户
*/
public function scopeAuth($query, $value)
{
if (!empty($value)) {
$deptIds = [$value['id']];
$deptLevel = $value['level'] . $value['id'] . ',';
$dept_ids = SystemDept::whereLike('level', $deptLevel . '%')->column('id');
$deptIds = array_merge($deptIds, $dept_ids);
$query->whereIn('dept_id', $deptIds);
}
}
/**
* 通过中间表关联角色
*/
public function roles()
{
return $this->belongsToMany(SystemRole::class, SystemUserRole::class, 'role_id', 'user_id');
}
/**
* 通过中间表关联岗位
*/
public function posts()
{
return $this->belongsToMany(SystemPost::class, SystemUserPost::class, 'post_id', 'user_id');
}
/**
* 通过中间表关联部门
*/
public function depts()
{
return $this->belongsTo(SystemDept::class, 'dept_id', 'id');
}
}

View File

@@ -0,0 +1,25 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use think\model\Pivot;
/**
* 用户岗位关联模型
*
* sa_system_user_post 用户与岗位关联表
*
* @property $id 主键
* @property $user_id 用户主键
* @property $post_id 岗位主键
*/
class SystemUserPost extends Pivot
{
protected $pk = 'id';
protected $table = 'sa_system_user_post';
}

View File

@@ -0,0 +1,35 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\system;
use think\model\Pivot;
/**
* 用户角色关联模型
*
* sa_system_user_role 用户角色关联
*
* @property $id
* @property $user_id
* @property $role_id
*/
class SystemUserRole extends Pivot
{
protected $pk = 'id';
protected $table = 'sa_system_user_role';
/**
* 获取角色id
* @param mixed $user_id
* @return array
*/
public static function getRoleIds($user_id): array
{
return static::where('user_id', $user_id)->column('role_id');
}
}

View File

@@ -0,0 +1,40 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\tool;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 定时任务模型
*
* sa_tool_crontab 定时任务信息表
*
* @property $id 主键
* @property $name 任务名称
* @property $type 任务类型
* @property $target 调用任务字符串
* @property $parameter 调用任务参数
* @property $task_style 执行类型
* @property $rule 任务执行表达式
* @property $status 状态
* @property $remark 备注
* @property $created_by 创建者
* @property $updated_by 更新者
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class Crontab extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_tool_crontab';
}

View File

@@ -0,0 +1,36 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\tool;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 定时任务日志模型
*
* sa_tool_crontab_log 定时任务执行日志表
*
* @property $id 主键
* @property $crontab_id 任务ID
* @property $name 任务名称
* @property $target 任务调用目标字符串
* @property $parameter 任务调用参数
* @property $exception_info 异常信息
* @property $status 执行状态
* @property $create_time 创建时间
* @property $update_time 修改时间
*/
class CrontabLog extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_tool_crontab_log';
}

View File

@@ -0,0 +1,29 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\tool;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 代码生成业务字段模型
*/
class GenerateColumns extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_tool_generate_columns';
public function getOptionsAttr($value)
{
return json_decode($value ?? '', true);
}
}

View File

@@ -0,0 +1,29 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\model\tool;
use plugin\saiadmin\basic\think\BaseModel;
/**
* 代码生成业务模型
*/
class GenerateTables extends BaseModel
{
/**
* 数据表主键
* @var string
*/
protected $pk = 'id';
protected $table = 'sa_tool_generate_tables';
public function getOptionsAttr($value)
{
return json_decode($value ?? '', true);
}
}

View File

@@ -0,0 +1,42 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
/**
* 附件分类验证器
*/
class SystemCategoryValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'category_name' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'category_name' => '分类名称必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'add' => [
'category_name',
],
'edit' => [
'category_name',
],
];
}

View File

@@ -0,0 +1,51 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
use plugin\saiadmin\app\model\system\SystemConfigGroup;
/**
* 字典类型验证器
*/
class SystemConfigGroupValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'name' => 'require|max:16',
'code' => 'require|alphaDash|unique:' . SystemConfigGroup::class,
];
/**
* 定义错误信息
*/
protected $message = [
'name.require' => '组名称必须填写',
'name.max' => '组名称最多不能超过16个字符',
'name.chs' => '组名称必须是中文',
'code.require' => '组标识必须填写',
'code.alphaDash' => '组标识只能由英文字母组成',
'code.unique' => '配置组标识不能重复',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'name',
'code',
],
'update' => [
'name',
'code',
],
];
}

View File

@@ -0,0 +1,54 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
/**
* 字典类型验证器
*/
class SystemConfigValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'name' => 'require',
'key' => 'require',
'group_id' => 'require',
'input_type' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'name' => '配置标题必须填写',
'key' => '配置标识必须填写',
'group_id' => '所属组必须填写',
'input_type' => '输入组件必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'name',
'key',
'group_id',
'input_type',
],
'update' => [
'name',
'key',
'group_id',
'input_type',
],
];
}

View File

@@ -0,0 +1,58 @@
<?php
// +----------------------------------------------------------------------
// | saithink [ saithink快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
/**
* 字典类型验证器
*/
class SystemCrontabValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'name' => 'require',
'type' => 'require',
'rule' => 'require',
'target' => 'require',
'status' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'name' => '任务名称必须填写',
'type' => '任务类型必须填写',
'rule' => '任务规则必须填写',
'target' => '调用目标必须填写',
'status' => '状态必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'name',
'type',
'rule',
'target',
'status',
],
'update' => [
'name',
'type',
'rule',
'target',
'status',
],
];
}

View File

@@ -0,0 +1,46 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
/**
* 部门验证器
*/
class SystemDeptValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'name' => 'require',
'status' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'name' => '部门名称必须填写',
'status' => '状态必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'add' => [
'name',
'status',
],
'edit' => [
'name',
'status',
],
];
}

View File

@@ -0,0 +1,55 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
/**
* 字典数据验证器
*/
class SystemDictDataValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'label' => 'require',
'value' => 'require',
'status' => 'require',
'type_id' => 'require',
'code' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'label' => '字典标签必须填写',
'value' => '字典键值必须填写',
'status' => '状态必须填写',
'type_id' => '字典类型必须填写',
'code' => '字典标识必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'label',
'value',
'status',
'type_id',
],
'update' => [
'label',
'value',
'status',
],
];
}

View File

@@ -0,0 +1,54 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
use plugin\saiadmin\app\model\system\SystemDictType;
/**
* 字典类型验证器
*/
class SystemDictTypeValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'name' => 'require|max:16',
'code' => 'require|alphaDash|unique:' . SystemDictType::class,
'status' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'name.require' => '字典名称必须填写',
'name.max' => '字典名称最多不能超过16个字符',
'code.require' => '字典标识必须填写',
'code.alphaDash' => '字典标识只能由英文字母组成',
'code.unique' => '字典标识已存在',
'status' => '状态必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'name',
'code',
'status',
],
'update' => [
'name',
'code',
'status',
],
];
}

View File

@@ -0,0 +1,50 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
/**
* 邮件验证器
*/
class SystemMailValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'gateway' => 'require',
'from' => 'require',
'email' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'gateway' => '网关必须填写',
'from' => '发件人必须填写',
'email' => '接收人必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'gateway',
'from',
'email',
],
'update' => [
'gateway',
'from',
'email',
],
];
}

View File

@@ -0,0 +1,47 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
/**
* 菜单验证器
*/
class SystemMenuValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'name' => 'require|max:16',
'status' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'name.require' => '菜单名称必须填写',
'name.max' => '菜单名称最多不能超过16个字符',
'status' => '状态必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'name',
'status',
],
'update' => [
'name',
'status',
],
];
}

View File

@@ -0,0 +1,51 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
/**
* 系统公告验证器
*/
class SystemNoticeValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'title' => 'require|min:4',
'content' => 'require',
'type' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'title.require' => '公告标题必须填写',
'title.min' => '公告标题必须大于4个字符',
'content' => '公告内容必须填写',
'type' => '公告类型必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'title',
'content',
'type',
],
'update' => [
'title',
'content',
'type',
],
];
}

View File

@@ -0,0 +1,50 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
/**
* 用户角色验证器
*/
class SystemPostValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'name' => 'require',
'code' => 'require',
'status' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'name' => '岗位名称必须填写',
'code' => '岗位标识必须填写',
'status' => '状态必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'name',
'code',
'status',
],
'update' => [
'name',
'code',
'status',
],
];
}

View File

@@ -0,0 +1,54 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
use plugin\saiadmin\app\model\system\SystemRole;
/**
* 用户角色验证器
*/
class SystemRoleValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'name' => 'require|max:16',
'code' => 'require|alphaDash|unique:' . SystemRole::class,
'status' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'name.require' => '角色名称必须填写',
'name.max' => '角色名称最多不能超过16个字符',
'code.require' => '角色标识必须填写',
'code.alphaDash' => '角色标识只能由英文字母组成',
'code.unique' => '角色标识不能重复',
'status' => '状态必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'add' => [
'name',
'code',
'status',
],
'edit' => [
'name',
'code',
'status',
],
];
}

View File

@@ -0,0 +1,53 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\system;
use plugin\saiadmin\basic\BaseValidate;
use plugin\saiadmin\app\model\system\SystemUser;
/**
* 用户信息验证器
*/
class SystemUserValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'username' => 'require|max:16|unique:' . SystemUser::class,
'password' => 'require|min:6|max:16',
'role_ids' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'username.require' => '用户名必须填写',
'username.max' => '用户名最多不能超过16个字符',
'username.unique' => '用户名不能重复',
'password.require' => '密码必须填写',
'password.min' => '密码最少为6位',
'password.max' => '密码长度不能超过16位',
'role_ids' => '角色必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'username',
'password',
'role_ids',
],
'update' => [
'username',
'role_ids',
],
];
}

View File

@@ -0,0 +1,54 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\tool;
use plugin\saiadmin\basic\BaseValidate;
/**
* 字典类型验证器
*/
class CrontabValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'name' => 'require',
'type' => 'require',
'target' => 'require',
'status' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'name' => '任务名称必须填写',
'type' => '任务类型必须填写',
'target' => '调用目标必须填写',
'status' => '状态必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'name',
'type',
'target',
'status',
],
'update' => [
'name',
'type',
'target',
'status',
],
];
}

View File

@@ -0,0 +1,68 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\app\validate\tool;
use plugin\saiadmin\basic\BaseValidate;
/**
* 用户角色验证器
*/
class GenerateTablesValidate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
'table_name' => 'require',
'table_comment' => 'require',
'class_name' => 'require|alphaDash',
'business_name' => 'require|alphaDash',
'template' => 'require',
'namespace' => 'require',
'menu_name' => 'require',
];
/**
* 定义错误信息
*/
protected $message = [
'table_name' => '表名称必须填写',
'table_comment' => '表描述必须填写',
'class_name.require' => '实体类必须填写',
'class_name.alphaDash' => '实体类必须是英文',
'business_name.require' => '实体别名必须填写',
'business_name.alphaDash' => '实体别名必须是英文',
'template' => '模板必须填写',
'namespace' => '命名空间必须填写',
'menu_name' => '菜单名称必须填写',
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
'table_name',
'table_comment',
'class_name',
'business_name',
'template',
'namespace',
'menu_name',
],
'update' => [
'table_name',
'table_comment',
'class_name',
'business_name',
'template',
'namespace',
'menu_name',
]
];
}

View File

@@ -0,0 +1,253 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>安装错误</title>
<link href="/app/saiadmin/assets/bootstrap.min.css" rel="stylesheet">
<style>
:root {
--primary-gradient: linear-gradient(135deg, #7166F0 0%, #8F85F3 100%);
--error-gradient: linear-gradient(135deg, #7166F0 0%, #8F85F3 100%);
--bg-gradient: linear-gradient(135deg, #F8FAFC 0%, #F1F5F9 100%);
--card-bg: rgba(255, 255, 255, 0.95);
--text-primary: #1E293B;
--text-secondary: #475569;
--border-color: rgba(113, 102, 240, 0.1);
}
body {
background: var(--bg-gradient);
min-height: 100vh;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
display: flex;
flex-direction: column;
color: var(--text-primary);
}
.logo-section {
text-align: left;
padding: 1rem 2rem;
background: var(--card-bg);
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.05);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border-color);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
display: flex;
align-items: center;
gap: 1rem;
}
.logo-icon {
animation: pulse 2s infinite;
flex-shrink: 0;
}
.logo-icon img {
width: 40px;
height: 40px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease;
}
.logo-icon img:hover {
transform: scale(1.05);
}
.logo-section h1 {
font-size: 1.4rem;
background: var(--primary-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin: 0;
font-weight: 700;
letter-spacing: 0.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.main-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
margin-top: 70px;
}
.install-wrapper {
width: 100%;
max-width: 800px;
animation: fadeIn 0.8s ease-out;
}
.error-container {
text-align: center;
padding: 3rem;
background: var(--card-bg);
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
backdrop-filter: blur(10px);
border: 1px solid var(--border-color);
transform: translateY(0);
transition: all 0.3s ease;
}
.error-container:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.08);
}
.error-icon {
font-size: 5rem;
margin-bottom: 1.5rem;
animation: shake 0.5s ease-in-out;
color: #7166F0;
}
.error-icon svg {
filter: drop-shadow(0 5px 15px rgba(113, 102, 240, 0.2));
}
.error-title {
font-size: 2rem;
font-weight: 700;
background: var(--error-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 1rem;
}
.error-message {
color: var(--text-secondary);
font-size: 1.1rem;
margin-bottom: 2.5rem;
line-height: 1.6;
background: rgba(113, 102, 240, 0.05);
padding: 1rem;
border-radius: 10px;
border: 1px solid rgba(113, 102, 240, 0.1);
}
.btn-custom {
padding: 1rem 2.5rem;
font-size: 1.1rem;
font-weight: 600;
border-radius: 50px;
background: var(--primary-gradient);
border: none;
color: white;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(113, 102, 240, 0.3);
text-decoration: none;
display: inline-block;
letter-spacing: 0.5px;
position: relative;
overflow: hidden;
}
.btn-custom:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(113, 102, 240, 0.4);
color: white;
}
.btn-custom:active {
transform: translateY(0);
}
.btn-custom::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(45deg, rgba(255,255,255,0.1), rgba(255,255,255,0));
transform: translateX(-100%);
transition: transform 0.6s ease;
}
.btn-custom:hover::after {
transform: translateX(100%);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
@media (max-width: 576px) {
.logo-section {
padding: 0.8rem 1rem;
}
.logo-section h1 {
font-size: 1rem;
}
.logo-icon img {
width: 32px;
height: 32px;
}
.main-content {
margin-top: 60px;
padding: 1rem;
}
.error-container {
padding: 2rem;
}
.error-title {
font-size: 1.6rem;
}
.btn-custom {
padding: 0.8rem 2rem;
font-size: 1rem;
}
}
</style>
</head>
<body>
<!-- Logo Section -->
<div class="logo-section">
<div class="logo-icon">
<img src="https://saithink.top/images/logo.png">
</div>
<h1>【{{app}}-{{version}}】安装配置向导</h1>
</div>
<div class="main-content">
<div class="install-wrapper">
<div class="error-container">
<div class="error-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-exclamation-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>
</div>
<h2 class="error-title">安装失败</h2>
<p class="error-message">错误提示:{{error}}</p>
<a href="/core/install" class="btn btn-custom">重新安装</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,1023 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>应用安装向导</title>
<!-- Bootstrap CSS -->
<link href="/app/saiadmin/assets/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<!-- Custom CSS -->
<style>
:root {
--primary: #7166F0;
--primary-light: #8B7FF7;
--primary-dark: #5B4FD9;
--success: #10B981;
--warning: #F59E0B;
--dark: #1F2937;
--light: #F8FAFC;
--border-radius: 10px;
--box-shadow: 0 10px 30px rgba(113, 102, 240, 0.1);
--transition: all 0.3s ease;
--primary-gradient: linear-gradient(135deg, #7166F0 0%, #8B7FF7 100%);
--card-bg: rgba(255, 255, 255, 0.95);
--border-color: rgba(113, 102, 240, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #F5F3FF 0%, #EDE9FE 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 30px 15px;
color: var(--dark);
}
.install-wrapper {
width: 100%;
max-width: 1000px;
}
.logo-section {
text-align: left;
padding: 1rem 2rem;
background: var(--card-bg);
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.05);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border-color);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
display: flex;
align-items: center;
gap: 1rem;
}
.logo-icon {
animation: pulse 2s infinite;
flex-shrink: 0;
}
.logo-icon img {
width: 40px;
height: 40px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease;
}
.logo-icon img:hover {
transform: scale(1.05);
}
.logo-section h1 {
font-size: 1.4rem;
background: var(--primary-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin: 0;
font-weight: 700;
letter-spacing: 0.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.main-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
margin-top: 70px;
}
.install-container {
background: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
overflow: hidden;
width: 100%;
backdrop-filter: blur(10px);
border: 1px solid var(--border-color);
}
.progress-container {
padding: 25px 30px 0;
}
.progress {
height: 8px;
border-radius: 50px;
background-color: #F5F3FF;
margin-bottom: 25px;
overflow: visible;
box-shadow: inset 0 1px 3px rgba(113, 102, 240, 0.1);
}
.progress-bar {
background: var(--primary-gradient);
border-radius: 50px;
position: relative;
transition: var(--transition);
}
.progress-bar::after {
content: '';
position: absolute;
right: -4px;
top: -4px;
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
border: 3px solid var(--primary);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.steps-container {
display: flex;
justify-content: space-between;
position: relative;
margin-bottom: 30px;
}
.step-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 1;
flex: 1;
}
.step-icon {
width: 50px;
height: 50px;
border-radius: 50%;
background: white;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
border: 2px solid #F5F3FF;
color: #94A3B8;
transition: var(--transition);
position: relative;
}
.step-icon i {
font-size: 20px;
}
.step-text {
font-size: 14px;
font-weight: 600;
color: #94A3B8;
transition: var(--transition);
}
.step-item.active .step-icon,
.step-item.completed .step-icon {
border-color: var(--primary);
color: white;
background: var(--primary-gradient);
}
.step-item.active .step-text,
.step-item.completed .step-text {
color: var(--primary);
}
.step-item.completed .step-icon::after {
content: '\f00c';
font-family: 'Font Awesome 6 Free';
font-weight: 900;
position: absolute;
}
.content-section {
padding: 30px;
width: 100%;
}
.step-content {
display: none;
animation: fadeIn 0.5s ease;
width: 100%;
}
.step-content.active {
display: block;
width: 100%;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.step-title {
font-size: 24px;
font-weight: 700;
margin-bottom: 25px;
color: var(--primary);
width: 100%;
text-align: left;
}
.form-group {
margin-bottom: 25px;
position: relative;
}
.form-label {
font-weight: 600;
font-size: 14px;
color: var(--dark);
margin-bottom: 10px;
display: block;
transition: var(--transition);
}
.form-control {
height: 50px;
border-radius: var(--border-radius);
border: 2px solid #F5F3FF;
padding: 10px 15px 10px 50px;
font-size: 15px;
transition: var(--transition);
background-color: #F8FAFC;
color: var(--dark);
}
.form-control:focus {
border-color: var(--primary);
background-color: #fff;
box-shadow: 0 0 0 4px rgba(113, 102, 240, 0.1);
outline: none;
}
.form-control::placeholder {
color: #94A3B8;
}
/* Custom Input Group Styles */
.custom-input-group {
position: relative;
display: flex;
align-items: center;
}
.custom-input-icon {
position: absolute;
left: 0;
top: 0;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
color: #94A3B8;
z-index: 5;
pointer-events: none;
transition: var(--transition);
}
.custom-input-group:hover .custom-input-icon {
color: var(--primary);
}
.custom-input-group .form-control:focus + .custom-input-icon {
color: var(--primary);
}
/* 添加输入框动画效果 */
.form-control:focus + .custom-input-icon {
transform: scale(1.1);
}
/* 添加输入框hover效果 */
.form-control:hover {
border-color: #ced4da;
background-color: #fff;
}
/* 添加密码输入框特殊样式 */
input[type="password"].form-control {
letter-spacing: 2px;
}
/* 添加数字输入框特殊样式 */
input[type="number"].form-control {
-moz-appearance: textfield;
}
input[type="number"].form-control::-webkit-outer-spin-button,
input[type="number"].form-control::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* 添加输入框聚焦时的标签动画 */
.form-group:focus-within .form-label {
color: var(--primary);
transform: translateY(-2px);
}
/* 添加输入框验证状态样式 */
.form-control.is-valid {
border-color: var(--success);
background-image: none;
}
.form-control.is-invalid {
border-color: var(--warning);
background-image: none;
}
/* 添加输入框加载状态样式 */
.form-control.is-loading {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%234361ee' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 12a9 9 0 1 1-6.219-8.56'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 20px;
padding-right: 40px;
}
.btn {
height: 50px;
border-radius: var(--border-radius);
padding: 0 25px;
font-weight: 600;
font-size: 15px;
transition: var(--transition);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.btn-primary {
background: var(--primary-gradient);
border: none;
color: white;
transition: var(--transition);
box-shadow: 0 5px 15px rgba(113, 102, 240, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(113, 102, 240, 0.4);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-outline-primary {
color: var(--primary);
border-color: var(--primary);
}
.btn-outline-primary:hover {
background: var(--primary);
border-color: var(--primary);
}
.log-container {
background: #F8FAFC;
border-radius: var(--border-radius);
border: 2px solid #F5F3FF;
height: 300px;
overflow-y: auto;
padding: 20px;
margin-top: 20px;
width: 100%;
}
.log-item {
padding: 15px;
border-radius: var(--border-radius);
background: white;
margin-bottom: 15px;
box-shadow: 0 2px 5px rgba(113, 102, 240, 0.05);
display: flex;
align-items: flex-start;
gap: 15px;
animation: slideIn 0.3s ease;
border: 1px solid #F5F3FF;
transition: var(--transition);
}
.log-item:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(113, 102, 240, 0.1);
}
.log-icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(113, 102, 240, 0.1);
display: flex;
align-items: center;
justify-content: center;
color: var(--primary);
flex-shrink: 0;
transition: var(--transition);
}
.log-item:hover .log-icon {
transform: scale(1.1);
background: rgba(113, 102, 240, 0.2);
}
.log-content {
flex-grow: 1;
}
.log-text {
font-size: 14px;
line-height: 1.6;
color: var(--dark);
margin-bottom: 5px;
}
.log-time {
font-size: 12px;
color: #94A3B8;
}
.completed-message {
text-align: center;
padding: 40px 20px;
animation: fadeIn 0.5s ease;
width: 100%;
max-width: 100%;
}
.completed-icon {
width: 100px;
height: 100px;
border-radius: 50%;
background: rgba(113, 102, 240, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 30px;
color: var(--primary);
font-size: 40px;
animation: scaleIn 0.5s ease;
transition: var(--transition);
}
.completed-icon:hover {
transform: scale(1.1);
background: rgba(113, 102, 240, 0.2);
}
.completed-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 15px;
color: var(--primary);
background: var(--primary-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.completed-subtitle {
font-size: 16px;
color: #6c757d;
margin-bottom: 30px;
line-height: 1.6;
max-width: 100%;
padding: 0 20px;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 15px;
width: 100%;
padding: 0 20px;
}
.action-buttons .btn {
padding: 12px 30px;
font-size: 16px;
border-radius: 50px;
background: var(--primary-gradient);
border: none;
color: white;
transition: var(--transition);
box-shadow: 0 5px 15px rgba(113, 102, 240, 0.3);
}
.action-buttons .btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(113, 102, 240, 0.4);
}
.action-buttons .btn:active {
transform: translateY(0);
}
@keyframes scaleIn {
from { transform: scale(0); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes slideIn {
from { transform: translateY(10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* Responsive adjustments */
@media (max-width: 768px) {
.content-section {
padding: 20px;
}
.step-title {
font-size: 20px;
}
.btn {
height: 45px;
padding: 0 20px;
font-size: 14px;
}
.form-control {
height: 45px;
}
.custom-input-icon {
height: 45px;
width: 45px;
}
}
@media (max-width: 576px) {
.steps-container {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.step-item {
flex-direction: row;
width: 100%;
justify-content: flex-start;
gap: 15px;
}
.step-icon {
margin-bottom: 0;
}
.log-container {
height: 250px;
}
.completed-icon {
width: 80px;
height: 80px;
font-size: 32px;
}
.completed-title {
font-size: 24px;
}
}
/* Step 2: Installation Process Styles */
#step2 {
width: 100%;
}
#step2 .progress {
width: 100%;
}
#step2 .log-container {
width: 100%;
margin: 20px 0;
}
/* Step 3: Installation Complete Styles */
#step3 {
width: 100%;
}
#step3 .completed-message {
width: 100%;
padding: 40px 0;
}
#step3 .completed-subtitle {
width: 100%;
padding: 0;
margin: 0 auto 30px;
}
#step3 .action-buttons {
width: 100%;
padding: 0;
}
/* 确保所有步骤内容区域宽度一致 */
.row {
width: 100%;
margin: 0;
}
.col-md-6 {
width: 100%;
padding: 0;
}
@media (min-width: 768px) {
.col-md-6 {
width: 50%;
padding: 0 15px;
}
}
</style>
</head>
<body>
<div class="install-wrapper">
<!-- Logo Section -->
<div class="logo-section">
<div class="logo-icon">
<img src="https://saithink.top/images/logo.png" alt="Logo">
</div>
<h1>【{{app}}-{{version}}】安装配置向导</h1>
</div>
<!-- Main Install Container -->
<div class="main-content">
<div class="install-container">
<!-- Progress Section -->
<div class="progress-container">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
<div class="steps-container">
<div class="step-item active" data-step="1">
<div class="step-icon">
<i class="fas fa-database"></i>
</div>
<div class="step-text">数据库配置</div>
</div>
<div class="step-item" data-step="2">
<div class="step-icon">
<i class="fas fa-cogs"></i>
</div>
<div class="step-text">执行安装</div>
</div>
<div class="step-item" data-step="3">
<div class="step-icon">
<i class="fas fa-check"></i>
</div>
<div class="step-text">安装完成</div>
</div>
</div>
</div>
<!-- Content Section -->
<div class="content-section">
<!-- Step 1: Database Config -->
<div class="step-content active" id="step1">
<h2 class="step-title">数据库配置</h2>
<form id="dbConfigForm">
<div class="row g-4">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">数据库数据</label>
<div class="radio-group" style="display: flex; gap: 20px;">
<label class="radio-label" style="display: flex; align-items: center; cursor: pointer; padding: 10px 15px; border: 2px solid #F5F3FF; border-radius: 10px; background: #F8FAFC; transition: all 0.3s ease;">
<input type="radio" name="dataType" id="dataTypeDemo" value="demo" checked style="width: 18px; height: 18px; margin-right: 10px; accent-color: #7166f0;">
<span style="display: flex; flex-direction: column;">
<strong style="color: #1e293b; font-size: 14px;">附带演示数据</strong>
</span>
</label>
<label class="radio-label" style="display: flex; align-items: center; cursor: pointer; padding: 10px 15px; border: 2px solid #F5F3FF; border-radius: 10px; background: #F8FAFC; transition: all 0.3s ease;">
<input type="radio" name="dataType" id="dataTypePure" value="pure" style="width: 18px; height: 18px; margin-right: 10px; accent-color: #7166f0;">
<span style="display: flex; flex-direction: column;">
<strong style="color: #1e293b; font-size: 14px;">纯净数据</strong>
</span>
</label>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="dbHost" class="form-label">数据库主机</label>
<div class="custom-input-group">
<input type="text" class="form-control" id="dbHost" value="127.0.0.1" required>
<div class="custom-input-icon">
<i class="fas fa-server"></i>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="dbPort" class="form-label">端口</label>
<div class="custom-input-group">
<input type="number" class="form-control" id="dbPort" value="3306" required>
<div class="custom-input-icon">
<i class="fas fa-plug"></i>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="dbName" class="form-label">数据库名</label>
<div class="custom-input-group">
<input type="text" class="form-control" id="dbName" required>
<div class="custom-input-icon">
<i class="fas fa-database"></i>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="dbUser" class="form-label">用户名</label>
<div class="custom-input-group">
<input type="text" class="form-control" id="dbUser" required>
<div class="custom-input-icon">
<i class="fas fa-user"></i>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="dbPassword" class="form-label">密码</label>
<div class="custom-input-group">
<input type="password" class="form-control" id="dbPassword" required>
<div class="custom-input-icon">
<i class="fas fa-key"></i>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-4">
<button type="button" class="btn btn-primary" onclick="nextStep(1)">
<span>下一步</span>
<i class="fas fa-arrow-right"></i>
</button>
</div>
</form>
</div>
<!-- Step 2: Installation Process -->
<div class="step-content" id="step2">
<h2 class="step-title">执行安装</h2>
<div class="progress mb-4">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
</div>
<div class="log-container" id="installLog">
<div class="log-item">
<div class="log-icon">
<i class="fas fa-info"></i>
</div>
<div class="log-content">
<div class="log-text">准备开始安装...</div>
<div class="log-time">刚刚</div>
</div>
</div>
</div>
</div>
<!-- Step 3: Installation Complete -->
<div class="step-content" id="step3">
<div class="completed-message">
<div class="completed-icon">
<i class="fas fa-check"></i>
</div>
<h2 class="completed-title">安装成功</h2>
<p class="completed-subtitle">恭喜已成功安装系统请重启webman启动前端项目进行访问。</p>
<div class="action-buttons">
<a href="http://localhost:3006" class="btn btn-primary">
<i class="fas fa-tachometer-alt"></i>
<span>进入管理后台</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- jQuery -->
<script src="/app/saiadmin/assets/jquery.min.js"></script>
<script>
// 显示通知
function showNotification(type, message) {
const icon = type === 'success' ? 'check-circle' : 'exclamation-circle';
const color = type === 'success' ? '#4cc9f0' : '#f72585';
const notification = $(`
<div class="notification" style="
position: fixed;
top: 20px;
right: 20px;
background: white;
padding: 15px 20px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 15px;
z-index: 1000;
transform: translateX(120%);
transition: transform 0.3s ease;
">
<div style="
width: 30px;
height: 30px;
background: ${color};
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas fa-${icon}"></i>
</div>
<div>${message}</div>
</div>
`);
$('body').append(notification);
setTimeout(() => {
notification.css('transform', 'translateX(0)');
}, 100);
setTimeout(() => {
notification.css('transform', 'translateX(120%)');
setTimeout(() => {
notification.remove();
}, 300);
}, 3000);
}
// 切换步骤
function nextStep(currentStep) {
if (currentStep === 1) {
// 验证数据库配置
if (!validateDbConfig()) {
return;
}
// 开始安装过程
startInstallation();
} else {
// 隐藏当前步骤,显示下一步
$('.step-content').removeClass('active');
$(`#step${currentStep + 1}`).addClass('active');
// 更新步骤指示器
$('.step-item').removeClass('active');
$(`.step-item[data-step="${currentStep + 1}"]`).addClass('active');
// 将之前的步骤标记为已完成
for (let i = 1; i <= currentStep; i++) {
$(`.step-item[data-step="${i}"]`).addClass('completed');
}
// 更新进度条
const progress = ((currentStep + 1) / 3) * 100;
$('.progress-bar').css('width', progress + '%');
}
}
// 验证数据库配置
function validateDbConfig() {
const required = ['dbHost', 'dbName', 'dbUser', 'dbPassword'];
for (const field of required) {
if (!$(`#${field}`).val()) {
showNotification('error', '请填写所有必填字段');
return false;
}
}
return true;
}
// 开始安装过程
function startInstallation() {
const dbConfig = {
host: $('#dbHost').val(),
port: $('#dbPort').val(),
database: $('#dbName').val(),
username: $('#dbUser').val(),
password: $('#dbPassword').val(),
prefix: $('#dbPrefix').val(),
dataType: $('input[name="dataType"]:checked').val()
};
// 模拟安装过程
const steps = [
{ icon: 'database', text: '正在创建数据库表...' },
{ icon: 'file-import', text: '正在导入基础数据...' },
{ icon: 'cogs', text: '正在配置系统参数...' },
{ icon: 'tachometer-alt', text: '正在优化系统性能...' }
];
let currentStep = 0;
const installLog = $('#installLog');
const progressBar = $('.progress-bar');
function runStep() {
if (currentStep < steps.length) {
// 获取当前时间
const now = new Date();
const timeString = now.getHours().toString().padStart(2, '0') + ':' +
now.getMinutes().toString().padStart(2, '0') + ':' +
now.getSeconds().toString().padStart(2, '0');
// 添加日志
const step = steps[currentStep];
installLog.append(`
<div class="log-item">
<div class="log-icon">
<i class="fas fa-${step.icon}"></i>
</div>
<div class="log-content">
<div class="log-text">${step.text}</div>
<div class="log-time">${timeString}</div>
</div>
</div>
`);
installLog.scrollTop(installLog[0].scrollHeight);
// 更新进度
const progress = ((currentStep + 1) / steps.length) * 100;
progressBar.css('width', progress + '%');
// 模拟异步操作
setTimeout(() => {
currentStep++;
runStep();
}, 1000);
} else {
// 安装完成
setTimeout(() => {
nextStep(2);
}, 500);
}
}
$.ajax({
url: '/core/install/install',
method: 'POST',
data: dbConfig,
success: function(response) {
if (response.code == 200) {
// 隐藏当前步骤,显示下一步
$('.step-content').removeClass('active');
$(`#step2`).addClass('active');
// 更新步骤指示器
$('.step-item').removeClass('active');
$(`.step-item[data-step=2]`).addClass('active');
runStep()
} else {
showNotification('error', '数据库连接失败:' + response.message);
}
},
error: function() {
showNotification('error', '请求失败,请检查网络连接');
}
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,109 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\basic;
use plugin\saiadmin\basic\contracts\LogicInterface;
/**
* 抽象逻辑层基类
* 定义通用属性和方法签名,具体实现由各 ORM 驱动完成
*/
abstract class AbstractLogic implements LogicInterface
{
/**
* 模型注入
* @var object
*/
protected $model;
/**
* 管理员信息
* @var array
*/
protected array $adminInfo;
/**
* 排序字段
* @var string
*/
protected string $orderField = '';
/**
* 排序方式
* @var string
*/
protected string $orderType = 'ASC';
/**
* 初始化
* @param $user
* @return void
*/
public function init($user): void
{
$this->adminInfo = $user;
}
/**
* 设置排序字段
* @param string $field
* @return static
*/
public function setOrderField(string $field): static
{
$this->orderField = $field;
return $this;
}
/**
* 设置排序方式
* @param string $type
* @return static
*/
public function setOrderType(string $type): static
{
$this->orderType = $type;
return $this;
}
/**
* 获取模型实例
* @return object
*/
public function getModel(): object
{
return $this->model;
}
/**
* 获取上传的导入文件
* @param $file
* @return string
*/
public function getImport($file): string
{
$full_dir = runtime_path() . '/resource/';
if (!is_dir($full_dir)) {
mkdir($full_dir, 0777, true);
}
$ext = $file->getUploadExtension() ?: null;
$full_path = $full_dir . md5(time()) . '.' . $ext;
$file->move($full_path);
return $full_path;
}
/**
* 方法调用代理到模型
* @param string $name
* @param array $arguments
* @return mixed
*/
public function __call(string $name, array $arguments): mixed
{
return call_user_func_array([$this->model, $name], $arguments);
}
}

View File

@@ -0,0 +1,74 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\basic;
use plugin\saiadmin\app\cache\UserInfoCache;
use plugin\saiadmin\exception\ApiException;
/**
* 基类 控制器继承此类
*/
class BaseController extends OpenController
{
/**
* 当前登陆管理员信息
*/
protected $adminInfo;
/**
* 当前登陆管理员ID
*/
protected int $adminId;
/**
* 当前登陆管理员账号
*/
protected string $adminName;
/**
* 逻辑层注入
*/
protected $logic;
/**
* 验证器注入
*/
protected $validate;
/**
* 初始化
*/
protected function init(): void
{
// 登录模式赋值
$isLogin = request()->header('check_login', false);
if ($isLogin) {
$result = request()->header('check_admin');
$this->adminId = $result['id'];
$this->adminName = $result['username'];
$this->adminInfo = UserInfoCache::getUserInfo($result['id']);
// 用户数据传递给逻辑层
$this->logic && $this->logic->init($this->adminInfo);
}
}
/**
* 验证器调用
*/
protected function validate(string $scene, $data): bool
{
if ($this->validate) {
if (!$this->validate->scene($scene)->check($data)) {
throw new ApiException($this->validate->getError());
}
}
return true;
}
}

View File

@@ -0,0 +1,77 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\basic;
use think\Validate;
/**
* 验证器基类
*/
class BaseValidate extends Validate
{
/**
* 验证是否唯一
* @access public
* @param mixed $value 字段值
* @param mixed $rule 验证规则 格式:数据表,字段名,排除ID,主键名
* @param array $data 数据
* @param string $field 验证字段名
* @return bool
*/
public function unique($value, $rule, array $data = [], string $field = ''): bool
{
if (is_string($rule)) {
$rule = explode(',', $rule);
}
if (str_contains($rule[0], '\\')) {
// 指定模型类
$db = new $rule[0];
} else {
return false;
}
$key = $rule[1] ?? $field;
$map = [];
if (str_contains($key, '^')) {
// 支持多个字段验证
$fields = explode('^', $key);
foreach ($fields as $key) {
if (isset($data[$key])) {
$map[] = [$key, '=', $data[$key]];
}
}
} elseif (strpos($key, '=')) {
// 支持复杂验证
parse_str($key, $array);
foreach ($array as $k => $val) {
$map[] = [$k, '=', $data[$k] ?? $val];
}
} elseif (isset($data[$field])) {
$map[] = [$key, '=', $data[$field]];
}
$pk = !empty($rule[3]) ? $rule[3] : $db->getPrimaryKeyName();
if (is_string($pk)) {
if (isset($rule[2])) {
$map[] = [$pk, '<>', $rule[2]];
} elseif (isset($data[$pk])) {
$map[] = [$pk, '<>', $data[$pk]];
}
}
if ($db->where($map)->count() > 0) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,60 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\basic;
use support\Request;
use support\Response;
/**
* 基类 控制器继承此类
*/
class OpenController
{
/**
* 构造方法
* @access public
*/
public function __construct()
{
// 控制器初始化
$this->init();
}
/**
* 成功返回json内容
* @param array|string $data
* @param string $msg
* @param int $option
* @return Response
*/
public function success(array | string $data = [], string $msg = 'success', int $option = JSON_UNESCAPED_UNICODE): Response
{
if (is_string($data)) {
$msg = $data;
}
return json(['code' => 200, 'message' => $msg, 'data' => $data], $option);
}
/**
* 失败返回json内容
* @param string $msg
* @return Response
*/
public function fail(string $msg = 'fail'): Response
{
return json(['code' => 400, 'message' => $msg]);
}
/**
* 初始化
*/
protected function init(): void
{
// TODO
}
}

View File

@@ -0,0 +1,79 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\basic\contracts;
/**
* Logic 接口定义
* 所有 Logic 基类必须实现此接口
*/
interface LogicInterface
{
/**
* 初始化
* @param mixed $user 用户信息
* @return void
*/
public function init($user): void;
/**
* 添加数据
* @param array $data
* @return mixed
*/
public function add(array $data): mixed;
/**
* 修改数据
* @param mixed $id
* @param array $data
* @return mixed
*/
public function edit($id, array $data): mixed;
/**
* 读取数据
* @param mixed $id
* @return mixed
*/
public function read($id): mixed;
/**
* 删除数据
* @param mixed $ids
* @return bool
*/
public function destroy($ids): bool;
/**
* 搜索器搜索
* @param array $searchWhere
* @return mixed
*/
public function search(array $searchWhere = []): mixed;
/**
* 分页查询数据
* @param mixed $query
* @return mixed
*/
public function getList($query): mixed;
/**
* 获取全部数据
* @param mixed $query
* @return mixed
*/
public function getAll($query): mixed;
/**
* 数据库事务操作
* @param callable $closure
* @param bool $isTran
* @return mixed
*/
public function transaction(callable $closure, bool $isTran = true): mixed;
}

View File

@@ -0,0 +1,27 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\basic\contracts;
/**
* Model 接口定义
* 所有 Model 基类必须实现此接口
*/
interface ModelInterface
{
/**
* 获取表名
* @return string
*/
public function getTableName(): string;
/**
* 获取主键名
* @return string
*/
public function getPrimaryKeyName(): string;
}

View File

@@ -0,0 +1,152 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\basic\eloquent;
use support\Db;
use plugin\saiadmin\basic\AbstractLogic;
use plugin\saiadmin\exception\ApiException;
/**
* Laravel Eloquent 逻辑层基类
*/
class BaseLogic extends AbstractLogic
{
/**
* 数据库事务操作
* @param callable $closure
* @param bool $isTran
* @return mixed
*/
public function transaction(callable $closure, bool $isTran = true): mixed
{
return $isTran ? Db::transaction($closure) : $closure();
}
/**
* 添加数据
* @param array $data
* @return mixed
*/
public function add(array $data): mixed
{
$model = $this->model->create($data);
return $model->getKey();
}
/**
* 修改数据
* @param mixed $id
* @param array $data
* @return mixed
*/
public function edit($id, array $data): mixed
{
$model = $this->model->find($id);
if (!$model) {
throw new ApiException('数据不存在');
}
return $model->update($data);
}
/**
* 读取数据
* @param mixed $id
* @return mixed
*/
public function read($id): mixed
{
$model = $this->model->find($id);
if (!$model) {
throw new ApiException('数据不存在');
}
return $model;
}
/**
* 删除数据
* @param mixed $ids
* @return bool
*/
public function destroy($ids): bool
{
return $this->model->destroy($ids);
}
/**
* 搜索器搜索
* @param array $searchWhere
* @return mixed
*/
public function search(array $searchWhere = []): mixed
{
$withSearch = array_keys($searchWhere);
$data = [];
foreach ($searchWhere as $key => $value) {
if ($value !== '' && $value !== null && $value !== []) {
$data[$key] = $value;
}
}
$withSearch = array_keys($data);
return $this->model->withSearch($withSearch, $data);
}
/**
* 分页查询数据
* @param mixed $query
* @return mixed
*/
public function getList($query): mixed
{
$request = request();
$saiType = $request ? $request->input('saiType', 'list') : 'list';
$page = $request ? $request->input('page', 1) : 1;
$limit = $request ? $request->input('limit', 10) : 10;
$orderField = $request ? $request->input('orderField', '') : '';
$orderType = $request ? $request->input('orderType', $this->orderType) : $this->orderType;
if (empty($orderField)) {
$orderField = $this->orderField !== '' ? $this->orderField : $this->model->getKeyName();
}
$query->orderBy($orderField, $orderType);
if ($saiType === 'all') {
return $query->get()->toArray();
}
$list = $query->paginate($limit, ['*'], 'page', $page);
return [
'current_page' => $list->currentPage(),
'per_page' => $list->perPage(),
'last_page' => $list->lastPage(),
'has_more' => $list->hasMorePages(),
'total' => $list->total(),
'data' => $list->items(),
];
}
/**
* 获取全部数据
* @param mixed $query
* @return mixed
*/
public function getAll($query): mixed
{
$request = request();
$orderField = $request ? $request->input('orderField', '') : '';
$orderType = $request ? $request->input('orderType', $this->orderType) : $this->orderType;
if (empty($orderField)) {
$orderField = $this->orderField !== '' ? $this->orderField : $this->model->getKeyName();
}
$query->orderBy($orderField, $orderType);
return $query->get()->toArray();
}
}

View File

@@ -0,0 +1,171 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\basic\eloquent;
use support\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use plugin\saiadmin\basic\contracts\ModelInterface;
/**
* Laravel Eloquent 模型基类
*/
class BaseModel extends Model implements ModelInterface
{
use SoftDeletes;
/**
* 创建时间字段
*/
const CREATED_AT = 'create_time';
/**
* 更新时间字段
*/
const UPDATED_AT = 'update_time';
/**
* 删除时间字段
*/
const DELETED_AT = 'delete_time';
/**
* 隐藏字段
* @var array
*/
protected $hidden = ['delete_time'];
/**
* 不可批量赋值的属性 (为空表示全部可赋值)
* @var array
*/
protected $guarded = [];
/**
* 类型转换
* @return array
*/
protected function casts(): array
{
return [
'create_time' => 'datetime:Y-m-d H:i:s',
'update_time' => 'datetime:Y-m-d H:i:s',
];
}
/**
* 处理时区问题
* @param \DateTimeInterface $date
* @return string
*/
protected function serializeDate(\DateTimeInterface $date): string
{
return $date->format($this->dateFormat ?: 'Y-m-d H:i:s');
}
/**
* 获取表名
* @return string
*/
public function getTableName(): string
{
return $this->getTable();
}
/**
* 获取主键名
* @return string
*/
public function getPrimaryKeyName(): string
{
return $this->getKeyName();
}
/**
* 搜索器搜索
* @param array $fields
* @param array $data
* @return mixed
*/
public function withSearch(array $fields, array $data): mixed
{
$query = $this->newQuery();
foreach ($fields as $field) {
$method = 'search' . ucfirst($this->toCamelCase($field)) . 'Attr';
if (method_exists($this, $method) && isset($data[$field]) && $data[$field] !== '') {
$this->$method($query, $data[$field]);
} else {
$query->where($field, $data[$field]);
}
}
return $query;
}
/**
* 将下划线命名转换为驼峰命名
* @param string $str
* @return string
*/
protected function toCamelCase(string $str): string
{
return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $str))));
}
/**
* 添加时间范围搜索
* @param $query
* @param $value
*/
public function searchCreateTimeAttr($query, $value)
{
if (is_array($value)) {
$query->whereBetween('create_time', $value);
} else {
$query->where('create_time', '=', $value);
}
}
/**
* 更新时间范围搜索
* @param mixed $query
* @param mixed $value
*/
public function searchUpdateTimeAttr($query, $value)
{
if (is_array($value)) {
$query->whereBetween('update_time', $value);
} else {
$query->where('update_time', '=', $value);
}
}
/**
* 模型启动事件
* @return void
*/
protected static function boot(): void
{
parent::boot();
// 创建前事件
static::creating(function ($model) {
$info = getCurrentInfo();
$schema = $model->getConnection()->getSchemaBuilder();
if ($info && $schema->hasColumn($model->getTable(), 'created_by')) {
$model->created_by = $info['id'];
}
});
// 保存前事件
static::saving(function ($model) {
$info = getCurrentInfo();
$schema = $model->getConnection()->getSchemaBuilder();
if ($info && $schema->hasColumn($model->getTable(), 'updated_by')) {
$model->updated_by = $info['id'];
}
});
}
}

View File

@@ -0,0 +1,142 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\basic\think;
use support\think\Db;
use plugin\saiadmin\basic\AbstractLogic;
use plugin\saiadmin\exception\ApiException;
/**
* ThinkORM 逻辑层基类
*/
class BaseLogic extends AbstractLogic
{
/**
* 数据库事务操作
* @param callable $closure
* @param bool $isTran
* @return mixed
*/
public function transaction(callable $closure, bool $isTran = true): mixed
{
return $isTran ? Db::transaction($closure) : $closure();
}
/**
* 添加数据
* @param array $data
* @return mixed
*/
public function add(array $data): mixed
{
$model = $this->model->create($data);
return $model->getKey();
}
/**
* 修改数据
* @param mixed $id
* @param array $data
* @return mixed
*/
public function edit($id, array $data): mixed
{
$model = $this->model->findOrEmpty($id);
if ($model->isEmpty()) {
throw new ApiException('数据不存在');
}
return $model->save($data);
}
/**
* 读取数据
* @param mixed $id
* @return mixed
*/
public function read($id): mixed
{
$model = $this->model->findOrEmpty($id);
if ($model->isEmpty()) {
throw new ApiException('数据不存在');
}
return $model;
}
/**
* 删除数据
* @param mixed $ids
* @return bool
*/
public function destroy($ids): bool
{
return $this->model->destroy($ids);
}
/**
* 搜索器搜索
* @param array $searchWhere
* @return mixed
*/
public function search(array $searchWhere = []): mixed
{
$withSearch = array_keys($searchWhere);
$data = [];
foreach ($searchWhere as $key => $value) {
if ($value !== '' && $value !== null && $value !== []) {
$data[$key] = $value;
}
}
$withSearch = array_keys($data);
return $this->model->withSearch($withSearch, $data);
}
/**
* 分页查询数据
* @param mixed $query
* @return mixed
*/
public function getList($query): mixed
{
$request = request();
$saiType = $request ? $request->input('saiType', 'list') : 'list';
$page = $request ? $request->input('page', 1) : 1;
$limit = $request ? $request->input('limit', 10) : 10;
$orderField = $request ? $request->input('orderField', '') : '';
$orderType = $request ? $request->input('orderType', $this->orderType) : $this->orderType;
if (empty($orderField)) {
$orderField = $this->orderField !== '' ? $this->orderField : $this->model->getPk();
}
$query->order($orderField, $orderType);
if ($saiType === 'all') {
return $query->select()->toArray();
}
return $query->paginate($limit, false, ['page' => $page])->toArray();
}
/**
* 获取全部数据
* @param mixed $query
* @return mixed
*/
public function getAll($query): mixed
{
$request = request();
$orderField = $request ? $request->input('orderField', '') : '';
$orderType = $request ? $request->input('orderType', $this->orderType) : $this->orderType;
if (empty($orderField)) {
$orderField = $this->orderField !== '' ? $this->orderField : $this->model->getPk();
}
$query->order($orderField, $orderType);
return $query->select()->toArray();
}
}

View File

@@ -0,0 +1,117 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\basic\think;
use support\think\Model;
use think\model\concern\SoftDelete;
use plugin\saiadmin\basic\contracts\ModelInterface;
/**
* ThinkORM 模型基类
*/
class BaseModel extends Model implements ModelInterface
{
use SoftDelete;
/**
* 删除时间字段
* @var string
*/
protected $deleteTime = 'delete_time';
/**
* 创建时间字段
* @var string
*/
protected $createTime = 'create_time';
/**
* 更新时间字段
* @var string
*/
protected $updateTime = 'update_time';
/**
* 隐藏字段
* @var array
*/
protected $hidden = ['delete_time'];
/**
* 只读字段
* @var array
*/
protected $readonly = ['created_by', 'create_time'];
/**
* 获取表名
* @return string
*/
public function getTableName(): string
{
return $this->getTable();
}
/**
* 获取主键名
* @return string
*/
public function getPrimaryKeyName(): string
{
return $this->getPk();
}
/**
* 添加时间范围搜索
* @param $query
* @param $value
*/
public function searchCreateTimeAttr($query, $value)
{
if (is_array($value)) {
$query->whereBetween('create_time', $value);
} else {
$query->where('create_time', '=', $value);
}
}
/**
* 更新时间范围搜索
* @param mixed $query
* @param mixed $value
*/
public function searchUpdateTimeAttr($query, $value)
{
if (is_array($value)) {
$query->whereBetween('update_time', $value);
} else {
$query->where('update_time', '=', $value);
}
}
/**
* 新增前事件
* @param Model $model
* @return void
*/
public static function onBeforeInsert($model): void
{
$info = getCurrentInfo();
$info && $model->setAttr('created_by', $info['id']);
}
/**
* 写入前事件
* @param Model $model
* @return void
*/
public static function onBeforeWrite($model): void
{
$info = getCurrentInfo();
$info && $model->setAttr('updated_by', $info['id']);
}
}

View File

@@ -0,0 +1,182 @@
<?php
namespace plugin\saiadmin\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Question\ChoiceQuestion;
/**
* SaiAdmin ORM 切换命令
* 用于切换 saiadmin 插件的 ORM 实现 (think / eloquent)
*/
class SaiOrm extends Command
{
protected static $defaultName = 'sai:orm';
protected static $defaultDescription = '切换 SaiAdmin 使用的 ORM';
/**
* ORM 源文件目录
*/
protected string $ormSourcePath;
/**
* 目标插件目录
*/
protected string $pluginAppPath;
/**
* ORM 选项配置
*/
protected array $ormOptions = [
'think' => '1. ThinkORM (TopThink)',
'eloquent' => '2. Eloquent ORM (Laravel)',
'exit' => '3. 退出,什么也不做',
];
protected function configure(): void
{
$this->setName('sai:orm')
->setDescription('切换 SaiAdmin 使用的 ORM');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('SaiAdmin ORM 切换工具');
$io->text('此命令只切换 saiadmin 框架核心使用的 ORM, 不影响其他模块功能, 多种 ORM 可以同时使用!');
$io->newLine();
// 创建选择问题编号从1开始
$helper = $this->getHelper('question');
$choices = [
1 => '1. ThinkORM (TopThink)',
2 => '2. Eloquent ORM (Laravel)',
3 => '3. 退出,什么也不做',
];
$question = new ChoiceQuestion(
'请选择要使用的 ORM 框架:',
$choices,
1 // 默认选中第一个
);
$question->setErrorMessage('选项 %s 无效');
// 获取用户选择
$selected = $helper->ask($input, $output, $question);
// 根据选择的文本反查 key
$selectedKey = array_search($selected, $choices);
// 如果选择退出
if ($selectedKey == 3) {
$io->newLine();
$io->info('已退出,什么也没做。');
return Command::SUCCESS;
}
// 映射选项到 ORM 类型
$ormMap = [1 => 'think', 2 => 'eloquent'];
$orm = $ormMap[$selectedKey];
$io->newLine();
$io->section("您选择了: {$selected}");
// 确认操作
if (!$io->confirm('确定要切换吗?这将覆盖 saiadmin 核心代码文件', true)) {
$io->warning('操作已取消');
return Command::SUCCESS;
}
// 设置路径
$this->ormSourcePath = BASE_PATH . '/vendor/saithink/saiadmin/src/orm/' . $orm . '/app';
$this->pluginAppPath = BASE_PATH . '/plugin/saiadmin/app';
// 检查源目录是否存在
if (!is_dir($this->ormSourcePath)) {
$io->error("ORM 源目录不存在: {$this->ormSourcePath}");
return Command::FAILURE;
}
$io->section('开始复制文件...');
try {
$copiedFiles = $this->copyDirectory($this->ormSourcePath, $this->pluginAppPath, $io);
$io->newLine();
$io->success([
"ORM 切换成功!",
"已切换到: {$selected}",
"复制文件数: {$copiedFiles}"
]);
$io->note([
'请重启 webman 服务使更改生效',
'命令: php webman restart 或 php windows.php'
]);
return Command::SUCCESS;
} catch (\Exception $e) {
$io->error("切换失败: " . $e->getMessage());
return Command::FAILURE;
}
}
/**
* 递归复制目录
* @param string $source 源目录
* @param string $dest 目标目录
* @param SymfonyStyle $io 输出接口
* @return int 复制的文件数量
*/
protected function copyDirectory(string $source, string $dest, SymfonyStyle $io): int
{
$count = 0;
if (!is_dir($dest)) {
mkdir($dest, 0755, true);
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
// 先计算文件总数用于进度条
$files = [];
foreach ($iterator as $item) {
if (!$item->isDir()) {
$files[] = $item;
}
}
// 创建进度条
$io->progressStart(count($files));
// 重新遍历并复制
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
$destPath = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName();
if ($item->isDir()) {
if (!is_dir($destPath)) {
mkdir($destPath, 0755, true);
}
} else {
copy($item->getPathname(), $destPath);
$count++;
$io->progressAdvance();
}
}
$io->progressFinish();
return $count;
}
}

View File

@@ -0,0 +1,297 @@
<?php
namespace plugin\saiadmin\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* SaiAdmin 插件创建命令
* 用于创建 saiadmin 插件
*/
class SaiPlugin extends Command
{
protected static $defaultName = 'sai:plugin';
protected static $defaultDescription = '创建 SaiAdmin 插件';
/**
* @return void
*/
protected function configure()
{
$this->addArgument('name', InputArgument::REQUIRED, 'App plugin name');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('SaiAdmin 插件创建工具');
$io->text('此命令用于创建基于webman的 saiadmin 插件, 用于扩展 saiadmin 框架功能!');
$io->newLine();
$name = $input->getArgument('name');
$io->text("创建 SaiAdmin 插件 $name");
if (strpos($name, '/') !== false) {
$io->error('名称错误,名称必须不包含字符 \'/\'');
return self::FAILURE;
}
// Create dir config/plugin/$name
if (is_dir($plugin_config_path = base_path() . "/plugin/$name")) {
$io->error("目录 $plugin_config_path 已存在");
return self::FAILURE;
}
$this->createAll($name);
$io->newLine();
$io->success("SaiAdmin 插件创建成功!");
return self::SUCCESS;
}
/**
* @param $name
* @return void
*/
protected function createAll($name)
{
$base_path = base_path();
$this->mkdir("$base_path/plugin/$name/app", 0777, true);
$this->mkdir("$base_path/plugin/$name/app/admin/controller", 0777, true);
$this->mkdir("$base_path/plugin/$name/app/admin/logic", 0777, true);
$this->mkdir("$base_path/plugin/$name/app/api/controller", 0777, true);
$this->mkdir("$base_path/plugin/$name/app/api/logic", 0777, true);
$this->mkdir("$base_path/plugin/$name/app/cache", 0777, true);
$this->mkdir("$base_path/plugin/$name/app/event", 0777, true);
$this->mkdir("$base_path/plugin/$name/app/model", 0777, true);
$this->mkdir("$base_path/plugin/$name/app/middleware", 0777, true);
$this->mkdir("$base_path/plugin/$name/config", 0777, true);
$this->createControllerFile("$base_path/plugin/$name/app/api/controller/IndexController.php", $name);
$this->createFunctionsFile("$base_path/plugin/$name/app/functions.php");
$this->createConfigFiles("$base_path/plugin/$name/config", $name);
}
/**
* @param $path
* @return void
*/
protected function mkdir($path, $mode = 0777, $recursive = false)
{
if (is_dir($path)) {
return;
}
echo "Create $path\r\n";
mkdir($path, $mode, $recursive);
}
/**
* @param $path
* @param $name
* @return void
*/
protected function createControllerFile($path, $name)
{
$content = <<<EOF
<?php
namespace plugin\\$name\\app\\api\\controller;
use plugin\\saiadmin\\basic\\OpenController;
class IndexController extends OpenController
{
public function index()
{
return \$this->success([
'app' => '$name',
'version' => '1.0.0',
]);
}
}
EOF;
file_put_contents($path, $content);
}
/**
* @param $file
* @return void
*/
protected function createFunctionsFile($file)
{
$content = <<<EOF
<?php
/**
* Here is your custom functions.
*/
EOF;
file_put_contents($file, $content);
}
/**
* @param $base
* @param $name
* @return void
*/
protected function createConfigFiles($base, $name)
{
// app.php
$content = <<<EOF
<?php
use support\\Request;
return [
'debug' => true,
'controller_suffix' => 'Controller',
'controller_reuse' => false,
'version' => '1.0.0'
];
EOF;
file_put_contents("$base/app.php", $content);
// autoload.php
$content = <<<EOF
<?php
return [
'files' => [
base_path() . '/plugin/$name/app/functions.php',
]
];
EOF;
file_put_contents("$base/autoload.php", $content);
// container.php
$content = <<<EOF
<?php
return new Webman\\Container;
EOF;
file_put_contents("$base/container.php", $content);
// exception.php
$content = <<<EOF
<?php
return [
'' => \\plugin\\saiadmin\\app\\exception\\Handler::class,
];
EOF;
file_put_contents("$base/exception.php", $content);
// log.php
$content = <<<EOF
<?php
return [
'default' => [
'handlers' => [
[
'class' => Monolog\\Handler\\RotatingFileHandler::class,
'constructor' => [
runtime_path() . '/logs/$name.log',
7,
Monolog\\Logger::DEBUG,
],
'formatter' => [
'class' => Monolog\\Formatter\\LineFormatter::class,
'constructor' => [null, 'Y-m-d H:i:s', true],
],
]
],
],
];
EOF;
file_put_contents("$base/log.php", $content);
// middleware.php
$content = <<<EOF
<?php
use plugin\saiadmin\app\middleware\SystemLog;
use plugin\saiadmin\app\middleware\CheckLogin;
use plugin\saiadmin\app\middleware\CheckAuth;
return [
'admin' => [
CheckLogin::class,
CheckAuth::class,
SystemLog::class,
],
'api' => [
]
];
EOF;
file_put_contents("$base/middleware.php", $content);
// process.php
$content = <<<EOF
<?php
return [];
EOF;
file_put_contents("$base/process.php", $content);
// route.php
$content = <<<EOF
<?php
use Webman\\Route;
EOF;
file_put_contents("$base/route.php", $content);
// static.php
$content = <<<EOF
<?php
return [
'enable' => true,
'middleware' => [], // Static file Middleware
];
EOF;
file_put_contents("$base/static.php", $content);
// translation.php
$content = <<<EOF
<?php
return [
// Default language
'locale' => 'zh_CN',
// Fallback language
'fallback_locale' => ['zh_CN', 'en'],
// Folder where language files are stored
'path' => base_path() . "/plugin/$name/resource/translations",
];
EOF;
file_put_contents("$base/translation.php", $content);
}
}

View File

@@ -0,0 +1,198 @@
<?php
namespace plugin\saiadmin\command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* SaiAdmin 升级命令
* 用于从 vendor 目录升级 saiadmin 插件到最新版本
*/
class SaiUpgrade extends Command
{
protected static $defaultName = 'sai:upgrade';
protected static $defaultDescription = '升级 SaiAdmin 插件到最新版本';
/**
* 升级源目录
*/
protected string $sourcePath;
/**
* 目标插件目录
*/
protected string $targetPath;
protected function configure(): void
{
$this->setName('sai:upgrade')
->setDescription('升级 SaiAdmin 插件到最新版本');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('SaiAdmin 升级工具');
$io->text([
'此命令将从 vendor 目录复制最新版本的 saiadmin 插件文件到 plugin 目录',
'源目录: vendor/saithink/saiadmin/src/plugin/saiadmin',
'目标目录: plugin/saiadmin',
]);
$io->newLine();
// 设置路径
$this->sourcePath = BASE_PATH . '/vendor/saithink/saiadmin/src/plugin/saiadmin';
$this->targetPath = BASE_PATH . '/plugin/saiadmin';
// 检查源目录是否存在
if (!is_dir($this->sourcePath)) {
$io->error([
"升级源目录不存在: {$this->sourcePath}",
"请确保已通过 composer 安装了 saithink/saiadmin 包",
]);
return Command::FAILURE;
}
// 获取版本信息
$currentVersion = $this->getVersion($this->targetPath);
$latestVersion = $this->getVersion($this->sourcePath);
// 显示版本信息
$io->section('版本信息');
$io->table(
['项目', '版本'],
[
['当前版本', $currentVersion ?: '未知'],
['最新版本', $latestVersion ?: '未知'],
]
);
// 版本对比提示
if ($currentVersion && $latestVersion) {
if (version_compare($currentVersion, $latestVersion, '>=')) {
$io->success('当前已是最新版本!');
if (!$io->confirm('是否仍要继续覆盖安装?', false)) {
$io->info('操作已取消');
return Command::SUCCESS;
}
} else {
$io->info("发现新版本: {$currentVersion}{$latestVersion}");
}
}
// 警告信息
$io->warning([
"注意:此操作将覆盖 {$this->targetPath} 目录的现有文件!",
"建议在执行前备份您的自定义修改。",
]);
// 确认操作
if (!$io->confirm('确定要执行升级操作吗?', false)) {
$io->info('操作已取消');
return Command::SUCCESS;
}
$io->section('开始升级...');
try {
$copiedFiles = $this->copyDirectory($this->sourcePath, $this->targetPath, $io);
$io->newLine();
$io->success([
"SaiAdmin 升级成功!",
"复制文件数: {$copiedFiles}",
]);
$io->note([
'请重启 webman 服务使更改生效',
'命令: php webman restart 或 php windows.php',
]);
return Command::SUCCESS;
} catch (\Exception $e) {
$io->error("升级失败: " . $e->getMessage());
return Command::FAILURE;
}
}
/**
* 递归复制目录
* @param string $source 源目录
* @param string $dest 目标目录
* @param SymfonyStyle $io 输出接口
* @return int 复制的文件数量
*/
protected function copyDirectory(string $source, string $dest, SymfonyStyle $io): int
{
$count = 0;
if (!is_dir($dest)) {
mkdir($dest, 0755, true);
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
// 先计算文件总数用于进度条
$files = [];
foreach ($iterator as $item) {
if (!$item->isDir()) {
$files[] = $item;
}
}
// 创建进度条
$io->progressStart(count($files));
// 重新遍历并复制
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
$destPath = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName();
if ($item->isDir()) {
if (!is_dir($destPath)) {
mkdir($destPath, 0755, true);
}
} else {
copy($item->getPathname(), $destPath);
$count++;
$io->progressAdvance();
}
}
$io->progressFinish();
return $count;
}
/**
* 从目录中获取版本号
* @param string $path 插件目录路径
* @return string|null 版本号
*/
protected function getVersion(string $path): ?string
{
$configFile = $path . '/config/app.php';
if (!file_exists($configFile)) {
return null;
}
try {
$config = include $configFile;
return $config['version'] ?? null;
} catch (\Exception $e) {
return null;
}
}
}

View File

@@ -0,0 +1,10 @@
<?php
use support\Request;
return [
'debug' => true,
'controller_suffix' => 'Controller',
'controller_reuse' => false,
'version' => '6.0.7'
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'files' => [
base_path() . '/plugin/saiadmin/app/functions.php',
]
];

View File

@@ -0,0 +1,2 @@
<?php
return new Webman\Container;

View File

@@ -0,0 +1,2 @@
<?php
return [];

View File

@@ -0,0 +1,9 @@
<?php
return [
'user.login' => [
[plugin\saiadmin\app\event\SystemUser::class, 'login'],
],
'user.operateLog' => [
[plugin\saiadmin\app\event\SystemUser::class, 'operateLog'],
]
];

View File

@@ -0,0 +1,5 @@
<?php
return [
'' => \plugin\saiadmin\app\exception\Handler::class,
];

View File

@@ -0,0 +1,20 @@
<?php
return [
'default' => [
'handlers' => [
[
'class' => Monolog\Handler\RotatingFileHandler::class,
'constructor' => [
runtime_path() . '/logs/saiadmin.log',
7,
Monolog\Logger::DEBUG,
],
'formatter' => [
'class' => Monolog\Formatter\LineFormatter::class,
'constructor' => [null, 'Y-m-d H:i:s', true],
],
]
],
],
];

View File

@@ -0,0 +1,13 @@
<?php
use plugin\saiadmin\app\middleware\SystemLog;
use plugin\saiadmin\app\middleware\CheckLogin;
use plugin\saiadmin\app\middleware\CheckAuth;
return [
'' => [
CheckLogin::class,
CheckAuth::class,
SystemLog::class,
]
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'task' => [
'handler' => plugin\saiadmin\process\Task::class
]
];

View File

@@ -0,0 +1,115 @@
<?php
use Webman\Route;
Route::group('/core', function () {
Route::get('/install', [plugin\saiadmin\app\controller\InstallController::class, 'index']);
Route::post('/install/install', [plugin\saiadmin\app\controller\InstallController::class, 'install']);
Route::get('/captcha', [plugin\saiadmin\app\controller\LoginController::class, 'captcha']);
Route::post('/login', [plugin\saiadmin\app\controller\LoginController::class, 'login']);
Route::get('/system/user', [plugin\saiadmin\app\controller\SystemController::class, 'userInfo']);
Route::get("/system/dictAll", [plugin\saiadmin\app\controller\SystemController::class, 'dictAll']);
Route::get('/system/menu', [plugin\saiadmin\app\controller\SystemController::class, 'menu']);
Route::get('/system/statistics', [plugin\saiadmin\app\controller\SystemController::class, 'statistics']);
Route::get('/system/loginChart', [plugin\saiadmin\app\controller\SystemController::class, 'loginChart']);
Route::get('/system/loginBarChart', [plugin\saiadmin\app\controller\SystemController::class, 'loginBarChart']);
Route::get('/system/clearAllCache', [plugin\saiadmin\app\controller\SystemController::class, 'clearAllCache']);
Route::get("/system/getResourceCategory", [plugin\saiadmin\app\controller\SystemController::class, 'getResourceCategory']);
Route::get("/system/getResourceList", [plugin\saiadmin\app\controller\SystemController::class, 'getResourceList']);
Route::post("/system/saveNetworkImage", [plugin\saiadmin\app\controller\SystemController::class, 'saveNetworkImage']);
Route::post("/system/uploadImage", [plugin\saiadmin\app\controller\SystemController::class, 'uploadImage']);
Route::post("/system/uploadFile", [plugin\saiadmin\app\controller\SystemController::class, 'uploadFile']);
Route::post("/system/chunkUpload", [plugin\saiadmin\app\controller\SystemController::class, 'chunkUpload']);
Route::get("/system/getUserList", [plugin\saiadmin\app\controller\SystemController::class, 'getUserList']);
Route::get("/system/getLoginLogList", [plugin\saiadmin\app\controller\SystemController::class, 'getLoginLogList']);
Route::get("/system/getOperationLogList", [plugin\saiadmin\app\controller\SystemController::class, 'getOperationLogList']);
// 用户管理
fastRoute("user", \plugin\saiadmin\app\controller\system\SystemUserController::class);
Route::post("/user/updateInfo", [\plugin\saiadmin\app\controller\system\SystemUserController::class, 'updateInfo']);
Route::post("/user/modifyPassword", [\plugin\saiadmin\app\controller\system\SystemUserController::class, 'modifyPassword']);
Route::post("/user/clearCache", [\plugin\saiadmin\app\controller\system\SystemUserController::class, 'clearCache']);
Route::post("/user/initUserPassword", [\plugin\saiadmin\app\controller\system\SystemUserController::class, 'initUserPassword']);
Route::post("/user/setHomePage", [\plugin\saiadmin\app\controller\system\SystemUserController::class, 'setHomePage']);
// 角色管理
fastRoute('role', \plugin\saiadmin\app\controller\system\SystemRoleController::class);
Route::get("/role/accessRole", [\plugin\saiadmin\app\controller\system\SystemRoleController::class, 'accessRole']);
Route::get("/role/getMenuByRole", [\plugin\saiadmin\app\controller\system\SystemRoleController::class, 'getMenuByRole']);
Route::post("/role/menuPermission", [\plugin\saiadmin\app\controller\system\SystemRoleController::class, 'menuPermission']);
// 部门管理
fastRoute("dept", \plugin\saiadmin\app\controller\system\SystemDeptController::class);
Route::get("/dept/accessDept", [\plugin\saiadmin\app\controller\system\SystemDeptController::class, 'accessDept']);
// 岗位管理
fastRoute('post', \plugin\saiadmin\app\controller\system\SystemPostController::class);
Route::get("/post/accessPost", [\plugin\saiadmin\app\controller\system\SystemPostController::class, 'accessPost']);
Route::post("/post/downloadTemplate", [plugin\saiadmin\app\controller\system\SystemPostController::class, 'downloadTemplate']);
// 菜单管理
fastRoute('menu', \plugin\saiadmin\app\controller\system\SystemMenuController::class);
Route::get("/menu/accessMenu", [\plugin\saiadmin\app\controller\system\SystemMenuController::class, 'accessMenu']);
// 字典类型管理
fastRoute('dictType', \plugin\saiadmin\app\controller\system\SystemDictTypeController::class);
// 字典数据管理
fastRoute('dictData', \plugin\saiadmin\app\controller\system\SystemDictDataController::class);
// 附件管理
fastRoute('attachment', \plugin\saiadmin\app\controller\system\SystemAttachmentController::class);
Route::post("/attachment/move", [\plugin\saiadmin\app\controller\system\SystemAttachmentController::class, 'move']);
// 附件分类
fastRoute('category', \plugin\saiadmin\app\controller\system\SystemCategoryController::class);
// 系统设置
fastRoute('configGroup', \plugin\saiadmin\app\controller\system\SystemConfigGroupController::class);
Route::post("/configGroup/email", [\plugin\saiadmin\app\controller\system\SystemConfigGroupController::class, 'email']);
fastRoute('config', \plugin\saiadmin\app\controller\system\SystemConfigController::class);
Route::post("/config/batchUpdate", [\plugin\saiadmin\app\controller\system\SystemConfigController::class, 'batchUpdate']);
// 日志管理
Route::get("/logs/getLoginLogPageList", [\plugin\saiadmin\app\controller\system\SystemLogController::class, 'getLoginLogPageList']);
Route::delete("/logs/deleteLoginLog", [\plugin\saiadmin\app\controller\system\SystemLogController::class, 'deleteLoginLog']);
Route::get("/logs/getOperLogPageList", [\plugin\saiadmin\app\controller\system\SystemLogController::class, 'getOperLogPageList']);
Route::delete("/logs/deleteOperLog", [\plugin\saiadmin\app\controller\system\SystemLogController::class, 'deleteOperLog']);
fastRoute("email", \plugin\saiadmin\app\controller\system\SystemMailController::class);
// 服务管理
Route::get("/server/monitor", [\plugin\saiadmin\app\controller\system\SystemServerController::class, 'monitor']);
Route::get("/server/cache", [\plugin\saiadmin\app\controller\system\SystemServerController::class, 'cache']);
Route::post("/server/clear", [\plugin\saiadmin\app\controller\system\SystemServerController::class, 'clear']);
// 数据表维护
Route::get("/database/index", [\plugin\saiadmin\app\controller\system\DataBaseController::class, 'index']);
Route::get("/database/recycle", [\plugin\saiadmin\app\controller\system\DataBaseController::class, 'recycle']);
Route::delete("/database/delete", [\plugin\saiadmin\app\controller\system\DataBaseController::class, 'delete']);
Route::post("/database/recovery", [\plugin\saiadmin\app\controller\system\DataBaseController::class, 'recovery']);
Route::get("/database/dataSource", [\plugin\saiadmin\app\controller\system\DataBaseController::class, 'source']);
Route::get("/database/detailed", [\plugin\saiadmin\app\controller\system\DataBaseController::class, 'detailed']);
Route::post("/database/optimize", [\plugin\saiadmin\app\controller\system\DataBaseController::class, 'optimize']);
Route::post("/database/fragment", [\plugin\saiadmin\app\controller\system\DataBaseController::class, 'fragment']);
});
Route::group('/tool', function () {
// 定时任务
fastRoute('crontab', \plugin\saiadmin\app\controller\tool\CrontabController::class);
Route::post("/crontab/run", [\plugin\saiadmin\app\controller\tool\CrontabController::class, 'run']);
Route::get("/crontab/logPageList", [\plugin\saiadmin\app\controller\tool\CrontabController::class, 'logPageList']);
Route::delete('/crontab/deleteCrontabLog', [\plugin\saiadmin\app\controller\tool\CrontabController::class, 'deleteCrontabLog']);
// 代码生成
fastRoute('code', \plugin\saiadmin\app\controller\tool\GenerateTablesController::class);
Route::get("/code/getTableColumns", [\plugin\saiadmin\app\controller\tool\GenerateTablesController::class, 'getTableColumns']);
Route::get("/code/preview", [\plugin\saiadmin\app\controller\tool\GenerateTablesController::class, 'preview']);
Route::post("/code/loadTable", [\plugin\saiadmin\app\controller\tool\GenerateTablesController::class, 'loadTable']);
Route::post("/code/generate", [\plugin\saiadmin\app\controller\tool\GenerateTablesController::class, 'generate']);
Route::post("/code/generateFile", [\plugin\saiadmin\app\controller\tool\GenerateTablesController::class, 'generateFile']);
Route::post("/code/sync", [\plugin\saiadmin\app\controller\tool\GenerateTablesController::class, 'sync']);
});
Route::disableDefaultRoute('saiadmin');

View File

@@ -0,0 +1,74 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
return [
'access_exp' => 8 * 60 * 60, // 登录token有效期默认8小时
// 验证码存储模式
'captcha' => [
// 验证码存储模式 session或者cache
'mode' => getenv('CAPTCHA_MODE'),
// 验证码过期时间 (秒)
'expire' => 300,
],
// excel模板下载路径
'template' => base_path(). '/plugin/saiadmin/public/template',
// excel导出文件路径
'export_path' => base_path() . '/plugin/saiadmin/public/export/',
// 文件开启hash验证开启后上传文件将会判断数据库中是否存在如果存在直接获取
'file_hash' => false,
// 用户信息缓存
'user_cache' => [
'prefix' => 'saiadmin:user_cache:info_',
'expire' => 60 * 60 * 4,
'dept' => 'saiadmin:user_cache:dept_',
'role' => 'saiadmin:user_cache:role_',
'post' => 'saiadmin:user_cache:post_',
],
// 用户权限缓存
'button_cache' => [
'prefix' => 'saiadmin:button_cache:user_',
'expire' => 60 * 60 * 2,
'all' => 'saiadmin:button_cache:all',
'role' => 'saiadmin:button_cache:role_',
'tag' => 'saiadmin:button_cache',
],
// 用户菜单缓存
'menu_cache' => [
'prefix' => 'saiadmin:menu_cache:user_',
'expire' => 60 * 60 * 24 * 7,
'tag' => 'saiadmin:menu_cache',
],
// 字典缓存
'dict_cache' => [
'expire' => 60 * 60 * 24 * 365,
'tag' => 'saiadmin:dict_cache',
],
// 配置数据缓存
'config_cache' => [
'expire' => 60 * 60 * 24 * 365,
'prefix' => 'saiadmin:config_cache:config_',
'tag' => 'saiadmin:config_cache'
],
// 反射缓存
'reflection_cache' => [
'tag' => 'saiadmin:reflection',
'expire' => 60 * 60 * 24 * 365,
'no_need' => 'saiadmin:reflection_cache:no_need_',
'attr' => 'saiadmin:reflection_cache:attr_',
],
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'enable' => true,
'middleware' => [], // Static file Middleware
];

View File

@@ -0,0 +1,10 @@
<?php
return [
// Default language
'locale' => 'zh_CN',
// Fallback language
'fallback_locale' => ['zh_CN', 'en'],
// Folder where language files are stored
'path' => base_path() . "/plugin/saiadmin/resource/translations",
];

View File

@@ -0,0 +1,10 @@
<?php
use support\view\Raw;
use support\view\Twig;
use support\view\Blade;
use support\view\ThinkPHP;
return [
'handler' => Twig::class
];

View File

@@ -0,0 +1,4 @@
-- ----------------------------
-- Records of sa_system_menu
-- ----------------------------
INSERT INTO `sa_system_menu` SELECT NULL, 0, '插件市场', 'Plugin', '', 2, '/plugin', '/plugin/saipackage/install/index', NULL, 'ri:apps-2-ai-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sa_system_menu` WHERE `code` = 'Plugin' AND `create_time` = '2026-01-01 00:00:00' AND ISNULL(`delete_time`));

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,766 @@
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sa_system_attachment
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_attachment`;
CREATE TABLE `sa_system_attachment` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`category_id` int(11) NULL DEFAULT 0 COMMENT '文件分类',
`storage_mode` smallint(6) NULL DEFAULT 1 COMMENT '存储模式 (1 本地 2 阿里云 3 七牛云 4 腾讯云)',
`origin_name` varchar(255) NULL DEFAULT NULL COMMENT '原文件名',
`object_name` varchar(50) NULL DEFAULT NULL COMMENT '新文件名',
`hash` varchar(64) NULL DEFAULT NULL COMMENT '文件hash',
`mime_type` varchar(255) NULL DEFAULT NULL COMMENT '资源类型',
`storage_path` varchar(100) NULL DEFAULT NULL COMMENT '存储目录',
`suffix` varchar(10) NULL DEFAULT NULL COMMENT '文件后缀',
`size_byte` bigint(20) NULL DEFAULT NULL COMMENT '字节数',
`size_info` varchar(50) NULL DEFAULT NULL COMMENT '文件大小',
`url` varchar(255) NULL DEFAULT NULL COMMENT 'url地址',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `hash`(`hash`) USING BTREE,
INDEX `idx_url`(`url`) USING BTREE,
INDEX `idx_create_time`(`create_time`) USING BTREE,
INDEX `idx_category_id`(`category_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '附件信息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_attachment
-- ----------------------------
-- ----------------------------
-- Table structure for sa_system_category
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_category`;
CREATE TABLE `sa_system_category` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
`parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父id',
`level` varchar(255) NULL DEFAULT NULL COMMENT '组集关系',
`category_name` varchar(100) NOT NULL DEFAULT '' COMMENT '分类名称',
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序',
`status` tinyint(1) NULL DEFAULT 1 COMMENT '状态',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `pid`(`parent_id`) USING BTREE,
INDEX `sort`(`sort`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 COMMENT = '附件分类表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_category
-- ----------------------------
INSERT INTO `sa_system_category` VALUES (1, 0, '0,', '全部分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_category` VALUES (2, 1, '0,1,', '图片分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_category` VALUES (3, 1, '0,1,', '文件分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_category` VALUES (4, 1, '0,1,', '系统图片', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_category` VALUES (5, 1, '0,1,', '其他分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
-- ----------------------------
-- Table structure for sa_system_config
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_config`;
CREATE TABLE `sa_system_config` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '编号',
`group_id` int(11) NULL DEFAULT NULL COMMENT '组id',
`key` varchar(32) NOT NULL COMMENT '配置键名',
`value` text NULL COMMENT '配置值',
`name` varchar(255) NULL DEFAULT NULL COMMENT '配置名称',
`input_type` varchar(32) NULL DEFAULT NULL COMMENT '数据输入类型',
`config_select_data` varchar(500) NULL DEFAULT NULL COMMENT '配置选项数据',
`sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建人',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新人',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`, `key`) USING BTREE,
INDEX `group_id`(`group_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 302 COMMENT = '参数配置信息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_config
-- ----------------------------
INSERT INTO `sa_system_config` VALUES (1, 1, 'site_copyright', 'Copyright © 2024 saithink', '版权信息', 'textarea', NULL, 96, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (2, 1, 'site_desc', '基于vue3 + webman 的极速开发框架', '网站描述', 'textarea', NULL, 97, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (3, 1, 'site_keywords', '后台管理系统', '网站关键字', 'input', NULL, 98, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (4, 1, 'site_name', 'SaiAdmin', '网站名称', 'input', NULL, 99, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (5, 1, 'site_record_number', '9527', '网站备案号', 'input', NULL, 95, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (6, 2, 'upload_allow_file', 'txt,doc,docx,xls,xlsx,ppt,pptx,rar,zip,7z,gz,pdf,wps,md,jpg,png,jpeg,mp4,pem,crt', '文件类型', 'input', NULL, 0, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (7, 2, 'upload_allow_image', 'jpg,jpeg,png,gif,svg,bmp', '图片类型', 'input', NULL, 0, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (8, 2, 'upload_mode', '1', '上传模式', 'select', '[{\"label\":\"本地上传\",\"value\":\"1\"},{\"label\":\"阿里云OSS\",\"value\":\"2\"},{\"label\":\"七牛云\",\"value\":\"3\"},{\"label\":\"腾讯云COS\",\"value\":\"4\"},{\"label\":\"亚马逊S3\",\"value\":\"5\"}]', 99, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (10, 2, 'upload_size', '52428800', '上传大小', 'input', NULL, 88, '单位Byte,1MB=1024*1024Byte', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (11, 2, 'local_root', 'public/storage/', '本地存储路径', 'input', NULL, 0, '本地存储文件路径', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (12, 2, 'local_domain', 'http://127.0.0.1:8787', '本地存储域名', 'input', NULL, 0, 'http://127.0.0.1:8787', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (13, 2, 'local_uri', '/storage/', '本地访问路径', 'input', NULL, 0, '访问是通过domain + uri', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (14, 2, 'qiniu_accessKey', '', '七牛key', 'input', NULL, 0, '七牛云存储secretId', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (15, 2, 'qiniu_secretKey', '', '七牛secret', 'input', NULL, 0, '七牛云存储secretKey', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (16, 2, 'qiniu_bucket', '', '七牛bucket', 'input', NULL, 0, '七牛云存储bucket', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (17, 2, 'qiniu_dirname', '', '七牛dirname', 'input', NULL, 0, '七牛云存储dirname', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (18, 2, 'qiniu_domain', '', '七牛domain', 'input', NULL, 0, '七牛云存储domain', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (19, 2, 'cos_secretId', '', '腾讯Id', 'input', NULL, 0, '腾讯云存储secretId', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (20, 2, 'cos_secretKey', '', '腾讯key', 'input', NULL, 0, '腾讯云secretKey', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (21, 2, 'cos_bucket', '', '腾讯bucket', 'input', NULL, 0, '腾讯云存储bucket', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (22, 2, 'cos_dirname', '', '腾讯dirname', 'input', NULL, 0, '腾讯云存储dirname', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (23, 2, 'cos_domain', '', '腾讯domain', 'input', NULL, 0, '腾讯云存储domain', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (24, 2, 'cos_region', '', '腾讯region', 'input', NULL, 0, '腾讯云存储region', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (25, 2, 'oss_accessKeyId', '', '阿里Id', 'input', NULL, 0, '阿里云存储accessKeyId', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (26, 2, 'oss_accessKeySecret', '', '阿里Secret', 'input', NULL, 0, '阿里云存储accessKeySecret', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (27, 2, 'oss_bucket', '', '阿里bucket', 'input', NULL, 0, '阿里云存储bucket', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (28, 2, 'oss_dirname', '', '阿里dirname', 'input', NULL, 0, '阿里云存储dirname', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (29, 2, 'oss_domain', '', '阿里domain', 'input', NULL, 0, '阿里云存储domain', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (30, 2, 'oss_endpoint', '', '阿里endpoint', 'input', NULL, 0, '阿里云存储endpoint', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (31, 3, 'Host', 'smtp.qq.com', 'SMTP服务器', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (32, 3, 'Port', '465', 'SMTP端口', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (33, 3, 'Username', '', 'SMTP用户名', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (34, 3, 'Password', '', 'SMTP密码', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (35, 3, 'SMTPSecure', 'ssl', 'SMTP验证方式', 'radio', '[\r\n {\"label\":\"ssl\",\"value\":\"ssl\"},\r\n {\"label\":\"tsl\",\"value\":\"tsl\"}\r\n]', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (36, 3, 'From', '', '默认发件人', 'input', '', 100, '默认发件的邮箱地址', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (37, 3, 'FromName', '账户注册', '默认发件名称', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (38, 3, 'CharSet', 'UTF-8', '编码', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (39, 3, 'SMTPDebug', '0', '调试模式', 'radio', '[\r\n {\"label\":\"关闭\",\"value\":\"0\"},\r\n {\"label\":\"client\",\"value\":\"1\"},\r\n {\"label\":\"server\",\"value\":\"2\"}\r\n]', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (40, 2, 's3_key', '', 'key', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (41, 2, 's3_secret', '', 'secret', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (42, 2, 's3_bucket', '', 'bucket', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (43, 2, 's3_dirname', '', 'dirname', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (44, 2, 's3_domain', '', 'domain', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (45, 2, 's3_region', '', 'region', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (46, 2, 's3_version', '', 'version', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (47, 2, 's3_use_path_style_endpoint', '', 'path_style_endpoint', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (48, 2, 's3_endpoint', '', 'endpoint', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config` VALUES (49, 2, 's3_acl', '', 'acl', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
-- ----------------------------
-- Table structure for sa_system_config_group
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_config_group`;
CREATE TABLE `sa_system_config_group` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(50) NULL DEFAULT NULL COMMENT '字典名称',
`code` varchar(100) NULL DEFAULT NULL COMMENT '字典标示',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建人',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新人',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 COMMENT = '参数配置分组表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_config_group
-- ----------------------------
INSERT INTO `sa_system_config_group` VALUES (1, '站点配置', 'site_config', '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config_group` VALUES (2, '上传配置', 'upload_config', NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_config_group` VALUES (3, '邮件服务', 'email_config', NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
-- ----------------------------
-- Table structure for sa_system_dept
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_dept`;
CREATE TABLE `sa_system_dept` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '父级ID0为根节点',
`name` varchar(64) NOT NULL COMMENT '部门名称',
`code` varchar(64) NULL DEFAULT NULL COMMENT '部门编码',
`leader_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '部门负责人ID',
`level` varchar(255) NULL DEFAULT '' COMMENT '祖级列表,格式: 0,1,5, (便于查询子孙节点)',
`sort` int(11) NULL DEFAULT 0 COMMENT '排序,数字越小越靠前',
`status` tinyint(1) NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_parent_id`(`parent_id`) USING BTREE,
INDEX `idx_path`(`level`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1114 COMMENT = '部门表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_dept
-- ----------------------------
INSERT INTO `sa_system_dept` VALUES (1, 0, '腾讯集团', 'GROUP', 1, '0,', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
-- ----------------------------
-- Table structure for sa_system_dict_data
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_dict_data`;
CREATE TABLE `sa_system_dict_data` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`type_id` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '字典类型ID',
`label` varchar(50) NULL DEFAULT NULL COMMENT '字典标签',
`value` varchar(100) NULL DEFAULT NULL COMMENT '字典值',
`color` varchar(50) NULL DEFAULT NULL COMMENT '字典颜色',
`code` varchar(100) NULL DEFAULT NULL COMMENT '字典标示',
`sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序',
`status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `type_id`(`type_id`) USING BTREE,
INDEX `idx_code`(`code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 50 COMMENT = '字典数据表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_dict_data
-- ----------------------------
INSERT INTO `sa_system_dict_data` VALUES (2, 2, '本地存储', '1', '#5d87ff', 'upload_mode', 99, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (3, 2, '阿里云OSS', '2', '#f9901f', 'upload_mode', 98, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (4, 2, '七牛云', '3', '#00ced1', 'upload_mode', 97, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (5, 2, '腾讯云COS', '4', '#1d84ff', 'upload_mode', 96, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (6, 2, '亚马逊S3', '5', '#ff80c8', 'upload_mode', 95, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (7, 3, '正常', '1', '#13deb9', 'data_status', 0, 1, '1为正常', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (8, 3, '停用', '2', '#ff4d4f', 'data_status', 0, 1, '2为停用', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (9, 4, '统计页面', 'statistics', '#00ced1', 'dashboard', 100, 1, '管理员用', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (10, 4, '工作台', 'work', '#ff8c00', 'dashboard', 50, 1, '员工使用', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (11, 5, '', '1', '#5d87ff', 'gender', 0, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (12, 5, '', '2', '#ff4500', 'gender', 0, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (13, 5, '未知', '3', '#b48df3', 'gender', 0, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (16, 12, '图片', 'image', '#60c041', 'attachment_type', 10, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (17, 12, '文档', 'text', '#1d84ff', 'attachment_type', 9, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (18, 12, '音频', 'audio', '#00ced1', 'attachment_type', 8, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (19, 12, '视频', 'video', '#ff4500', 'attachment_type', 7, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (20, 12, '应用程序', 'application', '#ff8c00', 'attachment_type', 6, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (21, 13, '目录', '1', '#909399', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (22, 13, '菜单', '2', '#1e90ff', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (23, 13, '按钮', '3', '#ff4500', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (24, 13, '外链', '4', '#00ced1', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (25, 14, '', '1', '#60c041', 'yes_or_no', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (26, 14, '', '2', '#ff4500', 'yes_or_no', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (47, 20, 'URL任务GET', '1', '#5d87ff', 'crontab_task_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (48, 20, 'URL任务POST', '2', '#00ced1', 'crontab_task_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_data` VALUES (49, 20, '类任务', '3', '#ff8c00', 'crontab_task_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
-- ----------------------------
-- Table structure for sa_system_dict_type
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_dict_type`;
CREATE TABLE `sa_system_dict_type` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(50) NULL DEFAULT NULL COMMENT '字典名称',
`code` varchar(100) NULL DEFAULT NULL COMMENT '字典标示',
`status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_code`(`code`) USING BTREE,
INDEX `idx_name`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 24 COMMENT = '字典类型表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_dict_type
-- ----------------------------
INSERT INTO `sa_system_dict_type` VALUES (2, '存储模式', 'upload_mode', 1, '上传文件存储模式', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_type` VALUES (3, '数据状态', 'data_status', 1, '通用数据状态', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_type` VALUES (4, '后台首页', 'dashboard', 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_type` VALUES (5, '性别', 'gender', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_type` VALUES (12, '附件类型', 'attachment_type', 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_type` VALUES (13, '菜单类型', 'menu_type', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_type` VALUES (14, '是否', 'yes_or_no', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_dict_type` VALUES (20, '定时任务类型', 'crontab_task_type', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
-- ----------------------------
-- Table structure for sa_system_login_log
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_login_log`;
CREATE TABLE `sa_system_login_log` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(20) NULL DEFAULT NULL COMMENT '用户名',
`ip` varchar(45) NULL DEFAULT NULL COMMENT '登录IP地址',
`ip_location` varchar(255) NULL DEFAULT NULL COMMENT 'IP所属地',
`os` varchar(50) NULL DEFAULT NULL COMMENT '操作系统',
`browser` varchar(50) NULL DEFAULT NULL COMMENT '浏览器',
`status` smallint(6) NULL DEFAULT 1 COMMENT '登录状态 (1成功 2失败)',
`message` varchar(50) NULL DEFAULT NULL COMMENT '提示消息',
`login_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '登录时间',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `username`(`username`) USING BTREE,
INDEX `idx_create_time`(`create_time`) USING BTREE,
INDEX `idx_login_time`(`login_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '登录日志表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_login_log
-- ----------------------------
-- ----------------------------
-- Table structure for sa_system_mail
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_mail`;
CREATE TABLE `sa_system_mail` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '编号',
`gateway` varchar(50) NULL DEFAULT NULL COMMENT '网关',
`from` varchar(50) NULL DEFAULT NULL COMMENT '发送人',
`email` varchar(50) NULL DEFAULT NULL COMMENT '接收人',
`code` varchar(20) NULL DEFAULT NULL COMMENT '验证码',
`content` varchar(500) NULL DEFAULT NULL COMMENT '邮箱内容',
`status` varchar(20) NULL DEFAULT NULL COMMENT '发送状态',
`response` varchar(500) NULL DEFAULT NULL COMMENT '返回结果',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_create_time`(`create_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '邮件记录' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_mail
-- ----------------------------
-- ----------------------------
-- Table structure for sa_system_menu
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_menu`;
CREATE TABLE `sa_system_menu` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '父级ID',
`name` varchar(64) NOT NULL COMMENT '菜单名称',
`code` varchar(64) NULL DEFAULT NULL COMMENT '组件名称',
`slug` varchar(100) NULL DEFAULT NULL COMMENT '权限标识,如 user:list, user:add',
`type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '类型: 1目录, 2菜单, 3按钮/API',
`path` varchar(255) NULL DEFAULT NULL COMMENT '路由地址(前端)或API路径(后端)',
`component` varchar(255) NULL DEFAULT NULL COMMENT '前端组件路径,如 layout/User',
`method` varchar(10) NULL DEFAULT NULL COMMENT '请求方式',
`icon` varchar(64) NULL DEFAULT NULL COMMENT '图标',
`sort` int(11) NULL DEFAULT 100 COMMENT '排序',
`link_url` varchar(255) NULL DEFAULT NULL COMMENT '外部链接',
`is_iframe` tinyint(1) NULL DEFAULT 2 COMMENT '是否iframe',
`is_keep_alive` tinyint(1) NULL DEFAULT 2 COMMENT '是否缓存',
`is_hidden` tinyint(1) NULL DEFAULT 2 COMMENT '是否隐藏',
`is_fixed_tab` tinyint(1) NULL DEFAULT 2 COMMENT '是否固定标签页',
`is_full_page` tinyint(1) NULL DEFAULT 2 COMMENT '是否全屏',
`generate_id` int(11) NULL DEFAULT 0 COMMENT '生成id',
`generate_key` varchar(255) NULL DEFAULT NULL COMMENT '生成key',
`status` tinyint(1) NULL DEFAULT 1 COMMENT '状态',
`remark` varchar(255) NULL DEFAULT NULL,
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_parent_id`(`parent_id`) USING BTREE,
INDEX `idx_slug`(`slug`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1000 COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_menu
-- ----------------------------
INSERT INTO `sa_system_menu` VALUES (1, 0, '仪表盘', 'Dashboard', NULL, 1, '/dashboard', NULL, NULL, 'ri:pie-chart-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (2, 1, '工作台', 'Console', NULL, 2, 'console', '/dashboard/console', NULL, 'ri:home-smile-2-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (3, 0, '系统管理', 'System', NULL, 1, '/system', NULL, NULL, 'ri:user-3-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (4, 3, '用户管理', 'User', NULL, 2, 'user', '/system/user', NULL, 'ri:user-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (5, 3, '部门管理', 'Dept', NULL, 2, 'dept', '/system/dept', NULL, 'ri:node-tree', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (6, 3, '角色管理', 'Role', NULL, 2, 'role', '/system/role', NULL, 'ri:admin-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (7, 3, '岗位管理', 'Post', '', 2, 'post', '/system/post', NULL, 'ri:signpost-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (8, 3, '菜单管理', 'Menu', NULL, 2, 'menu', '/system/menu', NULL, 'ri:menu-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (10, 0, '运维管理', 'Safeguard', NULL, 1, '/safeguard', '', NULL, 'ri:shield-check-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (11, 10, '缓存管理', 'Cache', '', 2, 'cache', '/safeguard/cache', NULL, 'ri:keyboard-box-line', 80, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (12, 10, '数据字典', 'Dict', NULL, 2, 'dict', '/safeguard/dict', NULL, 'ri:database-2-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (13, 10, '附件管理', 'Attachment', '', 2, 'attachment', '/safeguard/attachment', NULL, 'ri:file-cloud-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (14, 10, '数据表维护', 'Database', '', 2, 'database', '/safeguard/database', NULL, 'ri:database-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (15, 10, '登录日志', 'LoginLog', '', 2, 'login-log', '/safeguard/login-log', NULL, 'ri:login-circle-line', 50, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (16, 10, '操作日志', 'OperLog', '', 2, 'oper-log', '/safeguard/oper-log', NULL, 'ri:shield-keyhole-line', 50, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (17, 10, '邮件日志', 'EmailLog', '', 2, 'email-log', '/safeguard/email-log', NULL, 'ri:mail-line', 50, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (18, 3, '系统设置', 'Config', NULL, 2, 'config', '/system/config', NULL, 'ri:settings-4-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (19, 0, '官方文档', 'Document', '', 4, '', '', NULL, 'ri:file-copy-2-fill', 90, 'https://saithink.top', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (20, 4, '数据列表', '', 'core:user:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (21, 1, '个人中心', 'UserCenter', '', 2, 'user-center', '/dashboard/user-center/index', NULL, 'ri:user-2-line', 100, '', 2, 2, 1, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (22, 4, '添加', '', 'core:user:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (23, 4, '修改', '', 'core:user:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (24, 4, '读取', '', 'core:user:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (25, 4, '删除', '', 'core:user:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (26, 4, '重置密码', '', 'core:user:password', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (27, 4, '清理缓存', '', 'core:user:cache', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (28, 4, '设置工作台', '', 'core:user:home', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (29, 5, '数据列表', '', 'core:dept:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (30, 5, '添加', '', 'core:dept:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (31, 5, '修改', '', 'core:dept:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (32, 5, '读取', '', 'core:dept:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (33, 5, '删除', '', 'core:dept:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (34, 6, '添加', '', 'core:role:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (35, 6, '数据列表', '', 'core:role:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (36, 6, '修改', '', 'core:role:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (37, 6, '读取', '', 'core:role:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (38, 6, '删除', '', 'core:role:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (39, 6, '菜单权限', '', 'core:role:menu', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (41, 7, '数据列表', '', 'core:post:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (42, 7, '添加', '', 'core:post:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (43, 7, '修改', '', 'core:post:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (44, 7, '读取', '', 'core:post:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (45, 7, '删除', '', 'core:post:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (46, 7, '导入', '', 'core:post:import', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (47, 7, '导出', '', 'core:post:export', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (48, 8, '数据列表', '', 'core:menu:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (49, 8, '读取', '', 'core:menu:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (50, 8, '添加', '', 'core:menu:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (51, 8, '修改', '', 'core:menu:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (52, 8, '删除', '', 'core:menu:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (53, 18, '数据列表', '', 'core:config:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (54, 18, '管理', '', 'core:config:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (55, 18, '修改', '', 'core:config:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (56, 12, '数据列表', '', 'core:dict:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (57, 12, '管理', '', 'core:dict:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (58, 13, '数据列表', '', 'core:attachment:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (59, 13, '管理', '', 'core:attachment:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (60, 14, '数据表列表', '', 'core:database:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (61, 14, '数据表维护', '', 'core:database:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (62, 14, '回收站数据', '', 'core:recycle:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (63, 14, '回收站管理', '', 'core:recycle:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (64, 15, '数据列表', '', 'core:logs:login', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (65, 15, '删除', '', 'core:logs:deleteLogin', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (66, 16, '数据列表', '', 'core:logs:Oper', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (67, 16, '删除', '', 'core:logs:deleteOper', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (68, 17, '数据列表', '', 'core:email:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (69, 17, '删除', '', 'core:email:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (70, 10, '服务监控', 'Server', '', 2, 'server', '/safeguard/server', NULL, 'ri:server-line', 90, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (71, 70, '数据列表', '', 'core:server:monitor', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (72, 11, '数据列表', '', 'core:server:cache', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (73, 11, '缓存清理', '', 'core:server:clear', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (74, 2, '登录数据统计', '', 'core:console:list', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (75, 0, '附加权限', 'Permission', '', 1, 'permission', '', NULL, 'ri:apps-2-ai-line', 100, '', 2, 2, 1, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (76, 75, '上传图片', '', 'core:system:uploadImage', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (77, 75, '上传文件', '', 'core:system:uploadFile', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (78, 75, '附件列表', '', 'core:system:resource', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (79, 75, '用户列表', '', 'core:system:user', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (80, 0, '工具', 'Tool', '', 1, '/tool', '', NULL, 'ri:tools-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (81, 80, '代码生成', 'Code', '', 2, 'code', '/tool/code', NULL, 'ri:code-s-slash-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (82, 80, '定时任务', 'Crontab', '', 2, 'crontab', '/tool/crontab', NULL, 'ri:time-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (83, 82, '数据列表', '', 'tool:crontab:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (84, 82, '管理', '', 'tool:crontab:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (85, 82, '运行任务', '', 'tool:crontab:run', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (86, 81, '数据列表', '', 'tool:code:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (87, 81, '管理', '', 'tool:code:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
INSERT INTO `sa_system_menu` VALUES (88, 0, '插件市场', 'Plugin', '', 2, '/plugin', '/plugin/saipackage/install/index', NULL, 'ri:apps-2-ai-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
-- ----------------------------
-- Table structure for sa_system_oper_log
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_oper_log`;
CREATE TABLE `sa_system_oper_log` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(20) NULL DEFAULT NULL COMMENT '用户名',
`app` varchar(50) NULL DEFAULT NULL COMMENT '应用名称',
`method` varchar(20) NULL DEFAULT NULL COMMENT '请求方式',
`router` varchar(500) NULL DEFAULT NULL COMMENT '请求路由',
`service_name` varchar(30) NULL DEFAULT NULL COMMENT '业务名称',
`ip` varchar(45) NULL DEFAULT NULL COMMENT '请求IP地址',
`ip_location` varchar(255) NULL DEFAULT NULL COMMENT 'IP所属地',
`request_data` text NULL COMMENT '请求数据',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `username`(`username`) USING BTREE,
INDEX `idx_create_time`(`create_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '操作日志表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_oper_log
-- ----------------------------
-- ----------------------------
-- Table structure for sa_system_post
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_post`;
CREATE TABLE `sa_system_post` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(50) NULL DEFAULT NULL COMMENT '岗位名称',
`code` varchar(100) NULL DEFAULT NULL COMMENT '岗位代码',
`sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序',
`status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 87 COMMENT = '岗位信息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for sa_system_role
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_role`;
CREATE TABLE `sa_system_role` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL COMMENT '角色名称',
`code` varchar(64) NOT NULL COMMENT '角色标识(英文唯一),如: hr_manager',
`level` int(11) NULL DEFAULT 1 COMMENT '角色级别(1-100):用于行政控制,不可操作级别>=自己的角色',
`data_scope` tinyint(4) NULL DEFAULT 1 COMMENT '数据范围: 1全部, 2本部门及下属, 3本部门, 4仅本人, 5自定义',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`sort` int(11) NULL DEFAULT 100,
`status` tinyint(1) NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_slug`(`code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 17 COMMENT = '角色表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_role
-- ----------------------------
INSERT INTO `sa_system_role` VALUES (1, '超级管理员', 'super_admin', 100, 1, '系统维护者,拥有所有权限', 100, 1, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
-- ----------------------------
-- Table structure for sa_system_role_dept
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_role_dept`;
CREATE TABLE `sa_system_role_dept` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) UNSIGNED NOT NULL,
`dept_id` bigint(20) UNSIGNED NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_role_id`(`role_id`) USING BTREE,
INDEX `idx_dept_id`(`dept_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '角色-自定义数据权限关联' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_role_dept
-- ----------------------------
-- ----------------------------
-- Table structure for sa_system_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_role_menu`;
CREATE TABLE `sa_system_role_menu` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) UNSIGNED NOT NULL,
`menu_id` bigint(20) UNSIGNED NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_menu_id`(`menu_id`) USING BTREE,
INDEX `idx_role_id`(`role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '角色权限关联' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_role_menu
-- ----------------------------
-- ----------------------------
-- Table structure for sa_system_user
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_user`;
CREATE TABLE `sa_system_user` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`username` varchar(64) NOT NULL COMMENT '登录账号',
`password` varchar(255) NOT NULL COMMENT '加密密码',
`realname` varchar(64) NULL DEFAULT NULL COMMENT '真实姓名',
`gender` varchar(10) NULL DEFAULT NULL COMMENT '性别',
`avatar` varchar(255) NULL DEFAULT NULL COMMENT '头像',
`email` varchar(128) NULL DEFAULT NULL COMMENT '邮箱',
`phone` varchar(20) NULL DEFAULT NULL COMMENT '手机号',
`signed` varchar(255) NULL DEFAULT NULL COMMENT '个性签名',
`dashboard` varchar(255) NULL DEFAULT 'work' COMMENT '工作台',
`dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '主归属部门',
`is_super` tinyint(1) NULL DEFAULT 0 COMMENT '是否超级管理员: 1是(跳过权限检查), 0否',
`status` tinyint(1) NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`login_time` timestamp(0) NULL DEFAULT NULL COMMENT '最后登录时间',
`login_ip` varchar(45) NULL DEFAULT NULL COMMENT '最后登录IP',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_username`(`username`) USING BTREE,
INDEX `idx_dept_id`(`dept_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 110 COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_user
-- ----------------------------
INSERT INTO `sa_system_user` VALUES (1, 'admin', '$2y$10$wnixh48uDnaW/6D9EygDd.OHJK0vQY/4nHaTjMKBCVDBP2NiTatqS', '祭道之上', '2', 'https://image.saithink.top/saiadmin/avatar.jpg', 'saiadmin@admin.com', '15888888888', 'SaiAdmin是兼具设计美学与高效开发的后台系统!', 'statistics', 1, 1, 1, NULL, NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL);
-- ----------------------------
-- Table structure for sa_system_user_post
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_user_post`;
CREATE TABLE `sa_system_user_post` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` bigint(20) UNSIGNED NOT NULL COMMENT '用户主键',
`post_id` bigint(20) UNSIGNED NOT NULL COMMENT '岗位主键',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_user_id`(`user_id`) USING BTREE,
INDEX `idx_post_id`(`post_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '用户与岗位关联表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_user_post
-- ----------------------------
-- ----------------------------
-- Table structure for sa_system_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sa_system_user_role`;
CREATE TABLE `sa_system_user_role` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) UNSIGNED NOT NULL,
`role_id` bigint(20) UNSIGNED NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_role_id`(`role_id`) USING BTREE,
INDEX `idx_user_id`(`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 55 COMMENT = '用户角色关联' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_system_user_role
-- ----------------------------
INSERT INTO `sa_system_user_role` VALUES (1, 1, 1);
-- ----------------------------
-- Table structure for sa_tool_crontab
-- ----------------------------
DROP TABLE IF EXISTS `sa_tool_crontab`;
CREATE TABLE `sa_tool_crontab` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(100) NULL DEFAULT NULL COMMENT '任务名称',
`type` smallint(6) NULL DEFAULT 4 COMMENT '任务类型',
`target` varchar(500) NULL DEFAULT NULL COMMENT '调用任务字符串',
`parameter` varchar(1000) NULL DEFAULT NULL COMMENT '调用任务参数',
`task_style` tinyint(1) NULL DEFAULT NULL COMMENT '执行类型',
`rule` varchar(32) NULL DEFAULT NULL COMMENT '任务执行表达式',
`singleton` smallint(6) NULL DEFAULT 1 COMMENT '是否单次执行 (1 是 2 不是)',
`status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '定时任务信息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for sa_tool_crontab_log
-- ----------------------------
DROP TABLE IF EXISTS `sa_tool_crontab_log`;
CREATE TABLE `sa_tool_crontab_log` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`crontab_id` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '任务ID',
`name` varchar(255) NULL DEFAULT NULL COMMENT '任务名称',
`target` varchar(500) NULL DEFAULT NULL COMMENT '任务调用目标字符串',
`parameter` varchar(1000) NULL DEFAULT NULL COMMENT '任务调用参数',
`exception_info` varchar(2000) NULL DEFAULT NULL COMMENT '异常信息',
`status` smallint(6) NULL DEFAULT 1 COMMENT '执行状态 (1成功 2失败)',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '定时任务执行日志表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_tool_crontab_log
-- ----------------------------
-- ----------------------------
-- Table structure for sa_tool_generate_columns
-- ----------------------------
DROP TABLE IF EXISTS `sa_tool_generate_columns`;
CREATE TABLE `sa_tool_generate_columns` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`table_id` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '所属表ID',
`column_name` varchar(200) NULL DEFAULT NULL COMMENT '字段名称',
`column_comment` varchar(255) NULL DEFAULT NULL COMMENT '字段注释',
`column_type` varchar(50) NULL DEFAULT NULL COMMENT '字段类型',
`default_value` varchar(50) NULL DEFAULT NULL COMMENT '默认值',
`is_pk` smallint(6) NULL DEFAULT 1 COMMENT '1 非主键 2 主键',
`is_required` smallint(6) NULL DEFAULT 1 COMMENT '1 非必填 2 必填',
`is_insert` smallint(6) NULL DEFAULT 1 COMMENT '1 非插入字段 2 插入字段',
`is_edit` smallint(6) NULL DEFAULT 1 COMMENT '1 非编辑字段 2 编辑字段',
`is_list` smallint(6) NULL DEFAULT 1 COMMENT '1 非列表显示字段 2 列表显示字段',
`is_query` smallint(6) NULL DEFAULT 1 COMMENT '1 非查询字段 2 查询字段',
`is_sort` smallint(6) NULL DEFAULT 1 COMMENT '1 非排序 2 排序',
`query_type` varchar(100) NULL DEFAULT 'eq' COMMENT '查询方式 eq 等于, neq 不等于, gt 大于, lt 小于, like 范围',
`view_type` varchar(100) NULL DEFAULT 'text' COMMENT '页面控件,text, textarea, password, select, checkbox, radio, date, upload, ma-upload(封装的上传控件)',
`dict_type` varchar(200) NULL DEFAULT NULL COMMENT '字典类型',
`allow_roles` varchar(255) NULL DEFAULT NULL COMMENT '允许查看该字段的角色',
`options` varchar(1000) NULL DEFAULT NULL COMMENT '字段其他设置',
`sort` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '排序',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '代码生成业务字段表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_tool_generate_columns
-- ----------------------------
-- ----------------------------
-- Table structure for sa_tool_generate_tables
-- ----------------------------
DROP TABLE IF EXISTS `sa_tool_generate_tables`;
CREATE TABLE `sa_tool_generate_tables` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`table_name` varchar(200) NULL DEFAULT NULL COMMENT '表名称',
`table_comment` varchar(500) NULL DEFAULT NULL COMMENT '表注释',
`stub` varchar(50) NULL DEFAULT NULL COMMENT 'stub类型',
`template` varchar(50) NULL DEFAULT NULL COMMENT '模板名称',
`namespace` varchar(255) NULL DEFAULT NULL COMMENT '命名空间',
`package_name` varchar(100) NULL DEFAULT NULL COMMENT '控制器包名',
`business_name` varchar(50) NULL DEFAULT NULL COMMENT '业务名称',
`class_name` varchar(50) NULL DEFAULT NULL COMMENT '类名称',
`menu_name` varchar(100) NULL DEFAULT NULL COMMENT '生成菜单名',
`belong_menu_id` int(11) NULL DEFAULT NULL COMMENT '所属菜单',
`tpl_category` varchar(100) NULL DEFAULT NULL COMMENT '生成类型,single 单表CRUD,tree 树表CRUD,parent_sub父子表CRUD',
`generate_type` smallint(6) NULL DEFAULT 1 COMMENT '1 压缩包下载 2 生成到模块',
`generate_path` varchar(100) NULL DEFAULT 'saiadmin-artd' COMMENT '前端根目录',
`generate_model` smallint(6) NULL DEFAULT 1 COMMENT '1 软删除 2 非软删除',
`generate_menus` varchar(255) NULL DEFAULT NULL COMMENT '生成菜单列表',
`build_menu` smallint(6) NULL DEFAULT 1 COMMENT '是否构建菜单',
`component_type` smallint(6) NULL DEFAULT 1 COMMENT '组件显示方式',
`options` varchar(1500) NULL DEFAULT NULL COMMENT '其他业务选项',
`form_width` int(11) NULL DEFAULT 800 COMMENT '表单宽度',
`is_full` tinyint(1) NULL DEFAULT 1 COMMENT '是否全屏',
`remark` varchar(255) NULL DEFAULT NULL COMMENT '备注',
`source` varchar(255) NULL DEFAULT NULL COMMENT '数据源',
`created_by` int(11) NULL DEFAULT NULL COMMENT '创建者',
`updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '代码生成业务表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sa_tool_generate_tables
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,22 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\exception;
use Webman\Http\Request;
use Webman\Http\Response;
use support\exception\BusinessException;
/**
* 常规操作异常-只返回json数据,不记录异常日志
*/
class ApiException extends BusinessException
{
public function render(Request $request): ?Response
{
return json(['code' => $this->getCode() ?: 500, 'message' => $this->getMessage()]);
}
}

View File

@@ -0,0 +1,20 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\exception;
use Throwable;
/**
* 系统接口错误-返回json数据,并且记录异常日志
*/
class SystemException extends \RuntimeException
{
public function __construct($message, $code = 400, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,69 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\process;
use plugin\saiadmin\app\logic\tool\CrontabLogic;
use Webman\Channel\Client;
use Workerman\Crontab\Crontab;
class Task
{
protected $logic; //login对象
public $crontabIds = []; //定时任务表主键id => Crontab对象id
public function __construct()
{
$dbName = env('DB_NAME');
if (!empty($dbName)) {
$this->logic = new CrontabLogic();
// 连接webman channel服务
Client::connect();
// 订阅某个自定义事件并注册回调,收到事件后会自动触发此回调
Client::on('crontab', function ($data) {
$this->reload($data);
});
}
}
public function onWorkerStart()
{
$dbName = env('DB_NAME');
if (!empty($dbName)) {
$this->initStart();
}
}
public function initStart()
{
$logic = new CrontabLogic();
$taskList = $logic->getAll($logic->search(['status' => 1]));
foreach ($taskList as $item) {
$crontab = new Crontab($item['rule'], function () use ($item) {
$this->logic->run($item['id']);
});
$this->crontabIds[intval($item['id'])] = $crontab->getId(); //存储定时任务表主键id => Crontab对象id
echo PHP_EOL . date('Y-m-d H:i:s') . " => 定时任务[" . $item['id'] . "][" . $item['name'] . "]:启动成功" . PHP_EOL;
}
}
public function reload($data)
{
$id = intval($data['args'] ?? 0); //定时任务表主键id
if (isset($this->crontabIds[$id])) {
Crontab::remove($this->crontabIds[$id]);
unset($this->crontabIds[$id]); //删除定时任务表主键id => Crontab对象id
echo PHP_EOL . date('Y-m-d H:i:s') . " => 定时任务[" . $id . "]:移除成功" . PHP_EOL;
}
$item = $this->logic->read($id);// 查询定时任务表数据
if ($item && $item['status'] == 1) {
$crontab = new Crontab($item['rule'], function () use ($item) {
$this->logic->run($item['id']);
});
$this->crontabIds[$id] = $crontab->getId(); //存储定时任务表主键id => Crontab对象id
echo PHP_EOL . date('Y-m-d H:i:s') . " => 定时任务[" . $item['id'] . "][" . $item['name'] . "]:启动成功" . PHP_EOL;
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\process;
class Test
{
public function run($args): void
{
echo '任务[Test]调用:' . date('Y-m-d H:i:s') . "\n";
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -0,0 +1,100 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\service;
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\PHPMailer;
use plugin\saiadmin\app\logic\system\SystemConfigLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Arr;
/**
* 邮件服务类
*/
class EmailService
{
/**
* 读取配置
* @return array
*/
public static function getConfig(): array
{
$logic = new SystemConfigLogic();
$config = $logic->getGroup('email_config');
if (!$config) {
throw new ApiException('未设置邮件配置');
}
return $config;
}
/**
* Get Mailer
* @return PHPMailer
*/
public static function getMailer(): PHPMailer
{
if (!class_exists(PHPMailer::class)) {
throw new ApiException('请执行 composer require phpmailer/phpmailer 并重启');
}
$config = static::getConfig();
$mailer = new PHPMailer();
$mailer->SMTPDebug = intval(Arr::getConfigValue($config,'SMTPDebug'));
$mailer->isSMTP();
$mailer->Host = Arr::getConfigValue($config,'Host');
$mailer->SMTPAuth = true;
$mailer->CharSet = Arr::getConfigValue($config,'CharSet');
$mailer->Username = Arr::getConfigValue($config,'Username');
$mailer->Password = Arr::getConfigValue($config,'Password');
$mailer->SMTPSecure = Arr::getConfigValue($config,'SMTPSecure');
$mailer->Port = Arr::getConfigValue($config,'Port');
return $mailer;
}
/**
* 发送邮件
* @param $from
* @param $to
* @param $subject
* @param $content
* @return string
* @throws Exception
*/
public static function send($from, $to, $subject, $content): string
{
$mailer = static::getMailer();
call_user_func_array([$mailer, 'setFrom'], (array)$from);
call_user_func_array([$mailer, 'addAddress'], (array)$to);
$mailer->Subject = $subject;
$mailer->isHTML(true);
$mailer->Body = $content;
$mailer->send();
return $mailer->ErrorInfo;
}
/**
* 按照模版发送
* @param string|array $to
* @param $subject
* @param $content
* @param array $templateData
* @return string
* @throws Exception
*/
public static function sendByTemplate($to, $subject, $content, array $templateData = []): string
{
if ($templateData) {
$search = [];
foreach ($templateData as $key => $value) {
$search[] = '{' . $key . '}';
}
$content = str_replace($search, array_values($templateData), $content);
}
$config = static::getConfig();
return static::send([Arr::getConfigValue($config,'From'), Arr::getConfigValue($config,'FromName')], $to, $subject, $content);
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace plugin\saiadmin\service;
use OpenSpout\Common\Entity\Style\Border;
use OpenSpout\Common\Entity\Style\BorderPart;
use OpenSpout\Writer\XLSX\Writer;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Common\Entity\Style\Style;
/**
* Excel写入类
* OpenSpout
*/
class OpenSpoutWriter
{
/**
* 操作实例
* @var Writer
*/
protected $instance;
/**
* 文件路径
* @var string
*/
protected $filepath;
/**
* 初始化
* @param string $fileName 文件名称
*/
public function __construct(string $fileName)
{
$this->filepath = $this->getFileName($fileName);
$this->instance = new Writer();
$this->instance->openToFile($this->filepath);
}
/**
* 获取完整的文件路径
* @param string $fileName
* @return string
*/
public function getFileName(string $fileName): string
{
$path = config('plugin.saiadmin.saithink.export_path',base_path() . '/plugin/saiadmin/public/export/');
@mkdir($path, 0777, true);
return $path . $fileName;
}
/**
* 设置表格宽度
* @param array $width 宽度数组
* @return void
*/
public function setWidth(array $width = [])
{
if (empty($width)) {
return;
}
$sheet = $this->instance->getCurrentSheet();
foreach ($width as $key => $value) {
$sheet->setColumnWidth($value, $key + 1);
}
}
/**
* 设置表头
* @param array $header 表头数组
* @param $style
* @return void
*/
public function setHeader(array $header = [], $style = null): void
{
if (empty($style)) {
$border = new Border(
new BorderPart("top", "black", "thin"),
new BorderPart("right", "black", "thin"),
new BorderPart("bottom", "black", "thin"),
new BorderPart("left", "black", "thin"),
);
$style = new Style();
$style->setFontBold();
$style->setCellAlignment("center");
$style->setBorder($border);
}
$rowFromValues = Row::fromValues($header, $style);
$this->instance->addRow($rowFromValues);
}
/**
* 设置数据
* @param array $data 数据数组
* @param $style
* @return void
*/
public function setData(array $data = [], $style = null, array $filter = []): void
{
if (empty($style)) {
$border = new Border(
new BorderPart("top", "black", "thin"),
new BorderPart("right", "black", "thin"),
new BorderPart("bottom", "black", "thin"),
new BorderPart("left", "black", "thin"),
);
$style = new Style();
$style->setCellAlignment("center");
$style->setBorder($border);
}
foreach($data as $row) {
if (!empty($filter)) {
foreach ($filter as $key => $value) {
foreach ($value as $item) {
if ($item['value'] == $row[$key]) {
$row[$key] = $item['label'];
break;
}
}
}
}
$rowFromValues = Row::fromValues($row, $style);
$this->instance->addRow($rowFromValues);
}
}
/**
* 获取文件
* @return string
*/
public function returnFile(): string
{
$this->instance->close();
return $this->filepath;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace plugin\saiadmin\service;
/**
* 权限注解
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
class Permission
{
/**
* 权限标题/名称
*/
public string $title;
/**
* 权限标识唯一格式如module:controller:action
*/
public ?string $slug = null;
/**
* 构造函数 #[Permission(title:'标题', slug:'标识')]
* @param string|null $title
* @param string|null $slug
*/
public function __construct(
?string $title = null,
?string $slug = null,
)
{
$this->title = $title ?? '';
$this->slug = $slug;
}
/**
* 获取权限标题
*/
public function getTitle(): string
{
return $this->title;
}
/**
* 获取权限标识
*/
public function getSlug(): ?string
{
return $this->slug;
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace plugin\saiadmin\service\storage;
use plugin\saiadmin\app\logic\system\SystemConfigLogic;
use plugin\saiadmin\app\model\system\SystemAttachment;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Arr;
/**
* 切片上传服务
*/
class ChunkUploadService
{
/**
* 基础配置
*/
protected $config;
protected $path;
protected $folder = "chunk";
/**
* 初始化切片上传服务
*/
public function __construct($folder = "chunk")
{
$logic = new SystemConfigLogic();
$this->folder = $folder;
$this->config = $logic->getGroup('upload_config');
$this->path = $this->checkPath();
}
/**
* 检查并创建上传路径
* @return string
*/
public function checkPath(): string
{
$root = Arr::getConfigValue($this->config, 'local_root');
$path = base_path() . DIRECTORY_SEPARATOR . $root . $this->folder . DIRECTORY_SEPARATOR;
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
return $path;
}
/**
* 检查切片文件上传状态
* @param $data
* @return array
*/
public function checkChunk($data): array
{
$allow_file = Arr::getConfigValue($this->config, 'upload_allow_file');
if (!in_array($data['ext'], explode(',', $allow_file))) {
throw new ApiException('不支持该格式的文件上传');
}
// 检查已经上传的分片文件
for ($i = 0; $i < $data['total']; ++$i) {
$chunkFile = $this->path . "{$data['hash']}_{$data['total']}_{$i}.chunk";
if (!file_exists($chunkFile)) {
if ($i == 0) {
return $this->uploadChunk($data);
} else {
return ['chunk' => $i, 'status' => 'resume'];
}
}
}
// 分片文件已经全部上传
return ['chunk' => $i, 'status' => 'success'];
}
/**
* 上传切片
* @param $data
* @return array
*/
public function uploadChunk($data): array
{
$allow_file = Arr::getConfigValue($this->config, 'upload_allow_file');
if (!in_array($data['ext'], explode(',', $allow_file))) {
throw new ApiException('不支持该格式的文件上传');
}
$request = request();
if (!$request) {
throw new ApiException('切片上传服务必须在 HTTP 请求环境下调用');
}
$uploadFile = current($request->file());
$chunkName = $this->path . "{$data['hash']}_{$data['total']}_{$data['index']}.chunk";
$uploadFile->move($chunkName);
if (($data['index'] + 1) == $data['total']) {
return $this->mergeChunk($data);
}
return ['chunk' => $data['index'], 'status' => 'success'];
}
/**
* 合并切片文件
* @param $data
* @return array
*/
public function mergeChunk($data): array
{
$filePath = $this->path . $data['hash'] . '.' . $data['ext'];
$fileHandle = fopen($filePath, 'w');
for ($i = 0; $i < $data['total']; ++$i) {
$chunkFile = $this->path . "{$data['hash']}_{$data['total']}_{$i}.chunk";
if (!file_exists($chunkFile)) {
throw new ApiException('切片文件查找失败,请重新上传');
}
fwrite($fileHandle, file_get_contents($chunkFile));
unlink($chunkFile);
}
$domain = Arr::getConfigValue($this->config, 'local_domain');
$uri = Arr::getConfigValue($this->config, 'local_uri');
$baseUrl = $domain . $uri . $this->folder . '/';
$save_path = Arr::getConfigValue($this->config, 'local_root') . $this->folder . '/';
$object_name = $data['hash'] . '.' . $data['ext'];
$info['storage_mode'] = 1;
$info['category_id'] = 1;
$info['origin_name'] = $data['name'];
$info['object_name'] = $object_name;
$info['hash'] = $data['hash'];
$info['mime_type'] = $data['type'];
$info['storage_path'] = $save_path . $object_name;
$info['suffix'] = $data['ext'];
$info['size_byte'] = $data['size'];
$info['size_info'] = formatBytes($data['size']);
$info['url'] = $baseUrl . $object_name;
SystemAttachment::create($info);
return $info;
}
}

View File

@@ -0,0 +1,136 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\service\storage;
use plugin\saiadmin\app\logic\system\SystemConfigLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Arr;
/**
* 文件上传服务
* @method static array uploadFile(array $config = []) 上传文件
* @method static array uploadBase64(string $base64, string $extension = 'png') 上传Base64文件
* @method static array uploadServerFile(string $file_path) 上传服务端文件
*/
class UploadService
{
/**
* @desc 存储磁盘
* @param int $type
* @param string $upload
* @param bool $_is_file_upload
* @return mixed
*/
public static function disk(int $type = 1, string $upload = 'image', bool $_is_file_upload = true)
{
$logic = new SystemConfigLogic();
$uploadConfig = $logic->getGroup('upload_config');
$file = current(request()->file());
$ext = $file->getUploadExtension() ?: null;
$file_size = $file->getSize();
if ($file_size > Arr::getConfigValue($uploadConfig, 'upload_size')) {
throw new ApiException('文件大小超过限制');
}
$allow_file = Arr::getConfigValue($uploadConfig, 'upload_allow_file');
$allow_image = Arr::getConfigValue($uploadConfig, 'upload_allow_image');
if ($upload == 'image') {
if (!in_array($ext, explode(',', $allow_image))) {
throw new ApiException('不支持该格式的文件上传');
}
} else {
if (!in_array($ext, explode(',', $allow_file))) {
throw new ApiException('不支持该格式的文件上传');
}
}
switch ($type) {
case 1:
// 本地
$config = [
'adapter' => \Tinywan\Storage\Adapter\LocalAdapter::class,
'root' => Arr::getConfigValue($uploadConfig, 'local_root'),
'dirname' => function () {
return date('Ymd');
},
'domain' => Arr::getConfigValue($uploadConfig, 'local_domain'),
'uri' => Arr::getConfigValue($uploadConfig, 'local_uri'),
'algo' => 'sha1',
];
break;
case 2:
// 阿里云
$config = [
'adapter' => \Tinywan\Storage\Adapter\OssAdapter::class,
'accessKeyId' => Arr::getConfigValue($uploadConfig, 'oss_accessKeyId'),
'accessKeySecret' => Arr::getConfigValue($uploadConfig, 'oss_accessKeySecret'),
'bucket' => Arr::getConfigValue($uploadConfig, 'oss_bucket'),
'dirname' => Arr::getConfigValue($uploadConfig, 'oss_dirname'),
'domain' => Arr::getConfigValue($uploadConfig, 'oss_domain'),
'endpoint' => Arr::getConfigValue($uploadConfig, 'oss_endpoint'),
'algo' => 'sha1',
];
break;
case 3:
// 七牛
$config = [
'adapter' => \Tinywan\Storage\Adapter\QiniuAdapter::class,
'accessKey' => Arr::getConfigValue($uploadConfig, 'qiniu_accessKey'),
'secretKey' => Arr::getConfigValue($uploadConfig, 'qiniu_secretKey'),
'bucket' => Arr::getConfigValue($uploadConfig, 'qiniu_bucket'),
'dirname' => Arr::getConfigValue($uploadConfig, 'qiniu_dirname'),
'domain' => Arr::getConfigValue($uploadConfig, 'qiniu_domain'),
];
break;
case 4:
// 腾讯云
$config = [
'adapter' => \Tinywan\Storage\Adapter\CosAdapter::class,
'secretId' => Arr::getConfigValue($uploadConfig, 'cos_secretId'),
'secretKey' => Arr::getConfigValue($uploadConfig, 'cos_secretKey'),
'bucket' => Arr::getConfigValue($uploadConfig, 'cos_bucket'),
'dirname' => Arr::getConfigValue($uploadConfig, 'cos_dirname'),
'domain' => Arr::getConfigValue($uploadConfig, 'cos_domain'),
'region' => Arr::getConfigValue($uploadConfig, 'cos_region'),
];
break;
case 5:
// s3 亚马逊
$config = [
'adapter' => \Tinywan\Storage\Adapter\S3Adapter::class,
'key' => Arr::getConfigValue($uploadConfig, 's3_key'),
'secret' => Arr::getConfigValue($uploadConfig, 's3_secret'),
'bucket' => Arr::getConfigValue($uploadConfig, 's3_bucket'),
'dirname' => Arr::getConfigValue($uploadConfig, 's3_dirname'),
'domain' => Arr::getConfigValue($uploadConfig, 's3_domain'),
'region' => Arr::getConfigValue($uploadConfig, 's3_region'),
'version' => Arr::getConfigValue($uploadConfig, 's3_version'),
// 'use_path_style_endpoint' => Arr::getConfigValue($uploadConfig,'s3_use_path_style_endpoint'),
'use_path_style_endpoint' => filter_var(Arr::getConfigValue($uploadConfig, 's3_use_path_style_endpoint'), FILTER_VALIDATE_BOOLEAN),
'endpoint' => Arr::getConfigValue($uploadConfig, 's3_endpoint'),
'acl' => Arr::getConfigValue($uploadConfig, 's3_acl'),
];
break;
default:
throw new ApiException('该上传模式不存在');
}
return new $config['adapter'](array_merge(
$config,
['_is_file_upload' => $_is_file_upload]
));
}
/**
* @param $name
* @param $arguments
* @return mixed
* @author Tinywan(ShaoBo Wan)
*/
public static function __callStatic($name, $arguments)
{
return static::disk()->{$name}(...$arguments);
}
}

View File

@@ -0,0 +1,204 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\utils;
/**
* Array操作类
* Class Arr
*/
class Arr
{
/**
* 获取数组中指定的列
* @param array $source
* @param string $column
* @return array
*/
public static function getArrayColumn($source, $column): array
{
$columnArr = [];
foreach ($source as $item) {
$columnArr[] = $item[$column];
}
return $columnArr;
}
/**
* 批量获取数组中指定的列
* @param array $source
* @param array $column
* @return array
*/
public static function getArrayColumns($source, $columns): array
{
$columnArr = [];
foreach ($source as $item) {
$tempArr = [];
foreach ($columns as $key) {
$temp = explode('.', $key);
if (count($temp) > 1) {
$tempArr[$key] = $item[$temp[0]][$temp[1]];
} else {
$tempArr[$key] = $item[$key];
}
}
$columnArr[] = $tempArr;
}
return $columnArr;
}
/**
* 把二维数组中某列设置为key返回
* @param array $array 输入数组
* @param string $field 要作为键的字段名
* @param bool $unique 要做键的字段是否唯一(该字段与记录是否一一对应)
* @return array
*/
public static function fieldAsKey($array, $field, $unique = false) {
$result = [];
foreach ($array as $item) {
if (isset($item[$field])) {
if (!$unique && isset($result[$item[$field]])) {
$unique = true;
$result[$item[$field]] = [($result[$item[$field]])];
$result[$item[$field]][] = $item;
} elseif ($unique) {
$result[$item[$field]][] = $item;
} else {
$result[$item[$field]] = $item;
}
}
}
return $result;
}
/**
* 数组转字符串去重复
* @param array $data
* @return false|string[]
*/
public static function unique(array $data)
{
return array_unique(explode(',', implode(',', $data)));
}
/**
* 获取数组中去重复过后的指定key值
* @param array $list
* @param string $key
* @return array
*/
public static function getUniqueKey(array $list, string $key)
{
return array_unique(array_column($list, $key));
}
/**
* 合并二维数组并且指定key去重, 第一个覆盖第二个
* @param array $arr1
* @param array $arr2
* @param string $key
* @return array
*/
public static function mergeArray(array $arr1, array $arr2, string $key)
{
$arr = array_merge($arr1,$arr2);
$tmp_arr = [];
foreach($arr as $k => $v) {
if(in_array($v[$key], $tmp_arr)) {
unset($arr[$k]);
} else {
$tmp_arr[] = $v[$key];
}
}
return $arr;
}
/**
* 相同键值的合并作为键生成新数组
* @param array $data
* @param string $field
* @return array
*/
public static function groupSameField(array $data, string $field)
{
$result= [];
foreach ($data as $key => $info) {
$result[$info[$field]][] = $info;
}
return $result;
}
/**
* 生成无限级树算法
* @param array $arr 输入数组
* @param number $pid 根级的pid
* @param string $column_name 列名,id|pid父id的名字|children子数组的键名
* @return array $ret
*/
public static function makeTree($arr, $pid = 0, $column_name = 'id|pid|children') {
list($idname, $pidname, $cldname) = explode('|', $column_name);
$ret = array();
foreach ($arr as $k => $v) {
if ($v [$pidname] == $pid) {
$tmp = $arr [$k];
unset($arr [$k]);
$tmp [$cldname] = self::makeTree($arr, $v [$idname], $column_name);
$ret [] = $tmp;
}
}
return $ret;
}
/**
* 二位数组按某个键值排序
* @param array $arr
* @param string $key
* @param int $sort
* @return array
*/
public static function sortArray($arr, $key, $sort = SORT_ASC)
{
array_multisort(array_column($arr,$key),$sort,$arr);
return $arr;
}
/**
* 数组中根据某一列中某个字段的值来查询这一列数据
* @param $array
* @param $column
* @param $value
* @return array
*/
public static function getArrayByColumn($array, $column, $value): array
{
$result = [];
foreach ($array as $key => $item) {
if ($item[$column] == $value) {
$result = $item;
}
}
return $result;
}
/**
* 数组中根据key值获取value
* @param $array
* @param $key
* @return mixed|string
*/
public static function getConfigValue($array, $key)
{
foreach ($array as $item) {
if ($item['key'] === $key) {
return $item['value'];
}
}
return '';
}
}

View File

@@ -0,0 +1,121 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\utils;
use support\think\Cache;
use Ramsey\Uuid\Uuid;
use Webman\Captcha\CaptchaBuilder;
use Webman\Captcha\PhraseBuilder;
use plugin\saiadmin\exception\ApiException;
/**
* 验证码工具类
*/
class Captcha
{
/**
* 图形验证码
* @return array
*/
public static function imageCaptcha(): array
{
$builder = new PhraseBuilder(4, 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ');
$captcha = new CaptchaBuilder(null, $builder);
$captcha->setBackgroundColor(242, 243, 245);
$captcha->build(120, 36);
$uuid = Uuid::uuid4();
$key = $uuid->toString();
$mode = config('plugin.saiadmin.saithink.captcha.mode', 'session');
$expire = config('plugin.saiadmin.saithink.captcha.expire', 300);
$code = strtolower($captcha->getPhrase());
if ($mode === 'cache') {
try {
Cache::set($key, $code, $expire);
} catch (\Exception $e) {
return [
'result' => -1,
'message' => '验证码获取失败,请检查缓存配置'
];
}
} else {
$request = request();
if ($request) {
$request->session()->set($key, $code);
}
}
$img_content = $captcha->get();
return [
'result' => 1,
'uuid' => $key,
'image' => 'data:image/png;base64,' . base64_encode($img_content)
];
}
/**
* 数字验证码
* @param string $key
* @param int $length
* @return array
*/
public static function numberCaptcha(string $key, int $length = 4): array
{
$code = str_pad(rand(0, 999999), $length, '0', STR_PAD_LEFT);
$mode = config('plugin.saiadmin.saithink.captcha.mode', 'session');
$expire = config('plugin.saiadmin.saithink.captcha.expire', 300);
if ($mode === 'cache') {
try {
Cache::set($key, $code, $expire);
} catch (\Exception $e) {
return [
'result' => -1,
'message' => '验证码获取失败,请检查缓存配置'
];
}
} else {
$request = request();
if ($request) {
$request->session()->set($key, $code);
}
}
return [
'result' => 1,
'uuid' => $key,
'code' => $code,
];
}
/**
* 验证码验证
* @param string $uuid
* @param string|int $captcha
* @return bool
*/
public static function checkCaptcha(string $uuid, string|int $captcha): bool
{
$mode = config('plugin.saiadmin.saithink.captcha.mode', 'session');
if ($mode === 'cache') {
try {
$code = Cache::get($uuid);
Cache::delete($uuid);
} catch (\Exception $e) {
throw new ApiException($e->getMessage());
}
} else {
try {
$code = session($uuid);
session()->forget($uuid);
} catch (\Exception $e) {
throw new ApiException($e->getMessage());
}
}
if (strtolower($captcha) !== $code) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,254 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\utils;
/**
* 帮助类
*/
class Helper
{
/**
* 数据树形化
* @param array $data 数据
* @param string $childrenname 子数据名
* @param string $keyName 数据key名
* @param string $pidName 数据上级key名
* @return array
*/
public static function makeTree(array $data, string $childrenname = 'children', string $keyName = 'id', string $pidName = 'parent_id')
{
$list = [];
foreach ($data as $value) {
$list[$value[$keyName]] = $value;
}
$tree = []; //格式化好的树
foreach ($list as $item) {
if (isset($list[$item[$pidName]])) {
$list[$item[$pidName]][$childrenname][] = &$list[$item[$keyName]];
} else {
$tree[] = &$list[$item[$keyName]];
}
}
return $tree;
}
/**
* 生成Arco菜单
* @param array $data 数据
* @param string $childrenname 子数据名
* @param string $keyName 数据key名
* @param string $pidName 数据上级key名
* @return array
*/
public static function makeArcoMenus(array $data, string $childrenname = 'children', string $keyName = 'id', string $pidName = 'parent_id')
{
$list = [];
foreach ($data as $value) {
if ($value['type'] === 'M'){
$path = '/'.$value['route'];
$layout = isset($value['is_layout']) ? $value['is_layout'] : 1;
$temp = [
$keyName => $value[$keyName],
$pidName => $value[$pidName],
'name' => $value['route'],
'path' => $path,
'component' => $value['component'],
'redirect' => $value['redirect'],
'meta' => [
'title' => $value['name'],
'type' => $value['type'],
'hidden' => $value['is_hidden'] === 1,
'layout' => $layout === 1,
'hiddenBreadcrumb' => false,
'icon' => $value['icon'],
],
];
$list[$value[$keyName]] = $temp;
}
if ($value['type'] === 'I' || $value['type'] === 'L'){
$temp = [
$keyName => $value[$keyName],
$pidName => $value[$pidName],
'name' => $value['code'],
'path' => $value['route'],
'meta' => [
'title' => $value['name'],
'type' => $value['type'],
'hidden' => $value['is_hidden'] === 1,
'hiddenBreadcrumb' => false,
'icon' => $value['icon'],
],
];
$list[$value[$keyName]] = $temp;
}
}
$tree = []; //格式化好的树
foreach ($list as $item) {
if (isset($list[$item[$pidName]])) {
$list[$item[$pidName]][$childrenname][] = &$list[$item[$keyName]];
} else {
$tree[] = &$list[$item[$keyName]];
}
}
return $tree;
}
/**
* 生成Artd菜单
* @param array $data
* @param string $childrenname
* @param string $keyName
* @param string $pidName
* @return array
*/
public static function makeArtdMenus(array $data, string $childrenname = 'children', string $keyName = 'id', string $pidName = 'parent_id')
{
$list = [];
foreach ($data as $value) {
$component = '';
if ($value['type'] === 1) {
$component = '/index/index';
}
if ($value['type'] === 2) {
$component = $value['component'];
}
$temp = [
$keyName => $value[$keyName],
$pidName => $value[$pidName],
'name' => $value['code'],
'path' => $value['path'],
'component' => $component,
'meta' => [
'title' => $value['name'],
'icon' => $value['icon'],
'isIframe' => $value['is_iframe'] === 1,
'keepAlive' => $value['is_keep_alive'] === 1,
'isHide' => $value['is_hidden'] === 1,
'fixedTab' => $value['is_fixed_tab'] === 1,
'isFullPage' => $value['is_full_page'] === 1,
],
];
if ($value['type'] === 4) {
$temp['path'] = '/outside/Iframe';
$temp['meta']['link'] = $value['link_url'];
}
$list[$value[$keyName]] = $temp;
}
$tree = [];
foreach ($list as $item) {
if (isset($list[$item[$pidName]])) {
$list[$item[$pidName]][$childrenname][] = &$list[$item[$keyName]];
} else {
$tree[] = &$list[$item[$keyName]];
}
}
return $tree;
}
/**
* 下划线转驼峰
*/
public static function camelize($uncamelized_words,$separator='_')
{
$uncamelized_words = $separator. str_replace($separator, " ", strtolower($uncamelized_words));
return ltrim(str_replace(" ", "", ucwords($uncamelized_words)), $separator );
}
/**
* 驼峰命名转下划线命名
*/
public static function uncamelize($camelCaps,$separator='_')
{
return strtolower(preg_replace('/([a-z])([A-Z])/', "$1" . $separator . "$2", $camelCaps));
}
/**
* 转换为驼峰
* @param string $value
* @return string
*/
public static function camel(string $value): string
{
static $cache = [];
$key = $value;
if (isset($cache[$key])) {
return $cache[$key];
}
$value = ucwords(str_replace(['-', '_'], ' ', $value));
return $cache[$key] = str_replace(' ', '', $value);
}
/**
* 获取业务名称
* @param string $tableName
* @return mixed
*/
public static function get_business(string $tableName)
{
$start = strrpos($tableName,'_');
if ($start !== false) {
$result = substr($tableName, $start + 1);
} else {
$result = $tableName;
}
return static::camelize($result);
}
/**
* 获取业务名称
* @param string $tableName
* @return mixed
*/
public static function get_big_business(string $tableName)
{
$start = strrpos($tableName,'_');
$result = substr($tableName, $start + 1);
return static::camel($result);
}
/**
* 只替换一次字符串
* @param $needle
* @param $replace
* @param $haystack
* @return array|mixed|string|string[]
*/
public static function str_replace_once($needle, $replace, $haystack)
{
$pos = strpos($haystack, $needle);
if ($pos === false) {
return $haystack;
}
return substr_replace($haystack, $replace, $pos, strlen($needle));
}
/**
* 遍历目录
* @param $template_name
* @return array
*/
public static function get_dir($template_name)
{
$dir = base_path($template_name);
$fileDir = [];
if (is_dir($dir)){
if ($dh = opendir($dir)){
while (($file = readdir($dh)) !== false){
if($file != "." && $file != ".."){
array_push($fileDir, $file);
}
}
closedir($dh);
}
}
return $fileDir;
}
}

View File

@@ -0,0 +1,253 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\utils;
/**
* 服务器监控信息
*/
class ServerMonitor
{
/**
* 获取内存信息
* @return array
*/
public function getMemoryInfo(): array
{
$totalMem = 0; // 总内存 (Bytes)
$freeMem = 0; // 可用/剩余内存 (Bytes)
if (stristr(PHP_OS, 'WIN')) {
// Windows 系统
// 一次性获取 总可见内存 和 空闲物理内存 (单位都是 KB)
// TotalVisibleMemorySize: 操作系统可识别的内存总数 (比物理内存条总数略少,更准确反映可用上限)
// FreePhysicalMemory: 当前可用物理内存
$cmd = 'wmic OS get FreePhysicalMemory,TotalVisibleMemorySize /format:csv';
$output = shell_exec($cmd);
$output = mb_convert_encoding($output ?? '', 'UTF-8', 'GBK, UTF-8, ASCII');
$lines = explode("\n", trim($output));
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
// CSV 格式: Node,FreePhysicalMemory,TotalVisibleMemorySize
$parts = str_getcsv($line);
// 确保解析正确且排除标题行 (通常索引1是Free, 2是Total)
if (count($parts) >= 3 && is_numeric($parts[1])) {
$freeMem = floatval($parts[1]) * 1024; // KB -> Bytes
$totalMem = floatval($parts[2]) * 1024; // KB -> Bytes
break;
}
}
} else {
// Linux 系统
// 读取 /proc/meminfo效率远高于 shell_exec('cat ...')
$memInfo = @file_get_contents('/proc/meminfo');
if ($memInfo) {
// 使用正则提取 MemTotal 和 MemAvailable (单位 kB)
// MemAvailable 是较新的内核指标,比单纯的 MemFree 更准确(包含可回收的缓存)
if (preg_match('/^MemTotal:\s+(\d+)\s+kB/m', $memInfo, $matches)) {
$totalMem = floatval($matches[1]) * 1024;
}
if (preg_match('/^MemAvailable:\s+(\d+)\s+kB/m', $memInfo, $matches)) {
$freeMem = floatval($matches[1]) * 1024;
} else {
// 如果内核太老没有 MemAvailable退化使用 MemFree
if (preg_match('/^MemFree:\s+(\d+)\s+kB/m', $memInfo, $matches)) {
$freeMem = floatval($matches[1]) * 1024;
}
}
}
}
// 计算已用内存
$usedMem = $totalMem - $freeMem;
// 避免除以0
$rate = ($totalMem > 0) ? ($usedMem / $totalMem) * 100 : 0;
// PHP 自身占用
$phpMem = memory_get_usage(true);
return [
// 人类可读格式 (String)
'total' => $this->formatBytes($totalMem),
'free' => $this->formatBytes($freeMem),
'used' => $this->formatBytes($usedMem),
'php' => $this->formatBytes($phpMem),
'rate' => sprintf('%.2f', $rate) . '%',
// 原始数值 (Float/Int),方便前端图表使用或逻辑判断,统一单位 Bytes
'raw' => [
'total' => $totalMem,
'free' => $freeMem,
'used' => $usedMem,
'php' => $phpMem,
'rate' => round($rate, 2)
]
];
}
/**
* 获取PHP及环境信息
* @return array
*/
public function getPhpAndEnvInfo(): array
{
return [
'php_version' => PHP_VERSION,
'os' => PHP_OS,
'project_path' => BASE_PATH,
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'error_reporting' => ini_get('error_reporting'),
'display_errors' => ini_get('display_errors'),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'extension_dir' => ini_get('extension_dir'),
'loaded_extensions' => implode(', ', get_loaded_extensions()),
];
}
/**
* 获取磁盘信息
* @return array
*/
public function getDiskInfo(): array
{
$disk = [];
if (stristr(PHP_OS, 'WIN')) {
// Windows 系统
// 使用 CSV 格式输出避免空格解析错误SkipTop=1 跳过空行
// LogicalDisk 包含: Caption(盘符), FreeSpace(剩余字节), Size(总字节)
$cmd = 'wmic logicaldisk get Caption,FreeSpace,Size /format:csv';
$output = shell_exec($cmd);
// 转换编码,防止中文乱码(视服务器环境而定,通常 Windows CMD 输出为 GBK
$output = mb_convert_encoding($output ?? '', 'UTF-8', 'GBK, UTF-8, ASCII');
$lines = explode("\n", trim($output));
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
// CSV 格式: Node,Caption,FreeSpace,Size
$parts = str_getcsv($line);
// 确保数据列足够且是一个盘符 (例如 "C:")
// 索引通常是: 1=>Caption, 2=>FreeSpace, 3=>Size (索引0通常是计算机名)
if (count($parts) >= 4 && preg_match('/^[A-Z]:$/', $parts[1])) {
$caption = $parts[1];
$freeSpace = floatval($parts[2]);
$totalSize = floatval($parts[3]);
// 避免除以 0 错误(如光驱未放入光盘时 Size 可能为 0 或 null
if ($totalSize <= 0) continue;
$usedSpace = $totalSize - $freeSpace;
$disk[] = [
'filesystem' => $caption,
'mounted_on' => $caption,
'size' => $this->formatBytes($totalSize),
'available' => $this->formatBytes($freeSpace),
'used' => $this->formatBytes($usedSpace),
'use_percentage' => sprintf('%.2f', ($usedSpace / $totalSize) * 100) . '%',
'raw' => [ // 保留原始数据以便前端或其他逻辑使用
'size' => $totalSize,
'available' => $freeSpace,
'used' => $usedSpace
]
];
}
}
} else {
// Linux 系统
// -P: POSIX 输出格式(强制在一行显示,防止长挂载点换行)
// -T: 显示文件系统类型
// 默认单位是 1K-blocks (1024字节)
$output = shell_exec('df -TP 2>/dev/null');
$lines = explode("\n", trim($output ?? ''));
// 过滤表头
array_shift($lines);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
// 限制分割数量,防止挂载点名称中有空格导致解析错位(虽然 -P 很大程度避免了这个问题,但仍需谨慎)
$parts = preg_split('/\s+/', $line);
// df -TP 输出列: Filesystem(0), Type(1), 1024-blocks(2), Used(3), Available(4), Capacity(5), Mounted on(6)
if (count($parts) >= 7) {
$filesystem = $parts[0];
$type = $parts[1];
$totalKB = floatval($parts[2]); // 单位是 KB
$usedKB = floatval($parts[3]);
$availKB = floatval($parts[4]);
$mountedOn = $parts[6];
// 过滤逻辑:只显示物理硬盘或特定挂载点
// 通常过滤掉 tmpfs, devtmpfs, overlay, squashfs(snap) 等
// 如果你只想看 /dev/ 开头的物理盘,保留原来的正则即可
if (!preg_match('/^\/dev\//', $filesystem)) {
// continue; // 根据需求决定是否取消注释此行
}
// 过滤掉 Docker overlay 或 kubelet 等产生的繁杂挂载
if (strpos($filesystem, 'overlay') !== false) continue;
// 转换为字节
$totalSize = $totalKB * 1024;
$usedSize = $usedKB * 1024;
$freeSize = $availKB * 1024;
if ($totalSize <= 0) continue;
$disk[] = [
'filesystem' => $filesystem,
'type' => $type,
'mounted_on' => $mountedOn,
'size' => $this->formatBytes($totalSize),
'available' => $this->formatBytes($freeSize),
'used' => $this->formatBytes($usedSize),
'use_percentage' => sprintf('%.2f', ($usedSize / $totalSize) * 100) . '%',
'raw' => [
'size' => $totalSize,
'available' => $freeSize,
'used' => $usedSize
]
];
}
}
}
return $disk;
}
/**
* 格式化字节为可读格式 (B, KB, MB, GB, TB...)
* @param int|float $bytes 字节数
* @param int $precision 小数点后保留位数
* @return string
*/
private function formatBytes($bytes, int $precision = 2): string
{
if ($bytes <= 0) {
return '0 B';
}
$base = log($bytes, 1024);
$suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
// 确保不会数组越界
$class = min((int)floor($base), count($suffixes) - 1);
return sprintf("%." . $precision . "f", $bytes / pow(1024, $class)) . ' ' . $suffixes[$class];
}
}

View File

@@ -0,0 +1,289 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\utils\code;
use Twig\TwigFilter;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use plugin\saiadmin\exception\ApiException;
// 定义目录分隔符常量
defined('DS') or define('DS', DIRECTORY_SEPARATOR);
/**
* 代码生成引擎
*/
class CodeEngine
{
/**
* @var array 值栈
*/
private array $value = [];
/**
* 模板名称
* @var string
*/
private string $stub = 'saiadmin';
/**
* 获取配置文件
* @return string[]
*/
private static function _getConfig(): array
{
return [
'template_path' => base_path() . DS . 'plugin' . DS . 'saiadmin' . DS . 'utils' . DS . 'code' . DS . 'stub',
'generate_path' => runtime_path() . DS . 'code_engine' . DS . 'saiadmin',
];
}
/**
* 初始化
* @param array $data 数据
*/
public function __construct(array $data)
{
// 读取配置文件
$config = self::_getConfig();
// 判断模板是否存在
if (!is_dir($config['template_path'])) {
throw new ApiException('模板目录不存在!');
}
// 判断文件生成目录是否存在
if (!is_dir($config['generate_path'])) {
mkdir($config['generate_path'], 0770, true);
}
// 赋值
$this->value = $data;
}
/**
* 设置模板名称
* @param $stub
* @return void
*/
public function setStub($stub): void
{
$this->stub = $stub;
}
/**
* 渲染文件内容
*/
public function renderContent($path, $filename): string
{
$config = self::_getConfig();
$path = $config['template_path'] . DS . $this->stub . DS . $path;
$loader = new FilesystemLoader($path);
$twig = new Environment($loader);
$camelFilter = new TwigFilter('camel', function ($value) {
static $cache = [];
$key = $value;
if (isset($cache[$key])) {
return $cache[$key];
}
$value = ucwords(str_replace(['-', '_'], ' ', $value));
return $cache[$key] = str_replace(' ', '', $value);
});
$boolFilter = new TwigFilter('bool', function ($value) {
if ($value == 1) {
return 'true';
} else {
return 'false';
}
});
$formatFilter = new TwigFilter('formatNumber', function ($value) {
if (ctype_digit((string) $value)) {
return $value;
} else {
return '1';
}
});
$defaultFilter = new TwigFilter('parseNumber', function ($value) {
if ($value) {
return $value;
} else {
return 'null';
}
});
$containsFilter = new TwigFilter('str_contains', function ($haystack, $needle) {
return str_contains($haystack ?? '', $needle ?? '');
});
$twig->addFilter($camelFilter);
$twig->addFilter($boolFilter);
$twig->addFilter($containsFilter);
$twig->addFilter($formatFilter);
$twig->addFilter($defaultFilter);
return $twig->render($filename, $this->value);
}
/**
* 生成后端文件
*/
public function generateBackend($action, $content): void
{
$outPath = '';
if ($this->value['template'] == 'app') {
$rootPath = base_path() . DS . 'app' . DS . $this->value['namespace'];
$adminPath = '';
} else {
$rootPath = base_path() . DS . 'plugin' . DS . $this->value['namespace'] . DS . 'app';
$adminPath = DS . 'admin';
}
$subPath = DS . $this->value['package_name'];
switch ($action) {
case 'controller':
$outPath = $rootPath . $adminPath . DS . 'controller' . $subPath . DS . $this->value['class_name'] . 'Controller.php';
break;
case 'logic':
$outPath = $rootPath . $adminPath . DS . 'logic' . $subPath . DS . $this->value['class_name'] . 'Logic.php';
break;
case 'validate':
$outPath = $rootPath . $adminPath . DS . 'validate' . $subPath . DS . $this->value['class_name'] . 'Validate.php';
break;
case 'model':
$outPath = $rootPath . DS . 'model' . $subPath . DS . $this->value['class_name'] . '.php';
break;
default:
break;
}
if (empty($outPath)) {
throw new ApiException('文件类型异常,无法生成指定文件!');
}
if (!is_dir(dirname($outPath))) {
mkdir(dirname($outPath), 0777, true);
}
file_put_contents($outPath, $content);
}
/**
* 生成前端文件
*/
public function generateFrontend($action, $content): void
{
$rootPath = dirname(base_path()) . DS . $this->value['generate_path'];
if (!is_dir($rootPath)) {
throw new ApiException('前端目录查找失败,必须与后端目录为同级目录!');
}
$rootPath = $rootPath . DS . 'src' . DS . 'views' . DS . 'plugin' . DS . $this->value['namespace'];
$subPath = DS . $this->value['package_name'];
switch ($action) {
case 'index':
$outPath = $rootPath . $subPath . DS . $this->value['business_name'] . DS . 'index.vue';
break;
case 'edit-dialog':
$outPath = $rootPath . $subPath . DS . $this->value['business_name'] . DS . 'modules' . DS . 'edit-dialog.vue';
break;
case 'table-search':
$outPath = $rootPath . $subPath . DS . $this->value['business_name'] . DS . 'modules' . DS . 'table-search.vue';
break;
case 'api':
$outPath = $rootPath . DS . 'api' . $subPath . DS . $this->value['business_name'] . '.ts';
break;
default:
break;
}
if (empty($outPath)) {
throw new ApiException('文件类型异常,无法生成指定文件!');
}
if (!is_dir(dirname($outPath))) {
mkdir(dirname($outPath), 0777, true);
}
file_put_contents($outPath, $content);
}
/**
* 生成临时文件
*/
public function generateTemp(): void
{
$config = self::_getConfig();
$rootPath = $config['generate_path'];
$vuePath = $rootPath . DS . 'vue' . DS . 'src' . DS . 'views' . DS . 'plugin' . DS . $this->value['namespace'];
$phpPath = $rootPath . DS . 'php';
$sqlPath = $rootPath . DS . 'sql';
if ($this->value['template'] == 'app') {
$phpPath = $phpPath . DS . 'app' . DS . $this->value['namespace'];
$adminPath = '';
} else {
$phpPath = $phpPath . DS . 'plugin' . DS . $this->value['namespace'] . DS . 'app';
$adminPath = DS . 'admin';
}
$subPath = DS . $this->value['package_name'];
$indexOutPath = $vuePath . $subPath . DS . $this->value['business_name'] . DS . 'index.vue';
$this->checkPath($indexOutPath);
$indexContent = $this->renderContent('vue', 'index.stub');
file_put_contents($indexOutPath, $indexContent);
$editOutPath = $vuePath . $subPath . DS . $this->value['business_name'] . DS . 'modules' . DS . 'edit-dialog.vue';
$this->checkPath($editOutPath);
$editContent = $this->renderContent('vue', 'edit-dialog.stub');
file_put_contents($editOutPath, $editContent);
$searchOutPath = $vuePath . $subPath . DS . $this->value['business_name'] . DS . 'modules' . DS . 'table-search.vue';
$this->checkPath($searchOutPath);
$searchContent = $this->renderContent('vue', 'table-search.stub');
file_put_contents($searchOutPath, $searchContent);
$viewOutPath = $vuePath . DS . 'api' . $subPath . DS . $this->value['business_name'] . '.ts';
$this->checkPath($viewOutPath);
$viewContent = $this->renderContent('ts', 'api.stub');
file_put_contents($viewOutPath, $viewContent);
$controllerOutPath = $phpPath . $adminPath . DS . 'controller' . $subPath . DS . $this->value['class_name'] . 'Controller.php';
$this->checkPath($controllerOutPath);
$controllerContent = $this->renderContent('php', 'controller.stub');
file_put_contents($controllerOutPath, $controllerContent);
$logicOutPath = $phpPath . $adminPath . DS . 'logic' . $subPath . DS . $this->value['class_name'] . 'Logic.php';
$this->checkPath($logicOutPath);
$logicContent = $this->renderContent('php', 'logic.stub');
file_put_contents($logicOutPath, $logicContent);
$validateOutPath = $phpPath . $adminPath . DS . 'validate' . $subPath . DS . $this->value['class_name'] . 'Validate.php';
$this->checkPath($validateOutPath);
$validateContent = $this->renderContent('php', 'validate.stub');
file_put_contents($validateOutPath, $validateContent);
$modelOutPath = $phpPath . DS . 'model' . $subPath . DS . $this->value['class_name'] . '.php';
$this->checkPath($modelOutPath);
$modelContent = $this->renderContent('php', 'model.stub');
file_put_contents($modelOutPath, $modelContent);
$sqlOutPath = $sqlPath . DS . 'sql.sql';
$this->checkPath($sqlOutPath);
$sqlContent = $this->renderContent('sql', 'sql.stub');
file_put_contents($sqlOutPath, $sqlContent);
}
/**
* 检查并生成路径
* @param $path
* @return void
*/
protected function checkPath($path): void
{
if (!is_dir(dirname($path))) {
mkdir(dirname($path), 0777, true);
}
}
}

View File

@@ -0,0 +1,164 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: sai <1430792918@qq.com>
// +----------------------------------------------------------------------
namespace plugin\saiadmin\utils\code;
use plugin\saiadmin\exception\ApiException;
/**
* 代码构建 压缩类
*/
class CodeZip
{
/**
* 获取配置文件
* @return string[]
*/
private static function _getConfig(): array
{
return [
'template_path' => base_path().DIRECTORY_SEPARATOR.'plugin'.DIRECTORY_SEPARATOR.'saiadmin'.DIRECTORY_SEPARATOR.'utils'.DIRECTORY_SEPARATOR.'code'.DIRECTORY_SEPARATOR.'stub',
'generate_path' => runtime_path().DIRECTORY_SEPARATOR.'code_engine'.DIRECTORY_SEPARATOR.'saiadmin',
];
}
/**
* 构造器
*/
public function __construct()
{
// 读取配置文件
$config = self::_getConfig();
// 清理源目录
if (is_dir($config['generate_path'])) {
$this->recursiveDelete($config['generate_path']);
}
// 清理压缩文件
$zipName = $config['generate_path'].'.zip';
if (is_file($zipName)) {
unlink($zipName);
}
}
/**
* 文件压缩
*/
public function compress(bool $isDownload = false)
{
// 读取配置文件
$config = self::_getConfig();
$zipArc = new \ZipArchive;
$zipName = $config['generate_path'].'.zip';
$dirPath = $config['generate_path'];
if ($zipArc->open($zipName, \ZipArchive::OVERWRITE | \ZipArchive::CREATE) !== true) {
throw new ApiException('无法打开文件,或者文件创建失败');
}
$this->addFileToZip($dirPath, $zipArc);
$zipArc->close();
// 是否下载
if ($isDownload) {
$this->toBinary($zipName);
} else {
return $zipName;
}
}
/**
* 文件解压
*/
public function deCompress(string $file, string $dirName)
{
if (!file_exists($file)) {
return false;
}
// zip实例化对象
$zipArc = new \ZipArchive();
// 打开文件
if (!$zipArc->open($file)) {
return false;
}
// 解压文件
if (!$zipArc->extractTo($dirName)) {
// 关闭
$zipArc->close();
return false;
}
return $zipArc->close();
}
/**
* 将文件加入到压缩包
*/
public function addFileToZip($rootPath, $zip)
{
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($rootPath),
\RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $name => $file)
{
// Skip directories (they would be added automatically)
if (!$file->isDir())
{
// Get real and relative path for current file
$filePath = $file->getRealPath();
$relativePath = substr($filePath, strlen($rootPath) + 1);
// Add current file to archive
$zip->addFile($filePath, $relativePath);
}
}
}
/**
* 递归删除目录下所有文件和文件夹
*/
public function recursiveDelete($dir)
{
// 打开指定目录
if ($handle = @opendir($dir)) {
while (($file = readdir($handle)) !== false) {
if (($file == ".") || ($file == "..")) {
continue;
}
if (is_dir($dir . '/' . $file)) {
// 递归
self::recursiveDelete($dir . '/' . $file);
} else {
unlink($dir . '/' . $file); // 删除文件
}
}
@closedir($handle);
}
rmdir($dir);
}
/**
* 下载二进制流文件
*/
public function toBinary(string $fileName)
{
try {
header("Cache-Control: public");
header("Content-Description: File Transfer");
header('Content-disposition: attachment; filename=' . basename($fileName)); //文件名
header("Content-Type: application/zip"); //zip格式的
header("Content-Transfer-Encoding: binary"); //告诉浏览器,这是二进制文件
header('Content-Length: ' . filesize($fileName)); //告诉浏览器,文件大小
if(ob_get_length() > 0) {
ob_clean();
}
flush();
@readfile($fileName);
@unlink($fileName);
} catch (\Throwable $th) {
throw new ApiException('系统生成文件错误');
}
}
}

View File

@@ -0,0 +1,137 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace {{namespace_start}}controller{{namespace_end}};
use plugin\saiadmin\basic\BaseController;
use {{namespace_start}}logic{{namespace_end}}\{{class_name}}Logic;
use {{namespace_start}}validate{{namespace_end}}\{{class_name}}Validate;
use plugin\saiadmin\service\Permission;
use support\Request;
use support\Response;
/**
* {{menu_name}}控制器
*/
class {{class_name}}Controller extends BaseController
{
/**
* 构造函数
*/
public function __construct()
{
$this->logic = new {{class_name}}Logic();
$this->validate = new {{class_name}}Validate;
parent::__construct();
}
/**
* 数据列表
* @param Request $request
* @return Response
*/
#[Permission('{{menu_name}}列表', '{{namespace}}:{{package_name}}:{{business_name}}:index')]
public function index(Request $request): Response
{
$where = $request->more([
{% for column in columns %}
{% if column.is_query == '2' %}
['{{column.column_name}}', ''],
{% endif %}
{% endfor %}
]);
{% if tpl_category == 'single' %}
$query = $this->logic->search($where);
{% if options.relations != null %}
$query->with([
{% for item in options.relations %}
'{{item.name}}',
{% endfor %}
]);
{% endif %}
$data = $this->logic->getList($query);
{% endif %}
{% if tpl_category == 'tree' %}
$data = $this->logic->tree($where);
{% endif %}
return $this->success($data);
}
/**
* 读取数据
* @param Request $request
* @return Response
*/
#[Permission('{{menu_name}}读取', '{{namespace}}:{{package_name}}:{{business_name}}:read')]
public function read(Request $request): Response
{
$id = $request->input('id', '');
$model = $this->logic->read($id);
if ($model) {
$data = is_array($model) ? $model : $model->toArray();
return $this->success($data);
} else {
return $this->fail('未查找到信息');
}
}
/**
* 保存数据
* @param Request $request
* @return Response
*/
#[Permission('{{menu_name}}添加', '{{namespace}}:{{package_name}}:{{business_name}}:save')]
public function save(Request $request): Response
{
$data = $request->post();
$this->validate('save', $data);
$result = $this->logic->add($data);
if ($result) {
return $this->success('添加成功');
} else {
return $this->fail('添加失败');
}
}
/**
* 更新数据
* @param Request $request
* @return Response
*/
#[Permission('{{menu_name}}修改', '{{namespace}}:{{package_name}}:{{business_name}}:update')]
public function update(Request $request): Response
{
$data = $request->post();
$this->validate('update', $data);
$result = $this->logic->edit($data['id'], $data);
if ($result) {
return $this->success('修改成功');
} else {
return $this->fail('修改失败');
}
}
/**
* 删除数据
* @param Request $request
* @return Response
*/
#[Permission('{{menu_name}}删除', '{{namespace}}:{{package_name}}:{{business_name}}:destroy')]
public function destroy(Request $request): Response
{
$ids = $request->post('ids', '');
if (empty($ids)) {
return $this->fail('请选择要删除的数据');
}
$result = $this->logic->destroy($ids);
if ($result) {
return $this->success('删除成功');
} else {
return $this->fail('删除失败');
}
}
}

View File

@@ -0,0 +1,89 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace {{namespace_start}}logic{{namespace_end}};
{% if stub == 'eloquent' %}
use plugin\saiadmin\basic\eloquent\BaseLogic;
{% else %}
use plugin\saiadmin\basic\think\BaseLogic;
{% endif %}
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Helper;
use {{namespace_start_model}}model{{namespace_end}}\{{class_name}};
/**
* {{menu_name}}逻辑层
*/
class {{class_name}}Logic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new {{class_name}}();
}
{% if tpl_category == 'tree' %}
/**
* 修改数据
* @param $id
* @param $data
* @return mixed
*/
public function edit($id, $data): mixed
{
if (!isset($data['{{options.tree_parent_id}}'])) {
$data['{{options.tree_parent_id}}'] = 0;
}
if ($data['{{options.tree_parent_id}}'] == $data['{{options.tree_id}}']) {
throw new ApiException('不能设置父级为自身');
}
return parent::edit($id, $data);
}
/**
* 删除数据
* @param $ids
*/
public function destroy($ids): bool
{
$num = $this->model->whereIn('{{options.tree_parent_id}}', $ids)->count();
if ($num > 0) {
throw new ApiException('该分类下存在子分类,请先删除子分类');
} else {
return parent::destroy($ids);
}
}
/**
* 树形数据
*/
public function tree($where)
{
$query = $this->search($where);
$request = request();
if ($request && $request->input('tree', 'false') === 'true') {
{% if stub == 'eloquent' %}
$query->select('{{options.tree_id}}', '{{options.tree_id}} as value', '{{options.tree_name}} as label', '{{options.tree_parent_id}}');
{% else %}
$query->field('{{options.tree_id}}, {{options.tree_id}} as value, {{options.tree_name}} as label, {{options.tree_parent_id}}');
{% endif %}
}
{% if options.relations != null %}
$query->with([
{% for item in options.relations %}
'{{item.name}}',
{% endfor %}
]);
{% endif %}
$data = $this->getAll($query);
return Helper::makeTree($data);
}
{% endif %}
}

View File

@@ -0,0 +1,304 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace {{namespace_start_model}}model{{namespace_end}};
{% if stub == 'eloquent' %}
use plugin\saiadmin\basic\eloquent\BaseModel;
{% else %}
use plugin\saiadmin\basic\think\BaseModel;
{% endif %}
/**
* {{menu_name}}模型
*
* {{table_name}} {{table_comment}}
*
{% for column in columns %}
* @property {{column.php_type}} ${{column.column_name}} {{column.column_comment}}
{% endfor %}
*/
class {{class_name}} extends BaseModel
{
/**
* 数据表主键
* @var string
*/
{% if stub == 'eloquent' %}
protected $primaryKey = '{{pk}}';
{% else %}
protected $pk = '{{pk}}';
{% endif %}
/**
* 数据库表名称
* @var string
*/
protected $table = '{{table_name}}';
{% if source != db_source and source != '' %}
/**
* 数据库连接
* @var string
*/
protected $connection = '{{source}}';
{% endif %}
{% if stub == 'eloquent' %}
/**
* 属性转换
*/
protected function casts(): array
{
return array_merge(parent::casts(), [
{% for column in columns %}
{% if column.view_type == 'inputTag' or column.view_type == 'checkbox' %}
'{{column.column_name}}' => 'array',
{% endif %}
{% if column.view_type == 'uploadImage' and column.options.limit > 1 %}
'{{column.column_name}}' => 'array',
{% endif %}
{% if column.view_type == 'imagePicker' and column.options.limit > 1 %}
'{{column.column_name}}' => 'array',
{% endif %}
{% if column.view_type == 'uploadFile' and column.options.limit > 1 %}
'{{column.column_name}}' => 'array',
{% endif %}
{% if column.view_type == 'chunkUpload' and column.options.limit > 1 %}
'{{column.column_name}}' => 'array',
{% endif %}
{% endfor %}
]);
}
{% else %}
{% for column in columns %}
{% if column.view_type == 'inputTag' or column.view_type == 'checkbox' %}
/**
* {{column.column_comment}} 保存数组转换
*/
public function set{{column.column_name | camel}}Attr($value)
{
return json_encode($value, JSON_UNESCAPED_UNICODE);
}
/**
* {{column.column_comment}} 读取数组转换
*/
public function get{{column.column_name | camel}}Attr($value)
{
return json_decode($value ?? '', true);
}
{% endif %}
{% if column.view_type == 'uploadImage' and column.options.limit > 1 %}
/**
* {{column.column_comment}} 保存数组转换
*/
public function set{{column.column_name | camel}}Attr($value)
{
return json_encode($value, JSON_UNESCAPED_UNICODE);
}
/**
* {{column.column_comment}} 读取数组转换
*/
public function get{{column.column_name | camel}}Attr($value)
{
return json_decode($value ?? '', true);
}
{% endif %}
{% if column.view_type == 'imagePicker' and column.options.limit > 1 %}
/**
* {{column.column_comment}} 保存数组转换
*/
public function set{{column.column_name | camel}}Attr($value)
{
return json_encode($value, JSON_UNESCAPED_UNICODE);
}
/**
* {{column.column_comment}} 读取数组转换
*/
public function get{{column.column_name | camel}}Attr($value)
{
return json_decode($value ?? '', true);
}
{% endif %}
{% if column.view_type == 'uploadFile' and column.options.limit > 1 %}
/**
* {{column.column_comment}} 保存数组转换
*/
public function set{{column.column_name | camel}}Attr($value)
{
return json_encode($value, JSON_UNESCAPED_UNICODE);
}
/**
* {{column.column_comment}} 读取数组转换
*/
public function get{{column.column_name | camel}}Attr($value)
{
return json_decode($value ?? '', true);
}
{% endif %}
{% if column.view_type == 'chunkUpload' and column.options.limit > 1 %}
/**
* {{column.column_comment}} 保存数组转换
*/
public function set{{column.column_name | camel}}Attr($value)
{
return json_encode($value, JSON_UNESCAPED_UNICODE);
}
/**
* {{column.column_comment}} 读取数组转换
*/
public function get{{column.column_name | camel}}Attr($value)
{
return json_decode($value ?? '', true);
}
{% endif %}
{% endfor %}
{% endif %}
{% for column in columns %}
{% if column.is_query == 2 and column.query_type == 'neq' %}
/**
* {{column.column_comment}} 搜索
*/
public function search{{column.column_name | camel}}Attr($query, $value)
{
$query->where('{{column.column_name}}', '<>', $value);
}
{% endif %}
{% if column.is_query == 2 and column.query_type == 'gt' %}
/**
* {{column.column_comment}} 搜索
*/
public function search{{column.column_name | camel}}Attr($query, $value)
{
$query->where('{{column.column_name}}', '>', $value);
}
{% endif %}
{% if column.is_query == 2 and column.query_type == 'gte' %}
/**
* {{column.column_comment}} 搜索
*/
public function search{{column.column_name | camel}}Attr($query, $value)
{
$query->where('{{column.column_name}}', '>=', $value);
}
{% endif %}
{% if column.is_query == 2 and column.query_type == 'lt' %}
/**
* {{column.column_comment}} 搜索
*/
public function search{{column.column_name | camel}}Attr($query, $value)
{
$query->where('{{column.column_name}}', '<', $value);
}
{% endif %}
{% if column.is_query == 2 and column.query_type == 'lte' %}
/**
* {{column.column_comment}} 搜索
*/
public function search{{column.column_name | camel}}Attr($query, $value)
{
$query->where('{{column.column_name}}', '<=', $value);
}
{% endif %}
{% if column.is_query == 2 and column.query_type == 'like' %}
/**
* {{column.column_comment}} 搜索
*/
public function search{{column.column_name | camel}}Attr($query, $value)
{
$query->where('{{column.column_name}}', 'like', '%'.$value.'%');
}
{% endif %}
{% if column.is_query == 2 and column.query_type == 'in' %}
/**
* {{column.column_comment}} 搜索
*/
public function search{{column.column_name | camel}}Attr($query, $value)
{
$query->whereIn('{{column.column_name}}', $value);
}
{% endif %}
{% if column.is_query == 2 and column.query_type == 'notin' %}
/**
* {{column.column_comment}} 搜索
*/
public function search{{column.column_name | camel}}Attr($query, $value)
{
$query->whereNotIn('{{column.column_name}}', $value);
}
{% endif %}
{% if column.is_query == 2 and column.query_type == 'between' %}
/**
* {{column.column_comment}} 搜索
*/
public function search{{column.column_name | camel}}Attr($query, $value)
{
$query->whereBetween('{{column.column_name}}', $value);
}
{% endif %}
{% endfor %}
{% for item in options.relations %}
{% if item.type == 'belongsTo' %}
/**
* 关联模型 {{item.name}}
*/
public function {{item.name}}()
{
return $this->{{item.type}}({{item.model}}::class, '{{item.localKey}}', '{{item.foreignKey}}');
}
{% endif %}
{% if item.type == 'hasOne' or item.type == 'hasMany' %}
/**
* 关联模型 {{item.name}}
*/
public function {{item.name}}()
{
return $this->{{item.type}}({{item.model}}::class, '{{item.localKey}}', '{{item.foreignKey}}');
}
{% endif %}
{% if item.type == 'belongsToMany' and stub == 'think' %}
/**
* 关联模型 {{item.name}}
*/
public function {{item.name}}()
{
return $this->{{item.type}}({{item.model}}::class, {{item.table}}::class, '{{item.localKey}}', '{{item.foreignKey}}');
}
{% endif %}
{% if item.type == 'belongsToMany' and stub == 'eloquent' %}
/**
* 关联模型 {{item.name}}
*/
public function {{item.name}}()
{
return $this->{{item.type}}({{item.model}}::class, {{item.table}}::class, '{{item.foreignKey}}', '{{item.localKey}}');
}
{% endif %}
{% endfor %}
}

View File

@@ -0,0 +1,58 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace {{namespace_start}}validate{{namespace_end}};
use plugin\saiadmin\basic\BaseValidate;
/**
* {{menu_name}}验证器
*/
class {{class_name}}Validate extends BaseValidate
{
/**
* 定义验证规则
*/
protected $rule = [
{% for column in columns %}
{% if column.is_required == 2 and column.is_pk != 2 %}
'{{column.column_name}}' => 'require',
{% endif %}
{% endfor %}
];
/**
* 定义错误信息
*/
protected $message = [
{% for column in columns %}
{% if column.is_required == 2 and column.is_pk != 2 %}
'{{column.column_name}}' => '{{column.column_comment}}必须填写',
{% endif %}
{% endfor %}
];
/**
* 定义场景
*/
protected $scene = [
'save' => [
{% for column in columns %}
{% if column.is_required == 2 and column.is_pk != 2 %}
'{{column.column_name}}',
{% endif %}
{% endfor %}
],
'update' => [
{% for column in columns %}
{% if column.is_required == 2 and column.is_pk != 2 %}
'{{column.column_name}}',
{% endif %}
{% endfor %}
],
];
}

View File

@@ -0,0 +1,14 @@
-- 数据库语句--
{% for column in tables %}
-- 菜单[{{column.menu_name}}] SQL
INSERT INTO `sa_system_menu`(`parent_id`, `name`, `code`, `slug`, `type`, `path`, `component`, `icon`, `sort`, `is_iframe`, `is_keep_alive`, `is_hidden`, `is_fixed_tab`, `is_full_page`, `create_time`, `update_time`) VALUES ({{column.belong_menu_id}}, '{{column.menu_name}}', '{{column.namespace}}/{{column.package_name}}/{{column.business_name}}', '', 2, '{{column.package_name}}/{{column.business_name}}', '/plugin/{{column.namespace}}/{{column.package_name}}/{{column.business_name}}/index', 'ri:home-2-line', 100, 2, 2, 2, 2, 2, now(), now());
SET @id := LAST_INSERT_ID();
INSERT INTO `sa_system_menu`(`parent_id`, `name`, `slug`, `type`, `sort`, `is_iframe`, `is_keep_alive`, `is_hidden`, `is_fixed_tab`, `is_full_page`, `create_time`, `update_time`) VALUES (@id, '列表', '{{column.namespace}}:{{column.package_name}}:{{column.business_name}}:index', 3, 100, 2, 2, 2, 2, 2, now(), now());
INSERT INTO `sa_system_menu`(`parent_id`, `name`, `slug`, `type`, `sort`, `is_iframe`, `is_keep_alive`, `is_hidden`, `is_fixed_tab`, `is_full_page`, `create_time`, `update_time`) VALUES (@id, '保存', '{{column.namespace}}:{{column.package_name}}:{{column.business_name}}:save', 3, 100, 2, 2, 2, 2, 2, now(), now());
INSERT INTO `sa_system_menu`(`parent_id`, `name`, `slug`, `type`, `sort`, `is_iframe`, `is_keep_alive`, `is_hidden`, `is_fixed_tab`, `is_full_page`, `create_time`, `update_time`) VALUES (@id, '更新', '{{column.namespace}}:{{column.package_name}}:{{column.business_name}}:update', 3, 100, 2, 2, 2, 2, 2, now(), now());
INSERT INTO `sa_system_menu`(`parent_id`, `name`, `slug`, `type`, `sort`, `is_iframe`, `is_keep_alive`, `is_hidden`, `is_fixed_tab`, `is_full_page`, `create_time`, `update_time`) VALUES (@id, '读取', '{{column.namespace}}:{{column.package_name}}:{{column.business_name}}:read', 3, 100, 2, 2, 2, 2, 2, now(), now());
INSERT INTO `sa_system_menu`(`parent_id`, `name`, `slug`, `type`, `sort`, `is_iframe`, `is_keep_alive`, `is_hidden`, `is_fixed_tab`, `is_full_page`, `create_time`, `update_time`) VALUES (@id, '删除', '{{column.namespace}}:{{column.package_name}}:{{column.business_name}}:destroy', 3, 100, 2, 2, 2, 2, 2, now(), now());
{% endfor %}

View File

@@ -0,0 +1,69 @@
import request from '@/utils/http'
/**
* {{menu_name}} API接口
*/
export default {
/**
* 获取数据列表
* @param params 搜索参数
* @returns 数据列表
*/
list(params: Record<string, any>) {
{% if tpl_category == 'tree' %}
return request.get<Api.Common.ApiData[]>({
{% else %}
return request.get<Api.Common.ApiPage>({
{% endif %}
url: '/{{url_path}}/index',
params
})
},
/**
* 读取数据
* @param id 数据ID
* @returns 数据详情
*/
read(id: number | string) {
return request.get<Api.Common.ApiData>({
url: '/{{url_path}}/read?id=' + id
})
},
/**
* 创建数据
* @param params 数据参数
* @returns 执行结果
*/
save(params: Record<string, any>) {
return request.post<any>({
url: '/{{url_path}}/save',
data: params
})
},
/**
* 更新数据
* @param params 数据参数
* @returns 执行结果
*/
update(params: Record<string, any>) {
return request.put<any>({
url: '/{{url_path}}/update',
data: params
})
},
/**
* 删除数据
* @param id 数据ID
* @returns 执行结果
*/
delete(params: Record<string, any>) {
return request.del<any>({
url: '/{{url_path}}/destroy',
data: params
})
}
}

View File

@@ -0,0 +1,318 @@
<template>
{% if component_type == 1 %}
<el-dialog
{% else %}
<el-drawer
{% endif %}
v-model="visible"
:title="dialogType === 'add' ? '新增{{menu_name}}' : '编辑{{menu_name}}'"
{% if is_full == 2 %}
{% if component_type == 1 %}
:fullscreen="true"
{% else %}
size="100%"
{% endif %}
{% else %}
{% if component_type == 1 %}
width="{{form_width}}px"
{% else %}
:size="{{form_width}}"
{% endif %}
{% endif %}
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
{% for column in columns %}
{% if column.is_insert == 2 %}
{% if column.view_type == 'input' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-input v-model="formData.{{column.column_name}}" placeholder="请输入{{column.column_comment}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'password' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-input v-model="formData.{{column.column_name}}" type="password" placeholder="请输入{{column.column_comment}}" show-password />
</el-form-item>
{% endif %}
{% if column.view_type == 'textarea' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-input v-model="formData.{{column.column_name}}" type="textarea" :rows="2" placeholder="请输入{{column.column_comment}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'inputNumber' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-input-number v-model="formData.{{column.column_name}}" placeholder="请输入{{column.column_comment}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'inputTag' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-input-tag v-model="formData.{{column.column_name}}" placeholder="请输入{{column.column_comment}}" clearable />
</el-form-item>
{% endif %}
{% if column.view_type == 'switch' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<sa-switch v-model="formData.{{column.column_name}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'slider' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-slider v-model="formData.{{column.column_name}}" placeholder="请输入{{column.column_comment}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'select' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-select v-model="formData.{{column.column_name}}" :options="[]" placeholder="请选择{{column.column_comment}}" clearable />
</el-form-item>
{% endif %}
{% if column.view_type == 'saSelect' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<sa-select v-model="formData.{{column.column_name}}" dict="{{column.dict_type}}" placeholder="请选择{{column.column_comment}}" clearable />
</el-form-item>
{% endif %}
{% if column.view_type == 'treeSelect' and column.column_name == options.tree_parent_id %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-tree-select
v-model="formData.{{column.column_name}}"
:data="optionData.treeData"
placeholder="请选择{{column.column_comment}}"
check-strictly
clearable
/>
</el-form-item>
{% endif %}
{% if column.view_type == 'treeSelect' and column.column_name != options.tree_parent_id %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-tree-select v-model="formData.{{column.column_name}}" :data="[]" placeholder="请选择{{column.column_comment}}" clearable />
</el-form-item>
{% endif %}
{% if column.view_type == 'radio' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<sa-radio v-model="formData.{{column.column_name}}" dict="{{column.dict_type}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'checkbox' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<sa-checkbox v-model="formData.{{column.column_name}}" dict="{{column.dict_type}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'date' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-date-picker v-model="formData.{{column.column_name}}" type="{{column.options.mode}}" placeholder="请选择{{column.column_comment}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'time' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-time-picker v-model="formData.{{column.column_name}}" placeholder="请选择{{column.column_comment}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'rate' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-rate v-model="formData.{{column.column_name}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'cascader' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-cascader v-model="formData.{{column.column_name}}" :options="[]" placeholder="请选择{{column.column_comment}}" allow-clear />
</el-form-item>
{% endif %}
{% if column.view_type == 'userSelect' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<sa-user v-model="formData.{{column.column_name}}" :multiple="{{column.options.multiple|bool}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'uploadImage' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<sa-image-upload v-model="formData.{{column.column_name}}" :limit="{{column.options.limit|formatNumber}}" :multiple="{{column.options.multiple|bool}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'imagePicker' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<sa-image-picker v-model="formData.{{column.column_name}}" :limit="{{column.options.limit|formatNumber}}" :multiple="{{column.options.multiple|bool}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'uploadFile' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<sa-file-upload v-model="formData.{{column.column_name}}" :limit="{{column.options.limit|formatNumber}}" :multiple="{{column.options.multiple|bool}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'chunkUpload' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<sa-chunk-upload v-model="formData.{{column.column_name}}" :limit="{{column.options.limit|formatNumber}}" :multiple="{{column.options.multiple|bool}}" />
</el-form-item>
{% endif %}
{% if column.view_type == 'editor' %}
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<sa-editor v-model="formData.{{column.column_name}}" height="{{column.options.height}}px" />
</el-form-item>
{% endif %}
{% endif %}
{% endfor %}
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</template>
{% if component_type == 1 %}
</el-dialog>
{% else %}
</el-drawer>
{% endif %}
</template>
<script setup lang="ts">
import api from '../../../api/{{package_name}}/{{business_name}}'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
interface Props {
modelValue: boolean
dialogType: string
data?: Record<string, any>
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'success'): void
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
dialogType: 'add',
data: undefined
})
const emit = defineEmits<Emits>()
const formRef = ref<FormInstance>()
{% if tpl_category == 'tree' %}
const optionData = reactive({
treeData: <any[]>[]
})
{% endif %}
/**
* 弹窗显示状态双向绑定
*/
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
/**
* 表单验证规则
*/
const rules = reactive<FormRules>({
{% for column in columns %}
{% if column.is_required == 2 and column.is_pk == 1 %}
{{column.column_name}}: [{ required: true, message: '{{column.column_comment}}必需填写', trigger: 'blur' }],
{% endif %}
{% endfor %}
})
/**
* 初始数据
*/
const initialFormData = {
{% for column in columns %}
{% if column.is_pk == 2 %}
{{column.column_name}}: null,
{% elseif column.is_insert == 2 %}
{% if column.column_type == 'int' or column.column_type == 'smallint' or column.column_type == 'tinyint' %}
{{column.column_name}}: {{column.default_value | parseNumber}},
{% elseif column.view_type == 'inputTag' or column.view_type == 'checkbox' or column.options.limit > 1 %}
{{column.column_name}}: [],
{% elseif column.view_type == 'userSelect' %}
{{column.column_name}}: null,
{% else %}
{{column.column_name}}: '{{column.default_value}}',
{% endif %}
{% endif %}
{% endfor %}
}
/**
* 表单数据
*/
const formData = reactive({ ...initialFormData })
/**
* 监听弹窗打开,初始化表单数据
*/
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
initPage()
}
}
)
/**
* 初始化页面数据
*/
const initPage = async () => {
// 先重置为初始值
Object.assign(formData, initialFormData)
{% if tpl_category == 'tree' %}
// 初始化树形数据
const data = await api.list({ tree: true })
optionData.treeData = [
{
id: 0,
value: 0,
label: '无上级分类',
children: data
}
]
{% endif %}
// 如果有数据,则填充数据
if (props.data) {
await nextTick()
initForm()
}
}
/**
* 初始化表单数据
*/
const initForm = () => {
if (props.data) {
for (const key in formData) {
if (props.data[key] != null && props.data[key] != undefined) {
;(formData as any)[key] = props.data[key]
}
}
}
}
/**
* 关闭弹窗并重置表单
*/
const handleClose = () => {
visible.value = false
formRef.value?.resetFields()
}
/**
* 提交表单
*/
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
if (props.dialogType === 'add') {
await api.save(formData)
ElMessage.success('新增成功')
} else {
await api.update(formData)
ElMessage.success('修改成功')
}
emit('success')
handleClose()
} catch (error) {
console.log('表单验证失败:', error)
}
}
</script>

View File

@@ -0,0 +1,189 @@
<template>
<div class="art-full-height">
<!-- 搜索面板 -->
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
<ElCard class="art-table-card" shadow="never">
<!-- 表格头部 -->
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
<template #left>
<ElSpace wrap>
<ElButton v-permission="'{{namespace}}:{{package_name}}:{{business_name}}:save'" @click="showDialog('add')" v-ripple>
<template #icon>
<ArtSvgIcon icon="ri:add-fill" />
</template>
新增
</ElButton>
<ElButton
v-permission="'{{namespace}}:{{package_name}}:{{business_name}}:destroy'"
:disabled="selectedRows.length === 0"
@click="deleteSelectedRows(api.delete, refreshData)"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:delete-bin-5-line" />
</template>
删除
</ElButton>
{% if tpl_category == 'tree' %}
<ElButton @click="toggleExpand" v-ripple>
<template #icon>
<ArtSvgIcon v-if="isExpanded" icon="ri:collapse-diagonal-line" />
<ArtSvgIcon v-else icon="ri:expand-diagonal-line" />
</template>
{{ isExpanded ? '收起' : '展开' }}
</ElButton>
{% endif %}
</ElSpace>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
rowKey="id"
:loading="loading"
:data="data"
:columns="columns"
:pagination="pagination"
@sort-change="handleSortChange"
@selection-change="handleSelectionChange"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
>
<!-- 操作列 -->
<template #operation="{ row }">
<div class="flex gap-2">
<SaButton
v-permission="'{{namespace}}:{{package_name}}:{{business_name}}:update'"
type="secondary"
@click="showDialog('edit', row)"
/>
<SaButton
v-permission="'{{namespace}}:{{package_name}}:{{business_name}}:destroy'"
type="error"
@click="deleteRow(row, api.delete, refreshData)"
/>
</div>
</template>
</ArtTable>
</ElCard>
<!-- 编辑弹窗 -->
<EditDialog
v-model="dialogVisible"
:dialog-type="dialogType"
:data="dialogData"
@success="refreshData"
/>
</div>
</template>
<script setup lang="ts">
import { useTable } from '@/hooks/core/useTable'
import { useSaiAdmin } from '@/composables/useSaiAdmin'
import api from '../../api/{{package_name}}/{{business_name}}'
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-dialog.vue'
{% if tpl_category == 'tree' %}
// 状态管理
const isExpanded = ref(false)
const tableRef = ref()
{% endif %}
// 搜索表单
const searchForm = ref({
{% for column in columns %}
{% if column.is_query == 2 and column.query_type != 'between' %}
{{column.column_name}}: undefined,
{% endif %}
{% if column.is_query == 2 and column.query_type == 'between' %}
{{column.column_name}}: [],
{% endif %}
{% endfor %}
})
// 搜索处理
const handleSearch = (params: Record<string, any>) => {
Object.assign(searchParams, params)
getData()
}
// 表格配置
const {
columns,
columnChecks,
data,
loading,
getData,
searchParams,
pagination,
resetSearchParams,
handleSortChange,
handleSizeChange,
handleCurrentChange,
refreshData
} = useTable({
core: {
apiFn: api.list,
columnsFactory: () => [
{ type: 'selection' },
{% for column in columns %}
{% if column.is_list == 2 %}
{% if column.view_type == 'uploadImage' or column.view_type == 'imagePicker' %}
{ prop: '{{column.column_name}}', label: '{{column.column_comment}}', saiType: 'image' },
{% else %}
{% if column.is_sort == 2 %}
{% if column.view_type == 'saSelect' or column.view_type == 'radio' %}
{ prop: '{{column.column_name}}', label: '{{column.column_comment}}', saiType: 'dict', saiDict: '{{column.dict_type}}', sortable: true },
{% else %}
{ prop: '{{column.column_name}}', label: '{{column.column_comment}}', sortable: true },
{% endif %}
{% else %}
{% if column.view_type == 'saSelect' or column.view_type == 'radio' %}
{ prop: '{{column.column_name}}', label: '{{column.column_comment}}', saiType: 'dict', saiDict: '{{column.dict_type}}' },
{% else %}
{ prop: '{{column.column_name}}', label: '{{column.column_comment}}' },
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
]
}
})
// 编辑配置
const {
dialogType,
dialogVisible,
dialogData,
showDialog,
deleteRow,
deleteSelectedRows,
handleSelectionChange,
selectedRows
} = useSaiAdmin()
{% if tpl_category == 'tree' %}
// 切换展开/收起所有菜单
const toggleExpand = (): void => {
isExpanded.value = !isExpanded.value
nextTick(() => {
if (tableRef.value?.elTableRef && data.value) {
const processRows = (rows: any[]) => {
rows.forEach((row) => {
if (row.children?.length) {
tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value)
processRows(row.children)
}
})
}
processRows(data.value)
}
})
}
{% endif %}
</script>

View File

@@ -0,0 +1,115 @@
<template>
<sa-search-bar
ref="searchBarRef"
v-model="formData"
label-width="100px"
:showExpand="false"
@reset="handleReset"
@search="handleSearch"
@expand="handleExpand"
>
{% for column in columns %}
{% if column.is_query == 2 %}
{% if column.view_type == 'select' %}
<el-col v-bind="setSpan(6)">
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-select v-model="formData.{{column.column_name}}" :options="[]" placeholder="请选择{{column.column_comment}}" clearable />
</el-form-item>
</el-col>
{% endif %}
{% if column.view_type == 'saSelect' or column.view_type == 'radio' %}
<el-col v-bind="setSpan(6)">
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<sa-select v-model="formData.{{column.column_name}}" dict="{{column.dict_type}}" placeholder="请选择{{column.column_comment}}" clearable />
</el-form-item>
</el-col>
{% endif %}
{% if column.view_type == 'cascader' %}
<el-col v-bind="setSpan(6)">
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-cascader v-model="formData.{{column.column_name}}" :options="[]" placeholder="请选择{{column.column_comment}}" clearable />
</el-form-item>
</el-col>
{% endif %}
{% if column.view_type == 'date' and column.options.mode == 'date' and column.query_type == 'between' %}
<el-col v-bind="setSpan(6)">
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-date-picker v-model="formData.{{column.column_name}}" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" clearable />
</el-form-item>
</el-col>
{% endif %}
{% if column.view_type == 'date' and column.options.mode == 'datetime' and column.query_type == 'between' %}
<el-col v-bind="setSpan(12)">
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-date-picker v-model="formData.{{column.column_name}}" type="datetimerange" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD HH:mm:ss" clearable />
</el-form-item>
</el-col>
{% endif %}
{% if column.view_type == 'date' and column.query_type != 'between' %}
<el-col v-bind="setSpan(6)">
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-date-picker v-model="formData.{{column.column_name}}" type="{{column.options.mode}}" placeholder="请选择{{column.column_comment}}" value-format="YYYY-MM-DD HH:mm:ss" clearable />
</el-form-item>
</el-col>
{% endif %}
{% if column.view_type != 'select' and column.view_type != 'radio' and column.view_type != 'saSelect' and column.view_type != 'cascader' and column.view_type != 'date' %}
<el-col v-bind="setSpan(6)">
<el-form-item label="{{column.column_comment}}" prop="{{column.column_name}}">
<el-input v-model="formData.{{column.column_name}}" placeholder="请输入{{column.column_comment}}" clearable />
</el-form-item>
</el-col>
{% endif %}
{% endif %}
{% endfor %}
</sa-search-bar>
</template>
<script setup lang="ts">
interface Props {
modelValue: Record<string, any>
}
interface Emits {
(e: 'update:modelValue', value: Record<string, any>): void
(e: 'search', params: Record<string, any>): void
(e: 'reset'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// 展开/收起
const isExpanded = ref<boolean>(false)
// 表单数据双向绑定
const searchBarRef = ref()
const formData = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
// 重置
function handleReset() {
searchBarRef.value?.ref.resetFields()
emit('reset')
}
// 搜索
async function handleSearch() {
emit('search', formData.value)
}
// 展开/收起
function handleExpand(expanded: boolean) {
isExpanded.value = expanded
}
// 栅格占据的列数
const setSpan = (span: number) => {
return {
span: span,
xs: 24, // 手机:满宽显示
sm: span >= 12 ? span : 12, // 平板大于等于12保持否则用半宽
md: span >= 8 ? span : 8, // 中等屏幕大于等于8保持否则用三分之一宽
lg: span,
xl: span
}
}
</script>

View File

@@ -0,0 +1,51 @@
<?php
namespace plugin\saipackage\app\controller;
use plugin\saiadmin\basic\OpenController;
use Saithink\Saipackage\service\Terminal;
use support\Request;
use support\Response;
use Throwable;
use Workerman\Protocols\Http\ServerSentEvents;
class IndexController extends OpenController
{
/**
* 执行终端
* @param Request $request
* @return void
* @throws Throwable
*/
public function terminal(Request $request): void
{
// SSE 消息
$connection = $request->connection;
$connection->send(new Response(200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no',
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Expose-Headers' => 'Content-Type',
], "\r\n"));
// 消息开始
$connection->send(new ServerSentEvents([
'event' => 'message', 'data' => 'start'
]));
// 生成器
$generator = (new Terminal())->exec();
foreach ($generator as $chunk) {
$connection->send(new ServerSentEvents([
'event' => 'message', 'data' => $chunk
]));
}
// 关闭链接
$connection->close();
}
}

View File

@@ -0,0 +1,389 @@
<?php
namespace plugin\saipackage\app\controller;
use plugin\saiadmin\app\cache\UserMenuCache;
use plugin\saiadmin\app\middleware\SystemLog;
use plugin\saiadmin\app\middleware\CheckLogin;
use plugin\saiadmin\basic\BaseController;
use plugin\saiadmin\exception\ApiException;
use plugin\saipackage\app\logic\InstallLogic;
use Saithink\Saipackage\service\Server;
use Saithink\Saipackage\service\Version;
use support\annotation\Middleware;
use support\Request;
use support\Response;
use Throwable;
#[Middleware(CheckLogin::class, SystemLog::class)]
class InstallController extends BaseController
{
/**
* 构造
*/
public function __construct()
{
parent::__construct();
if ($this->adminId > 1) {
throw new ApiException('仅超级管理员能够操作');
}
}
/**
* 环境检查状态
*/
static string $ok = 'ok';
static string $fail = 'fail';
static string $warn = 'warn';
static array $needDependentVersion = [
'php' => '8.1.0',
'saiadmin' => '6.0.0',
'saipackage' => '6.0.0',
];
/**
* 应用列表
* @param Request $request
* @return Response
*/
public function index(Request $request): Response
{
$data = Server::installedList(runtime_path() . DIRECTORY_SEPARATOR . 'saipackage' . DIRECTORY_SEPARATOR);
$phpVersion = phpversion();
$phpVersionCompare = Version::compare(self::$needDependentVersion['php'], $phpVersion);
$phpVersionNotes = '正常';
if (!$phpVersionCompare) {
$phpVersionNotes = '需要版本' . ' >= ' . self::$needDependentVersion['php'];
}
$saiadminVersion = config('plugin.saiadmin.app.version');
$saiadminVersionCompare = Version::compare(self::$needDependentVersion['saiadmin'], $saiadminVersion);
$saiadminVersionNotes = '正常';
if (!$saiadminVersionCompare) {
$saiadminVersionNotes = '需要版本' . ' >= ' . self::$needDependentVersion['saiadmin'];
}
$saithinkVersion = config('plugin.saipackage.app.version');
$saithinkVersionCompare = Version::compare(self::$needDependentVersion['saipackage'], $saithinkVersion);
$saithinkVersionNotes = '正常';
if (!$saithinkVersionCompare) {
$saithinkVersionNotes = '需要版本' . ' >= ' . self::$needDependentVersion['saipackage'];
}
return $this->success([
'version' => [
'php_version' => [
'describe' => $phpVersion,
'state' => $phpVersionCompare ? self::$ok : self::$fail,
'notes' => $phpVersionNotes,
],
'saiadmin_version' => [
'describe' => $saiadminVersion,
'state' => $saiadminVersionCompare ? self::$ok : self::$fail,
'notes' => $saiadminVersionNotes,
],
'saipackage_version' => [
'describe' => $saithinkVersion,
'state' => $saithinkVersionCompare ? self::$ok : self::$fail,
'notes' => $saithinkVersionNotes,
],
],
'data' => $data
]);
}
/**
* 上传插件
* @param Request $request
* @return Response
* @throws Throwable
*/
public function upload(Request $request): Response
{
$spl_file = current($request->file());
if (!$spl_file->isValid()) {
return $this->fail('上传文件校验失败');
}
$config = config('plugin.saipackage.upload', [
'size' => 1024 * 1024 * 5,
'type' => ['zip']
]);
if (!in_array($spl_file->getUploadExtension(), $config['type'])) {
return $this->fail('文件格式上传失败,请选择zip格式文件上传');
}
if ($spl_file->getSize() > $config['size']) {
return $this->fail('文件大小不能超过5M');
}
$install = new InstallLogic();
$info = $install->upload($spl_file);
return $this->success($info);
}
/**
* 安装插件
* @param Request $request
* @return Response
* @throws Throwable
*/
public function install(Request $request): Response
{
$appName = $request->post("appName", '');
if (empty($appName)) {
return $this->fail('参数错误');
}
$install = new InstallLogic($appName);
$info = $install->install();
UserMenuCache::clearMenuCache();
return $this->success($info);
}
/**
* 卸载插件
* @param Request $request
* @return Response
* @throws Throwable
*/
public function uninstall(Request $request): Response
{
$appName = $request->post("appName", '');
if (empty($appName)) {
return $this->fail('参数错误');
}
$install = new InstallLogic($appName);
$install->uninstall();
UserMenuCache::clearMenuCache();
return $this->success('卸载插件成功');
}
/**
* 重启
* @param Request $request
* @return Response
*/
public function reload(Request $request): Response
{
Server::restart();
return $this->success('重载成功');
}
// ========== 商店代理接口 ==========
/**
* 代理请求封装
*/
protected function proxyRequest(string $url, string $method = 'GET', ?string $token = null, ?array $postData = null, int $timeout = 10): array
{
$headers = [];
if ($token) {
$headers[] = "Authorization: Bearer {$token}";
}
if ($postData !== null) {
$headers[] = "Content-Type: application/json";
}
$context = stream_context_create([
'http' => [
'method' => $method,
'header' => implode("\r\n", $headers),
'content' => $postData ? json_encode($postData) : null,
'timeout' => $timeout,
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$response = file_get_contents($url, false, $context);
if ($response === false) {
return ['success' => false, 'message' => '请求失败'];
}
// 尝试解析 JSON
$data = json_decode($response, true);
if ($data && isset($data['code'])) {
if ($data['code'] === 200) {
return ['success' => true, 'data' => $data['data'] ?? null];
}
return ['success' => false, 'message' => $data['message'] ?? '请求失败'];
}
// 非 JSON 响应(可能是文件)
return ['success' => true, 'raw' => $response, 'headers' => $http_response_header ?? []];
}
/**
* 获取应用商店列表
*/
public function appList(Request $request): Response
{
$params = http_build_query([
'page' => $request->input('page', 1),
'limit' => $request->input('limit', 16),
'price' => $request->input('price', 'all'),
'type' => $request->input('type', ''),
'keywords' => $request->input('keywords', ''),
]);
$result = $this->proxyRequest("https://saas.saithink.top/api/app/appstore/store/appList?{$params}");
return $result['success']
? $this->success($result['data'])
: $this->fail($result['message']);
}
/**
* 获取商店验证码
*/
public function storeCaptcha(): Response
{
$result = $this->proxyRequest("https://saas.saithink.top/api/app/appstore/index/captcha");
return $result['success']
? $this->success($result['data'])
: $this->fail($result['message']);
}
/**
* 商店登录
*/
public function storeLogin(Request $request): Response
{
$result = $this->proxyRequest(
"https://saas.saithink.top/api/app/appstore/index/login",
'POST',
null,
[
'username' => $request->input('username'),
'password' => $request->input('password'),
'code' => $request->input('code'),
'uuid' => $request->input('uuid'),
]
);
return $result['success']
? $this->success($result['data'])
: $this->fail($result['message']);
}
/**
* 获取商店用户信息
*/
public function storeUserInfo(Request $request): Response
{
$token = $request->input('token');
if (empty($token)) {
return $this->fail('未登录');
}
$result = $this->proxyRequest(
"https://saas.saithink.top/api/app/appstore/user/info",
'GET',
$token
);
return $result['success']
? $this->success($result['data'])
: $this->fail($result['message']);
}
/**
* 获取已购应用列表
*/
public function storePurchasedApps(Request $request): Response
{
$token = $request->input('token');
if (empty($token)) {
return $this->fail('未登录');
}
$result = $this->proxyRequest(
"https://saas.saithink.top/api/app/appstore/user/appList",
'GET',
$token
);
return $result['success']
? $this->success($result['data'])
: $this->fail($result['message']);
}
/**
* 获取应用版本列表
*/
public function storeAppVersions(Request $request): Response
{
$token = $request->input('token');
$appId = $request->input('app_id');
if (empty($token)) {
return $this->fail('未登录');
}
$result = $this->proxyRequest(
"https://saas.saithink.top/api/app/appstore/user/versionList?app_id={$appId}",
'GET',
$token
);
return $result['success']
? $this->success($result['data'])
: $this->fail($result['message']);
}
/**
* 下载应用 - 下载并调用 InstallLogic 处理
*/
public function storeDownloadApp(Request $request): Response
{
$token = $request->input('token');
$versionId = $request->input('id');
if (empty($token)) {
return $this->fail('未登录');
}
if (empty($versionId)) {
return $this->fail('版本ID不能为空');
}
$result = $this->proxyRequest(
"https://saas.saithink.top/api/app/appstore/user/downloadApp",
'POST',
$token,
['id' => (int) $versionId],
60
);
if (!$result['success']) {
return $this->fail($result['message'] ?? '下载失败');
}
if (!isset($result['raw'])) {
return $this->fail('下载失败');
}
// 保存临时 zip 文件
$tempZip = runtime_path() . DIRECTORY_SEPARATOR . 'saipackage' . DIRECTORY_SEPARATOR . 'downloadTemp' . date('YmdHis') . '.zip';
if (!is_dir(dirname($tempZip))) {
mkdir(dirname($tempZip), 0755, true);
}
file_put_contents($tempZip, $result['raw']);
try {
// 调用 InstallLogic 处理
$install = new InstallLogic();
$info = $install->uploadFromPath($tempZip);
return $this->success($info, '下载成功,请在插件列表中安装');
} catch (Throwable $e) {
@unlink($tempZip);
return $this->fail($e->getMessage());
}
}
}

View File

@@ -0,0 +1,6 @@
<?php
/**
* Here is your custom functions.
*/

View File

@@ -0,0 +1,532 @@
<?php
namespace plugin\saipackage\app\logic;
use Throwable;
use Saithink\Saipackage\service\Server;
use Saithink\Saipackage\service\Version;
use Saithink\Saipackage\service\Filesystem;
use Saithink\Saipackage\service\Depends;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\app\cache\UserMenuCache;
class InstallLogic
{
public const UNINSTALLED = 0;
public const INSTALLED = 1;
public const WAIT_INSTALL = 2;
public const CONFLICT_PENDING = 3;
public const DEPENDENT_WAIT_INSTALL = 4;
public const DIRECTORY_OCCUPIED = 5;
/**
* @var string 安装目录
*/
protected string $installDir;
/**
* @var string 备份目录
*/
protected string $backupsDir;
/**
* @var string 插件名称
*/
protected string $appName;
/**
* @var string 插件根目录
*/
protected string $appDir;
public function __construct(string $appName = '')
{
$this->installDir = runtime_path() . DIRECTORY_SEPARATOR . 'saipackage' . DIRECTORY_SEPARATOR;
$this->backupsDir = $this->installDir . 'backups' . DIRECTORY_SEPARATOR;
if (!is_dir($this->installDir)) {
mkdir($this->installDir, 0755, true);
}
if (!is_dir($this->backupsDir)) {
mkdir($this->backupsDir, 0755, true);
}
if ($appName) {
$this->appName = $appName;
$this->appDir = $this->installDir . $appName . DIRECTORY_SEPARATOR;
}
}
public function getInstallState()
{
if (!is_dir($this->appDir)) {
return self::UNINSTALLED;
}
$info = $this->getInfo();
if ($info && isset($info['state'])) {
return $info['state'];
}
// 目录已存在,但非正常的模块
return Filesystem::dirIsEmpty($this->appDir) ? self::UNINSTALLED : self::DIRECTORY_OCCUPIED;
}
/**
* 获取允许覆盖的目录
* @return string[]
*/
public function getAllowedPath(): array
{
$backend = 'plugin' . DIRECTORY_SEPARATOR . $this->appName;
$frontend = env('FRONTEND_DIR', 'saiadmin-artd') . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'plugin' . DIRECTORY_SEPARATOR . $this->appName;
return [
$this->appDir . $backend => base_path() . DIRECTORY_SEPARATOR . $backend,
$this->appDir . $frontend => dirname(base_path()) . DIRECTORY_SEPARATOR . $frontend
];
}
/**
* 上传安装
* @param mixed $file
* @return array 模块的基本信息
* @throws Throwable
*/
public function upload(mixed $file): array
{
$copyTo = $this->installDir . 'uploadTemp' . date('YmdHis') . '.zip';
$file->move($copyTo);
// 解压
$copyToDir = Filesystem::unzip($copyTo);
$copyToDir .= DIRECTORY_SEPARATOR;
// 删除zip
@unlink($file);
@unlink($copyTo);
// 读取ini
$info = Server::getIni($copyToDir);
if (empty($info['app'])) {
Filesystem::delDir($copyToDir);
// 基本配置不完整
throw new ApiException('插件的基础配置信息错误');
}
$this->appName = $info['app'];
$this->appDir = $this->installDir . $info['app'] . DIRECTORY_SEPARATOR;
$upgrade = false;
if (is_dir($this->appDir)) {
$oldInfo = $this->getInfo();
if ($oldInfo && !empty($oldInfo['app'])) {
$versions = explode('.', $oldInfo['version']);
if (isset($versions[2])) {
$versions[2]++;
}
$nextVersion = implode('.', $versions);
$upgrade = Version::compare($nextVersion, $info['version']);
if (!$upgrade) {
Filesystem::delDir($copyToDir);
throw new ApiException('插件已经存在');
}
}
if (Filesystem::dirIsEmpty($this->appDir) || (!Filesystem::dirIsEmpty($this->appDir) && !$upgrade)) {
Filesystem::delDir($copyToDir);
// 模块目录被占
throw new ApiException('该插件的安装目录已经被占用');
}
}
$newInfo = ['state' => self::WAIT_INSTALL];
if ($upgrade) {
$newInfo['update'] = 1;
// 清理旧版本代码
Filesystem::delDir($this->appDir);
}
// 放置新模块
rename($copyToDir, $this->appDir);
// 检查新包是否完整
$this->checkPackage();
// 设置为待安装状态
$this->setInfo($newInfo);
return $info;
}
/**
* 从本地 zip 文件路径安装(用于在线下载后安装)
* @param string $zipPath zip 文件完整路径
* @return array 模块的基本信息
* @throws Throwable
*/
public function uploadFromPath(string $zipPath): array
{
if (!is_file($zipPath)) {
throw new ApiException('文件不存在');
}
// 解压
$copyToDir = Filesystem::unzip($zipPath);
$copyToDir .= DIRECTORY_SEPARATOR;
// 删除 zip
@unlink($zipPath);
// 读取 ini
$info = Server::getIni($copyToDir);
if (empty($info['app'])) {
Filesystem::delDir($copyToDir);
throw new ApiException('插件的基础配置信息错误');
}
$this->appName = $info['app'];
$this->appDir = $this->installDir . $info['app'] . DIRECTORY_SEPARATOR;
$upgrade = false;
if (is_dir($this->appDir)) {
$oldInfo = $this->getInfo();
if ($oldInfo && !empty($oldInfo['app'])) {
$versions = explode('.', $oldInfo['version']);
if (isset($versions[2])) {
$versions[2]++;
}
$nextVersion = implode('.', $versions);
$upgrade = Version::compare($nextVersion, $info['version']);
if (!$upgrade) {
Filesystem::delDir($copyToDir);
throw new ApiException('插件已经存在');
}
}
if (Filesystem::dirIsEmpty($this->appDir) || (!Filesystem::dirIsEmpty($this->appDir) && !$upgrade)) {
Filesystem::delDir($copyToDir);
throw new ApiException('该插件的安装目录已经被占用');
}
}
$newInfo = ['state' => self::WAIT_INSTALL];
if ($upgrade) {
$newInfo['update'] = 1;
Filesystem::delDir($this->appDir);
}
// 放置新模块
rename($copyToDir, $this->appDir);
// 检查新包是否完整
$this->checkPackage();
// 设置为待安装状态
$this->setInfo($newInfo);
return $info;
}
/**
* 安装或更新
* @return array
* @throws Throwable
*/
public function install(): array
{
$state = $this->getInstallState();
if ($state == self::INSTALLED || $state == self::DIRECTORY_OCCUPIED) {
throw new ApiException('插件已经存在');
}
if ($state == self::DEPENDENT_WAIT_INSTALL) {
throw new ApiException('等待依赖安装');
}
echo '开始安装[' . $this->appName . ']' . PHP_EOL;
$info = $this->getInfo();
if ($state == self::WAIT_INSTALL) {
echo '安装数据库' . PHP_EOL;
$sql = $this->appDir . 'install.sql';
Server::importSql($sql);
}
if (isset($info['update']) && $info['update'] == 1) {
echo '更新数据库' . PHP_EOL;
$sql = $this->appDir . 'update.sql';
Server::importSql($sql);
unset($info['update']);
$this->setInfo([], $info);
}
// 依赖检查
$this->dependConflictHandle();
// 执行安装脚本
echo '安装文件' . PHP_EOL;
$pathRelation = $this->getAllowedPath();
Server::installByRelation($pathRelation);
// 依赖更新
echo '依赖更新' . PHP_EOL;
$this->dependUpdateHandle();
// 清理菜单缓存
UserMenuCache::clearMenuCache();
// 重启后端
Server::restart();
return $info;
}
/**
* @return void
* @throws Throwable
*/
public function uninstall(): void
{
$state = $this->getInstallState();
if ($state != self::INSTALLED) {
echo PHP_EOL . '删除插件[' . $this->appName . ']' . PHP_EOL;
$pathRelation = $this->getAllowedPath();
foreach ($pathRelation as $key => $value) {
if (is_dir($value)) {
Filesystem::delDir($value);
}
}
// 删除临时目录
Filesystem::delDir($this->appDir);
return;
}
echo '开始卸载[' . $this->appName . ']' . PHP_EOL;
echo '卸载数据库' . PHP_EOL;
$sql = $this->appDir . 'uninstall.sql';
Server::importSql($sql);
echo '备份文件' . PHP_EOL;
$backFiles = [];
$pathRelation = $this->getAllowedPath();
$index = 1;
foreach ($pathRelation as $key => $value) {
if (is_dir($value)) {
$backFiles[$this->appName . '-' . $index] = $value;
$index++;
}
}
$backupsZip = $this->backupsDir . $this->appName . '-uninstall-' . date('YmdHis') . '.zip';
Filesystem::zipDir($backFiles, $backupsZip);
echo '卸载文件' . PHP_EOL;
$pathRelation = $this->getAllowedPath();
foreach ($pathRelation as $key => $value) {
if (is_dir($value)) {
Filesystem::delDir($value);
}
}
// 删除临时目录
Filesystem::delDir($this->appDir);
// 清理菜单缓存
UserMenuCache::clearMenuCache();
// 重启后端
Server::restart();
}
/**
* 检查包是否完整
* @throws Throwable
*/
public function checkPackage(): bool
{
if (!is_dir($this->appDir)) {
throw new ApiException('插件目录不存在');
}
$info = $this->getInfo();
$infoKeys = ['app', 'title', 'about', 'author', 'version', 'state'];
foreach ($infoKeys as $value) {
if (!array_key_exists($value, $info)) {
Filesystem::delDir($this->appDir);
throw new ApiException('该插件的基础配置信息不完善');
}
}
return true;
}
/**
* 依赖安装完成标记
* @throws Throwable
*/
public function dependentInstallComplete(string $type): void
{
$info = $this->getInfo();
if ($info['state'] == self::DEPENDENT_WAIT_INSTALL) {
if ($type == 'npm') {
unset($info['npm_dependent_wait_install']);
}
if ($type == 'composer') {
unset($info['composer_dependent_wait_install']);
}
if ($type == 'all') {
unset($info['npm_dependent_wait_install'], $info['composer_dependent_wait_install']);
}
if (!isset($info['npm_dependent_wait_install']) && !isset($info['composer_dependent_wait_install'])) {
$info['state'] = self::INSTALLED;
}
$this->setInfo([], $info);
}
}
/**
* 依赖冲突检查
* @return bool
* @throws Throwable
*/
public function dependConflictHandle(): bool
{
$info = $this->getInfo();
if ($info['state'] != self::WAIT_INSTALL && $info['state'] != self::CONFLICT_PENDING) {
return false;
}
$coverFiles = [];// 要覆盖的文件-备份
$depends = Server::getDepend($this->appDir);
$serverDep = new Depends(base_path() . DIRECTORY_SEPARATOR . 'composer.json', 'composer');
$webDep = new Depends(dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-vue') . DIRECTORY_SEPARATOR . 'package.json');
// 如果有依赖更新,增加要备份的文件
if ($depends) {
foreach ($depends as $key => $item) {
if (!$item) {
continue;
}
if ($key == 'require' || $key == 'require-dev') {
$coverFiles[] = base_path() . DIRECTORY_SEPARATOR . 'composer.json';
continue;
}
if ($key == 'dependencies' || $key == 'devDependencies') {
$coverFiles[] = dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-vue') . DIRECTORY_SEPARATOR . 'package.json';
}
}
}
// 备份将被覆盖的文件
if ($coverFiles) {
$backupsZip = $this->backupsDir . $this->appName . '-cover-' . date('YmdHis') . '.zip';
Filesystem::zip($coverFiles, $backupsZip);
}
if ($depends) {
$npm = false;
$composer = false;
// composer config 更新
$composerConfig = Server::getConfig($this->appDir, 'composerConfig');
if ($composerConfig) {
$serverDep->setComposerConfig($composerConfig);
}
foreach ($depends as $key => $item) {
if (!$item) {
continue;
}
if ($key == 'require') {
$composer = true;
$serverDep->addDepends($item, false, true);
} elseif ($key == 'require-dev') {
$composer = true;
$serverDep->addDepends($item, true, true);
} elseif ($key == 'dependencies') {
$npm = true;
$webDep->addDepends($item, false, true);
} elseif ($key == 'devDependencies') {
$npm = true;
$webDep->addDepends($item, true, true);
}
}
if ($npm) {
$info['npm_dependent_wait_install'] = 1;
$info['state'] = self::DEPENDENT_WAIT_INSTALL;
}
if ($composer) {
$info['composer_dependent_wait_install'] = 1;
$info['state'] = self::DEPENDENT_WAIT_INSTALL;
}
if ($info['state'] != self::DEPENDENT_WAIT_INSTALL) {
// 无冲突
$this->setInfo([
'state' => self::INSTALLED,
]);
} else {
$this->setInfo([], $info);
}
} else {
// 无冲突
$this->setInfo([
'state' => self::INSTALLED,
]);
}
return true;
}
/**
* 依赖升级处理
* @throws Throwable
*/
public function dependUpdateHandle(): void
{
$info = $this->getInfo();
if ($info['state'] == self::DEPENDENT_WAIT_INSTALL) {
$waitInstall = [];
if (isset($info['composer_dependent_wait_install'])) {
$waitInstall[] = 'composer_dependent_wait_install';
}
if (isset($info['npm_dependent_wait_install'])) {
$waitInstall[] = 'npm_dependent_wait_install';
}
if (empty($waitInstall)) {
$this->setInfo([
'state' => self::INSTALLED,
]);
}
}
}
/**
* 获取模块基本信息
*/
public function getInfo(): array
{
return Server::getIni($this->appDir);
}
/**
* 设置模块基本信息
* @throws Throwable
*/
public function setInfo(array $kv = [], array $arr = []): bool
{
if ($kv) {
$info = $this->getInfo();
foreach ($kv as $k => $v) {
$info[$k] = $v;
}
return Server::setIni($this->appDir, $info);
} elseif ($arr) {
return Server::setIni($this->appDir, $arr);
}
throw new ApiException('参数错误');
}
}

View File

@@ -0,0 +1,10 @@
<?php
use support\Request;
return [
'debug' => true,
'controller_suffix' => 'Controller',
'controller_reuse' => false,
'version' => '6.0.1'
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'files' => [
base_path() . '/plugin/saipackage/app/functions.php',
]
];

View File

@@ -0,0 +1,2 @@
<?php
return new Webman\Container;

View File

@@ -0,0 +1,2 @@
<?php
return [];

View File

@@ -0,0 +1,5 @@
<?php
return [
'' => \plugin\saiadmin\app\exception\Handler::class,
];

View File

@@ -0,0 +1,20 @@
<?php
return [
'default' => [
'handlers' => [
[
'class' => Monolog\Handler\RotatingFileHandler::class,
'constructor' => [
runtime_path() . '/logs/saipackage.log',
7,
Monolog\Logger::DEBUG,
],
'formatter' => [
'class' => Monolog\Formatter\LineFormatter::class,
'constructor' => [null, 'Y-m-d H:i:s', true],
],
]
],
],
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'' => [
]
];

View File

@@ -0,0 +1,2 @@
<?php
return [];

View File

@@ -0,0 +1,14 @@
<?php
use Webman\Route;
Route::group('/tool/install', function () {
// 商店代理接口(在线安装)
Route::get('/online/appList', [plugin\saipackage\app\controller\InstallController::class, 'appList']);
Route::get('/online/storeCaptcha', [plugin\saipackage\app\controller\InstallController::class, 'storeCaptcha']);
Route::post('/online/storeLogin', [plugin\saipackage\app\controller\InstallController::class, 'storeLogin']);
Route::get('/online/storeUserInfo', [plugin\saipackage\app\controller\InstallController::class, 'storeUserInfo']);
Route::get('/online/storePurchasedApps', [plugin\saipackage\app\controller\InstallController::class, 'storePurchasedApps']);
Route::get('/online/storeAppVersions', [plugin\saipackage\app\controller\InstallController::class, 'storeAppVersions']);
Route::post('/online/storeDownloadApp', [plugin\saipackage\app\controller\InstallController::class, 'storeDownloadApp']);
});

View File

@@ -0,0 +1,6 @@
<?php
return [
'enable' => true,
'middleware' => [], // Static file Middleware
];

View File

@@ -0,0 +1,79 @@
<?php
return [
// 允许执行的命令
'commands' => [
// 查看版本的命令
'version' => [
'npm' => 'npm -v',
'yarn' => 'yarn -v',
'pnpm' => 'pnpm -v',
'node' => 'node -v',
],
// 测试命令
'test' => [
'npm' => [
'cwd' => public_path() . DIRECTORY_SEPARATOR . 'npm-install-test',
'command' => 'npm install',
],
'yarn' => [
'cwd' => public_path() . DIRECTORY_SEPARATOR . 'npm-install-test',
'command' => 'yarn install',
],
'pnpm' => [
'cwd' => public_path() . DIRECTORY_SEPARATOR . 'npm-install-test',
'command' => 'pnpm install',
],
],
// 安装 WEB 依赖包
'web-install' => [
'npm' => [
'cwd' => dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-artd'),
'command' => 'npm install',
],
'yarn' => [
'cwd' => dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-artd'),
'command' => 'yarn install',
],
'pnpm' => [
'cwd' => dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-artd'),
'command' => 'pnpm install',
],
],
// 构建 WEB 端
'web-build' => [
'npm' => [
'cwd' => dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-artd'),
'command' => 'npm run build',
],
'yarn' => [
'cwd' => dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-artd'),
'command' => 'yarn run build',
],
'pnpm' => [
'cwd' => dirname(base_path()) . DIRECTORY_SEPARATOR . env('FRONTEND_DIR', 'saiadmin-artd'),
'command' => 'pnpm run build',
],
],
// 设置 NPM 源
'set-npm-registry' => [
'npm' => 'npm config set registry https://registry.npmjs.org/ && npm config get registry',
'taobao' => 'npm config set registry https://registry.npmmirror.com/ && npm config get registry',
'tencent' => 'npm config set registry https://mirrors.cloud.tencent.com/npm/ && npm config get registry'
],
// 设置 composer 源
'set-composer-registry' => [
'composer' => 'composer config --unset repos.packagist',
'tencent' => 'composer config -g repos.packagist composer https://mirrors.cloud.tencent.com/composer/',
'huawei' => 'composer config -g repos.packagist composer https://mirrors.huaweicloud.com/repository/php/',
'kkame' => 'composer config -g repos.packagist composer https://packagist.kr',
],
// 安装 composer 包
'composer' => [
'update' => [
'cwd' => base_path(),
'command' => 'composer update --no-interaction',
],
]
],
];

View File

@@ -0,0 +1,10 @@
<?php
return [
// Default language
'locale' => 'zh_CN',
// Fallback language
'fallback_locale' => ['zh_CN', 'en'],
// Folder where language files are stored
'path' => base_path() . "/plugin/saipackage/resource/translations",
];

View File

@@ -0,0 +1,5 @@
<?php
return [
'type' => ['zip'],
'size' => 1024 * 1024 * 5, // 5MB
];

View File

@@ -0,0 +1,10 @@
<?php
use support\view\Raw;
use support\view\Twig;
use support\view\Blade;
use support\view\ThinkPHP;
return [
'handler' => Raw::class
];