Files
wecom_it_smart_desk/backend/alembic/versions/047_business_contacts.py
T

195 lines
9.1 KiB
Python
Raw Normal View History

"""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')