2026-07-02 23:13:23 +08:00
|
|
|
|
"""企微机器人路由
|
|
|
|
|
|
|
|
|
|
|
|
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)。
|
|
|
|
|
|
|
2026-07-12 14:17:18 +08:00
|
|
|
|
群聊/私聊:立即返回 "success",异步调 BaoDan 并主动发送回复。
|
2026-07-02 23:13:23 +08:00
|
|
|
|
"""
|
|
|
|
|
|
# 校验回调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)
|
|
|
|
|
|
|
2026-07-12 14:17:18 +08:00
|
|
|
|
if wecom_service.handle_message_for_private(msg_signature, timestamp, nonce, encrypted_data):
|
|
|
|
|
|
return Response("success", status=200)
|
|
|
|
|
|
|
|
|
|
|
|
# 兜底:同步处理,返回被动回复 XML
|
2026-07-02 23:13:23 +08:00
|
|
|
|
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():
|
|
|
|
|
|
"""重定向到企微授权页面。"""
|
2026-07-12 14:17:18 +08:00
|
|
|
|
from urllib.parse import quote
|
2026-07-02 23:13:23 +08:00
|
|
|
|
corp_id = _get_wecom_config("WECOM_CORP_ID")
|
2026-07-12 14:17:18 +08:00
|
|
|
|
# 优先使用 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"
|
2026-07-02 23:13:23 +08:00
|
|
|
|
auth_url = (
|
|
|
|
|
|
f"https://open.weixin.qq.com/connect/oauth2/authorize"
|
|
|
|
|
|
f"?appid={corp_id}"
|
2026-07-12 14:17:18 +08:00
|
|
|
|
f"&redirect_uri={quote(redirect_uri, safe='')}"
|
2026-07-02 23:13:23 +08:00
|
|
|
|
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:
|
2026-07-12 14:17:18 +08:00
|
|
|
|
frontend_url = _get_wecom_config("FRONTEND_URL", "").rstrip("/")
|
|
|
|
|
|
if frontend_url:
|
|
|
|
|
|
return redirect(f"{frontend_url}/login?error=auth_failed")
|
2026-07-02 23:13:23 +08:00
|
|
|
|
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
|
2026-07-12 14:17:18 +08:00
|
|
|
|
# 重定向到前端(Docker 中后端和前端端口不同)
|
|
|
|
|
|
frontend_url = _get_wecom_config("FRONTEND_URL", "").rstrip("/")
|
|
|
|
|
|
if frontend_url:
|
|
|
|
|
|
return redirect(f"{frontend_url}/?token={token}&user={quote(user_json)}")
|
2026-07-02 23:13:23 +08:00
|
|
|
|
return redirect(f"/?token={token}&user={quote(user_json)}")
|
|
|
|
|
|
else:
|
2026-07-12 14:17:18 +08:00
|
|
|
|
frontend_url = _get_wecom_config("FRONTEND_URL", "").rstrip("/")
|
|
|
|
|
|
if frontend_url:
|
|
|
|
|
|
return redirect(f"{frontend_url}/login?error=auth_failed")
|
2026-07-02 23:13:23 +08:00
|
|
|
|
return redirect("/login?error=auth_failed")
|