This commit is contained in:
2026-08-05 15:56:08 +08:00
parent 01729b1e0b
commit 2d9e2376b6
106 changed files with 10007 additions and 253 deletions
@@ -0,0 +1,83 @@
<?php
/**
* 访客管理权限迁移(可重复执行)。
* 用法: php database/migrate_guest_management.php
*/
require __DIR__ . '/../vendor/autoload.php';
$app = new think\App();
$app->initialize();
use app\service\PermissionCatalog;
use think\facade\Db;
function guestManagementTableExists(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 (guestManagementTableExists('users')) {
Db::execute("UPDATE users SET last_login_at = created_at WHERE LEFT(username, 6) = 'guest_' AND last_login_at IS NULL");
}
if (guestManagementTableExists('sys_permissions')) {
$parent = Db::name('sys_permissions')->where('code', 'dir:org')->find();
if ($parent) {
$menu = Db::name('sys_permissions')->where('code', 'menu:guests')->find();
if (!$menu) {
$menuId = Db::name('sys_permissions')->insertGetId([
'type' => 'menu',
'code' => 'menu:guests',
'name' => '访客管理',
'parent_id' => (int) $parent['id'],
'path' => '/guests',
'icon' => 'visitors',
'sort_order' => 15,
'is_system' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$menu = ['id' => $menuId];
echo "Added menu:guests\n";
}
foreach ([
['btn:guest:status', '启用/禁用访客'],
['btn:guest:delete', '删除访客'],
] as [$code, $name]) {
if (!Db::name('sys_permissions')->where('code', $code)->find()) {
Db::name('sys_permissions')->insert([
'type' => 'btn',
'code' => $code,
'name' => $name,
'parent_id' => (int) $menu['id'],
'sort_order' => 0,
'is_system' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
echo "Added {$code}\n";
}
}
}
if (guestManagementTableExists('roles')) {
Db::name('roles')->where('slug', 'super_admin')->update([
'permissions' => json_encode(PermissionCatalog::fullPermissions(), JSON_UNESCAPED_UNICODE),
]);
}
}
echo "Guest management migration completed.\n";
} catch (\Throwable $e) {
echo 'Error: ' . $e->getMessage() . "\n";
exit(1);
}
@@ -0,0 +1,98 @@
<?php
/**
* 邀请码与对应后台权限迁移(可重复执行)。
* 用法: php database/migrate_invitation_codes.php
*/
require __DIR__ . '/../vendor/autoload.php';
$app = new think\App();
$app->initialize();
use app\service\PermissionCatalog;
use think\facade\Db;
function invitationTableExists(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 (!invitationTableExists('invitation_codes')) {
Db::execute("CREATE TABLE invitation_codes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(32) NOT NULL UNIQUE,
department_id INT UNSIGNED NULL,
created_by INT UNSIGNED NULL,
used_by INT UNSIGNED NULL,
status ENUM('active','used','revoked') NOT NULL DEFAULT 'active',
expires_at TIMESTAMP NULL,
used_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_invitation_status (status),
INDEX idx_invitation_department (department_id),
INDEX idx_invitation_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
echo "Created table invitation_codes\n";
}
if (invitationTableExists('sys_permissions')) {
$parent = Db::name('sys_permissions')->where('code', 'dir:org')->find();
if ($parent) {
$menu = Db::name('sys_permissions')->where('code', 'menu:invitations')->find();
if (!$menu) {
$menuId = Db::name('sys_permissions')->insertGetId([
'type' => 'menu',
'code' => 'menu:invitations',
'name' => '邀请码管理',
'parent_id' => (int) $parent['id'],
'path' => '/invitations',
'icon' => 'ticket',
'sort_order' => 25,
'is_system' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$menu = ['id' => $menuId];
echo "Added menu:invitations\n";
}
foreach ([
['btn:invitation:create', '生成邀请码'],
['btn:invitation:revoke', '作废邀请码'],
] as [$code, $name]) {
if (!Db::name('sys_permissions')->where('code', $code)->find()) {
Db::name('sys_permissions')->insert([
'type' => 'btn',
'code' => $code,
'name' => $name,
'parent_id' => (int) $menu['id'],
'sort_order' => 0,
'is_system' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
echo "Added {$code}\n";
}
}
}
if (invitationTableExists('roles')) {
Db::name('roles')->where('slug', 'super_admin')->update([
'permissions' => json_encode(PermissionCatalog::fullPermissions(), JSON_UNESCAPED_UNICODE),
]);
}
}
echo "Invitation migration completed.\n";
} catch (\Throwable $e) {
echo 'Error: ' . $e->getMessage() . "\n";
exit(1);
}
+237
View File
@@ -0,0 +1,237 @@
<?php
/**
* MiniMax H3 短剧工坊迁移(可重复执行)。
* 用法: php database/migrate_short_drama.php
*/
require __DIR__ . '/../vendor/autoload.php';
$app = new think\App();
$app->initialize();
use app\service\SettingsService;
use app\service\ShortDramaPlannerService;
use think\facade\Db;
function shortDramaTableExists(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 shortDramaColumnExists(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 shortDramaColumnType(string $table, string $column): string
{
$db = Db::getConfig('database') ?: env('DB_NAME', 'ai_chat');
$rows = Db::query(
'SELECT DATA_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1',
[$db, $table, $column]
);
return strtolower((string) ($rows[0]['DATA_TYPE'] ?? ''));
}
try {
if (!shortDramaTableExists('video_projects')) {
Db::execute("CREATE TABLE video_projects (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
title VARCHAR(160) NOT NULL,
idea TEXT NOT NULL,
style VARCHAR(100) DEFAULT '电影写实',
aspect_ratio VARCHAR(10) NOT NULL DEFAULT '9:16',
episode_duration INT UNSIGNED NOT NULL DEFAULT 30,
duration_mode VARCHAR(10) NOT NULL DEFAULT 'fixed',
quality VARCHAR(20) NOT NULL DEFAULT 'fast',
voice_language VARCHAR(20) NOT NULL DEFAULT 'zh-CN',
show_subtitles TINYINT(1) NOT NULL DEFAULT 1,
character_origin VARCHAR(20) NOT NULL DEFAULT 'east_asian',
screen_text_language VARCHAR(20) NOT NULL DEFAULT 'zh-CN',
shot_duration_mode VARCHAR(10) NOT NULL DEFAULT 'auto',
status VARCHAR(30) NOT NULL DEFAULT 'draft',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_video_project_user (user_id, updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
echo "Created table video_projects\n";
}
if (!shortDramaColumnExists('video_projects', 'voice_language')) {
// 历史成片保留原音;尚未完成的项目升级为普通话重配音。
Db::execute("ALTER TABLE video_projects ADD COLUMN voice_language VARCHAR(20) NOT NULL DEFAULT 'native' AFTER quality");
Db::execute("UPDATE video_projects SET voice_language = 'zh-CN'
WHERE status IN ('storyboard', 'generating', 'needs_attention')
OR id = (SELECT id FROM (SELECT id FROM video_projects ORDER BY id DESC LIMIT 1) recent_project)");
Db::execute("ALTER TABLE video_projects ALTER COLUMN voice_language SET DEFAULT 'zh-CN'");
echo "Added video_projects.voice_language\n";
}
if (!shortDramaColumnExists('video_projects', 'duration_mode')) {
Db::execute("ALTER TABLE video_projects ADD COLUMN duration_mode VARCHAR(10) NOT NULL DEFAULT 'fixed' AFTER episode_duration");
echo "Added video_projects.duration_mode\n";
}
if (!shortDramaColumnExists('video_projects', 'show_subtitles')) {
// 已有成片没有烧录字幕,保持关闭;新建项目默认显示字幕。
Db::execute('ALTER TABLE video_projects ADD COLUMN show_subtitles TINYINT(1) NOT NULL DEFAULT 0 AFTER voice_language');
Db::execute('ALTER TABLE video_projects ALTER COLUMN show_subtitles SET DEFAULT 1');
echo "Added video_projects.show_subtitles\n";
}
if (!shortDramaColumnExists('video_projects', 'character_origin')) {
Db::execute("ALTER TABLE video_projects ADD COLUMN character_origin VARCHAR(20) NOT NULL DEFAULT 'east_asian' AFTER show_subtitles");
echo "Added video_projects.character_origin\n";
}
if (!shortDramaColumnExists('video_projects', 'screen_text_language')) {
// 历史项目没有精确场景文字层,保持关闭;新项目默认使用简体中文。
Db::execute("ALTER TABLE video_projects ADD COLUMN screen_text_language VARCHAR(20) NOT NULL DEFAULT 'none' AFTER character_origin");
Db::execute("ALTER TABLE video_projects ALTER COLUMN screen_text_language SET DEFAULT 'zh-CN'");
echo "Added video_projects.screen_text_language\n";
}
if (!shortDramaColumnExists('video_projects', 'shot_duration_mode')) {
// 历史项目已按 5 秒分镜生成;新项目默认交给 AI 在 5/10 秒间选择。
Db::execute("ALTER TABLE video_projects ADD COLUMN shot_duration_mode VARCHAR(10) NOT NULL DEFAULT '5' AFTER screen_text_language");
Db::execute("ALTER TABLE video_projects ALTER COLUMN shot_duration_mode SET DEFAULT 'auto'");
echo "Added video_projects.shot_duration_mode\n";
}
if (shortDramaColumnType('video_projects', 'episode_duration') !== 'int') {
Db::execute('ALTER TABLE video_projects MODIFY COLUMN episode_duration INT UNSIGNED NOT NULL DEFAULT 30');
echo "Expanded video_projects.episode_duration to INT UNSIGNED\n";
}
if (shortDramaColumnType('video_projects', 'idea') !== 'mediumtext') {
Db::execute('ALTER TABLE video_projects MODIFY COLUMN idea MEDIUMTEXT NOT NULL');
echo "Expanded video_projects.idea to MEDIUMTEXT\n";
}
if (!shortDramaTableExists('video_characters')) {
Db::execute("CREATE TABLE video_characters (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
project_id BIGINT UNSIGNED NOT NULL,
user_id INT UNSIGNED NOT NULL,
name VARCHAR(80) NOT NULL,
description TEXT NULL,
reference_upload_id INT UNSIGNED NULL,
voice_key VARCHAR(100) NULL,
is_locked TINYINT(1) NOT NULL DEFAULT 1,
asset_version INT UNSIGNED NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES video_projects(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_video_character_project (project_id, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
echo "Created table video_characters\n";
}
if (!shortDramaTableExists('video_episodes')) {
Db::execute("CREATE TABLE video_episodes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
project_id BIGINT UNSIGNED NOT NULL,
episode_no INT UNSIGNED NOT NULL DEFAULT 1,
title VARCHAR(160) NOT NULL,
script MEDIUMTEXT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'storyboard',
progress TINYINT UNSIGNED NOT NULL DEFAULT 0,
progress_message VARCHAR(255) NULL,
final_upload_id INT UNSIGNED NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES video_projects(id) ON DELETE CASCADE,
UNIQUE KEY uk_video_episode_number (project_id, episode_no),
INDEX idx_video_episode_status (status, updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
echo "Created table video_episodes\n";
}
if (!shortDramaTableExists('video_shots')) {
Db::execute("CREATE TABLE video_shots (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
episode_id BIGINT UNSIGNED NOT NULL,
shot_no INT UNSIGNED NOT NULL,
title VARCHAR(160) NOT NULL,
prompt TEXT NOT NULL,
dialogue TEXT NULL,
duration_seconds SMALLINT UNSIGNED NOT NULL DEFAULT 5,
workflow_type VARCHAR(20) NOT NULL DEFAULT 'fl2va',
status VARCHAR(30) NOT NULL DEFAULT 'draft',
prompt_id VARCHAR(80) NULL,
seed BIGINT UNSIGNED NULL,
output_upload_id INT UNSIGNED NULL,
error_message TEXT NULL,
meta JSON NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (episode_id) REFERENCES video_episodes(id) ON DELETE CASCADE,
UNIQUE KEY uk_video_shot_number (episode_id, shot_no),
INDEX idx_video_shot_prompt (prompt_id),
INDEX idx_video_shot_status (status, updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
echo "Created table video_shots\n";
}
// 旧版项目没有保存对白。为升级成普通话的项目补齐每个镜头的短句,避免保留 H3 含糊原声。
$dialogueBackfillCount = 0;
$mandarinProjects = Db::query("SELECT * FROM video_projects WHERE voice_language = 'zh-CN'");
foreach ($mandarinProjects as $mandarinProject) {
$plan = ShortDramaPlannerService::plan(
(string) $mandarinProject['idea'],
(int) $mandarinProject['episode_duration'],
(string) $mandarinProject['aspect_ratio'],
(string) $mandarinProject['style'],
'zh-CN',
(string) ($mandarinProject['character_origin'] ?? ShortDramaPlannerService::CHARACTER_ORIGIN_EAST_ASIAN),
(string) ($mandarinProject['screen_text_language'] ?? ShortDramaPlannerService::SCREEN_TEXT_LANGUAGE_NONE),
(string) ($mandarinProject['shot_duration_mode'] ?? ShortDramaPlannerService::SHOT_DURATION_FIVE)
);
$projectShots = Db::query(
'SELECT s.id, s.shot_no, s.dialogue FROM video_shots s '
. 'INNER JOIN video_episodes e ON e.id = s.episode_id '
. 'WHERE e.project_id = ? ORDER BY s.shot_no',
[(int) $mandarinProject['id']]
);
foreach ($projectShots as $projectShot) {
if (trim((string) ($projectShot['dialogue'] ?? '')) !== '') {
continue;
}
$plannedShot = $plan['shots'][max(0, (int) $projectShot['shot_no'] - 1)] ?? null;
if (!is_array($plannedShot) || trim((string) ($plannedShot['dialogue'] ?? '')) === '') {
continue;
}
Db::execute(
'UPDATE video_shots SET dialogue = ? WHERE id = ?',
[(string) $plannedShot['dialogue'], (int) $projectShot['id']]
);
$dialogueBackfillCount++;
}
}
if ($dialogueBackfillCount > 0) {
echo "Backfilled {$dialogueBackfillCount} Mandarin shot dialogues\n";
}
$features = SettingsService::getFeatures();
$features['short_drama'] = $features['short_drama'] ?? true;
SettingsService::set('features', $features);
echo "Short drama migration completed.\n";
} catch (\Throwable $e) {
echo 'Error: ' . $e->getMessage() . "\n";
exit(1);
}
@@ -0,0 +1,52 @@
<?php
/**
* Docker 内短剧接力进程。
*
* H3 的下一镜头必须等上一镜头落盘并提取尾帧后才能提交,因此由这个进程
* 持续触发同一套状态接口。页面关闭后,长视频仍会按顺序继续生成。
*/
require __DIR__ . '/../vendor/autoload.php';
$app = new think\App();
$app->initialize();
use app\model\VideoProject;
use app\service\JwtService;
set_time_limit(0);
$internalBaseUrl = rtrim((string) (getenv('SHORT_DRAMA_INTERNAL_URL') ?: 'http://web'), '/');
if (!preg_match('#^https?://#i', $internalBaseUrl)) {
fwrite(STDERR, "[short-drama-worker] SHORT_DRAMA_INTERNAL_URL 必须使用 http 或 https\n");
exit(1);
}
while (true) {
try {
$projects = VideoProject::where('status', 'generating')
->order('updated_at')
->limit(30)
->select();
foreach ($projects as $project) {
$token = JwtService::generateToken(['user_id' => (int) $project->user_id]);
$url = $internalBaseUrl . '/api/short-drama/projects/' . (int) $project->id . '/status';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPGET => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 25,
CURLOPT_CONNECTTIMEOUT => 3,
]);
curl_exec($ch);
curl_close($ch);
}
} catch (Throwable $error) {
fwrite(STDERR, '[short-drama-worker] ' . $error->getMessage() . "\n");
}
sleep(8);
}
+99 -1
View File
@@ -1,4 +1,5 @@
-- AI Chat Database Schema
SET NAMES utf8mb4;
CREATE DATABASE IF NOT EXISTS ai_chat DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE ai_chat;
@@ -58,6 +59,23 @@ CREATE TABLE IF NOT EXISTS users (
FOREIGN KEY (membership_level_id) REFERENCES membership_levels(id)
) ENGINE=InnoDB;
-- 一次性注册邀请码;指定 department_id 后,新用户会自动归属到该部门
CREATE TABLE IF NOT EXISTS invitation_codes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(32) NOT NULL UNIQUE,
department_id INT UNSIGNED NULL,
created_by INT UNSIGNED NULL,
used_by INT UNSIGNED NULL,
status ENUM('active', 'used', 'revoked') NOT NULL DEFAULT 'active',
expires_at TIMESTAMP NULL,
used_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_invitation_status (status),
INDEX idx_invitation_department (department_id),
INDEX idx_invitation_created (created_at)
) ENGINE=InnoDB;
-- 系统功能开关
CREATE TABLE IF NOT EXISTS system_settings (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
@@ -134,6 +152,86 @@ CREATE TABLE IF NOT EXISTS uploads (
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
-- 短剧项目(一个项目可包含多集,角色资产在项目内复用)
CREATE TABLE IF NOT EXISTS video_projects (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
title VARCHAR(160) NOT NULL,
idea MEDIUMTEXT NOT NULL,
style VARCHAR(100) DEFAULT '电影写实',
aspect_ratio VARCHAR(10) NOT NULL DEFAULT '9:16',
episode_duration INT UNSIGNED NOT NULL DEFAULT 30,
duration_mode VARCHAR(10) NOT NULL DEFAULT 'fixed',
quality VARCHAR(20) NOT NULL DEFAULT 'fast',
voice_language VARCHAR(20) NOT NULL DEFAULT 'zh-CN',
show_subtitles TINYINT(1) NOT NULL DEFAULT 1,
character_origin VARCHAR(20) NOT NULL DEFAULT 'east_asian',
screen_text_language VARCHAR(20) NOT NULL DEFAULT 'zh-CN',
shot_duration_mode VARCHAR(10) NOT NULL DEFAULT 'auto',
status VARCHAR(30) NOT NULL DEFAULT 'draft',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_video_project_user (user_id, updated_at)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS video_characters (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
project_id BIGINT UNSIGNED NOT NULL,
user_id INT UNSIGNED NOT NULL,
name VARCHAR(80) NOT NULL,
description TEXT NULL,
reference_upload_id INT UNSIGNED NULL,
voice_key VARCHAR(100) NULL,
is_locked TINYINT(1) NOT NULL DEFAULT 1,
asset_version INT UNSIGNED NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES video_projects(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_video_character_project (project_id, id)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS video_episodes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
project_id BIGINT UNSIGNED NOT NULL,
episode_no INT UNSIGNED NOT NULL DEFAULT 1,
title VARCHAR(160) NOT NULL,
script MEDIUMTEXT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'storyboard',
progress TINYINT UNSIGNED NOT NULL DEFAULT 0,
progress_message VARCHAR(255) NULL,
final_upload_id INT UNSIGNED NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES video_projects(id) ON DELETE CASCADE,
UNIQUE KEY uk_video_episode_number (project_id, episode_no),
INDEX idx_video_episode_status (status, updated_at)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS video_shots (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
episode_id BIGINT UNSIGNED NOT NULL,
shot_no INT UNSIGNED NOT NULL,
title VARCHAR(160) NOT NULL,
prompt TEXT NOT NULL,
dialogue TEXT NULL,
duration_seconds SMALLINT UNSIGNED NOT NULL DEFAULT 5,
workflow_type VARCHAR(20) NOT NULL DEFAULT 'fl2va',
status VARCHAR(30) NOT NULL DEFAULT 'draft',
prompt_id VARCHAR(80) NULL,
seed BIGINT UNSIGNED NULL,
output_upload_id INT UNSIGNED NULL,
error_message TEXT NULL,
meta JSON NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (episode_id) REFERENCES video_episodes(id) ON DELETE CASCADE,
UNIQUE KEY uk_video_shot_number (episode_id, shot_no),
INDEX idx_video_shot_prompt (prompt_id),
INDEX idx_video_shot_status (status, updated_at)
) ENGINE=InnoDB;
-- 每日消息统计
CREATE TABLE IF NOT EXISTS user_daily_stats (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
@@ -160,7 +258,7 @@ INSERT INTO departments (name, parent_id, sort_order) VALUES
-- 默认系统设置
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}', '功能开关'),
('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,"short_drama":true}', '功能开关'),
('site_name', 'AI Chat', '站点名称'),
('allow_register', 'true', '是否允许注册');