chore: initial baseline with P0-safety .gitignore
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# =============================================================================
|
||||
# Alembic migration environment
|
||||
# Handles both sync (alembic CLI) and async (app runtime) database URLs
|
||||
# =============================================================================
|
||||
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
from app.config import settings
|
||||
from app.database import Base
|
||||
import app.models # noqa: F401
|
||||
|
||||
config = context.config
|
||||
|
||||
# Convert async URL to sync for alembic CLI operations
|
||||
# aiosqlite -> sqlite, asyncpg -> psycopg2
|
||||
db_url = settings.database_url
|
||||
db_url = db_url.replace("+aiosqlite", "").replace("+asyncpg", "")
|
||||
config.set_main_option("sqlalchemy.url", db_url)
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Generate SQL scripts without connecting to the database."""
|
||||
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 run_migrations_online() -> None:
|
||||
"""Connect to the database and run migrations."""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -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"}
|
||||
@@ -0,0 +1,47 @@
|
||||
"""add media fields to messages table
|
||||
|
||||
为消息表添加媒体文件相关字段,支持图片、语音、文件等非文本消息。
|
||||
|
||||
新增字段:
|
||||
- media_id: 企微媒体文件ID(3天有效)
|
||||
- media_url: 本地存储的媒体文件URL
|
||||
- file_name: 文件名
|
||||
- file_size: 文件大小(字节)
|
||||
- extra_data: 扩展元数据(JSON)
|
||||
|
||||
Revision ID: 002_media_fields
|
||||
Revises: 6d5520491644
|
||||
Create Date: 2026-06-03 17:30:00.000000
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '002_media_fields'
|
||||
down_revision = '6d5520491644'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""添加媒体文件相关字段到 messages 表。"""
|
||||
# 企微媒体文件ID(图片/语音/视频消息携带,3天有效)
|
||||
op.add_column('messages', sa.Column('media_id', sa.String(256), nullable=True, comment='企微媒体文件ID(3天有效)'))
|
||||
# 本地存储的媒体文件URL(下载后保存到服务器/NAS的访问路径)
|
||||
op.add_column('messages', sa.Column('media_url', sa.String(512), nullable=True, comment='本地存储的媒体文件URL'))
|
||||
# 文件名(文件消息携带)
|
||||
op.add_column('messages', sa.Column('file_name', sa.String(256), nullable=True, comment='文件名'))
|
||||
# 文件大小(字节)
|
||||
op.add_column('messages', sa.Column('file_size', sa.Integer(), nullable=True, comment='文件大小(字节)'))
|
||||
# 扩展元数据(JSON格式,存储各消息类型的额外信息)
|
||||
op.add_column('messages', sa.Column('extra_data', sa.JSON(), nullable=True, comment='扩展元数据(JSON)'))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""移除媒体文件相关字段。"""
|
||||
op.drop_column('messages', 'extra_data')
|
||||
op.drop_column('messages', 'file_size')
|
||||
op.drop_column('messages', 'file_name')
|
||||
op.drop_column('messages', 'media_url')
|
||||
op.drop_column('messages', 'media_id')
|
||||
@@ -0,0 +1,40 @@
|
||||
"""add suggestion_action field to messages table
|
||||
|
||||
为消息表添加 suggestion_action 字段,用于追踪坐席对 AI 建议的操作行为。
|
||||
取值范围:accepted(采纳)/ edited(编辑后采纳)/ ignored(忽略)
|
||||
|
||||
新增字段:
|
||||
- suggestion_action: VARCHAR(20), nullable, 坐席对AI建议的操作行为
|
||||
|
||||
Revision ID: 003_suggestion_action
|
||||
Revises: 002_media_fields
|
||||
Create Date: 2026-07-14 10:00:00.000000
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '003_suggestion_action'
|
||||
down_revision = '002_media_fields'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""添加 suggestion_action 字段到 messages 表。"""
|
||||
# 坐席对 AI 建议的操作行为(accepted/edited/ignored)
|
||||
op.add_column(
|
||||
'messages',
|
||||
sa.Column(
|
||||
'suggestion_action',
|
||||
sa.String(20),
|
||||
nullable=True,
|
||||
comment='AI建议操作行为: accepted/edited/ignored',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""移除 suggestion_action 字段。"""
|
||||
op.drop_column('messages', 'suggestion_action')
|
||||
@@ -0,0 +1,58 @@
|
||||
"""add participants field to conversations table
|
||||
|
||||
为会话表添加 participants JSON 字段,支持邀请功能(P0-09~P0-11)。
|
||||
与 collaborating_agent_ids(摇人 = 坐席间协作)独立,
|
||||
participants 存储被邀请的员工/部门列表。
|
||||
|
||||
新增字段:
|
||||
- participants: JSON, 非空, 默认空列表, 被邀请参与会话的人员列表
|
||||
|
||||
数据格式:
|
||||
[
|
||||
{
|
||||
"id": "employee_user_id",
|
||||
"name": "员工姓名",
|
||||
"department": "部门名称",
|
||||
"type": "employee", # employee 或 department
|
||||
"joined": false, # 是否已加入会话
|
||||
"joined_at": null, # 加入时间
|
||||
"invited_by": "agent_id" # 邀请人坐席ID
|
||||
}
|
||||
]
|
||||
|
||||
Revision ID: 004_participants
|
||||
Revises: 003_suggestion_action
|
||||
Create Date: 2026-07-14 14:00:00.000000
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '004_participants'
|
||||
down_revision = '003_suggestion_action'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""添加 participants 字段到 conversations 表。"""
|
||||
# 被邀请参与会话的人员列表(JSON 数组)
|
||||
# 与 collaborating_agent_ids 区别:
|
||||
# collaborating_agent_ids = 坐席→坐席协作(摇人)
|
||||
# participants = 坐席→员工/部门(邀请)
|
||||
op.add_column(
|
||||
'conversations',
|
||||
sa.Column(
|
||||
'participants',
|
||||
sa.JSON,
|
||||
nullable=False,
|
||||
server_default='[]', # 默认空数组
|
||||
comment='被邀请参与会话的人员列表(邀请功能)',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""移除 participants 字段。"""
|
||||
op.drop_column('conversations', 'participants')
|
||||
@@ -0,0 +1,43 @@
|
||||
"""add reply_to_id field to messages table
|
||||
|
||||
为消息表添加 reply_to_id 字段,支持消息引用回复功能(M1)。
|
||||
当消息是对某条消息的回复时,此字段指向被回复的消息ID。
|
||||
|
||||
新增字段:
|
||||
- reply_to_id: VARCHAR(36), nullable, 被回复的消息ID
|
||||
|
||||
前端展示逻辑:
|
||||
- reply_to_id 非空时,在消息气泡上方显示被回复消息的摘要
|
||||
- 点击摘要可滚动到被回复的消息位置
|
||||
|
||||
Revision ID: 005_reply_to_id
|
||||
Revises: 004_participants
|
||||
Create Date: 2026-07-14 16:00:00.000000
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '005_reply_to_id'
|
||||
down_revision = '004_participants'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""添加 reply_to_id 字段到 messages 表。"""
|
||||
op.add_column(
|
||||
'messages',
|
||||
sa.Column(
|
||||
'reply_to_id',
|
||||
sa.String(36),
|
||||
nullable=True,
|
||||
comment='引用回复:被回复的消息ID',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""移除 reply_to_id 字段。"""
|
||||
op.drop_column('messages', 'reply_to_id')
|
||||
@@ -0,0 +1,116 @@
|
||||
"""admin extension — 管理后台数据库扩展迁移
|
||||
|
||||
新增 config_change_logs 表(配置变更日志)。
|
||||
扩展 agents 表:新增 role(角色)和 skill_tags(技能标签)字段。
|
||||
扩展 quick_reply_templates 表:新增 status(审核状态)、version(版本号)、
|
||||
submitted_by(提交人)字段。
|
||||
|
||||
Revision ID: 006_admin_ext
|
||||
Revises: 005_reply_to_id
|
||||
Create Date: 2026-07-15 10:00:00.000000
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '006_admin_ext'
|
||||
down_revision = '005_reply_to_id'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""执行管理后台数据库扩展迁移。"""
|
||||
|
||||
# 1. 创建 config_change_logs 表
|
||||
op.create_table(
|
||||
'config_change_logs',
|
||||
sa.Column('id', sa.String(36), primary_key=True),
|
||||
sa.Column('config_key', sa.String(128), nullable=False, comment='配置键'),
|
||||
sa.Column('old_value', sa.Text, nullable=False, server_default='', comment='变更前的值'),
|
||||
sa.Column('new_value', sa.Text, nullable=False, server_default='', comment='变更后的值'),
|
||||
sa.Column('changed_by', sa.String(36), nullable=False, comment='变更操作人 agent_id'),
|
||||
sa.Column('changed_at', sa.DateTime(timezone=True), nullable=False,
|
||||
server_default=sa.func.now(), comment='变更时间'),
|
||||
)
|
||||
# 创建索引
|
||||
op.create_index('idx_ccl_config_key', 'config_change_logs', ['config_key'])
|
||||
op.create_index('idx_ccl_changed_at', 'config_change_logs', ['changed_at'])
|
||||
|
||||
# 2. 给 agents 表新增 role 字段
|
||||
op.add_column(
|
||||
'agents',
|
||||
sa.Column(
|
||||
'role',
|
||||
sa.String(20),
|
||||
nullable=False,
|
||||
server_default='agent',
|
||||
comment='角色:admin=组长, agent=坐席',
|
||||
)
|
||||
)
|
||||
|
||||
# 3. 给 agents 表新增 skill_tags 字段
|
||||
op.add_column(
|
||||
'agents',
|
||||
sa.Column(
|
||||
'skill_tags',
|
||||
sa.JSON,
|
||||
nullable=False,
|
||||
server_default='[]',
|
||||
comment='技能标签列表(电脑/软件/外设/网络/安全/资产/其他)',
|
||||
)
|
||||
)
|
||||
|
||||
# 4. 给 quick_reply_templates 表新增 status 字段
|
||||
op.add_column(
|
||||
'quick_reply_templates',
|
||||
sa.Column(
|
||||
'status',
|
||||
sa.String(20),
|
||||
nullable=False,
|
||||
server_default='approved',
|
||||
comment='状态:draft/pending_review/approved/rejected',
|
||||
)
|
||||
)
|
||||
|
||||
# 5. 给 quick_reply_templates 表新增 version 字段
|
||||
op.add_column(
|
||||
'quick_reply_templates',
|
||||
sa.Column(
|
||||
'version',
|
||||
sa.Integer(),
|
||||
nullable=False,
|
||||
server_default='1',
|
||||
comment='版本号,每次审核通过后 +1',
|
||||
)
|
||||
)
|
||||
|
||||
# 6. 给 quick_reply_templates 表新增 submitted_by 字段
|
||||
op.add_column(
|
||||
'quick_reply_templates',
|
||||
sa.Column(
|
||||
'submitted_by',
|
||||
sa.String(36),
|
||||
nullable=True,
|
||||
comment='提交人 agent_id',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""回滚管理后台数据库扩展迁移。"""
|
||||
|
||||
# 删除 quick_reply_templates 新增字段
|
||||
op.drop_column('quick_reply_templates', 'submitted_by')
|
||||
op.drop_column('quick_reply_templates', 'version')
|
||||
op.drop_column('quick_reply_templates', 'status')
|
||||
|
||||
# 删除 agents 新增字段
|
||||
op.drop_column('agents', 'skill_tags')
|
||||
op.drop_column('agents', 'role')
|
||||
|
||||
# 删除 config_change_logs 表索引和表
|
||||
op.drop_index('idx_ccl_changed_at', table_name='config_change_logs')
|
||||
op.drop_index('idx_ccl_config_key', table_name='config_change_logs')
|
||||
op.drop_table('config_change_logs')
|
||||
@@ -0,0 +1,104 @@
|
||||
"""role system — 统一入口角色系统迁移
|
||||
|
||||
新增 roles 表(角色定义)。
|
||||
新增 user_roles 表(用户角色关联)。
|
||||
新增 role_mapping_rules 表(角色映射规则)。
|
||||
预置三个基础角色:user、agent、admin。
|
||||
|
||||
Revision ID: 007_role_sys
|
||||
Revises: 006_admin_ext
|
||||
Create Date: 2026-06-12 23:00:00.000000
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '007_role_sys'
|
||||
down_revision = '006_admin_ext'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""执行角色系统迁移。"""
|
||||
|
||||
# 1. 创建 roles 表
|
||||
op.create_table(
|
||||
'roles',
|
||||
sa.Column('id', sa.String(36), primary_key=True),
|
||||
sa.Column('name', sa.String(50), unique=True, nullable=False, comment='角色标识:user/agent/admin'),
|
||||
sa.Column('display_name', sa.String(100), nullable=False, comment='显示名称:用户/坐席/管理员'),
|
||||
sa.Column('description', sa.Text, nullable=True, comment='角色描述'),
|
||||
sa.Column('permissions', sa.JSON, nullable=False, server_default='[]', comment='权限列表'),
|
||||
sa.Column('is_default', sa.Boolean, nullable=False, server_default='0', comment='是否默认角色'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), comment='更新时间'),
|
||||
)
|
||||
|
||||
# 2. 创建 user_roles 表
|
||||
op.create_table(
|
||||
'user_roles',
|
||||
sa.Column('id', sa.String(36), primary_key=True),
|
||||
sa.Column('employee_id', sa.String(100), nullable=False, comment='企微 UserID'),
|
||||
sa.Column('role_id', sa.String(36), sa.ForeignKey('roles.id', ondelete='CASCADE'), nullable=False, comment='角色 ID'),
|
||||
sa.Column('source', sa.String(50), nullable=False, comment='角色来源:auto/tag/ehr/manual'),
|
||||
sa.Column('assigned_by', sa.String(100), nullable=True, comment='分配者'),
|
||||
sa.Column('assigned_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), comment='分配时间'),
|
||||
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True, comment='过期时间'),
|
||||
sa.UniqueConstraint('employee_id', 'role_id', name='uq_user_role'),
|
||||
)
|
||||
# 创建索引
|
||||
op.create_index('idx_user_roles_employee_id', 'user_roles', ['employee_id'])
|
||||
op.create_index('idx_user_roles_role_id', 'user_roles', ['role_id'])
|
||||
|
||||
# 3. 创建 role_mapping_rules 表
|
||||
op.create_table(
|
||||
'role_mapping_rules',
|
||||
sa.Column('id', sa.String(36), primary_key=True),
|
||||
sa.Column('role_id', sa.String(36), sa.ForeignKey('roles.id', ondelete='CASCADE'), nullable=False, comment='目标角色 ID'),
|
||||
sa.Column('source_type', sa.String(50), nullable=False, comment='来源类型:wecom_tag/ehr_position'),
|
||||
sa.Column('source_value', sa.String(200), nullable=False, comment='来源值:标签名/岗位关键词'),
|
||||
sa.Column('priority', sa.Integer(), nullable=False, server_default='0', comment='优先级'),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1', comment='是否启用'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), comment='创建时间'),
|
||||
)
|
||||
# 创建索引
|
||||
op.create_index('idx_role_mapping_rules_role_id', 'role_mapping_rules', ['role_id'])
|
||||
op.create_index('idx_role_mapping_rules_source_type', 'role_mapping_rules', ['source_type'])
|
||||
|
||||
# 4. 预置三个基础角色
|
||||
# 注意:使用 op.execute 直接插入数据,因为 server_default 不适用于 Python 端生成的 UUID
|
||||
# PostgreSQL 使用 NOW() 替代 SQLite 的 datetime('now')
|
||||
op.execute("""
|
||||
INSERT INTO roles (id, name, display_name, description, permissions, is_default, created_at, updated_at) VALUES
|
||||
('role_user_001', 'user', '用户', '所有在职员工默认角色,可提交工单、查看进度、浏览知识库', '["ticket.create", "ticket.view", "knowledge.view"]', TRUE, NOW(), NOW()),
|
||||
('role_agent_001', 'agent', '坐席', 'IT支持人员,可处理会话、使用AI辅助、管理工单', '["conversation.manage", "ticket.assign", "knowledge.edit", "ai.wingman"]', FALSE, NOW(), NOW()),
|
||||
('role_admin_001', 'admin', '管理员', '系统管理员,可配置系统、管理权限、查看数据分析', '["system.config", "user.manage", "role.manage", "analytics.view"]', FALSE, NOW(), NOW())
|
||||
""")
|
||||
|
||||
# 5. 预置默认映射规则(企微标签 → agent 角色)
|
||||
op.execute("""
|
||||
INSERT INTO role_mapping_rules (id, role_id, source_type, source_value, priority, is_active, created_at) VALUES
|
||||
('rule_agent_tag_001', 'role_agent_001', 'wecom_tag', 'IT坐席', 10, TRUE, NOW()),
|
||||
('rule_agent_ehr_001', 'role_agent_001', 'ehr_position', 'IT支持', 10, TRUE, NOW()),
|
||||
('rule_agent_ehr_002', 'role_agent_001', 'ehr_position', 'IT运维', 10, TRUE, NOW()),
|
||||
('rule_agent_ehr_003', 'role_agent_001', 'ehr_position', '技术支持', 10, TRUE, NOW())
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""回滚角色系统迁移。"""
|
||||
|
||||
# 删除 role_mapping_rules 表索引和表
|
||||
op.drop_index('idx_role_mapping_rules_source_type', table_name='role_mapping_rules')
|
||||
op.drop_index('idx_role_mapping_rules_role_id', table_name='role_mapping_rules')
|
||||
op.drop_table('role_mapping_rules')
|
||||
|
||||
# 删除 user_roles 表索引和表
|
||||
op.drop_index('idx_user_roles_role_id', table_name='user_roles')
|
||||
op.drop_index('idx_user_roles_employee_id', table_name='user_roles')
|
||||
op.drop_table('user_roles')
|
||||
|
||||
# 删除 roles 表
|
||||
op.drop_table('roles')
|
||||
@@ -0,0 +1,199 @@
|
||||
"""initial_all_tables
|
||||
|
||||
Revision ID: 6d5520491644
|
||||
Revises:
|
||||
Create Date: 2026-06-03 17:28:43.238581
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '6d5520491644'
|
||||
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('agents',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('user_id', sa.String(length=64), nullable=False, comment='企微用户ID(唯一)'),
|
||||
sa.Column('name', sa.String(length=128), nullable=False, comment='坐席姓名'),
|
||||
sa.Column('status', sa.String(length=20), nullable=False, comment='坐席状态: online/offline/busy'),
|
||||
sa.Column('current_load', sa.Integer(), nullable=False, comment='当前服务会话数'),
|
||||
sa.Column('max_load', sa.Integer(), nullable=False, comment='最大同时服务数'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('user_id')
|
||||
)
|
||||
op.create_table('approval_links',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('category', sa.String(length=64), nullable=False, comment='分类:IT/HR/行政/财务'),
|
||||
sa.Column('title', sa.String(length=128), nullable=False, comment='审批名称'),
|
||||
sa.Column('url', sa.Text(), nullable=False, comment='审批链接'),
|
||||
sa.Column('sort_order', sa.Integer(), nullable=False, comment='排序权重'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_al_category', 'approval_links', ['category'], unique=False)
|
||||
op.create_table('conversations',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('corp_id', sa.String(length=64), nullable=False, comment='企业微信企业ID(主企业或下游企业)'),
|
||||
sa.Column('employee_id', sa.String(length=64), nullable=False, comment='企微员工UserID'),
|
||||
sa.Column('employee_name', sa.String(length=128), nullable=False, comment='员工姓名'),
|
||||
sa.Column('department', sa.String(length=256), nullable=False, comment='部门'),
|
||||
sa.Column('position', sa.String(length=128), nullable=False, comment='岗位'),
|
||||
sa.Column('level', sa.String(length=64), nullable=False, comment='等级'),
|
||||
sa.Column('status', sa.String(length=20), nullable=False, comment='会话状态: ai_handling/queued/serving/resolved'),
|
||||
sa.Column('is_vip', sa.Boolean(), nullable=False, comment='VIP标记'),
|
||||
sa.Column('is_pinned', sa.Boolean(), nullable=False, comment='置顶标记'),
|
||||
sa.Column('is_todo', sa.Boolean(), nullable=False, comment='代办标记'),
|
||||
sa.Column('urgency_score', sa.Integer(), nullable=False, comment='紧急度1-5'),
|
||||
sa.Column('tags', sa.JSON(), nullable=False, comment='标签集合'),
|
||||
sa.Column('assigned_agent_id', sa.String(length=64), nullable=True, comment='分配的坐席ID'),
|
||||
sa.Column('collaborating_agent_ids', sa.JSON(), nullable=False, comment='协作坐席ID列表'),
|
||||
sa.Column('ai_substantive_reply_count', sa.Integer(), nullable=False, comment='AI实质性回复计数(满3次可呼叫坐席)'),
|
||||
sa.Column('last_message_at', sa.DateTime(timezone=True), nullable=True, comment='最后消息时间'),
|
||||
sa.Column('last_message_summary', sa.String(length=256), nullable=False, comment='最后消息摘要'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_conversations_assigned_agent', 'conversations', ['assigned_agent_id'], unique=False)
|
||||
op.create_index('idx_conversations_corp_id', 'conversations', ['corp_id'], unique=False)
|
||||
op.create_index('idx_conversations_employee_id', 'conversations', ['employee_id'], unique=False)
|
||||
op.create_index('idx_conversations_last_message_at', 'conversations', ['last_message_at'], unique=False)
|
||||
op.create_index('idx_conversations_status', 'conversations', ['status'], unique=False)
|
||||
op.create_index('idx_conversations_urgency_score', 'conversations', ['urgency_score'], unique=False)
|
||||
op.create_table('employees',
|
||||
sa.Column('id', sa.String(length=36), nullable=False, comment='员工记录唯一标识'),
|
||||
sa.Column('corp_id', sa.String(length=64), nullable=False, comment='企业微信企业ID'),
|
||||
sa.Column('employee_id', sa.String(length=64), nullable=False, comment='企微员工UserID(企业内唯一)'),
|
||||
sa.Column('name', sa.String(length=128), nullable=False, comment='员工姓名'),
|
||||
sa.Column('department', sa.String(length=512), nullable=False, comment='部门ID列表(JSON数组)'),
|
||||
sa.Column('position', sa.String(length=128), nullable=False, comment='岗位'),
|
||||
sa.Column('mobile', sa.String(length=32), nullable=False, comment='手机号'),
|
||||
sa.Column('email', sa.String(length=128), nullable=False, comment='邮箱'),
|
||||
sa.Column('avatar', sa.String(length=512), nullable=False, comment='头像URL'),
|
||||
sa.Column('status', sa.Integer(), nullable=False, comment='激活状态: 1=已激活, 2=已禁用, 4=未激活'),
|
||||
sa.Column('last_login_at', sa.DateTime(timezone=True), nullable=True, comment='最后登录时间'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('corp_id', 'employee_id', name='uq_employee_corp')
|
||||
)
|
||||
op.create_index('idx_employees_corp_id', 'employees', ['corp_id'], unique=False)
|
||||
op.create_index('idx_employees_employee_id', 'employees', ['employee_id'], unique=False)
|
||||
op.create_table('funny_phrases',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('scene', sa.String(length=64), nullable=False, comment='触发场景: shake/keyword/waiting/connected/timeout/vip'),
|
||||
sa.Column('content', sa.Text(), nullable=False, comment='话术内容'),
|
||||
sa.Column('tone', sa.String(length=32), nullable=False, comment='语气标签'),
|
||||
sa.Column('sort_order', sa.Integer(), nullable=False, comment='排序权重'),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, comment='是否启用'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_fp_scene', 'funny_phrases', ['scene'], unique=False)
|
||||
op.create_table('quick_reply_templates',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('category', sa.String(length=64), nullable=False, comment='分类:账号/网络/软件/硬件/通用'),
|
||||
sa.Column('title', sa.String(length=128), nullable=False, comment='模板标题'),
|
||||
sa.Column('content', sa.Text(), nullable=False, comment='模板内容,支持变量如 {employee_name}'),
|
||||
sa.Column('variables', sa.JSON(), nullable=False, comment='可用变量列表'),
|
||||
sa.Column('sort_order', sa.Integer(), nullable=False, comment='排序权重'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_qr_category', 'quick_reply_templates', ['category'], unique=False)
|
||||
op.create_table('software_downloads',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('category', sa.String(length=64), nullable=False, comment='分类:办公/开发/安全/工具'),
|
||||
sa.Column('name', sa.String(length=128), nullable=False, comment='软件名称'),
|
||||
sa.Column('version', sa.String(length=32), nullable=False, comment='版本号'),
|
||||
sa.Column('platform', sa.String(length=32), nullable=False, comment='平台: Windows/Mac/Linux/全平台'),
|
||||
sa.Column('download_url', sa.Text(), nullable=False, comment='下载链接'),
|
||||
sa.Column('sort_order', sa.Integer(), nullable=False, comment='排序权重'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_sd_category', 'software_downloads', ['category'], unique=False)
|
||||
op.create_table('system_configs',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('config_key', sa.String(length=128), nullable=False, comment='配置键'),
|
||||
sa.Column('config_value', sa.Text(), nullable=False, comment='配置值(JSON字符串或纯文本)'),
|
||||
sa.Column('description', sa.String(length=256), nullable=False, comment='配置说明'),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('config_key')
|
||||
)
|
||||
op.create_table('agent_notes',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('conversation_id', sa.String(length=36), nullable=False, comment='所属会话ID'),
|
||||
sa.Column('agent_id', sa.String(length=64), nullable=False, comment='坐席ID'),
|
||||
sa.Column('content', sa.Text(), nullable=False, comment='备注内容'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, comment='更新时间'),
|
||||
sa.ForeignKeyConstraint(['conversation_id'], ['conversations.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_an_conversation', 'agent_notes', ['conversation_id'], unique=False)
|
||||
op.create_table('messages',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('conversation_id', sa.String(length=36), nullable=False, comment='所属会话ID'),
|
||||
sa.Column('sender_type', sa.String(length=20), nullable=False, comment='发送者类型: employee/agent/ai/system'),
|
||||
sa.Column('sender_id', sa.String(length=64), nullable=False, comment='发送者ID'),
|
||||
sa.Column('sender_name', sa.String(length=128), nullable=False, comment='发送者姓名'),
|
||||
sa.Column('content', sa.Text(), nullable=False, comment='消息内容'),
|
||||
sa.Column('msg_type', sa.String(length=20), nullable=False, comment='消息类型: text/image/file/system'),
|
||||
sa.Column('ai_suggestion', sa.Boolean(), nullable=False, comment='是否为AI建议'),
|
||||
sa.Column('is_read', sa.Boolean(), nullable=False, comment='是否已读'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.ForeignKeyConstraint(['conversation_id'], ['conversations.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_messages_conversation_created', 'messages', ['conversation_id', 'created_at'], unique=False)
|
||||
op.create_index('idx_messages_conversation_id', 'messages', ['conversation_id'], unique=False)
|
||||
op.create_index('idx_messages_created_at', 'messages', ['created_at'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('idx_messages_created_at', table_name='messages')
|
||||
op.drop_index('idx_messages_conversation_id', table_name='messages')
|
||||
op.drop_index('idx_messages_conversation_created', table_name='messages')
|
||||
op.drop_table('messages')
|
||||
op.drop_index('idx_an_conversation', table_name='agent_notes')
|
||||
op.drop_table('agent_notes')
|
||||
op.drop_table('system_configs')
|
||||
op.drop_index('idx_sd_category', table_name='software_downloads')
|
||||
op.drop_table('software_downloads')
|
||||
op.drop_index('idx_qr_category', table_name='quick_reply_templates')
|
||||
op.drop_table('quick_reply_templates')
|
||||
op.drop_index('idx_fp_scene', table_name='funny_phrases')
|
||||
op.drop_table('funny_phrases')
|
||||
op.drop_index('idx_employees_employee_id', table_name='employees')
|
||||
op.drop_index('idx_employees_corp_id', table_name='employees')
|
||||
op.drop_table('employees')
|
||||
op.drop_index('idx_conversations_urgency_score', table_name='conversations')
|
||||
op.drop_index('idx_conversations_status', table_name='conversations')
|
||||
op.drop_index('idx_conversations_last_message_at', table_name='conversations')
|
||||
op.drop_index('idx_conversations_employee_id', table_name='conversations')
|
||||
op.drop_index('idx_conversations_corp_id', table_name='conversations')
|
||||
op.drop_index('idx_conversations_assigned_agent', table_name='conversations')
|
||||
op.drop_table('conversations')
|
||||
op.drop_index('idx_al_category', table_name='approval_links')
|
||||
op.drop_table('approval_links')
|
||||
op.drop_table('agents')
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user