From 598a2cbc2988068074ad022d453da036250e1663 Mon Sep 17 00:00:00 2001 From: kx_guo Date: Sat, 5 Sep 2026 18:18:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B4=E7=90=86=E9=92=89=E9=92=89=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E5=85=8D=E7=99=BB=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/main.py | 11 ++++-- frontend/index.html | 2 +- frontend/src/main.jsx | 82 ++++++++++++++++++++++++++++++------------- 3 files changed, 67 insertions(+), 28 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 5a415a5..d81e1b1 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -9,6 +9,7 @@ import secrets import time import urllib.parse import urllib.request +import urllib.error import uuid import io import zipfile @@ -333,8 +334,11 @@ def _dingtalk_workbench_user(code: str) -> dict: try: with urllib.request.urlopen(token_request, timeout=10) as response: token_data = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace")[:500] + raise HTTPException(status_code=502, detail=f"钉钉应用凭证校验失败:{detail}") from exc except Exception as exc: - raise HTTPException(status_code=502, detail="钉钉应用凭证校验失败,请稍后重试") from exc + raise HTTPException(status_code=502, detail=f"钉钉应用凭证校验失败:{exc}") from exc access_token = token_data.get("access_token") if not access_token: raise HTTPException(status_code=502, detail="钉钉未返回应用访问令牌,请检查应用权限") @@ -347,8 +351,11 @@ def _dingtalk_workbench_user(code: str) -> dict: try: with urllib.request.urlopen(user_request, timeout=10) as response: user_data = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace")[:500] + raise HTTPException(status_code=502, detail=f"无法读取钉钉用户信息:{detail}") from exc except Exception as exc: - raise HTTPException(status_code=502, detail="无法读取钉钉用户信息,请检查应用免登权限") from exc + raise HTTPException(status_code=502, detail=f"无法读取钉钉用户信息:{exc}") from exc if user_data.get("errcode", 0) not in (0, None): raise HTTPException(status_code=502, detail=f"钉钉用户信息读取失败:{user_data.get('errmsg', '请检查应用权限')}") return user_data.get("result") or user_data diff --git a/frontend/index.html b/frontend/index.html index 32c33c2..0365e01 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,7 +7,7 @@
- + diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 9423235..4f6a519 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -86,16 +86,37 @@ function LoginPage({ onLogin }) { const [loading, setLoading] = useState(false); const [dingtalkLoading, setDingtalkLoading] = useState(false); const [autoDingtalkLogin, setAutoDingtalkLogin] = useState(false); + const [dingtalkLoginFailed, setDingtalkLoginFailed] = useState(false); const autoLoginStarted = useRef(false); const query = new URLSearchParams(window.location.search); - const inDingtalkWorkbench = /DingTalk|AliApp\(DT/i.test(navigator.userAgent || "") || query.get("from") === "dingtalk" || Boolean(window.dd || window.DingTalkPC); + const getDingTalkBridge = () => window.dd || window.DingTalkPC || window.dingtalk; + const inDingtalkWorkbench = /DingTalk|AliApp\(DT/i.test(navigator.userAgent || "") || query.get("from") === "dingtalk" || Boolean(getDingTalkBridge()); function loadDingTalkSdk() { - if (window.dd) return Promise.resolve(window.dd); + if (getDingTalkBridge()) return Promise.resolve(getDingTalkBridge()); return new Promise((resolve, reject) => { const existing = document.querySelector('script[data-dingtalk-sdk="true"]'); + const urls = [ + existing?.src || "https://g.alicdn.com/dingding/dingtalk-jsapi/2.10.3/dingtalk.open.js", + "https://g.alicdn.com/dingding/dingtalk-jsapi/2.15.15/dingtalk.open.js", + ]; + let urlIndex = 0; + let settled = false; const finish = () => { - if (window.dd) resolve(window.dd); - else reject(new Error("钉钉JSAPI未加载,请确认工作台应用类型为H5微应用,并将 ask.dieya.net.cn 加入应用安全域名")); + if (settled) return; + if (getDingTalkBridge()) resolve(getDingTalkBridge()); + else if (urlIndex < urls.length - 1) { + urlIndex += 1; + const fallback = document.createElement("script"); + fallback.src = urls[urlIndex]; + fallback.async = true; + fallback.dataset.dingtalkSdk = "true"; + fallback.onload = finish; + fallback.onerror = finish; + document.head.appendChild(fallback); + } else { + settled = true; + reject(new Error("钉钉JSAPI未加载,请确认应用类型为H5微应用,并将 ask.dieya.net.cn 加入应用安全域名")); + } }; if (existing) { existing.addEventListener("load", finish, { once: true }); @@ -104,7 +125,7 @@ function LoginPage({ onLogin }) { return; } const script = document.createElement("script"); - script.src = "https://g.alicdn.com/dingding/dingtalk-jsapi/2.15.15/dingtalk.open.js"; + script.src = urls[0]; script.async = true; script.dataset.dingtalkSdk = "true"; script.onload = finish; @@ -114,7 +135,7 @@ function LoginPage({ onLogin }) { }); } async function requestDingTalkAuthCode(corpId) { - const ding = (await loadDingTalkSdk()) || window.dd || window.DingTalkPC; + const ding = (await loadDingTalkSdk()) || getDingTalkBridge(); return new Promise((resolve, reject) => { if (!ding) return reject(new Error("未检测到钉钉JSAPI,请将当前地址配置为钉钉H5微应用首页,并加入应用安全域名")); let settled = false; @@ -131,8 +152,15 @@ function LoginPage({ onLogin }) { const invoke = () => { try { const requestAuthCode = ding.runtime?.permission?.requestAuthCode || ding.permission?.requestAuthCode; - if (!requestAuthCode) throw new Error("当前钉钉版本不支持工作台免登JSAPI"); - requestAuthCode.call(ding.runtime?.permission || ding.permission, { corpId, onSuccess: success, onFail: failure }); + if (requestAuthCode) { + requestAuthCode.call(ding.runtime?.permission || ding.permission, { corpId, onSuccess: success, onFail: failure }); + return; + } + if (typeof ding.getAuthCode === "function") { + ding.getAuthCode({ onSuccess: success, onFail: failure }); + return; + } + throw new Error("当前钉钉版本不支持工作台免登JSAPI"); } catch (error) { failure(error); } @@ -142,21 +170,10 @@ function LoginPage({ onLogin }) { window.setTimeout(() => failure(new Error("钉钉免登请求超时,请确认应用已发布并开通免登权限")), 10000); }); } - async function loginWithDingTalk() { - setDingtalkLoading(true); - setError(""); - try { - const result = await api("/api/admin/auth/dingtalk/start"); - window.location.assign(result.data.url); - } catch (err) { - setError(err.message); - setDingtalkLoading(false); - setAutoDingtalkLogin(false); - } - } async function loginWithDingTalkWorkbench() { setDingtalkLoading(true); setError(""); + setDingtalkLoginFailed(false); try { const config = await api("/api/admin/auth/dingtalk/config"); if (!config.data?.enabled || !config.data?.corp_id) throw new Error("钉钉工作台免登尚未配置,请联系管理员"); @@ -165,6 +182,7 @@ function LoginPage({ onLogin }) { onLogin(result.data.user); } catch (err) { setError(err.message); + setDingtalkLoginFailed(true); setAutoDingtalkLogin(false); setDingtalkLoading(false); } @@ -188,7 +206,7 @@ function LoginPage({ onLogin }) { } } useEffect(() => { - if (!inDingtalkWorkbench || query.get("dingtalk") === "success" || autoLoginStarted.current) return; + if (!inDingtalkWorkbench || autoLoginStarted.current) return; autoLoginStarted.current = true; setAutoDingtalkLogin(true); loginWithDingTalkWorkbench(); @@ -196,6 +214,23 @@ function LoginPage({ onLogin }) { if (autoDingtalkLogin) { return
正在验证钉钉身份…
; } + if (inDingtalkWorkbench && dingtalkLoginFailed) { + return ( +
+
+
+
{Z.brand}
+

钉钉工作台免登

+

正在使用当前钉钉账号验证身份,不需要输入后台账号密码。

+
{error || "钉钉免登失败,请稍后重试"}
+ +
请从钉钉工作台打开此应用,并确认应用已发布、配置安全域名和免登权限。
+
+
+ ); + } return (
@@ -210,10 +245,7 @@ function LoginPage({ onLogin }) {
{Z.brand}

{Z.backend}

从钉钉工作台进入,使用钉钉身份安全登录

- -
或使用后台账号
+ {!inDingtalkWorkbench &&
运营后台账号登录
}