更新
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
$app = new think\App();
|
||||
$app->initialize();
|
||||
|
||||
use think\facade\Db;
|
||||
|
||||
echo "=== users ===\n";
|
||||
foreach (Db::name('users')->field('id,username,role,role_id,department_id')->select() as $u) {
|
||||
echo json_encode($u, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
}
|
||||
|
||||
echo "\n=== departments ===\n";
|
||||
foreach (Db::name('departments')->select() as $d) {
|
||||
echo json_encode($d, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
}
|
||||
|
||||
echo "\n=== roles view scope ===\n";
|
||||
foreach (Db::name('roles')->field('id,name,slug,permissions')->select() as $r) {
|
||||
$p = json_decode($r['permissions'], true) ?: [];
|
||||
echo sprintf(
|
||||
"%s(%s) view_all=%s view_sub=%s dept_menus=%s\n",
|
||||
$r['slug'],
|
||||
$r['id'],
|
||||
!empty($p['can_view_all_conversations']) || in_array('btn:conv:view_all', $p['buttons'] ?? [], true) ? 'Y' : 'N',
|
||||
!empty($p['can_view_subordinate_conversations']) || in_array('btn:conv:view_subordinate', $p['buttons'] ?? [], true) ? 'Y' : 'N',
|
||||
implode(',', $p['menus'] ?? [])
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 初始化数据库脚本
|
||||
* 用法: php database/install.php
|
||||
*/
|
||||
|
||||
$sqlFile = __DIR__ . '/schema.sql';
|
||||
$sql = file_get_contents($sqlFile);
|
||||
|
||||
$host = '127.0.0.1';
|
||||
$user = 'root';
|
||||
$pass = 'root';
|
||||
$port = 3306;
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host={$host};port={$port};charset=utf8mb4", $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
$statements = array_filter(array_map('trim', explode(';', $sql)));
|
||||
foreach ($statements as $statement) {
|
||||
if ($statement !== '') {
|
||||
$pdo->exec($statement);
|
||||
}
|
||||
}
|
||||
|
||||
echo "Database installed successfully.\n";
|
||||
} catch (PDOException $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 迁移脚本:为支持 Dify 接口新增所需字段(已存在则跳过)
|
||||
* 用法: php database/migrate_add_dify_support.php
|
||||
*/
|
||||
|
||||
$host = '127.0.0.1';
|
||||
$user = 'root';
|
||||
$pass = 'root';
|
||||
$port = 3306;
|
||||
$dbName = 'ai_chat';
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4", $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
$changes = [
|
||||
['table' => 'ai_models', 'column' => 'provider', 'sql' => "ALTER TABLE ai_models ADD COLUMN provider VARCHAR(20) NOT NULL DEFAULT 'openai' COMMENT '接口类型:openai/dify' AFTER name"],
|
||||
['table' => 'conversations', 'column' => 'external_conversation_id', 'sql' => "ALTER TABLE conversations ADD COLUMN external_conversation_id VARCHAR(64) NULL COMMENT '第三方平台会话ID' AFTER message_count"],
|
||||
];
|
||||
|
||||
foreach ($changes as $change) {
|
||||
$exists = $pdo->query("SHOW COLUMNS FROM {$change['table']} LIKE '{$change['column']}'")->fetch();
|
||||
if ($exists) {
|
||||
echo "{$change['table']}.{$change['column']} 已存在,跳过。\n";
|
||||
continue;
|
||||
}
|
||||
$pdo->exec($change['sql']);
|
||||
echo "已为 {$change['table']} 添加 {$change['column']} 字段。\n";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 迁移脚本:为 ai_models 表添加 frequency_penalty / presence_penalty 字段(已存在则跳过)
|
||||
* 用法: php database/migrate_add_penalty_fields.php
|
||||
*/
|
||||
|
||||
$host = '127.0.0.1';
|
||||
$user = 'root';
|
||||
$pass = 'root';
|
||||
$port = 3306;
|
||||
$dbName = 'ai_chat';
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4", $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
$columns = [
|
||||
'frequency_penalty' => "ALTER TABLE ai_models ADD COLUMN frequency_penalty DECIMAL(3,2) DEFAULT 0.00 COMMENT '频率惩罚,抑制模型重复输出' AFTER support_image",
|
||||
'presence_penalty' => "ALTER TABLE ai_models ADD COLUMN presence_penalty DECIMAL(3,2) DEFAULT 0.00 COMMENT '存在惩罚,抑制模型重复话题' AFTER frequency_penalty",
|
||||
];
|
||||
|
||||
foreach ($columns as $name => $sql) {
|
||||
$exists = $pdo->query("SHOW COLUMNS FROM ai_models LIKE '{$name}'")->fetch();
|
||||
if ($exists) {
|
||||
echo "{$name} 字段已存在,跳过。\n";
|
||||
continue;
|
||||
}
|
||||
$pdo->exec($sql);
|
||||
echo "已添加 {$name} 字段。\n";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 迁移脚本:为 ai_models 表添加 support_context 字段(已存在则跳过)
|
||||
* 用法: php database/migrate_add_support_context.php
|
||||
*/
|
||||
|
||||
$host = '127.0.0.1';
|
||||
$user = 'root';
|
||||
$pass = 'root';
|
||||
$port = 3306;
|
||||
$dbName = 'ai_chat';
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4", $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
$exists = $pdo->query("SHOW COLUMNS FROM ai_models LIKE 'support_context'")->fetch();
|
||||
|
||||
if ($exists) {
|
||||
echo "support_context 字段已存在,跳过。\n";
|
||||
} else {
|
||||
$pdo->exec("ALTER TABLE ai_models ADD COLUMN support_context TINYINT(1) DEFAULT 1 COMMENT '是否支持上下文(多轮对话)' AFTER enabled");
|
||||
echo "已添加 support_context 字段。\n";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 迁移脚本:为 ai_models 表添加 support_image 字段(已存在则跳过)
|
||||
* 用法: php database/migrate_add_support_image.php
|
||||
*/
|
||||
|
||||
$host = '127.0.0.1';
|
||||
$user = 'root';
|
||||
$pass = 'root';
|
||||
$port = 3306;
|
||||
$dbName = 'ai_chat';
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4", $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
$exists = $pdo->query("SHOW COLUMNS FROM ai_models LIKE 'support_image'")->fetch();
|
||||
|
||||
if ($exists) {
|
||||
echo "support_image 字段已存在,跳过。\n";
|
||||
} else {
|
||||
$pdo->exec("ALTER TABLE ai_models ADD COLUMN support_image TINYINT(1) DEFAULT 1 COMMENT '是否支持图片/多模态输入' AFTER support_context");
|
||||
echo "已添加 support_image 字段。\n";
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 为 ai_models 增加 extra_config 字段(可重复执行)
|
||||
* 用法: php database/migrate_extra_config.php
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$envFile = dirname(__DIR__) . '/.env';
|
||||
$env = [];
|
||||
if (is_file($envFile)) {
|
||||
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$env[$k] = trim($v, " \t\"'");
|
||||
}
|
||||
}
|
||||
|
||||
$host = $env['DB_HOST'] ?? '127.0.0.1';
|
||||
$name = $env['DB_NAME'] ?? 'ai_chat';
|
||||
$user = $env['DB_USER'] ?? 'root';
|
||||
$pass = $env['DB_PASS'] ?? '';
|
||||
$port = $env['DB_PORT'] ?? '3306';
|
||||
|
||||
$pdo = new PDO("mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4", $user, $pass);
|
||||
$cols = $pdo->query("SHOW COLUMNS FROM ai_models LIKE 'extra_config'")->fetchAll();
|
||||
if ($cols) {
|
||||
echo "extra_config already exists\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$pdo->exec("ALTER TABLE ai_models ADD COLUMN extra_config JSON NULL COMMENT 'ComfyUI工作流等扩展配置' AFTER presence_penalty");
|
||||
echo "extra_config column added\n";
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 增量补种会员等级 新增/删除 按钮权限
|
||||
* 用法: php database/migrate_membership_buttons.php
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$app = new think\App();
|
||||
$app->initialize();
|
||||
|
||||
use app\service\PermissionCatalog;
|
||||
use think\facade\Db;
|
||||
|
||||
try {
|
||||
$parent = Db::name('sys_permissions')->where('code', 'menu:memberships')->find();
|
||||
if (!$parent) {
|
||||
echo "menu:memberships not found, skip.\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$buttons = [
|
||||
['code' => 'btn:membership:create', 'name' => '新增会员等级'],
|
||||
['code' => 'btn:membership:edit', 'name' => '编辑会员等级'],
|
||||
['code' => 'btn:membership:delete', 'name' => '删除会员等级'],
|
||||
];
|
||||
|
||||
foreach ($buttons as $i => $btn) {
|
||||
if (Db::name('sys_permissions')->where('code', $btn['code'])->find()) {
|
||||
echo "exists {$btn['code']}\n";
|
||||
continue;
|
||||
}
|
||||
Db::name('sys_permissions')->insert([
|
||||
'type' => 'btn',
|
||||
'code' => $btn['code'],
|
||||
'name' => $btn['name'],
|
||||
'parent_id' => $parent['id'],
|
||||
'path' => null,
|
||||
'icon' => null,
|
||||
'sort_order' => $i,
|
||||
'is_system' => 1,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
echo "added {$btn['code']}\n";
|
||||
}
|
||||
|
||||
// 超级管理员补齐全部权限
|
||||
if (Db::name('roles')->where('slug', 'super_admin')->find()) {
|
||||
Db::name('roles')->where('slug', 'super_admin')->update([
|
||||
'permissions' => json_encode(PermissionCatalog::fullPermissions(), JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
echo "Updated super_admin permissions\n";
|
||||
}
|
||||
|
||||
echo "Done.\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo 'Error: ' . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 将已有角色权限升级为 目录/菜单/按钮 结构
|
||||
* 若 roles 表不存在,会先自动执行角色/部门建表迁移。
|
||||
*
|
||||
* 用法: php database/migrate_permission_tree.php
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$app = new think\App();
|
||||
$app->initialize();
|
||||
|
||||
use app\service\PermissionCatalog;
|
||||
use think\facade\Db;
|
||||
|
||||
try {
|
||||
$dbName = Db::getConfig('database') ?: env('DB_NAME', 'ai_chat');
|
||||
$tableExists = function (string $table) use ($dbName) {
|
||||
$rows = Db::query(
|
||||
'SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? LIMIT 1',
|
||||
[$dbName, $table]
|
||||
);
|
||||
return !empty($rows);
|
||||
};
|
||||
|
||||
if (!$tableExists('roles') || !$tableExists('departments')) {
|
||||
echo "roles/departments 表不存在,先执行 migrate_roles_departments.php ...\n";
|
||||
$cmd = 'php ' . escapeshellarg(__DIR__ . DIRECTORY_SEPARATOR . 'migrate_roles_departments.php');
|
||||
passthru($cmd, $code);
|
||||
if ($code !== 0) {
|
||||
throw new RuntimeException('migrate_roles_departments.php 执行失败');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$tableExists('roles')) {
|
||||
throw new RuntimeException('roles 表仍不存在,请检查数据库连接');
|
||||
}
|
||||
|
||||
$roles = Db::name('roles')->select();
|
||||
if (!$roles || count($roles) === 0) {
|
||||
echo "roles 表为空,跳过权限升级。\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$raw = $role['permissions'] ?? '{}';
|
||||
$perms = is_string($raw) ? json_decode($raw, true) : $raw;
|
||||
if (!is_array($perms)) {
|
||||
$perms = [];
|
||||
}
|
||||
|
||||
if (($role['slug'] ?? '') === 'super_admin') {
|
||||
$normalized = PermissionCatalog::fullPermissions();
|
||||
} elseif (($role['slug'] ?? '') === 'dept_manager') {
|
||||
$normalized = PermissionCatalog::normalize([
|
||||
'can_access_admin' => true,
|
||||
'dirs' => ['dir:overview', 'dir:business'],
|
||||
'menus' => ['menu:dashboard', 'menu:conversations'],
|
||||
'buttons' => ['btn:conv:view_subordinate'],
|
||||
]);
|
||||
} else {
|
||||
$normalized = PermissionCatalog::normalize($perms);
|
||||
}
|
||||
|
||||
Db::name('roles')->where('id', $role['id'])->update([
|
||||
'permissions' => json_encode($normalized, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
echo "Updated role: {$role['name']}\n";
|
||||
}
|
||||
|
||||
echo "Permission tree migration completed.\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo 'Error: ' . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 角色与部门迁移
|
||||
* 用法: php database/migrate_roles_departments.php
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$app = new think\App();
|
||||
$app->initialize();
|
||||
|
||||
use think\facade\Db;
|
||||
|
||||
function columnExists(string $table, string $column): bool
|
||||
{
|
||||
$db = Db::getConfig('database') ?: env('DB_NAME', 'ai_chat');
|
||||
$rows = Db::query(
|
||||
'SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1',
|
||||
[$db, $table, $column]
|
||||
);
|
||||
return !empty($rows);
|
||||
}
|
||||
|
||||
function tableExists(string $table): bool
|
||||
{
|
||||
$db = Db::getConfig('database') ?: env('DB_NAME', 'ai_chat');
|
||||
$rows = Db::query(
|
||||
'SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? LIMIT 1',
|
||||
[$db, $table]
|
||||
);
|
||||
return !empty($rows);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!tableExists('roles')) {
|
||||
Db::execute("CREATE TABLE roles (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
slug VARCHAR(50) NOT NULL UNIQUE,
|
||||
permissions JSON NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
echo "Created table roles\n";
|
||||
}
|
||||
|
||||
if (!tableExists('departments')) {
|
||||
Db::execute("CREATE TABLE departments (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
parent_id INT UNSIGNED NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_parent (parent_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
echo "Created table departments\n";
|
||||
}
|
||||
|
||||
if (!columnExists('users', 'role_id')) {
|
||||
Db::execute('ALTER TABLE users ADD COLUMN role_id INT UNSIGNED NULL AFTER role');
|
||||
echo "Added users.role_id\n";
|
||||
}
|
||||
|
||||
if (!columnExists('users', 'department_id')) {
|
||||
Db::execute('ALTER TABLE users ADD COLUMN department_id INT UNSIGNED NULL AFTER role_id');
|
||||
echo "Added users.department_id\n";
|
||||
}
|
||||
|
||||
$roleCount = (int) Db::name('roles')->count();
|
||||
if ($roleCount === 0) {
|
||||
$allTrue = json_encode([
|
||||
'can_access_admin' => true,
|
||||
'can_manage_users' => true,
|
||||
'can_manage_roles' => true,
|
||||
'can_manage_departments' => true,
|
||||
'can_view_all_conversations' => true,
|
||||
'can_view_subordinate_conversations' => true,
|
||||
'can_manage_models' => true,
|
||||
'can_manage_settings' => true,
|
||||
'can_manage_memberships' => true,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$deptManager = json_encode([
|
||||
'can_access_admin' => true,
|
||||
'can_manage_users' => false,
|
||||
'can_manage_roles' => false,
|
||||
'can_manage_departments' => false,
|
||||
'can_view_all_conversations' => false,
|
||||
'can_view_subordinate_conversations' => true,
|
||||
'can_manage_models' => false,
|
||||
'can_manage_settings' => false,
|
||||
'can_manage_memberships' => false,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$userPerms = json_encode([
|
||||
'can_access_admin' => false,
|
||||
'can_manage_users' => false,
|
||||
'can_manage_roles' => false,
|
||||
'can_manage_departments' => false,
|
||||
'can_view_all_conversations' => false,
|
||||
'can_view_subordinate_conversations' => false,
|
||||
'can_manage_models' => false,
|
||||
'can_manage_settings' => false,
|
||||
'can_manage_memberships' => false,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
Db::name('roles')->insertAll([
|
||||
['name' => '超级管理员', 'slug' => 'super_admin', 'permissions' => $allTrue, 'sort_order' => 1],
|
||||
['name' => '部门管理员', 'slug' => 'dept_manager', 'permissions' => $deptManager, 'sort_order' => 2],
|
||||
['name' => '普通用户', 'slug' => 'user', 'permissions' => $userPerms, 'sort_order' => 3],
|
||||
]);
|
||||
echo "Seeded default roles\n";
|
||||
}
|
||||
|
||||
$superAdminId = (int) Db::name('roles')->where('slug', 'super_admin')->value('id');
|
||||
$userRoleId = (int) Db::name('roles')->where('slug', 'user')->value('id');
|
||||
|
||||
if ($superAdminId > 0) {
|
||||
Db::name('users')->where('role', 'admin')->where(function ($q) {
|
||||
$q->whereNull('role_id')->whereOr('role_id', 0);
|
||||
})->update(['role_id' => $superAdminId]);
|
||||
}
|
||||
|
||||
if ($userRoleId > 0) {
|
||||
Db::name('users')->where('role', 'user')->where(function ($q) {
|
||||
$q->whereNull('role_id')->whereOr('role_id', 0);
|
||||
})->update(['role_id' => $userRoleId]);
|
||||
|
||||
Db::name('users')->where(function ($q) {
|
||||
$q->whereNull('role_id')->whereOr('role_id', 0);
|
||||
})->update(['role_id' => $userRoleId]);
|
||||
}
|
||||
|
||||
if ((int) Db::name('departments')->count() === 0) {
|
||||
Db::name('departments')->insert([
|
||||
'name' => '总公司',
|
||||
'parent_id' => null,
|
||||
'sort_order' => 0,
|
||||
]);
|
||||
echo "Seeded root department\n";
|
||||
}
|
||||
|
||||
echo "Migration completed.\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo 'Error: ' . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 系统权限节点表(目录/菜单/按钮)
|
||||
* 用法: php database/migrate_sys_permissions.php
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$app = new think\App();
|
||||
$app->initialize();
|
||||
|
||||
use app\service\PermissionCatalog;
|
||||
use think\facade\Db;
|
||||
|
||||
function tableExists(string $table): bool
|
||||
{
|
||||
$db = Db::getConfig('database') ?: env('DB_NAME', 'ai_chat');
|
||||
$rows = Db::query(
|
||||
'SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? LIMIT 1',
|
||||
[$db, $table]
|
||||
);
|
||||
return !empty($rows);
|
||||
}
|
||||
|
||||
function seedNode(array $node, ?int $parentId = null, int $sort = 0): int
|
||||
{
|
||||
$existing = Db::name('sys_permissions')->where('code', $node['code'])->find();
|
||||
if ($existing) {
|
||||
return (int) $existing['id'];
|
||||
}
|
||||
|
||||
return (int) Db::name('sys_permissions')->insertGetId([
|
||||
'type' => $node['type'],
|
||||
'code' => $node['code'],
|
||||
'name' => $node['name'],
|
||||
'parent_id' => $parentId,
|
||||
'path' => $node['path'] ?? null,
|
||||
'icon' => $node['icon'] ?? null,
|
||||
'sort_order' => $node['sort_order'] ?? $sort,
|
||||
'is_system' => $node['is_system'] ?? 1,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!tableExists('sys_permissions')) {
|
||||
Db::execute("CREATE TABLE sys_permissions (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
type ENUM('dir','menu','btn') NOT NULL,
|
||||
code VARCHAR(100) NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
parent_id INT UNSIGNED NULL,
|
||||
path VARCHAR(200) NULL,
|
||||
icon VARCHAR(50) NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
is_system TINYINT(1) DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_parent (parent_id),
|
||||
INDEX idx_type (type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
echo "Created table sys_permissions\n";
|
||||
}
|
||||
|
||||
$builtin = PermissionCatalog::builtinTree();
|
||||
|
||||
// 补齐:权限管理菜单 + 用户增删按钮
|
||||
foreach ($builtin as &$dir) {
|
||||
if ($dir['code'] === 'dir:org') {
|
||||
foreach ($dir['children'] as &$menu) {
|
||||
if ($menu['code'] === 'menu:users') {
|
||||
$codes = array_column($menu['children'], 'code');
|
||||
if (!in_array('btn:user:create', $codes, true)) {
|
||||
array_unshift($menu['children'], ['code' => 'btn:user:create', 'name' => '新增用户', 'type' => 'btn']);
|
||||
}
|
||||
if (!in_array('btn:user:delete', $codes, true)) {
|
||||
$menu['children'][] = ['code' => 'btn:user:delete', 'name' => '删除用户', 'type' => 'btn'];
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($menu);
|
||||
}
|
||||
if ($dir['code'] === 'dir:system') {
|
||||
$menuCodes = array_column($dir['children'], 'code');
|
||||
if (!in_array('menu:permissions', $menuCodes, true)) {
|
||||
$dir['children'][] = [
|
||||
'code' => 'menu:permissions',
|
||||
'name' => '权限管理',
|
||||
'type' => 'menu',
|
||||
'path' => '/permissions',
|
||||
'icon' => '🔑',
|
||||
'children' => [
|
||||
['code' => 'btn:perm:create', 'name' => '新增权限', 'type' => 'btn'],
|
||||
['code' => 'btn:perm:edit', 'name' => '编辑权限', 'type' => 'btn'],
|
||||
['code' => 'btn:perm:delete', 'name' => '删除权限', 'type' => 'btn'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($dir);
|
||||
|
||||
$count = (int) Db::name('sys_permissions')->count();
|
||||
if ($count === 0) {
|
||||
$sortDir = 0;
|
||||
foreach ($builtin as $dir) {
|
||||
$dirId = seedNode($dir, null, $sortDir++);
|
||||
$sortMenu = 0;
|
||||
foreach ($dir['children'] ?? [] as $menu) {
|
||||
$menuId = seedNode($menu, $dirId, $sortMenu++);
|
||||
$sortBtn = 0;
|
||||
foreach ($menu['children'] ?? [] as $btn) {
|
||||
seedNode($btn, $menuId, $sortBtn++);
|
||||
}
|
||||
}
|
||||
}
|
||||
echo "Seeded builtin permissions\n";
|
||||
} else {
|
||||
// 增量补种新增节点
|
||||
$extra = [
|
||||
['type' => 'btn', 'code' => 'btn:user:create', 'name' => '新增用户', 'parent_code' => 'menu:users'],
|
||||
['type' => 'btn', 'code' => 'btn:user:delete', 'name' => '删除用户', 'parent_code' => 'menu:users'],
|
||||
['type' => 'menu', 'code' => 'menu:permissions', 'name' => '权限管理', 'parent_code' => 'dir:system', 'path' => '/permissions', 'icon' => '🔑'],
|
||||
['type' => 'btn', 'code' => 'btn:perm:create', 'name' => '新增权限', 'parent_code' => 'menu:permissions'],
|
||||
['type' => 'btn', 'code' => 'btn:perm:edit', 'name' => '编辑权限', 'parent_code' => 'menu:permissions'],
|
||||
['type' => 'btn', 'code' => 'btn:perm:delete', 'name' => '删除权限', 'parent_code' => 'menu:permissions'],
|
||||
];
|
||||
foreach ($extra as $item) {
|
||||
if (Db::name('sys_permissions')->where('code', $item['code'])->find()) {
|
||||
continue;
|
||||
}
|
||||
$parent = Db::name('sys_permissions')->where('code', $item['parent_code'])->find();
|
||||
if (!$parent && $item['code'] === 'btn:perm:create') {
|
||||
continue;
|
||||
}
|
||||
// 若菜单还不存在,先建菜单
|
||||
if ($item['code'] === 'menu:permissions' && $parent) {
|
||||
seedNode([
|
||||
'type' => 'menu',
|
||||
'code' => 'menu:permissions',
|
||||
'name' => '权限管理',
|
||||
'path' => '/permissions',
|
||||
'icon' => '🔑',
|
||||
'is_system' => 1,
|
||||
], (int) $parent['id']);
|
||||
continue;
|
||||
}
|
||||
if (!$parent) {
|
||||
if ($item['parent_code'] === 'menu:permissions') {
|
||||
$parent = Db::name('sys_permissions')->where('code', 'menu:permissions')->find();
|
||||
}
|
||||
}
|
||||
if (!$parent) {
|
||||
continue;
|
||||
}
|
||||
seedNode([
|
||||
'type' => $item['type'],
|
||||
'code' => $item['code'],
|
||||
'name' => $item['name'],
|
||||
'path' => $item['path'] ?? null,
|
||||
'icon' => $item['icon'] ?? null,
|
||||
'is_system' => 1,
|
||||
], (int) $parent['id']);
|
||||
echo "Added {$item['code']}\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 超级管理员补齐全部权限码
|
||||
if (tableExists('roles')) {
|
||||
$full = PermissionCatalog::fullPermissions();
|
||||
Db::name('roles')->where('slug', 'super_admin')->update([
|
||||
'permissions' => json_encode($full, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
echo "Updated super_admin permissions\n";
|
||||
}
|
||||
|
||||
echo "Migration completed.\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo 'Error: ' . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
-- AI Chat Database Schema
|
||||
CREATE DATABASE IF NOT EXISTS ai_chat DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE ai_chat;
|
||||
|
||||
-- 会员等级
|
||||
CREATE TABLE IF NOT EXISTS membership_levels (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
slug VARCHAR(50) NOT NULL UNIQUE,
|
||||
max_conversations INT UNSIGNED DEFAULT 50,
|
||||
max_messages_per_day INT UNSIGNED DEFAULT 100,
|
||||
max_upload_size_mb INT UNSIGNED DEFAULT 10,
|
||||
allowed_models JSON NULL COMMENT '允许使用的模型ID列表,null表示全部',
|
||||
permissions JSON NULL COMMENT '额外权限配置',
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 角色
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
slug VARCHAR(50) NOT NULL UNIQUE,
|
||||
permissions JSON NULL COMMENT '后台权限配置',
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 部门(支持上下级)
|
||||
CREATE TABLE IF NOT EXISTS departments (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
parent_id INT UNSIGNED NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_parent (parent_id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 用户
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
nickname VARCHAR(50) DEFAULT NULL,
|
||||
avatar VARCHAR(255) DEFAULT NULL,
|
||||
role ENUM('user', 'admin') DEFAULT 'user',
|
||||
role_id INT UNSIGNED NULL,
|
||||
department_id INT UNSIGNED NULL,
|
||||
membership_level_id INT UNSIGNED DEFAULT 1,
|
||||
status ENUM('active', 'disabled') DEFAULT 'active',
|
||||
last_login_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (membership_level_id) REFERENCES membership_levels(id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 系统功能开关
|
||||
CREATE TABLE IF NOT EXISTS system_settings (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
setting_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
setting_value TEXT NOT NULL,
|
||||
description VARCHAR(255) DEFAULT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- AI 模型配置(OpenAI 格式)
|
||||
CREATE TABLE IF NOT EXISTS ai_models (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
provider VARCHAR(20) NOT NULL DEFAULT 'openai' COMMENT '接口类型:openai=OpenAI兼容协议,dify=Dify应用API',
|
||||
model_id VARCHAR(100) NOT NULL COMMENT 'API model 参数(Dify 模式下可留空,应用由 API Key 决定)',
|
||||
api_base_url VARCHAR(255) NOT NULL DEFAULT 'https://api.openai.com/v1',
|
||||
api_key VARCHAR(255) NOT NULL,
|
||||
max_tokens INT UNSIGNED DEFAULT 4096,
|
||||
temperature DECIMAL(3,2) DEFAULT 0.70,
|
||||
is_default TINYINT(1) DEFAULT 0,
|
||||
enabled TINYINT(1) DEFAULT 1,
|
||||
support_context TINYINT(1) DEFAULT 1 COMMENT '是否支持上下文(多轮对话),关闭则每次只发送当前消息',
|
||||
support_image TINYINT(1) DEFAULT 1 COMMENT '是否支持图片/多模态输入,关闭则图片不会发送给模型',
|
||||
frequency_penalty DECIMAL(3,2) DEFAULT 0.00 COMMENT '频率惩罚,值越大越能抑制模型重复输出相同内容(部分自部署模型容易陷入重复循环)',
|
||||
presence_penalty DECIMAL(3,2) DEFAULT 0.00 COMMENT '存在惩罚,抑制模型重复讨论相同话题/短语',
|
||||
extra_config JSON NULL COMMENT '扩展配置(ComfyUI 工作流 JSON、节点映射等)',
|
||||
sort_order INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 会话
|
||||
CREATE TABLE IF NOT EXISTS conversations (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
title VARCHAR(200) DEFAULT '新对话',
|
||||
model_id INT UNSIGNED NULL,
|
||||
is_pinned TINYINT(1) DEFAULT 0,
|
||||
message_count INT UNSIGNED DEFAULT 0,
|
||||
external_conversation_id VARCHAR(64) NULL COMMENT '第三方平台(如 Dify)自己维护的会话ID,用于保持上下文连续',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (model_id) REFERENCES ai_models(id) ON DELETE SET NULL,
|
||||
INDEX idx_user_updated (user_id, updated_at DESC)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 消息
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
conversation_id INT UNSIGNED NOT NULL,
|
||||
role ENUM('user', 'assistant', 'system') NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
content_type ENUM('text', 'markdown', 'mixed') DEFAULT 'text',
|
||||
attachments JSON NULL COMMENT '[{type,url,name,size,mime}]',
|
||||
tokens_used INT UNSIGNED DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE,
|
||||
INDEX idx_conversation (conversation_id, created_at)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 上传文件
|
||||
CREATE TABLE IF NOT EXISTS uploads (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
stored_name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
mime_type VARCHAR(100) NOT NULL,
|
||||
file_size INT UNSIGNED NOT NULL,
|
||||
file_type ENUM('image', 'video', 'audio', 'document', 'other') NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 每日消息统计
|
||||
CREATE TABLE IF NOT EXISTS user_daily_stats (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
stat_date DATE NOT NULL,
|
||||
message_count INT UNSIGNED DEFAULT 0,
|
||||
UNIQUE KEY uk_user_date (user_id, stat_date),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 初始会员等级
|
||||
INSERT INTO membership_levels (name, slug, max_conversations, max_messages_per_day, max_upload_size_mb, permissions) VALUES
|
||||
('免费用户', 'free', 20, 50, 5, '{"can_upload_image":true,"can_upload_video":false,"can_upload_file":false,"can_use_voice":false}'),
|
||||
('高级会员', 'premium', 200, 500, 50, '{"can_upload_image":true,"can_upload_video":true,"can_upload_file":true,"can_use_voice":true}'),
|
||||
('管理员', 'admin', 9999, 9999, 100, '{"can_upload_image":true,"can_upload_video":true,"can_upload_file":true,"can_use_voice":true}');
|
||||
|
||||
INSERT INTO roles (name, slug, permissions, sort_order) VALUES
|
||||
('超级管理员', 'super_admin', '{"can_access_admin":true,"dirs":["dir:overview","dir:org","dir:business","dir:system"],"menus":["menu:dashboard","menu:users","menu:departments","menu:roles","menu:conversations","menu:memberships","menu:models","menu:settings"],"buttons":["btn:user:edit","btn:user:reset_password","btn:dept:create","btn:dept:edit","btn:dept:delete","btn:role:create","btn:role:edit","btn:role:delete","btn:conv:view_all","btn:conv:view_subordinate","btn:membership:edit","btn:model:create","btn:model:edit","btn:model:delete","btn:model:test","btn:settings:save"],"can_manage_users":true,"can_manage_roles":true,"can_manage_departments":true,"can_view_all_conversations":true,"can_view_subordinate_conversations":true,"can_manage_models":true,"can_manage_settings":true,"can_manage_memberships":true}', 1),
|
||||
('部门管理员', 'dept_manager', '{"can_access_admin":true,"dirs":["dir:overview","dir:business"],"menus":["menu:dashboard","menu:conversations"],"buttons":["btn:conv:view_subordinate"],"can_manage_users":false,"can_manage_roles":false,"can_manage_departments":false,"can_view_all_conversations":false,"can_view_subordinate_conversations":true,"can_manage_models":false,"can_manage_settings":false,"can_manage_memberships":false}', 2),
|
||||
('普通用户', 'user', '{"can_access_admin":false,"dirs":[],"menus":[],"buttons":[],"can_manage_users":false,"can_manage_roles":false,"can_manage_departments":false,"can_view_all_conversations":false,"can_view_subordinate_conversations":false,"can_manage_models":false,"can_manage_settings":false,"can_manage_memberships":false}', 3);
|
||||
|
||||
INSERT INTO departments (name, parent_id, sort_order) VALUES
|
||||
('总公司', NULL, 0);
|
||||
|
||||
-- 默认系统设置
|
||||
INSERT INTO system_settings (setting_key, setting_value, description) VALUES
|
||||
('features', '{"markdown":true,"image":true,"video":true,"voice":true,"document":true,"emoji":true,"upload_image":true,"upload_video":true,"upload_file":true,"paste_image":true}', '功能开关'),
|
||||
('site_name', 'AI Chat', '站点名称'),
|
||||
('allow_register', 'true', '是否允许注册');
|
||||
|
||||
-- 默认管理员 (密码: admin123)
|
||||
INSERT INTO users (username, email, password_hash, nickname, role, role_id, department_id, membership_level_id) VALUES
|
||||
('admin', 'admin@example.com', '$2y$10$VJCJHjJoAdUAENQZzLgKO.DrJaVsquBXGWSg5ok3u2FHAKRkVMWX.', '管理员', 'admin', 1, 1, 3);
|
||||
|
||||
-- 示例 AI 模型(需替换 api_key)
|
||||
INSERT INTO ai_models (name, model_id, api_base_url, api_key, is_default, enabled) VALUES
|
||||
('GPT-4o Mini', 'gpt-4o-mini', 'https://api.openai.com/v1', 'your-api-key-here', 1, 1);
|
||||
Reference in New Issue
Block a user