feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
@@ -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