docs: 移动蓝绿部署指南到 troubleshooting 目录

This commit is contained in:
Simon
2026-07-05 17:03:36 +08:00
parent ab90db3d3d
commit ca7c6d937a
91 changed files with 4841 additions and 406 deletions
+161 -3
View File
@@ -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,
})