feat: OTP首次绑定 + 三端登录修复 + 管理端权限修复 (2026-07-08)

OTP首次绑定:
- 新增统一 OTP 路由 /auth/otp-* (otp.py + router.py)
- 坐席端 OTP 绑定面板 (OtpBindPanel.vue)
- 管理端 OTP 管理列表 (MfaManage.vue)
- agent_login 签发半认证 token 支持首次绑定流程

三端登录修复:
- 坐席/管理端去掉'返回扫码登录'按钮
- 管理端改为二维码始终可见+轮询扫码状态
- 员工端 /itdesk/ 改为 alias 直接服务 H5 (不再301重定向)
- docker-compose 添加 h5 volume 挂载

管理端权限修复:
- 扫码登录改用 get_user_roles() 替代写死 roles=['agent']
- get_user_roles() 增加 agents.role 回退
- 新增 GET /admin/roles/user-roles 端点
- 角色管理页加载用户角色分配数据

文档更新:
- OTP PRD + 系统设计文档
- 故障排查手册 v1.1 (新增6案例)
- nginx 生产基准配置
This commit is contained in:
Simon
2026-07-08 21:54:57 +08:00
parent 6f0fbbb066
commit 400ce3ddcb
27 changed files with 2822 additions and 312 deletions
+51 -1
View File
@@ -150,6 +150,7 @@ async def scan_qrcode(
state: Optional[str] = Query(None, description="扫码登录票据(企微 OAuth state 标准参数名)"),
code: Optional[str] = Query(None, description="企微 OAuth 授权码"),
redis_client: aioredis.Redis = Depends(dep_redis),
db: AsyncSession = Depends(get_db),
):
"""处理企微 OAuth2 扫码回调。
@@ -186,12 +187,61 @@ async def scan_qrcode(
service = _get_qrcode_service(redis_client)
result = await service.process_scan(ticket=final_ticket, code=final_code)
# ==========================================================================
# 扫码后自动确认(auto-confirm
# ==========================================================================
# 原设计:requester 需已登录坐席调 /confirm 来授权新登录
# 问题:首次登录时电脑端无人登录,没有合法 current_user 可调 confirm
# 修正:扫码即确认,直接为扫码的企微用户签发 token
# ==========================================================================
from app.services.token_service import TokenService
from app.services.role_mapping_service import RoleMappingService
import json
from datetime import datetime
token_service = TokenService(redis_client)
# 获取用户的真实角色(而非写死 agent)
role_service = RoleMappingService(db)
user_roles = await role_service.get_user_roles(result["employee_id"])
logger.info(
f"扫码登录角色: employee_id={result['employee_id']}, "
f"roles={user_roles}"
)
auto_token = await token_service.create_token(
employee_id=result["employee_id"],
name=result["name"],
roles=user_roles,
avatar=result.get("avatar", ""),
login_source="qrcode_scan",
)
confirm_payload = {
"token": auto_token,
"confirmed_at": datetime.now().isoformat(),
"roles": user_roles,
"employee_id": result["employee_id"],
"name": result["name"],
}
CONFIRM_TTL = 60
await redis_client.setex(
f"qrcode:confirm:{final_ticket}",
CONFIRM_TTL,
json.dumps(confirm_payload, ensure_ascii=False),
)
logger.info(
f"扫码自动确认: ticket={final_ticket[:8]}..., "
f"employee_id={result['employee_id']}, name={result['name']}"
)
# GET 请求(企微 OAuth 回调)→ 重定向到前端选择页
# 因为企微 OAuth 流程不在这个端点完成最终登录,只标记 scanned,
# 等用户在坐席端点 confirm 后才能拿到 token。
# 但企微 WebView 期望看到跳转后的页面,所以这里给个提示页。
from fastapi.responses import HTMLResponse
if code is not None and ticket is not None and body is None:
if final_code is not None and final_ticket is not None and body is None:
# GET 模式:渲染一个 "扫码成功" 的 HTML 提示页 + 引导用户到登录页
html = f"""<!DOCTYPE html>
<html lang="zh-CN">