111 lines
4.4 KiB
Python
111 lines
4.4 KiB
Python
"""企微机器人路由
|
||
|
||
GET /wecom/callback 企微 URL 验证
|
||
POST /wecom/callback 企微消息接收
|
||
GET /wecom/oauth 企微 OAuth 登录入口
|
||
GET /wecom/oauth/callback 企微 OAuth 回调
|
||
"""
|
||
from flask import Blueprint, request, Response, redirect
|
||
from insurance.config import get_config as _get_wecom_config
|
||
from insurance.wecom.service import WeComService
|
||
|
||
wecom_bp = Blueprint("wecom", __name__)
|
||
wecom_service = WeComService()
|
||
|
||
|
||
@wecom_bp.route("/callback", methods=["GET"])
|
||
def verify_callback():
|
||
"""企微服务器 URL 验证(GET)。"""
|
||
msg_signature = request.args.get("msg_signature", "")
|
||
timestamp = request.args.get("timestamp", "")
|
||
nonce = request.args.get("nonce", "")
|
||
echostr = request.args.get("echostr", "")
|
||
|
||
result = wecom_service.verify_url(msg_signature, timestamp, nonce, echostr)
|
||
return Response(result, content_type="text/plain")
|
||
|
||
|
||
@wecom_bp.route("/callback", methods=["POST"])
|
||
def receive_message():
|
||
"""企微消息接收(POST)。
|
||
|
||
群聊/私聊:立即返回 "success",异步调 BaoDan 并主动发送回复。
|
||
"""
|
||
# 校验回调IP白名单(Nginx透传 X-Forwarded-For)
|
||
client_ip = request.headers.get("X-Forwarded-For", request.remote_addr or "").split(",")[0].strip()
|
||
if not wecom_service.is_callback_ip(client_ip):
|
||
print(f"[WECOM ERROR] 非法回调IP: {client_ip}", flush=True)
|
||
return Response("", status=200)
|
||
|
||
msg_signature = request.args.get("msg_signature", "")
|
||
timestamp = request.args.get("timestamp", "")
|
||
nonce = request.args.get("nonce", "")
|
||
encrypted_data = request.data
|
||
|
||
# 先判断是否群聊(群聊必须立即返回 "success",5秒内不回复会被企微重试)
|
||
if wecom_service.handle_message_for_group(msg_signature, timestamp, nonce, encrypted_data):
|
||
return Response("success", status=200)
|
||
|
||
if wecom_service.handle_message_for_private(msg_signature, timestamp, nonce, encrypted_data):
|
||
return Response("success", status=200)
|
||
|
||
# 兜底:同步处理,返回被动回复 XML
|
||
reply_xml = wecom_service.handle_message(msg_signature, timestamp, nonce, encrypted_data)
|
||
if reply_xml:
|
||
return Response(reply_xml, content_type="application/xml", status=200)
|
||
return Response("", status=200)
|
||
|
||
|
||
@wecom_bp.route("/oauth", methods=["GET"])
|
||
def oauth_login():
|
||
"""重定向到企微授权页面。"""
|
||
from urllib.parse import quote
|
||
corp_id = _get_wecom_config("WECOM_CORP_ID")
|
||
# 优先使用 BASE_URL 环境变量(Docker/Vite 代理下 request.host_url 是内部地址)
|
||
base_url = _get_wecom_config("BASE_URL", "").rstrip("/")
|
||
if not base_url:
|
||
base_url = request.host_url.rstrip("/")
|
||
redirect_uri = f"{base_url}/insurance/wecom/oauth/callback"
|
||
auth_url = (
|
||
f"https://open.weixin.qq.com/connect/oauth2/authorize"
|
||
f"?appid={corp_id}"
|
||
f"&redirect_uri={quote(redirect_uri, safe='')}"
|
||
f"&response_type=code"
|
||
f"&scope=snsapi_userinfo"
|
||
f"&state=state"
|
||
f"#wechat_redirect"
|
||
)
|
||
return redirect(auth_url)
|
||
|
||
|
||
@wecom_bp.route("/oauth/callback", methods=["GET"])
|
||
def oauth_callback():
|
||
"""企微 OAuth 回调:code 换用户信息 -> 生成 JWT -> 重定向到前端。"""
|
||
code = request.args.get("code", "")
|
||
if not code:
|
||
frontend_url = _get_wecom_config("FRONTEND_URL", "").rstrip("/")
|
||
if frontend_url:
|
||
return redirect(f"{frontend_url}/login?error=auth_failed")
|
||
return redirect("/login?error=auth_failed")
|
||
|
||
from insurance.auth.service import AuthService
|
||
auth_service = AuthService()
|
||
result = auth_service.wework_login(code, "")
|
||
|
||
if result.get("code") == 0:
|
||
token = result["data"]["token"]
|
||
user = result["data"].get("user", {})
|
||
import json
|
||
user_json = json.dumps(user, ensure_ascii=False)
|
||
from urllib.parse import quote
|
||
# 重定向到前端(Docker 中后端和前端端口不同)
|
||
frontend_url = _get_wecom_config("FRONTEND_URL", "").rstrip("/")
|
||
if frontend_url:
|
||
return redirect(f"{frontend_url}/?token={token}&user={quote(user_json)}")
|
||
return redirect(f"/?token={token}&user={quote(user_json)}")
|
||
else:
|
||
frontend_url = _get_wecom_config("FRONTEND_URL", "").rstrip("/")
|
||
if frontend_url:
|
||
return redirect(f"{frontend_url}/login?error=auth_failed")
|
||
return redirect("/login?error=auth_failed")
|