diff --git a/backend/alembic/versions/010_add_agent_otp.py b/backend/alembic/versions/010_add_agent_otp.py new file mode 100644 index 0000000..c7eb81f --- /dev/null +++ b/backend/alembic/versions/010_add_agent_otp.py @@ -0,0 +1,56 @@ +"""add agent OTP fields + +Revision ID: 010_add_agent_otp +Revises: 009_add_message_status +Create Date: 2026-06-16 + +v0.5.6: 添加坐席 OTP 二次验证字段 +- 新增 otp_secret 字段(存储 TOTP secret,绑定时生成) +- 新增 otp_enabled 字段(是否启用 OTP 二次验证) +- 都是 nullable=True,默认 False,不破坏现有坐席 + +为什么需要这个 migration: + Agent 模型里加了 otp_secret 和 otp_enabled 字段, + 但没有对应的 alembic migration 把它落到 DB schema 里。 + 查询时报 UndefinedColumnError: + column agents.otp_secret does not exist + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers +revision = '010_add_agent_otp' +down_revision = '009_add_message_status' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """添加 otp_secret + otp_enabled 字段""" + op.add_column( + 'agents', + sa.Column( + 'otp_secret', + sa.String(64), + nullable=True, + comment='TOTP 密钥(base32,绑定时生成)' + ) + ) + op.add_column( + 'agents', + sa.Column( + 'otp_enabled', + sa.Boolean(), + nullable=False, + server_default=sa.text('false'), + comment='是否启用 OTP 二次验证' + ) + ) + + +def downgrade() -> None: + """删除 OTP 字段""" + op.drop_column('agents', 'otp_enabled') + op.drop_column('agents', 'otp_secret') \ No newline at end of file diff --git a/backend/alembic/versions/011_add_conversation_impact_fields.py b/backend/alembic/versions/011_add_conversation_impact_fields.py new file mode 100644 index 0000000..50c22f9 --- /dev/null +++ b/backend/alembic/versions/011_add_conversation_impact_fields.py @@ -0,0 +1,69 @@ +"""add conversation impact fields + +Revision ID: 011_add_conversation_impact +Revises: 010_add_agent_otp +Create Date: 2026-06-16 + +v0.5.6: 补齐 Conversation 模型的 3 个评估字段 +- impact_scope (int, default 0): 影响范围(受影响人数) +- is_blocking (bool, default False): 是否阻断员工工作 +- emotion_state (str(20), default 'normal'): 情绪状态 + +为什么需要这个 migration: + Conversation 模型里加了 impact_scope/is_blocking/emotion_state, + 但缺 alembic migration 落库。坐席发消息时 SQLAlchemy 查 + conversations.* 全字段,报: + column conversations.impact_scope does not exist + + 跟 010_add_agent_otp 是同一类问题(模型新字段无 migration)。 +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers +revision = '011_add_conversation_impact' +down_revision = '010_add_agent_otp' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """添加 impact_scope + is_blocking + emotion_state 字段""" + op.add_column( + 'conversations', + sa.Column( + 'impact_scope', + sa.Integer(), + nullable=False, + server_default=sa.text('0'), + comment='影响范围(受影响人数,0=未评估)' + ) + ) + op.add_column( + 'conversations', + sa.Column( + 'is_blocking', + sa.Boolean(), + nullable=False, + server_default=sa.text('false'), + comment='是否阻断员工工作' + ) + ) + op.add_column( + 'conversations', + sa.Column( + 'emotion_state', + sa.String(20), + nullable=False, + server_default=sa.text("'normal'"), + comment='情绪状态(normal/worried/angry/urgent)' + ) + ) + + +def downgrade() -> None: + """删除 3 个评估字段""" + op.drop_column('conversations', 'emotion_state') + op.drop_column('conversations', 'is_blocking') + op.drop_column('conversations', 'impact_scope') diff --git a/backend/alembic/versions/012_sync_remaining_fields.py b/backend/alembic/versions/012_sync_remaining_fields.py new file mode 100644 index 0000000..dda5f16 --- /dev/null +++ b/backend/alembic/versions/012_sync_remaining_fields.py @@ -0,0 +1,87 @@ +"""sync remaining model fields + +Revision ID: 012_sync_remaining_fields +Revises: 011_add_conversation_impact +Create Date: 2026-06-16 + +v0.5.6: 补齐 dev-check-schema-drift 找到的 4 个漂移字段 +- conversations.dify_conversation_id (VARCHAR(128), nullable) +- employees.it_level (VARCHAR(20), default 'silver') +- employees.it_level_source (VARCHAR(20), default 'system') +- employees.notes (JSON, default '{}') + +为什么需要这个 migration: + 之前手动 011 只补了 NOT NULL 那些(坐席发消息会 500 的), + 但 dev-check-schema-drift.ps1 又发现 4 个字段也没建 migration。 + 之前是 nullable 没立即暴露,运行 SELECT * FROM conversations 时 + PostgreSQL 会按顺序填,nullable 列缺不会立刻 500,但 INSERT/UPDATE + 涉及这些字段时会出错,或者 Alembic autogenerate 会持续报告漂移。 +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers +revision = '012_sync_remaining_fields' +down_revision = '011_add_conversation_impact' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """加 4 个漂移字段""" + + # 1) conversations.dify_conversation_id - Dify 多轮对话上下文 + op.add_column( + 'conversations', + sa.Column( + 'dify_conversation_id', + sa.String(128), + nullable=True, + comment='Dify会话ID(多轮对话上下文)' + ) + ) + + # 2) employees.it_level - IT 技能等级 + op.add_column( + 'employees', + sa.Column( + 'it_level', + sa.String(20), + nullable=False, + server_default=sa.text("'silver'"), + comment='IT技能等级(bronze/silver/gold/platinum/diamond/star/king)' + ) + ) + + # 3) employees.it_level_source - 等级来源 + op.add_column( + 'employees', + sa.Column( + 'it_level_source', + sa.String(20), + nullable=False, + server_default=sa.text("'system'"), + comment='等级来源(system/manual/assessment)' + ) + ) + + # 4) employees.notes - 坐席备注 JSON + op.add_column( + 'employees', + sa.Column( + 'notes', + sa.JSON(), + nullable=False, + server_default=sa.text("'{}'"), + comment='坐席备注(JSON 格式)' + ) + ) + + +def downgrade() -> None: + """删除 4 个字段""" + op.drop_column('employees', 'notes') + op.drop_column('employees', 'it_level_source') + op.drop_column('employees', 'it_level') + op.drop_column('conversations', 'dify_conversation_id') diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 7d23946..a419be9 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -7,6 +7,7 @@ # 3. require_admin: 管理员权限验证 # ============================================================================= +import inspect import json import logging from dataclasses import dataclass @@ -225,12 +226,26 @@ def require_role(*required_roles: str): """ def decorator(func): + # 合并 func 签名 + current_user 参数,让 FastAPI 能正确解析 Depends + # (v0.5.6 修复:之前用 @wraps,FastAPI 看到的是 __wrapped__ 的签名, + # 没有 current_user,导致 Depends 默认值未被解析,current_user 实际是 Depends 对象) + sig = inspect.signature(func) + params = list(sig.parameters.values()) + params.append( + inspect.Parameter( + 'current_user', + inspect.Parameter.KEYWORD_ONLY, + annotation=UserInfo, + default=Depends(get_current_user), + ) + ) + new_sig = sig.replace(parameters=params) + @wraps(func) - async def wrapper( - *args, - current_user: UserInfo = Depends(get_current_user), - **kwargs, - ): + async def wrapper(*args, **kwargs): + # FastAPI 已经把 current_user 注入了 kwargs + current_user = kwargs.pop('current_user') + # 检查用户是否有任一所需角色 user_roles = set(current_user.roles) required = set(required_roles) @@ -247,6 +262,8 @@ def require_role(*required_roles: str): return await func(*args, current_user=current_user, **kwargs) + # 关键:让 FastAPI 用合并后的签名,这样它能看到 current_user 这个 Depends 参数 + wrapper.__signature__ = new_sig return wrapper return decorator diff --git a/backend/requirements.txt b/backend/requirements.txt index 693a37d..e5600a5 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -37,7 +37,8 @@ redis==5.0.7 # 数据验证 # -------------------------------------------------------------------------- # pydantic: 数据验证和设置管理,FastAPI 的核心依赖 -pydantic==2.7.5 +# 注意:必须用 2.7.4 或 2.8.0+,2.7.5 被 PyPI yank(清华源/官方源都没有) +pydantic==2.7.4 # pydantic-settings: 从环境变量读取配置,支持 .env 文件 pydantic-settings==2.3.4