From 83269e067a13c0d70af775a237228a977c2e7c99 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Tue, 21 Jul 2026 11:17:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(wechat):=20=E5=85=B3=E6=B3=A8=E5=85=AC?= =?UTF-8?q?=E4=BC=97=E5=8F=B7=E5=90=8E=E8=87=AA=E5=8A=A8=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?=E6=AC=A2=E8=BF=8E=E8=AF=AD=EF=BC=88=E8=A2=AB=E5=8A=A8=E5=9B=9E?= =?UTF-8?q?=E5=A4=8D=EF=BC=8C=E5=8F=AF=E5=90=8E=E5=8F=B0=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户关注服务号(扫登录码后关注 或 直接搜索关注)时,在回调 HTTP 响应里 回一条文本消息(微信「被动回复」)。被动回复不需要 access_token、不受 IP 白名单 限制,永远能发。欢迎语在管理端「登录设置」可配,留空用默认。 Co-Authored-By: Claude Opus 4.8 --- sundynix-admin/src/api.ts | 5 ++- sundynix-admin/src/pages/LoginConfigPage.tsx | 12 +++++- .../internal/handler/wechat_login.go | 36 ++++++++++++---- sundynix-gateway/internal/wechat/mp.go | 23 +++++++++- sundynix-gateway/internal/wechat/mp_test.go | 42 +++++++++++++++++++ 5 files changed, 106 insertions(+), 12 deletions(-) diff --git a/sundynix-admin/src/api.ts b/sundynix-admin/src/api.ts index 83b2fbc..03618aa 100644 --- a/sundynix-admin/src/api.ts +++ b/sundynix-admin/src/api.ts @@ -232,6 +232,7 @@ export interface WechatMPConfig { appid: string; token: string; // 消息推送签名校验用(与公众平台服务器配置一致) app_secret: string; // 单管理员后台:明文回显 + welcome: string; // 关注后自动回复的欢迎语(空=用默认) enabled: boolean; } @@ -255,11 +256,11 @@ export async function getWechatMP(): Promise { const res = guard(await fetch(`${ADMIN}/wechat-mp`, { headers: authHeaders() })); const d = (await res.json().catch(() => ({}))) as Partial & { error?: string }; if (!res.ok) throw new Error(d.error ?? `wechat-mp failed: ${res.status}`); - return { appid: d.appid ?? "", token: d.token ?? "", app_secret: d.app_secret ?? "", enabled: !!d.enabled }; + return { appid: d.appid ?? "", token: d.token ?? "", app_secret: d.app_secret ?? "", welcome: d.welcome ?? "", enabled: !!d.enabled }; } // app_secret 传空串 = 沿用已保存的。 -export async function saveWechatMP(body: { appid: string; app_secret: string; token: string }): Promise<{ enabled: boolean }> { +export async function saveWechatMP(body: { appid: string; app_secret: string; token: string; welcome: string }): Promise<{ enabled: boolean }> { const res = guard(await fetch(`${ADMIN}/wechat-mp`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify(body) })); const d = (await res.json().catch(() => ({}))) as { enabled?: boolean; error?: string }; if (!res.ok) throw new Error(d.error ?? `save failed: ${res.status}`); diff --git a/sundynix-admin/src/pages/LoginConfigPage.tsx b/sundynix-admin/src/pages/LoginConfigPage.tsx index 077042c..ffce5f7 100644 --- a/sundynix-admin/src/pages/LoginConfigPage.tsx +++ b/sundynix-admin/src/pages/LoginConfigPage.tsx @@ -8,6 +8,7 @@ export function LoginConfigPage() { const [appid, setAppid] = useState(""); const [token, setToken] = useState(""); const [secret, setSecret] = useState(""); // 留空=沿用已存 + const [welcome, setWelcome] = useState(""); const [busy, setBusy] = useState(false); const [err, setErr] = useState(""); const [ok, setOk] = useState(false); @@ -19,6 +20,7 @@ export function LoginConfigPage() { setAppid(c.appid); setToken(c.token); setSecret(c.app_secret); + setWelcome(c.welcome); }) .catch((e) => setErr((e as Error).message)); }, []); @@ -29,8 +31,8 @@ export function LoginConfigPage() { setErr(""); setOk(false); try { - const r = await saveWechatMP({ appid: appid.trim(), app_secret: secret, token: token.trim() }); - setCfg((c) => (c ? { ...c, appid: appid.trim(), token: token.trim(), app_secret: secret, enabled: r.enabled } : c)); + const r = await saveWechatMP({ appid: appid.trim(), app_secret: secret, token: token.trim(), welcome: welcome.trim() }); + setCfg((c) => (c ? { ...c, appid: appid.trim(), token: token.trim(), app_secret: secret, welcome: welcome.trim(), enabled: r.enabled } : c)); setOk(true); } catch (e) { setErr((e as Error).message); @@ -77,6 +79,12 @@ export function LoginConfigPage() { setToken(e.target.value)} placeholder="自定义一串字母数字" className="mt-1 block w-full rounded-lg border border-gray-200 px-2.5 py-1.5 font-mono text-sm text-gray-800 focus:border-violet-400 focus:outline-none" /> +