feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
0
backend/migrations/__init__.py
Normal file
0
backend/migrations/__init__.py
Normal file
20
backend/migrations/add_broadcast_fields.sql
Normal file
20
backend/migrations/add_broadcast_fields.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- 添加播课功能相关字段到 courses 表
|
||||
-- 日期: 2025-10-14
|
||||
|
||||
USE kaopeilian;
|
||||
|
||||
-- 添加播课音频URL字段
|
||||
ALTER TABLE courses
|
||||
ADD COLUMN broadcast_audio_url VARCHAR(500) NULL COMMENT '播课音频URL';
|
||||
|
||||
-- 添加播课生成时间字段
|
||||
ALTER TABLE courses
|
||||
ADD COLUMN broadcast_generated_at DATETIME NULL COMMENT '播课生成时间';
|
||||
|
||||
-- 验证字段添加
|
||||
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'kaopeilian'
|
||||
AND TABLE_NAME = 'courses'
|
||||
AND COLUMN_NAME IN ('broadcast_audio_url', 'broadcast_generated_at');
|
||||
|
||||
24
backend/migrations/add_broadcast_status_fields.sql
Normal file
24
backend/migrations/add_broadcast_status_fields.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- 添加播课异步生成状态管理字段
|
||||
-- 日期: 2025-10-14
|
||||
|
||||
USE kaopeilian;
|
||||
|
||||
-- 添加播课生成状态字段
|
||||
ALTER TABLE courses
|
||||
ADD COLUMN broadcast_status VARCHAR(20) NULL COMMENT '播课生成状态: pending/generating/completed/failed';
|
||||
|
||||
-- 添加Coze工作流任务ID字段
|
||||
ALTER TABLE courses
|
||||
ADD COLUMN broadcast_task_id VARCHAR(100) NULL COMMENT 'Coze工作流任务ID';
|
||||
|
||||
-- 添加错误信息字段
|
||||
ALTER TABLE courses
|
||||
ADD COLUMN broadcast_error_message TEXT NULL COMMENT '生成失败错误信息';
|
||||
|
||||
-- 验证字段添加
|
||||
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'kaopeilian'
|
||||
AND TABLE_NAME = 'courses'
|
||||
AND COLUMN_NAME IN ('broadcast_status', 'broadcast_task_id', 'broadcast_error_message');
|
||||
|
||||
25
backend/migrations/add_course_allow_download.sql
Normal file
25
backend/migrations/add_course_allow_download.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- 迁移脚本:为课程表添加资料下载开关字段
|
||||
-- 创建时间:2025-12-17
|
||||
-- 功能描述:添加 allow_download 字段,用于控制学员是否可以下载课程资料
|
||||
|
||||
-- 检查并添加 allow_download 字段
|
||||
ALTER TABLE courses
|
||||
ADD COLUMN IF NOT EXISTS allow_download TINYINT(1) NOT NULL DEFAULT 0
|
||||
COMMENT '是否允许下载资料:0=不允许,1=允许';
|
||||
|
||||
-- 验证字段是否添加成功
|
||||
SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_DEFAULT, COLUMN_COMMENT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'courses'
|
||||
AND COLUMN_NAME = 'allow_download';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
298
backend/migrations/admin_platform_schema.sql
Normal file
298
backend/migrations/admin_platform_schema.sql
Normal file
@@ -0,0 +1,298 @@
|
||||
-- ============================================
|
||||
-- 考培练系统 SaaS 超级管理后台数据库架构
|
||||
-- 数据库名:kaopeilian_admin
|
||||
-- 创建日期:2026-01-18
|
||||
-- ============================================
|
||||
|
||||
-- 创建数据库
|
||||
CREATE DATABASE IF NOT EXISTS kaopeilian_admin
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE kaopeilian_admin;
|
||||
|
||||
-- ============================================
|
||||
-- 1. 平台管理员表 (admin_users)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS `admin_users` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
|
||||
`email` VARCHAR(100) UNIQUE COMMENT '邮箱',
|
||||
`phone` VARCHAR(20) UNIQUE COMMENT '手机号',
|
||||
`password_hash` VARCHAR(200) NOT NULL COMMENT '密码哈希',
|
||||
`full_name` VARCHAR(100) COMMENT '姓名',
|
||||
`avatar_url` VARCHAR(500) COMMENT '头像URL',
|
||||
`role` VARCHAR(20) DEFAULT 'admin' COMMENT '角色: superadmin, admin, viewer',
|
||||
`is_active` BOOLEAN DEFAULT TRUE COMMENT '是否激活',
|
||||
`last_login_at` DATETIME COMMENT '最后登录时间',
|
||||
`last_login_ip` VARCHAR(50) COMMENT '最后登录IP',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_role (role),
|
||||
INDEX idx_is_active (is_active)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台管理员表';
|
||||
|
||||
-- ============================================
|
||||
-- 2. 租户表 (tenants)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS `tenants` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`code` VARCHAR(20) NOT NULL UNIQUE COMMENT '租户编码(如:hua, yy, hl)',
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '租户名称',
|
||||
`display_name` VARCHAR(200) COMMENT '显示名称(如:华尔倍丽-考培练系统)',
|
||||
`domain` VARCHAR(200) NOT NULL COMMENT '域名(如:hua.ireborn.com.cn)',
|
||||
`logo_url` VARCHAR(500) COMMENT 'Logo URL',
|
||||
`favicon_url` VARCHAR(500) COMMENT 'Favicon URL',
|
||||
`contact_name` VARCHAR(50) COMMENT '联系人',
|
||||
`contact_phone` VARCHAR(20) COMMENT '联系电话',
|
||||
`contact_email` VARCHAR(100) COMMENT '联系邮箱',
|
||||
`industry` VARCHAR(50) DEFAULT 'medical_beauty' COMMENT '行业:medical_beauty, pet, education',
|
||||
`status` VARCHAR(20) DEFAULT 'active' COMMENT '状态:active, inactive, suspended',
|
||||
`expire_at` DATE COMMENT '服务到期日期',
|
||||
`remarks` TEXT COMMENT '备注',
|
||||
`created_by` INT COMMENT '创建人ID',
|
||||
`updated_by` INT COMMENT '更新人ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (created_by) REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (updated_by) REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
INDEX idx_code (code),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_domain (domain)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户表';
|
||||
|
||||
-- ============================================
|
||||
-- 3. 租户配置表 (tenant_configs)
|
||||
-- Key-Value 形式存储各类配置
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS `tenant_configs` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`tenant_id` INT NOT NULL COMMENT '租户ID',
|
||||
`config_group` VARCHAR(50) NOT NULL COMMENT '配置分组:database, redis, dify, coze, ai, yanji, security',
|
||||
`config_key` VARCHAR(100) NOT NULL COMMENT '配置键',
|
||||
`config_value` TEXT COMMENT '配置值',
|
||||
`value_type` VARCHAR(20) DEFAULT 'string' COMMENT '值类型:string, int, bool, json, secret',
|
||||
`is_encrypted` BOOLEAN DEFAULT FALSE COMMENT '是否加密存储',
|
||||
`description` VARCHAR(500) COMMENT '配置说明',
|
||||
`is_required` BOOLEAN DEFAULT FALSE COMMENT '是否必填',
|
||||
`default_value` TEXT COMMENT '默认值',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uk_tenant_group_key (tenant_id, config_group, config_key),
|
||||
INDEX idx_tenant_id (tenant_id),
|
||||
INDEX idx_config_group (config_group)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户配置表';
|
||||
|
||||
-- ============================================
|
||||
-- 4. AI 提示词模板表 (ai_prompts)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS `ai_prompts` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`code` VARCHAR(50) NOT NULL COMMENT '提示词编码(如:knowledge_analysis, exam_generator)',
|
||||
`name` VARCHAR(100) NOT NULL COMMENT '提示词名称',
|
||||
`description` TEXT COMMENT '提示词说明',
|
||||
`module` VARCHAR(50) NOT NULL COMMENT '所属模块:course, exam, practice, ability',
|
||||
`system_prompt` TEXT NOT NULL COMMENT '系统提示词',
|
||||
`user_prompt_template` TEXT COMMENT '用户提示词模板',
|
||||
`variables` JSON COMMENT '变量列表(如:["course_name", "content"])',
|
||||
`output_schema` JSON COMMENT '输出 JSON Schema',
|
||||
`model_recommendation` VARCHAR(100) COMMENT '推荐模型',
|
||||
`max_tokens` INT DEFAULT 4096 COMMENT '最大 token 数',
|
||||
`temperature` DECIMAL(3,2) DEFAULT 0.70 COMMENT '温度参数',
|
||||
`is_system` BOOLEAN DEFAULT TRUE COMMENT '是否系统内置(内置不可删除)',
|
||||
`is_active` BOOLEAN DEFAULT TRUE COMMENT '是否启用',
|
||||
`version` INT DEFAULT 1 COMMENT '当前版本号',
|
||||
`created_by` INT COMMENT '创建人ID',
|
||||
`updated_by` INT COMMENT '更新人ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (created_by) REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (updated_by) REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
UNIQUE KEY uk_code (code),
|
||||
INDEX idx_module (module),
|
||||
INDEX idx_is_active (is_active)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI提示词模板表';
|
||||
|
||||
-- ============================================
|
||||
-- 5. AI 提示词版本历史表 (ai_prompt_versions)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS `ai_prompt_versions` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`prompt_id` INT NOT NULL COMMENT '提示词ID',
|
||||
`version` INT NOT NULL COMMENT '版本号',
|
||||
`system_prompt` TEXT NOT NULL COMMENT '系统提示词',
|
||||
`user_prompt_template` TEXT COMMENT '用户提示词模板',
|
||||
`variables` JSON COMMENT '变量列表',
|
||||
`output_schema` JSON COMMENT '输出 JSON Schema',
|
||||
`change_summary` VARCHAR(500) COMMENT '变更说明',
|
||||
`created_by` INT COMMENT '创建人ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (prompt_id) REFERENCES ai_prompts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
UNIQUE KEY uk_prompt_version (prompt_id, version),
|
||||
INDEX idx_prompt_id (prompt_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI提示词版本历史表';
|
||||
|
||||
-- ============================================
|
||||
-- 6. 租户自定义提示词表 (tenant_prompts)
|
||||
-- 租户可覆盖系统默认提示词
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS `tenant_prompts` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`tenant_id` INT NOT NULL COMMENT '租户ID',
|
||||
`prompt_id` INT NOT NULL COMMENT '基础提示词ID',
|
||||
`system_prompt` TEXT COMMENT '自定义系统提示词(为空则使用默认)',
|
||||
`user_prompt_template` TEXT COMMENT '自定义用户提示词模板',
|
||||
`is_active` BOOLEAN DEFAULT TRUE COMMENT '是否启用自定义',
|
||||
`created_by` INT COMMENT '创建人ID',
|
||||
`updated_by` INT COMMENT '更新人ID',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (prompt_id) REFERENCES ai_prompts(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (created_by) REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (updated_by) REFERENCES admin_users(id) ON DELETE SET NULL,
|
||||
UNIQUE KEY uk_tenant_prompt (tenant_id, prompt_id),
|
||||
INDEX idx_tenant_id (tenant_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户自定义提示词表';
|
||||
|
||||
-- ============================================
|
||||
-- 7. 功能开关表 (feature_switches)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS `feature_switches` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`tenant_id` INT COMMENT '租户ID(NULL表示全局默认)',
|
||||
`feature_code` VARCHAR(50) NOT NULL COMMENT '功能编码',
|
||||
`feature_name` VARCHAR(100) NOT NULL COMMENT '功能名称',
|
||||
`feature_group` VARCHAR(50) COMMENT '功能分组:exam, practice, broadcast, yanji',
|
||||
`is_enabled` BOOLEAN DEFAULT TRUE COMMENT '是否启用',
|
||||
`config` JSON COMMENT '功能配置参数',
|
||||
`description` VARCHAR(500) COMMENT '功能说明',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uk_tenant_feature (tenant_id, feature_code),
|
||||
INDEX idx_tenant_id (tenant_id),
|
||||
INDEX idx_feature_code (feature_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='功能开关表';
|
||||
|
||||
-- ============================================
|
||||
-- 8. 操作审计日志表 (operation_logs)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS `operation_logs` (
|
||||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
`admin_user_id` INT COMMENT '操作人ID',
|
||||
`admin_username` VARCHAR(50) COMMENT '操作人用户名',
|
||||
`tenant_id` INT COMMENT '涉及租户ID',
|
||||
`tenant_code` VARCHAR(20) COMMENT '涉及租户编码',
|
||||
`operation_type` VARCHAR(50) NOT NULL COMMENT '操作类型:create, update, delete, enable, disable',
|
||||
`resource_type` VARCHAR(50) NOT NULL COMMENT '资源类型:tenant, config, prompt, feature',
|
||||
`resource_id` INT COMMENT '资源ID',
|
||||
`resource_name` VARCHAR(200) COMMENT '资源名称',
|
||||
`old_value` JSON COMMENT '变更前值',
|
||||
`new_value` JSON COMMENT '变更后值',
|
||||
`ip_address` VARCHAR(50) COMMENT '操作IP',
|
||||
`user_agent` VARCHAR(500) COMMENT '浏览器信息',
|
||||
`remarks` TEXT COMMENT '备注',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_admin_user_id (admin_user_id),
|
||||
INDEX idx_tenant_id (tenant_id),
|
||||
INDEX idx_operation_type (operation_type),
|
||||
INDEX idx_resource_type (resource_type),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作审计日志表';
|
||||
|
||||
-- ============================================
|
||||
-- 9. 配置模板表 (config_templates)
|
||||
-- 定义各配置项的元数据
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS `config_templates` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`config_group` VARCHAR(50) NOT NULL COMMENT '配置分组',
|
||||
`config_key` VARCHAR(100) NOT NULL COMMENT '配置键',
|
||||
`display_name` VARCHAR(100) NOT NULL COMMENT '显示名称',
|
||||
`description` TEXT COMMENT '配置说明',
|
||||
`value_type` VARCHAR(20) DEFAULT 'string' COMMENT '值类型',
|
||||
`default_value` TEXT COMMENT '默认值',
|
||||
`is_required` BOOLEAN DEFAULT FALSE COMMENT '是否必填',
|
||||
`is_secret` BOOLEAN DEFAULT FALSE COMMENT '是否敏感信息',
|
||||
`validation_rule` VARCHAR(500) COMMENT '验证规则(正则)',
|
||||
`options` JSON COMMENT '可选值列表(下拉选择)',
|
||||
`sort_order` INT DEFAULT 0 COMMENT '排序',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_group_key (config_group, config_key),
|
||||
INDEX idx_config_group (config_group)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='配置模板表';
|
||||
|
||||
-- ============================================
|
||||
-- 初始化数据
|
||||
-- ============================================
|
||||
|
||||
-- 1. 插入超级管理员
|
||||
INSERT INTO admin_users (username, email, password_hash, full_name, role) VALUES
|
||||
('superadmin', 'admin@ireborn.com.cn', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.ynB8dC.m5QZ9Hy', '超级管理员', 'superadmin');
|
||||
-- 密码: Superadmin123!
|
||||
|
||||
-- 2. 插入配置模板
|
||||
INSERT INTO config_templates (config_group, config_key, display_name, description, value_type, default_value, is_required, is_secret, sort_order) VALUES
|
||||
-- 数据库配置
|
||||
('database', 'MYSQL_HOST', 'MySQL主机', 'MySQL数据库主机地址', 'string', 'prod-mysql', TRUE, FALSE, 1),
|
||||
('database', 'MYSQL_PORT', 'MySQL端口', 'MySQL数据库端口', 'int', '3306', TRUE, FALSE, 2),
|
||||
('database', 'MYSQL_USER', 'MySQL用户', 'MySQL数据库用户名', 'string', 'root', TRUE, FALSE, 3),
|
||||
('database', 'MYSQL_PASSWORD', 'MySQL密码', 'MySQL数据库密码', 'string', NULL, TRUE, TRUE, 4),
|
||||
('database', 'MYSQL_DATABASE', '数据库名', '租户数据库名称', 'string', NULL, TRUE, FALSE, 5),
|
||||
-- Redis配置
|
||||
('redis', 'REDIS_HOST', 'Redis主机', 'Redis缓存主机地址', 'string', 'localhost', TRUE, FALSE, 1),
|
||||
('redis', 'REDIS_PORT', 'Redis端口', 'Redis端口', 'int', '6379', TRUE, FALSE, 2),
|
||||
('redis', 'REDIS_DB', 'Redis DB', 'Redis数据库编号', 'int', '0', FALSE, FALSE, 3),
|
||||
-- 安全配置
|
||||
('security', 'SECRET_KEY', 'JWT密钥', 'JWT Token签名密钥', 'string', NULL, TRUE, TRUE, 1),
|
||||
('security', 'CORS_ORIGINS', 'CORS域名', '允许跨域的域名列表', 'json', '[]', FALSE, FALSE, 2),
|
||||
-- Dify配置
|
||||
('dify', 'DIFY_API_KEY', '知识点分析 Key', 'Dify 01-知识点分析工作流 API Key', 'string', NULL, FALSE, TRUE, 1),
|
||||
('dify', 'DIFY_EXAM_GENERATOR_API_KEY', '试题生成器 Key', 'Dify 02-试题生成器工作流 API Key', 'string', NULL, FALSE, TRUE, 2),
|
||||
('dify', 'DIFY_PRACTICE_API_KEY', '陪练知识准备 Key', 'Dify 03-陪练知识准备工作流 API Key', 'string', NULL, FALSE, TRUE, 3),
|
||||
('dify', 'DIFY_COURSE_CHAT_API_KEY', '课程对话 Key', 'Dify 04-与课程对话工作流 API Key', 'string', NULL, FALSE, TRUE, 4),
|
||||
('dify', 'DIFY_YANJI_ANALYSIS_API_KEY', '智能工牌分析 Key', 'Dify 05-智能工牌能力分析工作流 API Key', 'string', NULL, FALSE, TRUE, 5),
|
||||
-- Coze配置
|
||||
('coze', 'COZE_PRACTICE_BOT_ID', '陪练Bot ID', 'Coze 陪练机器人ID', 'string', '7560643598174683145', FALSE, FALSE, 1),
|
||||
('coze', 'COZE_BROADCAST_WORKFLOW_ID', '播课工作流ID', 'Coze 播课工作流ID', 'string', NULL, FALSE, FALSE, 2),
|
||||
('coze', 'COZE_BROADCAST_SPACE_ID', '播课空间ID', 'Coze 播课工作流空间ID', 'string', '7474971491470688296', FALSE, FALSE, 3),
|
||||
('coze', 'COZE_OAUTH_CLIENT_ID', 'OAuth Client ID', 'Coze OAuth 客户端ID', 'string', NULL, FALSE, FALSE, 4),
|
||||
('coze', 'COZE_OAUTH_PUBLIC_KEY_ID', 'OAuth Public Key ID', 'Coze OAuth 公钥ID', 'string', NULL, FALSE, FALSE, 5),
|
||||
-- AI服务配置
|
||||
('ai', 'AI_PRIMARY_API_KEY', '首选AI服务Key', '首选AI服务商(4sapi.com) API Key', 'string', NULL, FALSE, TRUE, 1),
|
||||
('ai', 'AI_PRIMARY_BASE_URL', '首选AI服务地址', '首选AI服务商API地址', 'string', 'https://4sapi.com/v1', FALSE, FALSE, 2),
|
||||
('ai', 'AI_FALLBACK_API_KEY', '备选AI服务Key', '备选AI服务商(OpenRouter) API Key', 'string', NULL, FALSE, TRUE, 3),
|
||||
('ai', 'AI_FALLBACK_BASE_URL', '备选AI服务地址', '备选AI服务商API地址', 'string', 'https://openrouter.ai/api/v1', FALSE, FALSE, 4),
|
||||
('ai', 'AI_DEFAULT_MODEL', '默认模型', '默认使用的AI模型', 'string', 'gemini-3-flash-preview', FALSE, FALSE, 5),
|
||||
('ai', 'AI_TIMEOUT', 'AI请求超时', 'AI服务请求超时时间(秒)', 'int', '120', FALSE, FALSE, 6),
|
||||
-- 言迹工牌配置
|
||||
('yanji', 'YANJI_CLIENT_ID', '客户端ID', '言迹开放平台Client ID', 'string', NULL, FALSE, FALSE, 1),
|
||||
('yanji', 'YANJI_CLIENT_SECRET', '客户端密钥', '言迹开放平台Client Secret', 'string', NULL, FALSE, TRUE, 2),
|
||||
('yanji', 'YANJI_TENANT_ID', '租户ID', '言迹租户ID', 'string', NULL, FALSE, FALSE, 3),
|
||||
('yanji', 'YANJI_ESTATE_ID', '门店ID', '言迹门店ID', 'string', NULL, FALSE, FALSE, 4),
|
||||
-- 文件存储配置
|
||||
('storage', 'UPLOAD_DIR', '上传目录', '文件上传目录', 'string', 'uploads', FALSE, FALSE, 1),
|
||||
('storage', 'MAX_UPLOAD_SIZE', '最大上传大小', '最大文件上传大小(字节)', 'int', '15728640', FALSE, FALSE, 2);
|
||||
|
||||
-- 3. 插入功能开关模板(全局默认)
|
||||
INSERT INTO feature_switches (tenant_id, feature_code, feature_name, feature_group, is_enabled, description) VALUES
|
||||
(NULL, 'exam_module', '考试模块', 'exam', TRUE, '考试功能总开关'),
|
||||
(NULL, 'exam_ai_generate', 'AI试题生成', 'exam', TRUE, '使用AI自动生成试题'),
|
||||
(NULL, 'exam_three_rounds', '三轮考试', 'exam', TRUE, '启用三轮考试机制'),
|
||||
(NULL, 'practice_module', '陪练模块', 'practice', TRUE, '陪练功能总开关'),
|
||||
(NULL, 'practice_voice', '语音陪练', 'practice', TRUE, '支持语音对话陪练'),
|
||||
(NULL, 'broadcast_module', '播课模块', 'broadcast', TRUE, '播课功能总开关'),
|
||||
(NULL, 'broadcast_auto_generate', '自动生成播课', 'broadcast', TRUE, '自动生成课程播课内容'),
|
||||
(NULL, 'course_chat', '课程对话', 'course', TRUE, '与课程知识点对话功能'),
|
||||
(NULL, 'knowledge_ai_analyze', 'AI知识点分析', 'course', TRUE, '使用AI分析提取知识点'),
|
||||
(NULL, 'yanji_module', '智能工牌模块', 'yanji', FALSE, '言迹智能工牌对接功能'),
|
||||
(NULL, 'yanji_ability_analysis', '能力分析', 'yanji', FALSE, '基于工牌数据的能力分析');
|
||||
|
||||
-- ============================================
|
||||
-- 完成
|
||||
-- ============================================
|
||||
|
||||
0
backend/migrations/alembic/__init__.py
Normal file
0
backend/migrations/alembic/__init__.py
Normal file
0
backend/migrations/alembic/versions/__init__.py
Normal file
0
backend/migrations/alembic/versions/__init__.py
Normal file
17
backend/migrations/cleanup_broadcast_fields.sql
Normal file
17
backend/migrations/cleanup_broadcast_fields.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- 清理播课功能不必要的字段
|
||||
-- 日期: 2025-10-14
|
||||
-- 说明: 新策略下Coze工作流直接写数据库,不需要状态跟踪字段
|
||||
|
||||
USE kaopeilian;
|
||||
|
||||
-- 删除不再需要的字段
|
||||
ALTER TABLE courses DROP COLUMN broadcast_status;
|
||||
ALTER TABLE courses DROP COLUMN broadcast_task_id;
|
||||
ALTER TABLE courses DROP COLUMN broadcast_error_message;
|
||||
|
||||
-- 验证剩余字段
|
||||
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'kaopeilian'
|
||||
AND TABLE_NAME = 'courses'
|
||||
AND COLUMN_NAME LIKE 'broadcast%';
|
||||
0
backend/migrations/create_ability_assessments.sql
Normal file
0
backend/migrations/create_ability_assessments.sql
Normal file
104
backend/migrations/env.py
Normal file
104
backend/migrations/env.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Alembic 环境配置
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from logging.config import fileConfig
|
||||
from pathlib import Path
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.base import Base
|
||||
|
||||
# 导入所有模型以确保 Alembic 能够检测到它们
|
||||
from app.models.user import User, Team # noqa
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# 设置数据库URL
|
||||
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_async_migrations() -> None:
|
||||
"""In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
connectable = async_engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode."""
|
||||
|
||||
asyncio.run(run_async_migrations())
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
131
backend/migrations/manual_course_tables.sql
Normal file
131
backend/migrations/manual_course_tables.sql
Normal file
@@ -0,0 +1,131 @@
|
||||
-- 课程相关表结构
|
||||
-- 用于手动创建课程模块所需的数据库表
|
||||
|
||||
-- 1. 课程表
|
||||
CREATE TABLE IF NOT EXISTS `courses` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '课程名称',
|
||||
`description` TEXT COMMENT '课程描述',
|
||||
`category` ENUM('technology', 'management', 'business', 'general') NOT NULL DEFAULT 'general' COMMENT '课程分类',
|
||||
`status` ENUM('draft', 'published', 'archived') NOT NULL DEFAULT 'draft' COMMENT '课程状态',
|
||||
`cover_image` VARCHAR(500) COMMENT '封面图片URL',
|
||||
`duration_hours` FLOAT COMMENT '课程时长(小时)',
|
||||
`difficulty_level` INT COMMENT '难度等级(1-5)',
|
||||
`tags` JSON COMMENT '标签列表',
|
||||
`published_at` DATETIME COMMENT '发布时间',
|
||||
`publisher_id` INT COMMENT '发布人ID',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序顺序',
|
||||
`is_featured` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否推荐',
|
||||
|
||||
-- 基础字段
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 软删除字段
|
||||
`is_deleted` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
`deleted_at` DATETIME,
|
||||
|
||||
-- 审计字段
|
||||
`created_by` INT,
|
||||
`updated_by` INT,
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_courses_status` (`status`),
|
||||
INDEX `idx_courses_category` (`category`),
|
||||
INDEX `idx_courses_is_deleted` (`is_deleted`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='课程表';
|
||||
|
||||
-- 2. 课程资料表
|
||||
CREATE TABLE IF NOT EXISTS `course_materials` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`course_id` INT NOT NULL COMMENT '课程ID',
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '资料名称',
|
||||
`description` TEXT COMMENT '资料描述',
|
||||
`file_url` VARCHAR(500) NOT NULL COMMENT '文件URL',
|
||||
`file_type` VARCHAR(50) NOT NULL COMMENT '文件类型',
|
||||
`file_size` INT NOT NULL COMMENT '文件大小(字节)',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序顺序',
|
||||
|
||||
-- 基础字段
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 软删除字段
|
||||
`is_deleted` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
`deleted_at` DATETIME,
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
FOREIGN KEY (`course_id`) REFERENCES `courses`(`id`) ON DELETE CASCADE,
|
||||
INDEX `idx_course_materials_course_id` (`course_id`),
|
||||
INDEX `idx_course_materials_is_deleted` (`is_deleted`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='课程资料表';
|
||||
|
||||
-- 3. 知识点表
|
||||
CREATE TABLE IF NOT EXISTS `knowledge_points` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`course_id` INT NOT NULL COMMENT '课程ID',
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '知识点名称',
|
||||
`description` TEXT COMMENT '知识点描述',
|
||||
`parent_id` INT COMMENT '父知识点ID',
|
||||
`level` INT NOT NULL DEFAULT 1 COMMENT '层级深度',
|
||||
`path` VARCHAR(500) COMMENT '路径(如: 1.2.3)',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序顺序',
|
||||
`weight` FLOAT NOT NULL DEFAULT 1.0 COMMENT '权重',
|
||||
`is_required` BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否必修',
|
||||
`estimated_hours` FLOAT COMMENT '预计学习时间(小时)',
|
||||
|
||||
-- 基础字段
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 软删除字段
|
||||
`is_deleted` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
`deleted_at` DATETIME,
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
FOREIGN KEY (`course_id`) REFERENCES `courses`(`id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`parent_id`) REFERENCES `knowledge_points`(`id`) ON DELETE CASCADE,
|
||||
INDEX `idx_knowledge_points_course_id` (`course_id`),
|
||||
INDEX `idx_knowledge_points_parent_id` (`parent_id`),
|
||||
INDEX `idx_knowledge_points_is_deleted` (`is_deleted`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='知识点表';
|
||||
|
||||
-- 4. 成长路径表
|
||||
CREATE TABLE IF NOT EXISTS `growth_paths` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(200) NOT NULL COMMENT '路径名称',
|
||||
`description` TEXT COMMENT '路径描述',
|
||||
`target_role` VARCHAR(100) COMMENT '目标角色',
|
||||
`courses` JSON COMMENT '课程列表[{course_id, order, is_required}]',
|
||||
`estimated_duration_days` INT COMMENT '预计完成天数',
|
||||
`is_active` BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否启用',
|
||||
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序顺序',
|
||||
|
||||
-- 基础字段
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 软删除字段
|
||||
`is_deleted` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
`deleted_at` DATETIME,
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_growth_paths_is_active` (`is_active`),
|
||||
INDEX `idx_growth_paths_is_deleted` (`is_deleted`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='成长路径表';
|
||||
|
||||
-- 添加测试数据
|
||||
INSERT INTO `courses` (`name`, `description`, `category`, `status`, `difficulty_level`, `tags`) VALUES
|
||||
('Python基础入门', 'Python编程语言基础教程,适合零基础学员', 'technology', 'published', 1, '["Python", "编程基础", "入门"]'),
|
||||
('项目管理实战', '敏捷项目管理方法论与实践', 'management', 'published', 3, '["项目管理", "敏捷", "Scrum"]'),
|
||||
('商业分析技巧', '商业数据分析与决策支持', 'business', 'draft', 2, '["数据分析", "商业智能", "Excel"]');
|
||||
|
||||
-- 为Python课程添加知识点
|
||||
INSERT INTO `knowledge_points` (`course_id`, `name`, `description`, `parent_id`, `level`, `path`, `sort_order`, `weight`, `is_required`, `estimated_hours`) VALUES
|
||||
(1, 'Python基础语法', 'Python语言基础语法知识', NULL, 1, '1', 1, 1.0, TRUE, 5.0),
|
||||
(1, '变量与数据类型', '学习Python中的变量定义和基本数据类型', 1, 2, '1.1', 1, 0.8, TRUE, 2.0),
|
||||
(1, '控制流程', '条件语句和循环结构', 1, 2, '1.2', 2, 0.9, TRUE, 3.0);
|
||||
|
||||
-- 添加成长路径
|
||||
INSERT INTO `growth_paths` (`name`, `description`, `target_role`, `courses`, `estimated_duration_days`) VALUES
|
||||
('Python开发工程师', '从零基础到Python开发工程师的成长路径', 'Python开发工程师', '[{"course_id": 1, "order": 1, "is_required": true}]', 90);
|
||||
@@ -0,0 +1,26 @@
|
||||
-- 修改知识点表,将material_id设为必填字段
|
||||
-- 执行日期:2025-09-27
|
||||
-- 说明:将knowledge_points表的material_id字段从可选改为必填
|
||||
|
||||
-- 1. 首先检查是否有material_id为NULL的记录
|
||||
SELECT COUNT(*) as null_count FROM knowledge_points WHERE material_id IS NULL AND is_deleted = FALSE;
|
||||
|
||||
-- 2. 如果有NULL值,需要先处理这些记录(可以删除或设置默认值)
|
||||
-- 这里先删除material_id为NULL的记录(如果有的话)
|
||||
DELETE FROM knowledge_points WHERE material_id IS NULL;
|
||||
|
||||
-- 3. 修改字段为NOT NULL,并修改外键约束
|
||||
ALTER TABLE knowledge_points
|
||||
MODIFY COLUMN material_id INT NOT NULL COMMENT '关联资料ID';
|
||||
|
||||
-- 4. 删除旧的外键约束
|
||||
ALTER TABLE knowledge_points
|
||||
DROP FOREIGN KEY knowledge_points_ibfk_2;
|
||||
|
||||
-- 5. 添加新的外键约束(CASCADE删除)
|
||||
ALTER TABLE knowledge_points
|
||||
ADD CONSTRAINT knowledge_points_ibfk_2
|
||||
FOREIGN KEY (material_id) REFERENCES course_materials(id) ON DELETE CASCADE;
|
||||
|
||||
-- 6. 验证修改结果
|
||||
DESCRIBE knowledge_points;
|
||||
92
backend/migrations/manual_training_tables.sql
Normal file
92
backend/migrations/manual_training_tables.sql
Normal file
@@ -0,0 +1,92 @@
|
||||
-- 手动创建training模块相关表的SQL脚本
|
||||
-- 用于解决Alembic迁移失败的备选方案
|
||||
|
||||
-- 1. 创建training_scenes表
|
||||
CREATE TABLE IF NOT EXISTS training_scenes (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL COMMENT '场景名称',
|
||||
description TEXT COMMENT '场景描述',
|
||||
category VARCHAR(50) NOT NULL COMMENT '场景分类',
|
||||
ai_config JSON COMMENT 'AI配置(如Coze Bot ID等)',
|
||||
prompt_template TEXT COMMENT '提示词模板',
|
||||
evaluation_criteria JSON COMMENT '评估标准',
|
||||
status ENUM('DRAFT', 'ACTIVE', 'INACTIVE') NOT NULL DEFAULT 'DRAFT' COMMENT '场景状态',
|
||||
is_public BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否公开',
|
||||
required_level INTEGER COMMENT '所需用户等级',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
deleted_at DATETIME,
|
||||
created_by INTEGER,
|
||||
updated_by INTEGER,
|
||||
PRIMARY KEY (id),
|
||||
INDEX ix_training_scenes_id (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练场景表';
|
||||
|
||||
-- 2. 创建training_sessions表
|
||||
CREATE TABLE IF NOT EXISTS training_sessions (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
scene_id INTEGER NOT NULL COMMENT '场景ID',
|
||||
coze_conversation_id VARCHAR(100) COMMENT 'Coze会话ID',
|
||||
start_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '开始时间',
|
||||
end_time DATETIME COMMENT '结束时间',
|
||||
duration_seconds INTEGER COMMENT '持续时长(秒)',
|
||||
status ENUM('CREATED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', 'ERROR') NOT NULL DEFAULT 'CREATED' COMMENT '会话状态',
|
||||
session_config JSON COMMENT '会话配置',
|
||||
total_score FLOAT COMMENT '总分',
|
||||
evaluation_result JSON COMMENT '评估结果详情',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_by INTEGER,
|
||||
updated_by INTEGER,
|
||||
PRIMARY KEY (id),
|
||||
INDEX ix_training_sessions_id (id),
|
||||
INDEX ix_training_sessions_user_id (user_id),
|
||||
FOREIGN KEY (scene_id) REFERENCES training_scenes(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练会话表';
|
||||
|
||||
-- 3. 创建training_messages表
|
||||
CREATE TABLE IF NOT EXISTS training_messages (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT,
|
||||
session_id INTEGER NOT NULL COMMENT '会话ID',
|
||||
role ENUM('USER', 'ASSISTANT', 'SYSTEM') NOT NULL COMMENT '消息角色',
|
||||
type ENUM('TEXT', 'VOICE', 'SYSTEM') NOT NULL COMMENT '消息类型',
|
||||
content TEXT NOT NULL COMMENT '消息内容',
|
||||
voice_url VARCHAR(500) COMMENT '语音文件URL',
|
||||
voice_duration FLOAT COMMENT '语音时长(秒)',
|
||||
message_metadata JSON COMMENT '消息元数据',
|
||||
coze_message_id VARCHAR(100) COMMENT 'Coze消息ID',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
INDEX ix_training_messages_id (id),
|
||||
FOREIGN KEY (session_id) REFERENCES training_sessions(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练消息表';
|
||||
|
||||
-- 4. 创建training_reports表
|
||||
CREATE TABLE IF NOT EXISTS training_reports (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT,
|
||||
session_id INTEGER NOT NULL COMMENT '会话ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
overall_score FLOAT NOT NULL COMMENT '总体得分',
|
||||
dimension_scores JSON NOT NULL COMMENT '各维度得分',
|
||||
strengths JSON NOT NULL COMMENT '优势点',
|
||||
weaknesses JSON NOT NULL COMMENT '待改进点',
|
||||
suggestions JSON NOT NULL COMMENT '改进建议',
|
||||
detailed_analysis TEXT COMMENT '详细分析',
|
||||
transcript TEXT COMMENT '对话文本记录',
|
||||
statistics JSON COMMENT '统计数据',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_by INTEGER,
|
||||
updated_by INTEGER,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY (session_id),
|
||||
INDEX ix_training_reports_id (id),
|
||||
INDEX ix_training_reports_user_id (user_id),
|
||||
FOREIGN KEY (session_id) REFERENCES training_sessions(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='陪练报告表';
|
||||
|
||||
-- 5. 更新Alembic版本表,标记迁移已完成
|
||||
INSERT INTO alembic_version (version_num) VALUES ('9245f8845fe1') ON DUPLICATE KEY UPDATE version_num = '9245f8845fe1';
|
||||
26
backend/migrations/script.py.mako
Normal file
26
backend/migrations/script.py.mako
Normal file
@@ -0,0 +1,26 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
13
backend/migrations/update_production_broadcast_fields.sql
Normal file
13
backend/migrations/update_production_broadcast_fields.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- 更新生产环境数据库播课字段
|
||||
-- 日期: 2025-10-14
|
||||
-- 说明: 清理旧的异步状态字段,只保留必要的播课字段
|
||||
|
||||
USE kaopeilian;
|
||||
|
||||
-- 查看当前字段结构
|
||||
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'kaopeilian'
|
||||
AND TABLE_NAME = 'courses'
|
||||
AND COLUMN_NAME LIKE 'broadcast%'
|
||||
ORDER BY COLUMN_NAME;
|
||||
@@ -0,0 +1,25 @@
|
||||
-- 步骤1:检查生产环境数据库播课字段
|
||||
-- 日期: 2025-10-14
|
||||
|
||||
USE kaopeilian;
|
||||
|
||||
-- 查看当前字段结构
|
||||
SELECT '=== 当前播课相关字段 ===' AS info;
|
||||
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'kaopeilian'
|
||||
AND TABLE_NAME = 'courses'
|
||||
AND COLUMN_NAME LIKE 'broadcast%'
|
||||
ORDER BY COLUMN_NAME;
|
||||
|
||||
-- 检查是否有播课数据
|
||||
SELECT '=== 已生成的播课数据 ===' AS info;
|
||||
SELECT COUNT(*) as broadcast_count
|
||||
FROM courses
|
||||
WHERE broadcast_audio_url IS NOT NULL;
|
||||
|
||||
SELECT id, name, broadcast_audio_url, broadcast_generated_at
|
||||
FROM courses
|
||||
WHERE broadcast_audio_url IS NOT NULL
|
||||
LIMIT 5;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
-- 步骤2:更新生产环境数据库播课字段
|
||||
-- 日期: 2025-10-14
|
||||
-- 说明: 清理旧的异步状态字段,只保留必要的播课字段
|
||||
-- 执行前请先运行 step1_check.sql 查看当前字段情况
|
||||
|
||||
USE kaopeilian;
|
||||
|
||||
-- 删除不再需要的字段
|
||||
-- 如果字段不存在会报错,可以忽略错误继续执行
|
||||
|
||||
-- 删除 broadcast_status
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'kaopeilian'
|
||||
AND TABLE_NAME = 'courses'
|
||||
AND COLUMN_NAME = 'broadcast_status') > 0,
|
||||
'ALTER TABLE courses DROP COLUMN broadcast_status',
|
||||
'SELECT "broadcast_status 不存在,跳过" AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 删除 broadcast_task_id
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'kaopeilian'
|
||||
AND TABLE_NAME = 'courses'
|
||||
AND COLUMN_NAME = 'broadcast_task_id') > 0,
|
||||
'ALTER TABLE courses DROP COLUMN broadcast_task_id',
|
||||
'SELECT "broadcast_task_id 不存在,跳过" AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 删除 broadcast_error_message
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'kaopeilian'
|
||||
AND TABLE_NAME = 'courses'
|
||||
AND COLUMN_NAME = 'broadcast_error_message') > 0,
|
||||
'ALTER TABLE courses DROP COLUMN broadcast_error_message',
|
||||
'SELECT "broadcast_error_message 不存在,跳过" AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 添加必要字段(如果不存在)
|
||||
|
||||
-- 添加 broadcast_audio_url
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'kaopeilian'
|
||||
AND TABLE_NAME = 'courses'
|
||||
AND COLUMN_NAME = 'broadcast_audio_url') = 0,
|
||||
'ALTER TABLE courses ADD COLUMN broadcast_audio_url VARCHAR(500) NULL COMMENT ''播课音频URL''',
|
||||
'SELECT "broadcast_audio_url 已存在,跳过" AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 添加 broadcast_generated_at
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'kaopeilian'
|
||||
AND TABLE_NAME = 'courses'
|
||||
AND COLUMN_NAME = 'broadcast_generated_at') = 0,
|
||||
'ALTER TABLE courses ADD COLUMN broadcast_generated_at DATETIME NULL COMMENT ''播课生成时间''',
|
||||
'SELECT "broadcast_generated_at 已存在,跳过" AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 验证最终字段
|
||||
SELECT '=== 更新后的播课字段 ===' AS info;
|
||||
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'kaopeilian'
|
||||
AND TABLE_NAME = 'courses'
|
||||
AND COLUMN_NAME LIKE 'broadcast%'
|
||||
ORDER BY COLUMN_NAME;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""add_position_courses_table
|
||||
|
||||
Revision ID: 0487635b5e95
|
||||
Revises: 5448c81e7afd
|
||||
Create Date: 2025-09-22 08:27:52.507507
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '0487635b5e95'
|
||||
down_revision: Union[str, None] = '5448c81e7afd'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('position_courses',
|
||||
sa.Column('position_id', sa.Integer(), nullable=False, comment='岗位ID'),
|
||||
sa.Column('course_id', sa.Integer(), nullable=False, comment='课程ID'),
|
||||
sa.Column('course_type', sa.String(length=20), nullable=False, comment='课程类型'),
|
||||
sa.Column('priority', sa.Integer(), nullable=True, comment='优先级/排序'),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=False),
|
||||
sa.Column('deleted_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['course_id'], ['courses.id'], ),
|
||||
sa.ForeignKeyConstraint(['position_id'], ['positions.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('position_id', 'course_id', 'is_deleted', name='uix_position_course')
|
||||
)
|
||||
op.create_index(op.f('ix_position_courses_id'), 'position_courses', ['id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_position_courses_id'), table_name='position_courses')
|
||||
op.drop_table('position_courses')
|
||||
# ### end Alembic commands ###
|
||||
157
backend/migrations/versions/20250921_align_schema_to_design.py
Normal file
157
backend/migrations/versions/20250921_align_schema_to_design.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Align database schema to the unified design spec.
|
||||
|
||||
Changes:
|
||||
- users: id INT, set defaults, drop extra columns not in design
|
||||
- user_teams/exams: user_id INT
|
||||
- teams: drop created_by/updated_by
|
||||
- training_*: fix defaults; set created_by/updated_by to BIGINT where required
|
||||
- training_reports: add unique(session_id)
|
||||
- create or replace view v_user_course_progress
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "20250921_align_schema_to_design"
|
||||
down_revision = "9245f8845fe1"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
connection = op.get_bind()
|
||||
|
||||
# users: enforce INT PK, defaults, and drop extra columns
|
||||
op.execute("""
|
||||
ALTER TABLE `users`
|
||||
MODIFY COLUMN `id` INT AUTO_INCREMENT,
|
||||
MODIFY COLUMN `username` VARCHAR(50) NOT NULL,
|
||||
MODIFY COLUMN `email` VARCHAR(100) NOT NULL,
|
||||
MODIFY COLUMN `password_hash` VARCHAR(200) NOT NULL,
|
||||
MODIFY COLUMN `role` VARCHAR(20) DEFAULT 'trainee',
|
||||
MODIFY COLUMN `is_active` TINYINT(1) DEFAULT 1,
|
||||
MODIFY COLUMN `is_verified` TINYINT(1) DEFAULT 0,
|
||||
MODIFY COLUMN `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY COLUMN `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
""")
|
||||
|
||||
# Drop extra columns that are not in the design (ignore errors if absent)
|
||||
for col in [
|
||||
"is_superuser",
|
||||
"department",
|
||||
"position",
|
||||
"last_login",
|
||||
"login_count",
|
||||
"failed_login_count",
|
||||
"locked_until",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"is_deleted",
|
||||
"deleted_at",
|
||||
]:
|
||||
try:
|
||||
op.execute(f"ALTER TABLE `users` DROP COLUMN `{col}`")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# user_teams: user_id INT
|
||||
op.execute("""
|
||||
ALTER TABLE `user_teams`
|
||||
MODIFY COLUMN `user_id` INT NOT NULL
|
||||
""")
|
||||
|
||||
# exams: user_id INT
|
||||
op.execute("""
|
||||
ALTER TABLE `exams`
|
||||
MODIFY COLUMN `user_id` INT NOT NULL
|
||||
""")
|
||||
|
||||
# teams: drop created_by/updated_by to match design
|
||||
for col in ["created_by", "updated_by"]:
|
||||
try:
|
||||
op.execute(f"ALTER TABLE `teams` DROP COLUMN `{col}`")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# training_scenes: set defaults and BIGINT audit fields
|
||||
op.execute("""
|
||||
ALTER TABLE `training_scenes`
|
||||
MODIFY COLUMN `status` ENUM('DRAFT','ACTIVE','INACTIVE') NOT NULL DEFAULT 'DRAFT',
|
||||
MODIFY COLUMN `is_public` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
MODIFY COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY COLUMN `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
MODIFY COLUMN `created_by` BIGINT NULL,
|
||||
MODIFY COLUMN `updated_by` BIGINT NULL
|
||||
""")
|
||||
|
||||
# training_sessions: defaults and BIGINT audit fields
|
||||
op.execute("""
|
||||
ALTER TABLE `training_sessions`
|
||||
MODIFY COLUMN `start_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY COLUMN `status` ENUM('CREATED','IN_PROGRESS','COMPLETED','CANCELLED','ERROR') NOT NULL DEFAULT 'CREATED',
|
||||
MODIFY COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY COLUMN `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
MODIFY COLUMN `created_by` BIGINT NULL,
|
||||
MODIFY COLUMN `updated_by` BIGINT NULL
|
||||
""")
|
||||
|
||||
# training_messages: timestamps defaults
|
||||
op.execute("""
|
||||
ALTER TABLE `training_messages`
|
||||
MODIFY COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY COLUMN `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
""")
|
||||
|
||||
# training_reports: timestamps defaults and BIGINT audit fields
|
||||
op.execute("""
|
||||
ALTER TABLE `training_reports`
|
||||
MODIFY COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
MODIFY COLUMN `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
MODIFY COLUMN `created_by` BIGINT NULL,
|
||||
MODIFY COLUMN `updated_by` BIGINT NULL
|
||||
""")
|
||||
|
||||
# Add unique constraint for training_reports.session_id per design
|
||||
try:
|
||||
op.create_unique_constraint(
|
||||
"uq_training_reports_session_id", "training_reports", ["session_id"]
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Create or replace view v_user_course_progress
|
||||
op.execute("""
|
||||
CREATE OR REPLACE VIEW v_user_course_progress AS
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.username,
|
||||
c.id AS course_id,
|
||||
c.name AS course_name,
|
||||
COUNT(DISTINCT e.id) AS exam_count,
|
||||
AVG(e.score) AS avg_score,
|
||||
MAX(e.score) AS best_score
|
||||
FROM users u
|
||||
CROSS JOIN courses c
|
||||
LEFT JOIN exams e
|
||||
ON e.user_id = u.id
|
||||
AND e.course_id = c.id
|
||||
AND e.status = 'submitted'
|
||||
GROUP BY u.id, c.id
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
# Drop the view to rollback
|
||||
try:
|
||||
op.execute("DROP VIEW IF EXISTS v_user_course_progress")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Note: Full downgrade to previous heterogeneous state is not implemented to avoid data loss.
|
||||
# Keep this as a no-op for column-level reversions.
|
||||
pass
|
||||
|
||||
|
||||
55
backend/migrations/versions/20250922_add_positions_table.py
Normal file
55
backend/migrations/versions/20250922_add_positions_table.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
add positions table
|
||||
|
||||
Revision ID: 20250922_add_positions
|
||||
Revises: 20250921_align_schema_to_design
|
||||
Create Date: 2025-09-22
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "20250922_add_positions"
|
||||
down_revision = "20250921_align_schema_to_design"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
|
||||
if not inspector.has_table("positions"):
|
||||
op.create_table(
|
||||
"positions",
|
||||
sa.Column("id", sa.Integer(), primary_key=True),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("code", sa.String(length=100), nullable=False, unique=True),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("parent_id", sa.Integer(), sa.ForeignKey("positions.id", ondelete="SET NULL"), nullable=True),
|
||||
sa.Column("status", sa.String(length=20), nullable=False, server_default="active"),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("deleted_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("created_by", sa.Integer(), nullable=True),
|
||||
sa.Column("updated_by", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
# 创建索引(若不存在)
|
||||
try:
|
||||
indexes = [ix.get("name") for ix in inspector.get_indexes("positions")] # type: ignore
|
||||
except Exception:
|
||||
indexes = []
|
||||
if "ix_positions_name" not in indexes:
|
||||
op.create_index("ix_positions_name", "positions", ["name"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_positions_name", table_name="positions")
|
||||
op.drop_table("positions")
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""merge_multiple_heads
|
||||
|
||||
Revision ID: 3d5b88fe1875
|
||||
Revises: 20250922_add_positions, add_users_soft_delete
|
||||
Create Date: 2025-09-22 08:09:11.966673
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '3d5b88fe1875'
|
||||
down_revision: Union[str, None] = ('20250922_add_positions', 'add_users_soft_delete')
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -0,0 +1,46 @@
|
||||
"""add_position_members_table
|
||||
|
||||
Revision ID: 5448c81e7afd
|
||||
Revises: 3d5b88fe1875
|
||||
Create Date: 2025-09-22 08:13:54.755269
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '5448c81e7afd'
|
||||
down_revision: Union[str, None] = '3d5b88fe1875'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('position_members',
|
||||
sa.Column('position_id', sa.Integer(), nullable=False, comment='岗位ID'),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False, comment='用户ID'),
|
||||
sa.Column('role', sa.String(length=50), nullable=True, comment='成员角色(预留字段)'),
|
||||
sa.Column('joined_at', sa.DateTime(), nullable=True, comment='加入时间'),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=False),
|
||||
sa.Column('deleted_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['position_id'], ['positions.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('position_id', 'user_id', 'is_deleted', name='uix_position_user')
|
||||
)
|
||||
op.create_index(op.f('ix_position_members_id'), 'position_members', ['id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_position_members_id'), table_name='position_members')
|
||||
op.drop_table('position_members')
|
||||
# ### end Alembic commands ###
|
||||
708
backend/migrations/versions/9245f8845fe1_add_training_models.py
Normal file
708
backend/migrations/versions/9245f8845fe1_add_training_models.py
Normal file
@@ -0,0 +1,708 @@
|
||||
"""add training models
|
||||
|
||||
Revision ID: 9245f8845fe1
|
||||
Revises: 001
|
||||
Create Date: 2025-09-21 22:11:03.319902
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '9245f8845fe1'
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('training_scenes',
|
||||
sa.Column('name', sa.String(length=100), nullable=False, comment='场景名称'),
|
||||
sa.Column('description', sa.Text(), nullable=True, comment='场景描述'),
|
||||
sa.Column('category', sa.String(length=50), nullable=False, comment='场景分类'),
|
||||
sa.Column('ai_config', sa.JSON(), nullable=True, comment='AI配置(如Coze Bot ID等)'),
|
||||
sa.Column('prompt_template', sa.Text(), nullable=True, comment='提示词模板'),
|
||||
sa.Column('evaluation_criteria', sa.JSON(), nullable=True, comment='评估标准'),
|
||||
sa.Column('status', sa.Enum('DRAFT', 'ACTIVE', 'INACTIVE', name='trainingscenestatus'), nullable=False, comment='场景状态'),
|
||||
sa.Column('is_public', sa.Boolean(), nullable=False, comment='是否公开'),
|
||||
sa.Column('required_level', sa.Integer(), nullable=True, comment='所需用户等级'),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=False),
|
||||
sa.Column('deleted_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_by', sa.Integer(), nullable=True),
|
||||
sa.Column('updated_by', sa.Integer(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_training_scenes_id'), 'training_scenes', ['id'], unique=False)
|
||||
op.create_table('training_sessions',
|
||||
sa.Column('user_id', sa.Integer(), nullable=False, comment='用户ID'),
|
||||
sa.Column('scene_id', sa.Integer(), nullable=False, comment='场景ID'),
|
||||
sa.Column('coze_conversation_id', sa.String(length=100), nullable=True, comment='Coze会话ID'),
|
||||
sa.Column('start_time', sa.DateTime(), nullable=False, comment='开始时间'),
|
||||
sa.Column('end_time', sa.DateTime(), nullable=True, comment='结束时间'),
|
||||
sa.Column('duration_seconds', sa.Integer(), nullable=True, comment='持续时长(秒)'),
|
||||
sa.Column('status', sa.Enum('CREATED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', 'ERROR', name='trainingsessionstatus'), nullable=False, comment='会话状态'),
|
||||
sa.Column('session_config', sa.JSON(), nullable=True, comment='会话配置'),
|
||||
sa.Column('total_score', sa.Float(), nullable=True, comment='总分'),
|
||||
sa.Column('evaluation_result', sa.JSON(), nullable=True, comment='评估结果详情'),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('created_by', sa.Integer(), nullable=True),
|
||||
sa.Column('updated_by', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['scene_id'], ['training_scenes.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_training_sessions_id'), 'training_sessions', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_training_sessions_user_id'), 'training_sessions', ['user_id'], unique=False)
|
||||
op.create_table('training_messages',
|
||||
sa.Column('session_id', sa.Integer(), nullable=False, comment='会话ID'),
|
||||
sa.Column('role', sa.Enum('USER', 'ASSISTANT', 'SYSTEM', name='messagerole'), nullable=False, comment='消息角色'),
|
||||
sa.Column('type', sa.Enum('TEXT', 'VOICE', 'SYSTEM', name='messagetype'), nullable=False, comment='消息类型'),
|
||||
sa.Column('content', sa.Text(), nullable=False, comment='消息内容'),
|
||||
sa.Column('voice_url', sa.String(length=500), nullable=True, comment='语音文件URL'),
|
||||
sa.Column('voice_duration', sa.Float(), nullable=True, comment='语音时长(秒)'),
|
||||
sa.Column('message_metadata', sa.JSON(), nullable=True, comment='消息元数据'),
|
||||
sa.Column('coze_message_id', sa.String(length=100), nullable=True, comment='Coze消息ID'),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['session_id'], ['training_sessions.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_training_messages_id'), 'training_messages', ['id'], unique=False)
|
||||
op.create_table('training_reports',
|
||||
sa.Column('session_id', sa.Integer(), nullable=False, comment='会话ID'),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False, comment='用户ID'),
|
||||
sa.Column('overall_score', sa.Float(), nullable=False, comment='总体得分'),
|
||||
sa.Column('dimension_scores', sa.JSON(), nullable=False, comment='各维度得分'),
|
||||
sa.Column('strengths', sa.JSON(), nullable=False, comment='优势点'),
|
||||
sa.Column('weaknesses', sa.JSON(), nullable=False, comment='待改进点'),
|
||||
sa.Column('suggestions', sa.JSON(), nullable=False, comment='改进建议'),
|
||||
sa.Column('detailed_analysis', sa.Text(), nullable=True, comment='详细分析'),
|
||||
sa.Column('transcript', sa.Text(), nullable=True, comment='对话文本记录'),
|
||||
sa.Column('statistics', sa.JSON(), nullable=True, comment='统计数据'),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('created_by', sa.Integer(), nullable=True),
|
||||
sa.Column('updated_by', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['session_id'], ['training_sessions.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('session_id')
|
||||
)
|
||||
op.create_index(op.f('ix_training_reports_id'), 'training_reports', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_training_reports_user_id'), 'training_reports', ['user_id'], unique=False)
|
||||
op.create_table('user_teams',
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('team_id', sa.Integer(), nullable=False),
|
||||
sa.Column('role', sa.String(length=50), nullable=False),
|
||||
sa.Column('joined_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['team_id'], ['teams.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('user_id', 'team_id'),
|
||||
sa.UniqueConstraint('user_id', 'team_id', name='uq_user_team')
|
||||
)
|
||||
op.alter_column('course_materials', 'course_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='课程ID',
|
||||
existing_comment='课程ID',
|
||||
existing_nullable=False)
|
||||
op.alter_column('course_materials', 'name',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=200),
|
||||
comment='资料名称',
|
||||
existing_comment='资料å\x90\x8dç§°',
|
||||
existing_nullable=False)
|
||||
op.alter_column('course_materials', 'description',
|
||||
existing_type=mysql.TEXT(collation='utf8mb4_unicode_ci'),
|
||||
comment='资料描述',
|
||||
existing_comment='资料æ\x8f\x8fè¿°',
|
||||
existing_nullable=True)
|
||||
op.alter_column('course_materials', 'file_url',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=500),
|
||||
comment='文件URL',
|
||||
existing_comment='文件URL',
|
||||
existing_nullable=False)
|
||||
op.alter_column('course_materials', 'file_type',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=50),
|
||||
comment='文件类型',
|
||||
existing_comment='文件类型',
|
||||
existing_nullable=False)
|
||||
op.alter_column('course_materials', 'file_size',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='文件大小(字节)',
|
||||
existing_comment='文件大å°\x8f(å\xad—节)',
|
||||
existing_nullable=False)
|
||||
op.alter_column('course_materials', 'sort_order',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='排序顺序',
|
||||
existing_comment='排åº\x8f顺åº\x8f',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'0'"))
|
||||
op.drop_index('idx_course_materials_course_id', table_name='course_materials')
|
||||
op.drop_index('idx_course_materials_is_deleted', table_name='course_materials')
|
||||
op.create_index(op.f('ix_course_materials_id'), 'course_materials', ['id'], unique=False)
|
||||
op.drop_table_comment(
|
||||
'course_materials',
|
||||
existing_comment='课程资料表',
|
||||
schema=None
|
||||
)
|
||||
op.alter_column('courses', 'name',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=200),
|
||||
comment='课程名称',
|
||||
existing_comment='课程å\x90\x8dç§°',
|
||||
existing_nullable=False)
|
||||
op.alter_column('courses', 'description',
|
||||
existing_type=mysql.TEXT(collation='utf8mb4_unicode_ci'),
|
||||
comment='课程描述',
|
||||
existing_comment='课程æ\x8f\x8fè¿°',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'category',
|
||||
existing_type=mysql.ENUM('technology', 'management', 'business', 'general', collation='utf8mb4_unicode_ci'),
|
||||
comment='课程分类',
|
||||
existing_comment='课程分类',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'general'"))
|
||||
op.alter_column('courses', 'status',
|
||||
existing_type=mysql.ENUM('draft', 'published', 'archived', collation='utf8mb4_unicode_ci'),
|
||||
comment='课程状态',
|
||||
existing_comment='课程状æ€\x81',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'draft'"))
|
||||
op.alter_column('courses', 'cover_image',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=500),
|
||||
comment='封面图片URL',
|
||||
existing_comment='å°\x81é\x9d¢å›¾ç‰‡URL',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'duration_hours',
|
||||
existing_type=mysql.FLOAT(),
|
||||
comment='课程时长(小时)',
|
||||
existing_comment='课程时长(å°\x8fæ—¶)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'difficulty_level',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='难度等级(1-5)',
|
||||
existing_comment='难度ç\xad‰çº§(1-5)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'tags',
|
||||
existing_type=mysql.JSON(),
|
||||
comment='标签列表',
|
||||
existing_comment='æ\xa0‡ç\xad¾åˆ—表',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'published_at',
|
||||
existing_type=mysql.DATETIME(),
|
||||
comment='发布时间',
|
||||
existing_comment='å\x8f‘布时间',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'publisher_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='发布人ID',
|
||||
existing_comment='å\x8f‘布人ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'sort_order',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='排序顺序',
|
||||
existing_comment='排åº\x8f顺åº\x8f',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'0'"))
|
||||
op.alter_column('courses', 'is_featured',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
comment='是否推荐',
|
||||
existing_comment='是å\x90¦æŽ¨è\x8d\x90',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'0'"))
|
||||
op.drop_index('idx_courses_category', table_name='courses')
|
||||
op.drop_index('idx_courses_is_deleted', table_name='courses')
|
||||
op.drop_index('idx_courses_status', table_name='courses')
|
||||
op.create_index(op.f('ix_courses_id'), 'courses', ['id'], unique=False)
|
||||
op.drop_table_comment(
|
||||
'courses',
|
||||
existing_comment='课程表',
|
||||
schema=None
|
||||
)
|
||||
op.alter_column('growth_paths', 'name',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=200),
|
||||
comment='路径名称',
|
||||
existing_comment='路径å\x90\x8dç§°',
|
||||
existing_nullable=False)
|
||||
op.alter_column('growth_paths', 'description',
|
||||
existing_type=mysql.TEXT(collation='utf8mb4_unicode_ci'),
|
||||
comment='路径描述',
|
||||
existing_comment='路径æ\x8f\x8fè¿°',
|
||||
existing_nullable=True)
|
||||
op.alter_column('growth_paths', 'target_role',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=100),
|
||||
comment='目标角色',
|
||||
existing_comment='ç›®æ\xa0‡è§’色',
|
||||
existing_nullable=True)
|
||||
op.alter_column('growth_paths', 'courses',
|
||||
existing_type=mysql.JSON(),
|
||||
comment='课程列表[{course_id, order, is_required}]',
|
||||
existing_comment='课程列表[{course_id, order, is_required}]',
|
||||
existing_nullable=True)
|
||||
op.alter_column('growth_paths', 'estimated_duration_days',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='预计完成天数',
|
||||
existing_comment='预计完æˆ\x90天数',
|
||||
existing_nullable=True)
|
||||
op.alter_column('growth_paths', 'is_active',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
comment='是否启用',
|
||||
existing_comment='是å\x90¦å\x90¯ç”¨',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'1'"))
|
||||
op.alter_column('growth_paths', 'sort_order',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='排序顺序',
|
||||
existing_comment='排åº\x8f顺åº\x8f',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'0'"))
|
||||
op.drop_index('idx_growth_paths_is_active', table_name='growth_paths')
|
||||
op.drop_index('idx_growth_paths_is_deleted', table_name='growth_paths')
|
||||
op.create_index(op.f('ix_growth_paths_id'), 'growth_paths', ['id'], unique=False)
|
||||
op.drop_table_comment(
|
||||
'growth_paths',
|
||||
existing_comment='æˆ\x90长路径表',
|
||||
schema=None
|
||||
)
|
||||
op.alter_column('knowledge_points', 'course_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='课程ID',
|
||||
existing_comment='课程ID',
|
||||
existing_nullable=False)
|
||||
op.alter_column('knowledge_points', 'name',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=200),
|
||||
comment='知识点名称',
|
||||
existing_comment='知识点å\x90\x8dç§°',
|
||||
existing_nullable=False)
|
||||
op.alter_column('knowledge_points', 'description',
|
||||
existing_type=mysql.TEXT(collation='utf8mb4_unicode_ci'),
|
||||
comment='知识点描述',
|
||||
existing_comment='知识点æ\x8f\x8fè¿°',
|
||||
existing_nullable=True)
|
||||
op.alter_column('knowledge_points', 'parent_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='父知识点ID',
|
||||
existing_comment='父知识点ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('knowledge_points', 'level',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='层级深度',
|
||||
existing_comment='层级深度',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'1'"))
|
||||
op.alter_column('knowledge_points', 'path',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=500),
|
||||
comment='路径(如: 1.2.3)',
|
||||
existing_comment='路径(如: 1.2.3)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('knowledge_points', 'sort_order',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='排序顺序',
|
||||
existing_comment='排åº\x8f顺åº\x8f',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'0'"))
|
||||
op.alter_column('knowledge_points', 'weight',
|
||||
existing_type=mysql.FLOAT(),
|
||||
comment='权重',
|
||||
existing_comment='æ\x9dƒé‡\x8d',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'1'"))
|
||||
op.alter_column('knowledge_points', 'is_required',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
comment='是否必修',
|
||||
existing_comment='是å\x90¦å¿…ä¿®',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'1'"))
|
||||
op.alter_column('knowledge_points', 'estimated_hours',
|
||||
existing_type=mysql.FLOAT(),
|
||||
comment='预计学习时间(小时)',
|
||||
existing_comment='预计å\xad¦ä¹\xa0æ—¶é—´(å°\x8fæ—¶)',
|
||||
existing_nullable=True)
|
||||
op.drop_index('idx_knowledge_points_course_id', table_name='knowledge_points')
|
||||
op.drop_index('idx_knowledge_points_is_deleted', table_name='knowledge_points')
|
||||
op.drop_index('idx_knowledge_points_parent_id', table_name='knowledge_points')
|
||||
op.create_index(op.f('ix_knowledge_points_id'), 'knowledge_points', ['id'], unique=False)
|
||||
op.drop_table_comment(
|
||||
'knowledge_points',
|
||||
existing_comment='知识点表',
|
||||
schema=None
|
||||
)
|
||||
op.drop_index('ix_teams_is_deleted', table_name='teams')
|
||||
op.drop_index('ix_teams_parent_id', table_name='teams')
|
||||
op.create_foreign_key(None, 'teams', 'users', ['leader_id'], ['id'], ondelete='SET NULL')
|
||||
op.create_foreign_key(None, 'teams', 'teams', ['parent_id'], ['id'], ondelete='CASCADE')
|
||||
op.drop_column('teams', 'deleted_at')
|
||||
op.drop_column('teams', 'created_by')
|
||||
op.drop_column('teams', 'updated_by')
|
||||
op.drop_column('teams', 'is_deleted')
|
||||
op.add_column('users', sa.Column('bio', sa.Text(), nullable=True))
|
||||
op.add_column('users', sa.Column('is_verified', sa.Boolean(), nullable=False))
|
||||
op.add_column('users', sa.Column('last_login_at', sa.DateTime(), nullable=True))
|
||||
op.add_column('users', sa.Column('password_changed_at', sa.DateTime(), nullable=True))
|
||||
op.alter_column('users', 'username',
|
||||
existing_type=mysql.VARCHAR(length=50),
|
||||
comment=None,
|
||||
existing_comment='用户名',
|
||||
existing_nullable=False)
|
||||
op.alter_column('users', 'email',
|
||||
existing_type=mysql.VARCHAR(length=100),
|
||||
comment=None,
|
||||
existing_comment='邮箱',
|
||||
existing_nullable=False)
|
||||
op.alter_column('users', 'phone',
|
||||
existing_type=mysql.VARCHAR(length=20),
|
||||
comment=None,
|
||||
existing_comment='手机号',
|
||||
existing_nullable=True)
|
||||
op.alter_column('users', 'password_hash',
|
||||
existing_type=mysql.VARCHAR(length=200),
|
||||
comment=None,
|
||||
existing_comment='密码哈希',
|
||||
existing_nullable=False)
|
||||
op.alter_column('users', 'full_name',
|
||||
existing_type=mysql.VARCHAR(length=100),
|
||||
comment=None,
|
||||
existing_comment='全名',
|
||||
existing_nullable=True)
|
||||
op.alter_column('users', 'avatar_url',
|
||||
existing_type=mysql.VARCHAR(length=500),
|
||||
comment=None,
|
||||
existing_comment='头像URL',
|
||||
existing_nullable=True)
|
||||
op.alter_column('users', 'role',
|
||||
existing_type=mysql.VARCHAR(length=20),
|
||||
nullable=False,
|
||||
comment=None,
|
||||
existing_comment='角色: trainee(学员), manager(管理者), admin(管理员)')
|
||||
op.alter_column('users', 'is_active',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
nullable=False,
|
||||
comment=None,
|
||||
existing_comment='是否激活')
|
||||
op.alter_column('users', 'id',
|
||||
existing_type=mysql.BIGINT(),
|
||||
type_=sa.Integer(),
|
||||
existing_nullable=False,
|
||||
autoincrement=True)
|
||||
op.create_unique_constraint(None, 'users', ['phone'])
|
||||
op.drop_column('users', 'position')
|
||||
op.drop_column('users', 'is_superuser')
|
||||
op.drop_column('users', 'locked_until')
|
||||
op.drop_column('users', 'login_count')
|
||||
op.drop_column('users', 'failed_login_count')
|
||||
op.drop_column('users', 'department')
|
||||
op.drop_column('users', 'created_by')
|
||||
op.drop_column('users', 'updated_by')
|
||||
op.drop_column('users', 'last_login')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('users', sa.Column('last_login', mysql.VARCHAR(length=100), nullable=True, comment='最后登录时间'))
|
||||
op.add_column('users', sa.Column('updated_by', mysql.BIGINT(), autoincrement=False, nullable=True))
|
||||
op.add_column('users', sa.Column('created_by', mysql.BIGINT(), autoincrement=False, nullable=True))
|
||||
op.add_column('users', sa.Column('department', mysql.VARCHAR(length=100), nullable=True, comment='部门'))
|
||||
op.add_column('users', sa.Column('failed_login_count', mysql.VARCHAR(length=100), nullable=True, comment='失败登录次数'))
|
||||
op.add_column('users', sa.Column('login_count', mysql.VARCHAR(length=100), nullable=True, comment='登录次数'))
|
||||
op.add_column('users', sa.Column('locked_until', mysql.VARCHAR(length=100), nullable=True, comment='锁定到期时间'))
|
||||
op.add_column('users', sa.Column('is_superuser', mysql.TINYINT(display_width=1), autoincrement=False, nullable=True, comment='是否超级管理员'))
|
||||
op.add_column('users', sa.Column('position', mysql.VARCHAR(length=100), nullable=True, comment='职位'))
|
||||
op.drop_constraint(None, 'users', type_='unique')
|
||||
op.alter_column('users', 'id',
|
||||
existing_type=sa.Integer(),
|
||||
type_=mysql.BIGINT(),
|
||||
existing_nullable=False,
|
||||
autoincrement=True)
|
||||
op.alter_column('users', 'is_active',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
nullable=True,
|
||||
comment='是否激活')
|
||||
op.alter_column('users', 'role',
|
||||
existing_type=mysql.VARCHAR(length=20),
|
||||
nullable=True,
|
||||
comment='角色: trainee(学员), manager(管理者), admin(管理员)')
|
||||
op.alter_column('users', 'avatar_url',
|
||||
existing_type=mysql.VARCHAR(length=500),
|
||||
comment='头像URL',
|
||||
existing_nullable=True)
|
||||
op.alter_column('users', 'full_name',
|
||||
existing_type=mysql.VARCHAR(length=100),
|
||||
comment='全名',
|
||||
existing_nullable=True)
|
||||
op.alter_column('users', 'password_hash',
|
||||
existing_type=mysql.VARCHAR(length=200),
|
||||
comment='密码哈希',
|
||||
existing_nullable=False)
|
||||
op.alter_column('users', 'phone',
|
||||
existing_type=mysql.VARCHAR(length=20),
|
||||
comment='手机号',
|
||||
existing_nullable=True)
|
||||
op.alter_column('users', 'email',
|
||||
existing_type=mysql.VARCHAR(length=100),
|
||||
comment='邮箱',
|
||||
existing_nullable=False)
|
||||
op.alter_column('users', 'username',
|
||||
existing_type=mysql.VARCHAR(length=50),
|
||||
comment='用户名',
|
||||
existing_nullable=False)
|
||||
op.drop_column('users', 'password_changed_at')
|
||||
op.drop_column('users', 'last_login_at')
|
||||
op.drop_column('users', 'is_verified')
|
||||
op.drop_column('users', 'bio')
|
||||
op.add_column('teams', sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text("'0'"), autoincrement=False, nullable=False))
|
||||
op.add_column('teams', sa.Column('updated_by', mysql.INTEGER(), autoincrement=False, nullable=True))
|
||||
op.add_column('teams', sa.Column('created_by', mysql.INTEGER(), autoincrement=False, nullable=True))
|
||||
op.add_column('teams', sa.Column('deleted_at', mysql.DATETIME(), nullable=True))
|
||||
op.drop_constraint(None, 'teams', type_='foreignkey')
|
||||
op.drop_constraint(None, 'teams', type_='foreignkey')
|
||||
op.create_index('ix_teams_parent_id', 'teams', ['parent_id'], unique=False)
|
||||
op.create_index('ix_teams_is_deleted', 'teams', ['is_deleted'], unique=False)
|
||||
op.create_table_comment(
|
||||
'knowledge_points',
|
||||
'知识点表',
|
||||
existing_comment=None,
|
||||
schema=None
|
||||
)
|
||||
op.drop_index(op.f('ix_knowledge_points_id'), table_name='knowledge_points')
|
||||
op.create_index('idx_knowledge_points_parent_id', 'knowledge_points', ['parent_id'], unique=False)
|
||||
op.create_index('idx_knowledge_points_is_deleted', 'knowledge_points', ['is_deleted'], unique=False)
|
||||
op.create_index('idx_knowledge_points_course_id', 'knowledge_points', ['course_id'], unique=False)
|
||||
op.alter_column('knowledge_points', 'estimated_hours',
|
||||
existing_type=mysql.FLOAT(),
|
||||
comment='预计å\xad¦ä¹\xa0æ—¶é—´(å°\x8fæ—¶)',
|
||||
existing_comment='预计学习时间(小时)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('knowledge_points', 'is_required',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
comment='是å\x90¦å¿…ä¿®',
|
||||
existing_comment='是否必修',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'1'"))
|
||||
op.alter_column('knowledge_points', 'weight',
|
||||
existing_type=mysql.FLOAT(),
|
||||
comment='æ\x9dƒé‡\x8d',
|
||||
existing_comment='权重',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'1'"))
|
||||
op.alter_column('knowledge_points', 'sort_order',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='排åº\x8f顺åº\x8f',
|
||||
existing_comment='排序顺序',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'0'"))
|
||||
op.alter_column('knowledge_points', 'path',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=500),
|
||||
comment='路径(如: 1.2.3)',
|
||||
existing_comment='路径(如: 1.2.3)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('knowledge_points', 'level',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='层级深度',
|
||||
existing_comment='层级深度',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'1'"))
|
||||
op.alter_column('knowledge_points', 'parent_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='父知识点ID',
|
||||
existing_comment='父知识点ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('knowledge_points', 'description',
|
||||
existing_type=mysql.TEXT(collation='utf8mb4_unicode_ci'),
|
||||
comment='知识点æ\x8f\x8fè¿°',
|
||||
existing_comment='知识点描述',
|
||||
existing_nullable=True)
|
||||
op.alter_column('knowledge_points', 'name',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=200),
|
||||
comment='知识点å\x90\x8dç§°',
|
||||
existing_comment='知识点名称',
|
||||
existing_nullable=False)
|
||||
op.alter_column('knowledge_points', 'course_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='课程ID',
|
||||
existing_comment='课程ID',
|
||||
existing_nullable=False)
|
||||
op.create_table_comment(
|
||||
'growth_paths',
|
||||
'æˆ\x90长路径表',
|
||||
existing_comment=None,
|
||||
schema=None
|
||||
)
|
||||
op.drop_index(op.f('ix_growth_paths_id'), table_name='growth_paths')
|
||||
op.create_index('idx_growth_paths_is_deleted', 'growth_paths', ['is_deleted'], unique=False)
|
||||
op.create_index('idx_growth_paths_is_active', 'growth_paths', ['is_active'], unique=False)
|
||||
op.alter_column('growth_paths', 'sort_order',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='排åº\x8f顺åº\x8f',
|
||||
existing_comment='排序顺序',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'0'"))
|
||||
op.alter_column('growth_paths', 'is_active',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
comment='是å\x90¦å\x90¯ç”¨',
|
||||
existing_comment='是否启用',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'1'"))
|
||||
op.alter_column('growth_paths', 'estimated_duration_days',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='预计完æˆ\x90天数',
|
||||
existing_comment='预计完成天数',
|
||||
existing_nullable=True)
|
||||
op.alter_column('growth_paths', 'courses',
|
||||
existing_type=mysql.JSON(),
|
||||
comment='课程列表[{course_id, order, is_required}]',
|
||||
existing_comment='课程列表[{course_id, order, is_required}]',
|
||||
existing_nullable=True)
|
||||
op.alter_column('growth_paths', 'target_role',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=100),
|
||||
comment='ç›®æ\xa0‡è§’色',
|
||||
existing_comment='目标角色',
|
||||
existing_nullable=True)
|
||||
op.alter_column('growth_paths', 'description',
|
||||
existing_type=mysql.TEXT(collation='utf8mb4_unicode_ci'),
|
||||
comment='路径æ\x8f\x8fè¿°',
|
||||
existing_comment='路径描述',
|
||||
existing_nullable=True)
|
||||
op.alter_column('growth_paths', 'name',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=200),
|
||||
comment='路径å\x90\x8dç§°',
|
||||
existing_comment='路径名称',
|
||||
existing_nullable=False)
|
||||
op.create_table_comment(
|
||||
'courses',
|
||||
'课程表',
|
||||
existing_comment=None,
|
||||
schema=None
|
||||
)
|
||||
op.drop_index(op.f('ix_courses_id'), table_name='courses')
|
||||
op.create_index('idx_courses_status', 'courses', ['status'], unique=False)
|
||||
op.create_index('idx_courses_is_deleted', 'courses', ['is_deleted'], unique=False)
|
||||
op.create_index('idx_courses_category', 'courses', ['category'], unique=False)
|
||||
op.alter_column('courses', 'is_featured',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
comment='是å\x90¦æŽ¨è\x8d\x90',
|
||||
existing_comment='是否推荐',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'0'"))
|
||||
op.alter_column('courses', 'sort_order',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='排åº\x8f顺åº\x8f',
|
||||
existing_comment='排序顺序',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'0'"))
|
||||
op.alter_column('courses', 'publisher_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='å\x8f‘布人ID',
|
||||
existing_comment='发布人ID',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'published_at',
|
||||
existing_type=mysql.DATETIME(),
|
||||
comment='å\x8f‘布时间',
|
||||
existing_comment='发布时间',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'tags',
|
||||
existing_type=mysql.JSON(),
|
||||
comment='æ\xa0‡ç\xad¾åˆ—表',
|
||||
existing_comment='标签列表',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'difficulty_level',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='难度ç\xad‰çº§(1-5)',
|
||||
existing_comment='难度等级(1-5)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'duration_hours',
|
||||
existing_type=mysql.FLOAT(),
|
||||
comment='课程时长(å°\x8fæ—¶)',
|
||||
existing_comment='课程时长(小时)',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'cover_image',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=500),
|
||||
comment='å°\x81é\x9d¢å›¾ç‰‡URL',
|
||||
existing_comment='封面图片URL',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'status',
|
||||
existing_type=mysql.ENUM('draft', 'published', 'archived', collation='utf8mb4_unicode_ci'),
|
||||
comment='课程状æ€\x81',
|
||||
existing_comment='课程状态',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'draft'"))
|
||||
op.alter_column('courses', 'category',
|
||||
existing_type=mysql.ENUM('technology', 'management', 'business', 'general', collation='utf8mb4_unicode_ci'),
|
||||
comment='课程分类',
|
||||
existing_comment='课程分类',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'general'"))
|
||||
op.alter_column('courses', 'description',
|
||||
existing_type=mysql.TEXT(collation='utf8mb4_unicode_ci'),
|
||||
comment='课程æ\x8f\x8fè¿°',
|
||||
existing_comment='课程描述',
|
||||
existing_nullable=True)
|
||||
op.alter_column('courses', 'name',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=200),
|
||||
comment='课程å\x90\x8dç§°',
|
||||
existing_comment='课程名称',
|
||||
existing_nullable=False)
|
||||
op.create_table_comment(
|
||||
'course_materials',
|
||||
'课程资料表',
|
||||
existing_comment=None,
|
||||
schema=None
|
||||
)
|
||||
op.drop_index(op.f('ix_course_materials_id'), table_name='course_materials')
|
||||
op.create_index('idx_course_materials_is_deleted', 'course_materials', ['is_deleted'], unique=False)
|
||||
op.create_index('idx_course_materials_course_id', 'course_materials', ['course_id'], unique=False)
|
||||
op.alter_column('course_materials', 'sort_order',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='排åº\x8f顺åº\x8f',
|
||||
existing_comment='排序顺序',
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("'0'"))
|
||||
op.alter_column('course_materials', 'file_size',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='文件大å°\x8f(å\xad—节)',
|
||||
existing_comment='文件大小(字节)',
|
||||
existing_nullable=False)
|
||||
op.alter_column('course_materials', 'file_type',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=50),
|
||||
comment='文件类型',
|
||||
existing_comment='文件类型',
|
||||
existing_nullable=False)
|
||||
op.alter_column('course_materials', 'file_url',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=500),
|
||||
comment='文件URL',
|
||||
existing_comment='文件URL',
|
||||
existing_nullable=False)
|
||||
op.alter_column('course_materials', 'description',
|
||||
existing_type=mysql.TEXT(collation='utf8mb4_unicode_ci'),
|
||||
comment='资料æ\x8f\x8fè¿°',
|
||||
existing_comment='资料描述',
|
||||
existing_nullable=True)
|
||||
op.alter_column('course_materials', 'name',
|
||||
existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=200),
|
||||
comment='资料å\x90\x8dç§°',
|
||||
existing_comment='资料名称',
|
||||
existing_nullable=False)
|
||||
op.alter_column('course_materials', 'course_id',
|
||||
existing_type=mysql.INTEGER(),
|
||||
comment='课程ID',
|
||||
existing_comment='课程ID',
|
||||
existing_nullable=False)
|
||||
op.drop_table('user_teams')
|
||||
op.drop_index(op.f('ix_training_reports_user_id'), table_name='training_reports')
|
||||
op.drop_index(op.f('ix_training_reports_id'), table_name='training_reports')
|
||||
op.drop_table('training_reports')
|
||||
op.drop_index(op.f('ix_training_messages_id'), table_name='training_messages')
|
||||
op.drop_table('training_messages')
|
||||
op.drop_index(op.f('ix_training_sessions_user_id'), table_name='training_sessions')
|
||||
op.drop_index(op.f('ix_training_sessions_id'), table_name='training_sessions')
|
||||
op.drop_table('training_sessions')
|
||||
op.drop_index(op.f('ix_training_scenes_id'), table_name='training_scenes')
|
||||
op.drop_table('training_scenes')
|
||||
# ### end Alembic commands ###
|
||||
35
backend/migrations/versions/add_position_skills_level.py
Normal file
35
backend/migrations/versions/add_position_skills_level.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Add skills and level fields to positions table
|
||||
|
||||
Revision ID: add_position_skills_level
|
||||
Revises: 0487635b5e95
|
||||
Create Date: 2025-09-22 09:00:00
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'add_position_skills_level'
|
||||
down_revision = '0487635b5e95'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
"""添加skills和level字段到positions表"""
|
||||
# 添加skills字段(JSON类型存储技能数组)
|
||||
op.add_column('positions', sa.Column('skills', sa.JSON, nullable=True, comment='核心技能'))
|
||||
|
||||
# 添加level字段(岗位等级)
|
||||
op.add_column('positions', sa.Column('level', sa.String(20), nullable=True, comment='岗位等级: junior/intermediate/senior/expert'))
|
||||
|
||||
# 添加sort_order字段(排序)
|
||||
op.add_column('positions', sa.Column('sort_order', sa.Integer, nullable=True, default=0, comment='排序'))
|
||||
|
||||
|
||||
def downgrade():
|
||||
"""移除添加的字段"""
|
||||
op.drop_column('positions', 'skills')
|
||||
op.drop_column('positions', 'level')
|
||||
op.drop_column('positions', 'sort_order')
|
||||
36
backend/migrations/versions/add_users_soft_delete.py
Normal file
36
backend/migrations/versions/add_users_soft_delete.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""add users soft delete columns
|
||||
|
||||
Revision ID: add_users_soft_delete
|
||||
Revises: 20250921_align_schema_to_design
|
||||
Create Date: 2025-09-22 03:00:00
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'add_users_soft_delete'
|
||||
down_revision: Union[str, None] = '20250921_align_schema_to_design'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 为 users 表添加软删除字段
|
||||
op.add_column('users', sa.Column('is_deleted', sa.Boolean(), nullable=False, server_default='0'))
|
||||
op.add_column('users', sa.Column('deleted_at', sa.DateTime(), nullable=True))
|
||||
|
||||
# 添加索引
|
||||
op.create_index('idx_users_is_deleted', 'users', ['is_deleted'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# 删除索引
|
||||
op.drop_index('idx_users_is_deleted', table_name='users')
|
||||
|
||||
# 删除字段
|
||||
op.drop_column('users', 'deleted_at')
|
||||
op.drop_column('users', 'is_deleted')
|
||||
Reference in New Issue
Block a user