"""add business_contacts and routing_events tables 新建 business_contacts 表(业务联系人)和 routing_events 表(路由命中统计), 并预置初始联系人数据(行政/HR/财务/法务/物业 各1-2人)。 关联设计文档:docs/03-技术架构/业务路由推荐-架构设计.md Revision ID: 047_business_contacts Revises: 046_add_login_logs Create Date: 2026-07-15 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = '047_business_contacts' down_revision: Union[str, None] = '046_add_login_logs' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # -------------------------------------------------------------------------- # 新建 business_contacts 表 # -------------------------------------------------------------------------- op.create_table( 'business_contacts', sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True, comment='主键'), sa.Column('name', sa.String(50), nullable=False, comment='联系人姓名'), sa.Column('gender', sa.String(10), nullable=False, server_default='male', comment='性别(male/female)'), sa.Column('department', sa.String(100), nullable=False, comment='部门名称'), sa.Column('position', sa.String(100), nullable=False, comment='岗位'), sa.Column('responsibility', sa.String(500), nullable=False, comment='负责业务描述'), sa.Column('extension', sa.String(20), nullable=True, comment='分机号'), sa.Column('service_area', sa.String(200), nullable=True, comment='服务区域/办公地点'), sa.Column('wecom_userid', sa.String(100), nullable=False, comment='企微用户ID'), sa.Column('avatar_url', sa.String(500), nullable=True, comment='头像URL'), sa.Column('business_category', sa.String(50), nullable=False, comment='业务类别(行政/人力资源/财务/法务/行政-物业)'), sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('true'), 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='更新时间'), ) # 索引:按业务类别 + 是否启用查询 op.create_index( 'idx_business_contacts_category', 'business_contacts', ['business_category', 'is_active'], ) # -------------------------------------------------------------------------- # 新建 routing_events 表(P1) # -------------------------------------------------------------------------- op.create_table( 'routing_events', sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True, comment='主键'), sa.Column('conversation_id', sa.String(36), sa.ForeignKey('conversations.id', ondelete='CASCADE'), nullable=False, comment='会话ID'), sa.Column('employee_id', sa.String(64), nullable=False, comment='员工ID'), sa.Column('message_content', sa.String(500), nullable=False, comment='触发路由的员工消息(截断)'), sa.Column('business_category', sa.String(50), nullable=False, comment='识别的业务类别'), sa.Column('routing_confidence', sa.Float(), nullable=False, comment='路由置信度'), sa.Column('contact_id', sa.Integer(), sa.ForeignKey('business_contacts.id', ondelete='SET NULL'), nullable=True, comment='推荐的联系人ID'), sa.Column('contact_name', sa.String(50), nullable=False, comment='联系人姓名(冗余)'), sa.Column('is_clicked', sa.Boolean(), nullable=False, server_default=sa.text('false'), comment='是否点击了联系TA'), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), comment='创建时间'), ) # 索引 op.create_index('idx_routing_events_category', 'routing_events', ['business_category']) op.create_index('idx_routing_events_conv', 'routing_events', ['conversation_id']) # -------------------------------------------------------------------------- # 预置初始联系人数据 # -------------------------------------------------------------------------- # 说明:wecom_userid 使用占位符,上线前需替换为真实的企微通讯录 userid # -------------------------------------------------------------------------- contacts_data = [ # 行政(2人) { 'name': '王芳', 'gender': 'female', 'department': '行政部', 'position': '设备管理岗', 'responsibility': '打印机/复印机/扫描仪', 'extension': '8002', 'service_area': '滨江园区 3-5楼', 'wecom_userid': 'WangFang', 'avatar_url': '', 'business_category': '行政', 'is_active': True, }, { 'name': '陈伟', 'gender': 'male', 'department': '行政部', 'position': '行政事务岗', 'responsibility': '办公用品/名片印刷/保洁服务', 'extension': '8003', 'service_area': '滨江园区 1-2楼', 'wecom_userid': 'ChenWei', 'avatar_url': '', 'business_category': '行政', 'is_active': True, }, # 人力资源(2人) { 'name': '李娜', 'gender': 'female', 'department': '人力资源部', 'position': '员工服务岗', 'responsibility': '工牌补办/考勤异常/入职手续', 'extension': '8005', 'service_area': '滨江园区 A栋3楼', 'wecom_userid': 'LiNa', 'avatar_url': '', 'business_category': '人力资源', 'is_active': True, }, { 'name': '张磊', 'gender': 'male', 'department': '人力资源部', 'position': '薪酬福利岗', 'responsibility': '社保/公积金/离职手续', 'extension': '8006', 'service_area': '滨江园区 A栋3楼', 'wecom_userid': 'ZhangLei', 'avatar_url': '', 'business_category': '人力资源', 'is_active': True, }, # 财务(1人) { 'name': '刘洋', 'gender': 'male', 'department': '财务部', 'position': '费用报销岗', 'responsibility': '报销/发票/借款/工资条', 'extension': '8010', 'service_area': '滨江园区 B栋4楼', 'wecom_userid': 'LiuYang', 'avatar_url': '', 'business_category': '财务', 'is_active': True, }, # 法务(1人) { 'name': '赵敏', 'gender': 'female', 'department': '法务部', 'position': '法务顾问岗', 'responsibility': '合同/法律咨询/知识产权', 'extension': '8015', 'service_area': '滨江园区 C栋5楼', 'wecom_userid': 'ZhaoMin', 'avatar_url': '', 'business_category': '法务', 'is_active': True, }, # 行政-物业(1人) { 'name': '孙强', 'gender': 'male', 'department': '行政部', 'position': '物业管理岗', 'responsibility': '空调/电梯/门禁/停车', 'extension': '8008', 'service_area': '滨江园区 全园区', 'wecom_userid': 'SunQiang', 'avatar_url': '', 'business_category': '行政-物业', 'is_active': True, }, ] # 批量插入初始数据 op.bulk_insert( sa.table( 'business_contacts', sa.column('name', sa.String), sa.column('gender', sa.String), sa.column('department', sa.String), sa.column('position', sa.String), sa.column('responsibility', sa.String), sa.column('extension', sa.String), sa.column('service_area', sa.String), sa.column('wecom_userid', sa.String), sa.column('avatar_url', sa.String), sa.column('business_category', sa.String), sa.column('is_active', sa.Boolean), sa.column('created_at', sa.DateTime), sa.column('updated_at', sa.DateTime), ), [ { **c, 'avatar_url': c['avatar_url'] or None, 'extension': c.get('extension') or None, 'service_area': c.get('service_area') or None, 'created_at': sa.func.now(), 'updated_at': sa.func.now(), } for c in contacts_data ], ) def downgrade() -> None: # 删除索引 op.drop_index('idx_routing_events_conv', table_name='routing_events') op.drop_index('idx_routing_events_category', table_name='routing_events') op.drop_index('idx_business_contacts_category', table_name='business_contacts') # 删除表 op.drop_table('routing_events') op.drop_table('business_contacts')