78f60c6857
P0 修复: - /api/ready import 错误 (_get_engine + settings.create_redis_client) - 删 agent.otp_secret/otp_enabled 双字段 (migration 026) - 重建 021_rbac migration (IF NOT EXISTS 兼容) P1 新增: - 企微 SSO (auth_wecom_sso.py, useWeChatWorkSSO composable, PortalSelect UA 检测) - RBAC 5 角色 × 4 资源 × 4 操作 × 3 范围 (rbac_service + seed_rbac + require_permission) - audit_log 模型 + migration 027 + 服务 + API - 管理后台 RBAC 权限矩阵 UI (PermissionsMatrix.vue) 质量: - pytest 405 passed / 33 pre-existing failed / 4 xfailed (v0.7.1 引入失败 = 0) - conftest GBK patch 强制 UTF-8 读 .env - .gitignore 排除 *.b64 (含 admin token 凭据) - DEPLOY-v0.7.1.md 7 步 runbook + 4 坑 + 回滚预案
186 lines
6.4 KiB
Python
186 lines
6.4 KiB
Python
# =============================================================================
|
||
# 企微IT智能服务台 — 坐席模型
|
||
# =============================================================================
|
||
# 说明:对应数据库 agents 表,存储坐席(IT服务人员)信息
|
||
# 坐席状态:online(在线)/offline(离线)/busy(忙碌)
|
||
# =============================================================================
|
||
|
||
import uuid
|
||
from datetime import datetime
|
||
from typing import Optional
|
||
|
||
from sqlalchemy import Boolean, DateTime, Integer, JSON, String, text
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from app.database import Base
|
||
|
||
|
||
class Agent(Base):
|
||
"""坐席模型 — 对应 agents 表。
|
||
|
||
记录坐席的基本信息和状态,用于消息分配和负载管理。
|
||
|
||
Attributes:
|
||
id: 坐席唯一标识(UUID,数据库自动生成)
|
||
user_id: 企微用户ID(唯一,关联企微通讯录)
|
||
name: 坐席姓名
|
||
status: 坐席状态(online/offline/busy)
|
||
current_load: 当前服务会话数
|
||
max_load: 最大同时服务数(默认5)
|
||
created_at: 创建时间
|
||
updated_at: 更新时间
|
||
"""
|
||
|
||
# 表名(必须和架构文档 DDL 一致)
|
||
__tablename__ = "agents"
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 字段定义
|
||
# --------------------------------------------------------------------------
|
||
|
||
# 主键:UUID,Python端生成(兼容PostgreSQL和SQLite)
|
||
id: Mapped[str] = mapped_column(
|
||
String(36),
|
||
primary_key=True,
|
||
default=lambda: str(uuid.uuid4()),
|
||
)
|
||
|
||
# 企微用户ID(唯一,用于关联企微通讯录和登录认证)
|
||
user_id: Mapped[str] = mapped_column(
|
||
String(64),
|
||
unique=True,
|
||
nullable=False,
|
||
comment="企微用户ID(唯一)",
|
||
)
|
||
|
||
# 坐席姓名
|
||
name: Mapped[str] = mapped_column(
|
||
String(128),
|
||
nullable=False,
|
||
comment="坐席姓名",
|
||
)
|
||
|
||
# 坐席状态(CHECK 约束:只能取三种值)
|
||
# online: 在线,可以接收新的会话分配
|
||
# offline: 离线,不接收任何会话
|
||
# busy: 忙碌,不接收新会话但继续处理已有的
|
||
status: Mapped[str] = mapped_column(
|
||
String(20),
|
||
nullable=False,
|
||
default="offline",
|
||
comment="坐席状态: online/offline/busy",
|
||
)
|
||
|
||
# 当前服务会话数(分配新会话时 +1,结单时 -1)
|
||
current_load: Mapped[int] = mapped_column(
|
||
Integer,
|
||
nullable=False,
|
||
default=0,
|
||
comment="当前服务会话数",
|
||
)
|
||
|
||
# 最大同时服务数(坐席同时处理的会话数上限)
|
||
# 默认5个,可根据坐席能力调整
|
||
max_load: Mapped[int] = mapped_column(
|
||
Integer,
|
||
nullable=False,
|
||
default=5,
|
||
comment="最大同时服务数",
|
||
)
|
||
|
||
# 创建时间
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True),
|
||
nullable=False,
|
||
default=datetime.now,
|
||
comment="创建时间",
|
||
)
|
||
|
||
# 更新时间
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True),
|
||
nullable=False,
|
||
default=datetime.now,
|
||
onupdate=datetime.now,
|
||
comment="更新时间",
|
||
)
|
||
|
||
# 角色(admin=组长, agent=坐席)
|
||
# 管理后台需要 admin 角色才能访问,坐席端无限制
|
||
role: Mapped[str] = mapped_column(
|
||
String(20),
|
||
nullable=False,
|
||
default="agent",
|
||
comment="角色:admin=组长, agent=坐席",
|
||
)
|
||
|
||
# 技能标签列表(JSON 数组,存储坐席的技能分类)
|
||
# 可选值:电脑/软件/外设/网络/安全/资产/其他
|
||
skill_tags: Mapped[list] = mapped_column(
|
||
JSON,
|
||
nullable=False,
|
||
default=list,
|
||
comment="技能标签列表(电脑/软件/外设/网络/安全/资产/其他)",
|
||
)
|
||
|
||
# v0.7.1: 删除 otp_secret / otp_enabled 字段
|
||
# 原因: 与下方 mfa_secret / mfa_enabled 完全重复(都是 TOTP secret)
|
||
# 旧 OTP 字段只用于高危操作前的二次验证,mfa 字段已涵盖该用途
|
||
# 迁移策略: alembic 010 改为 DROP COLUMN otp_secret, otp_enabled
|
||
|
||
# 本地密码哈希(可选,用于本地密码认证)
|
||
# 使用 bcrypt 加密存储,不存储明文密码
|
||
# 当企微验证不可用时,可作为备用认证方式
|
||
# P1 修复: Mapped[Optional[str]] 解决严格模式下 None 赋值报错
|
||
password_hash: Mapped[Optional[str]] = mapped_column(
|
||
String(128),
|
||
nullable=True,
|
||
default=None,
|
||
comment="本地密码哈希(bcrypt)",
|
||
)
|
||
|
||
# --------------------------------------------------------------------------
|
||
# MFA 二次认证字段(Phase 2.1 task #17)
|
||
# --------------------------------------------------------------------------
|
||
# 说明:MFA TOTP 独立于早期 OTP 字段,采用全新字段名以便区分演进阶段。
|
||
# - mfa_secret: TOTP 共享密钥(base32),绑定时生成,首次验证前不算启用
|
||
# - mfa_enabled: 是否启用(仅当 bind/confirm 验证成功后置 true)
|
||
# - mfa_bound_at: 首次绑定完成时间(用于审计 + 回收策略)
|
||
# - mfa_last_verified_at: 最近一次 verify 成功时间(用于安全审计)
|
||
# --------------------------------------------------------------------------
|
||
mfa_secret: Mapped[Optional[str]] = mapped_column(
|
||
String(32),
|
||
nullable=True,
|
||
default=None,
|
||
comment="MFA TOTP 共享密钥(base32,绑定时生成)",
|
||
)
|
||
|
||
mfa_enabled: Mapped[bool] = mapped_column(
|
||
Boolean,
|
||
nullable=False,
|
||
default=False,
|
||
server_default=text("false"),
|
||
comment="MFA 是否启用(False/True)",
|
||
)
|
||
|
||
mfa_bound_at: Mapped[Optional[datetime]] = mapped_column(
|
||
DateTime(timezone=True),
|
||
nullable=True,
|
||
default=None,
|
||
comment="MFA 首次绑定完成时间",
|
||
)
|
||
|
||
mfa_last_verified_at: Mapped[Optional[datetime]] = mapped_column(
|
||
DateTime(timezone=True),
|
||
nullable=True,
|
||
default=None,
|
||
comment="MFA 最近一次验证成功时间",
|
||
)
|
||
|
||
def __repr__(self) -> str:
|
||
"""坐席对象的字符串表示,方便调试。"""
|
||
return (
|
||
f"<Agent(id={self.id}, name={self.name}, "
|
||
f"status={self.status}, load={self.current_load}/{self.max_load})>"
|
||
)
|