docs: 移动蓝绿部署指南到 troubleshooting 目录
This commit is contained in:
+161
-3
@@ -257,9 +257,9 @@ async def agent_login(
|
||||
await db.flush()
|
||||
logger.info(f"坐席登录: user_id={body.user_id}, name={body.name}")
|
||||
|
||||
# 2. MFA 二次验证(admin 角色且已绑定 MFA)
|
||||
# v0.7.1: 用 mfa_secret/mfa_enabled 替代旧 otp_secret/otp_enabled
|
||||
if agent.role == "admin" and agent.mfa_enabled:
|
||||
# 2. MFA 二次验证(已绑定 MFA 的坐席/管理员)
|
||||
# v1.5: 坐席和管理员都需要 OTP 验证
|
||||
if agent.mfa_enabled:
|
||||
if not body.otp_code:
|
||||
# 需要 OTP 验证,返回 require_otp 标记
|
||||
return success_response(data={
|
||||
@@ -593,3 +593,161 @@ async def update_agent_password(
|
||||
except Exception as e:
|
||||
logger.error(f"密码更新异常: {e}", exc_info=True)
|
||||
raise AppException(1014, f"密码更新失败: {str(e)}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 企微 OAuth2 一键登录(坐席端)
|
||||
# ============================================================================
|
||||
|
||||
import urllib.parse
|
||||
import secrets as secrets_module
|
||||
|
||||
|
||||
def _build_agent_oauth_url(redirect_uri: str) -> str:
|
||||
"""构建坐席端企微OAuth2授权URL。
|
||||
|
||||
文档: https://developer.work.weixin.qq.com/document/path/91022
|
||||
"""
|
||||
params = {
|
||||
"appid": settings.wecom_corp_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
"response_type": "code",
|
||||
"scope": "snsapi_base", # 静默授权
|
||||
"state": "agent_login", # 标记为坐席登录
|
||||
}
|
||||
# 如果有 agentid 也加上
|
||||
if getattr(settings, "wecom_agent_id", None):
|
||||
params["agentid"] = str(settings.wecom_agent_id)
|
||||
|
||||
query = urllib.parse.urlencode(params)
|
||||
# 企业微信 OAuth2 地址(注意是 open.work.weixin.qq.com)
|
||||
return f"https://open.work.weixin.qq.com/connect/oauth2/authorize?{query}#wechat_redirect"
|
||||
|
||||
|
||||
@router.get("/agents/oauth/authorize")
|
||||
async def get_oauth_authorize_url(
|
||||
redirect_uri: str = Query(None, description="OAuth回调地址(可选,默认坐席端地址)"),
|
||||
):
|
||||
"""获取企微OAuth2授权URL(JSON格式,供前端跳转)。
|
||||
|
||||
前端调用此接口获取授权URL,然后自行跳转到企微授权页。
|
||||
授权成功后企微会携带 code 回调到此接口的 redirect_uri。
|
||||
|
||||
Args:
|
||||
redirect_uri: 授权成功后的回调地址(可选)
|
||||
默认: https://itsupport.servyou.com.cn/itagent/
|
||||
|
||||
Returns:
|
||||
JSON: { code: 0, data: { authorize_url: "https://open.weixin.qq.com/..." } }
|
||||
"""
|
||||
# 确定回调地址
|
||||
if redirect_uri:
|
||||
# 前端传入的回调地址
|
||||
pass
|
||||
else:
|
||||
# 默认回调地址:坐席端首页
|
||||
redirect_uri = "https://itsupport.servyou.com.cn/itagent/"
|
||||
|
||||
# 编码回调地址
|
||||
encoded_redirect = urllib.parse.quote(redirect_uri, safe='')
|
||||
|
||||
# 构建授权URL
|
||||
authorize_url = _build_agent_oauth_url(redirect_uri)
|
||||
|
||||
logger.info(f"生成坐席端OAuth授权URL: redirect_uri={redirect_uri}")
|
||||
|
||||
return success_response(data={
|
||||
"authorize_url": authorize_url,
|
||||
"redirect_uri": redirect_uri,
|
||||
})
|
||||
|
||||
|
||||
# OAuth 回调请求模型
|
||||
class OAuthCallbackRequest(BaseModel):
|
||||
code: str = Field(..., description="企微授权码")
|
||||
state: str = Field(default="agent_login", description="state参数")
|
||||
|
||||
|
||||
@router.post("/agents/oauth/callback")
|
||||
async def oauth_callback(
|
||||
body: OAuthCallbackRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""企微OAuth2回调处理(坐席端)。
|
||||
|
||||
用授权码换取员工ID,验证坐席身份,生成登录token。
|
||||
|
||||
Args:
|
||||
body: { code: "xxx", state: "agent_login" }
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
JSON: { code: 0, data: { token, user_id, name, roles } }
|
||||
"""
|
||||
code = body.code
|
||||
state = body.state
|
||||
|
||||
if not code:
|
||||
raise AppException(2007, "授权码不能为空")
|
||||
|
||||
# 1. 用 code 换取员工身份
|
||||
wecom_service = WecomService()
|
||||
try:
|
||||
oauth_info = await wecom_service.get_oauth_user_info(code)
|
||||
user_id = oauth_info.get("userid", "")
|
||||
|
||||
if not user_id:
|
||||
raise AppException(2007, "OAuth授权失败:未获取到员工ID")
|
||||
except Exception as e:
|
||||
logger.error(f"企微OAuth换取userid失败: {e}")
|
||||
raise AppException(2007, f"OAuth授权失败: {str(e)}")
|
||||
|
||||
# 2. 获取员工详细信息(包含姓名)
|
||||
employee_name = ""
|
||||
try:
|
||||
detail = await wecom_service.get_user_info(user_id)
|
||||
employee_name = detail.get("name", "")
|
||||
except Exception as e:
|
||||
logger.warning(f"获取员工详细信息失败: user_id={user_id}, error={e}")
|
||||
|
||||
# 3. 验证是否为坐席
|
||||
stmt = select(Agent).where(Agent.user_id == user_id)
|
||||
result = await db.execute(stmt)
|
||||
agent = result.scalars().first()
|
||||
|
||||
if not agent:
|
||||
raise AppException(2008, f"您不是坐席,无法通过企业微信登录")
|
||||
|
||||
# 4. 生成登录token
|
||||
token = secrets_module.token_urlsafe(32)
|
||||
redis_client = _get_redis()
|
||||
|
||||
if redis_client:
|
||||
try:
|
||||
# 存储 token -> agent信息(JSON格式)
|
||||
token_data = {
|
||||
"user_id": agent.user_id,
|
||||
"name": agent.name,
|
||||
"roles": [agent.role],
|
||||
"login_source": "agent_oauth",
|
||||
}
|
||||
import json as json_module
|
||||
await redis_client.setex(
|
||||
f"user:token:{token}",
|
||||
TOKEN_TTL_SECONDS,
|
||||
json_module.dumps(token_data),
|
||||
)
|
||||
|
||||
# 记录登录日志
|
||||
logger.info(f"企微OAuth登录成功: user_id={user_id}, name={employee_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Token存储Redis失败: {e}")
|
||||
raise AppException(1003, "登录失败,请重试")
|
||||
|
||||
return success_response(data={
|
||||
"token": token,
|
||||
"user_id": agent.user_id,
|
||||
"name": employee_name or agent.name,
|
||||
"role": agent.role,
|
||||
"require_otp": agent.otp_secret is not None,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user