From 0f2afdaac1ad5f5ff25426b633fc645d59833541 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Tue, 21 Jul 2026 10:36:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=BE=AE=E4=BF=A1=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=A7=82=E6=B5=8B=E9=A1=B5=20+=20=E5=94=AF=E4=B8=80?= =?UTF-8?q?=E6=98=B5=E7=A7=B0=20+=20=E5=AF=86=E9=92=A5=E6=98=8E=E6=96=87?= =?UTF-8?q?=E5=9B=9E=E6=98=BE=EF=BC=88=E5=8D=95=E7=AE=A1=E7=90=86=E5=91=98?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按需求四项: 1. 微信新用户昵称改「微信用户_XXXXX」:后缀从 openid 的 sha1 派生(去混淆字母表), openid 唯一 → 后缀实际不重复,且同 openid 每次一致(重登不换名)。用户可自行改名。 2. 后台加「平台 → 微信用户」列表:昵称 / openid(点击复制)/ 积分余额 / 加入时间。 每人仍独立租户与积分(各买各的,确认过不共享积分池),此页只做统一观测。 3. 登录设置的 AppSecret 明文回显(不再只显示"已保存")。 4. 支付配置的 APIv3 密钥明文回显。 —— 用户明确后台单人使用、RequireAdmin 已拦,接受这一安全降级;密文仍加密入库。 修一个 gorm Scan 坑:WechatUserRow 的 WechatOpenID/BalanceMicro 没加 column tag, gorm 把 WechatOpenID 断成列名 wechat_open_id,与 SQL alias wechat_openid 对不上 → openid 静默返回空。教训:Scan 到自定义结构 + SQL 用 alias 时,字段一律显式加 column tag。 本地验证:真库造两个微信用户,列表接口正确返回 openid(修 tag 前是空); 登录设置页 AppSecret 已是可见文本框;nickname 单测覆盖唯一/稳定/去混淆。 Co-Authored-By: Claude Opus 4.8 --- sundynix-admin/src/api.ts | 22 +++++- .../src/components/TopupChannels.tsx | 13 ++-- sundynix-admin/src/pages/LoginConfigPage.tsx | 14 ++-- sundynix-admin/src/pages/WechatUsersPage.tsx | 76 +++++++++++++++++++ sundynix-admin/src/routes.tsx | 8 ++ .../internal/handler/payment_admin.go | 2 +- .../internal/handler/wechat_login.go | 31 ++++++-- .../internal/handler/wechat_nickname_test.go | 37 +++++++++ sundynix-gateway/internal/router/router.go | 1 + sundynix-gateway/internal/store/user.go | 34 +++++++++ 10 files changed, 214 insertions(+), 24 deletions(-) create mode 100644 sundynix-admin/src/pages/WechatUsersPage.tsx create mode 100644 sundynix-gateway/internal/handler/wechat_nickname_test.go diff --git a/sundynix-admin/src/api.ts b/sundynix-admin/src/api.ts index 9122b0c..83b2fbc 100644 --- a/sundynix-admin/src/api.ts +++ b/sundynix-admin/src/api.ts @@ -231,15 +231,31 @@ export async function listRedeemCodes(): Promise { export interface WechatMPConfig { appid: string; token: string; // 消息推送签名校验用(与公众平台服务器配置一致) - has_app_secret: boolean; + app_secret: string; // 单管理员后台:明文回显 enabled: boolean; } +export interface WechatUserRow { + id: string; + name: string; + wechat_openid: string; + created_at: string; + tenant_name: string; + balance_micro: number; +} + +export async function adminWechatUsers(): Promise { + const res = guard(await fetch(`${ADMIN}/wechat-users`, { headers: authHeaders() })); + const d = (await res.json().catch(() => ({}))) as { users?: WechatUserRow[]; error?: string }; + if (!res.ok) throw new Error(d.error ?? `wechat users failed: ${res.status}`); + return d.users ?? []; +} + 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 ?? "", has_app_secret: !!d.has_app_secret, enabled: !!d.enabled }; + return { appid: d.appid ?? "", token: d.token ?? "", app_secret: d.app_secret ?? "", enabled: !!d.enabled }; } // app_secret 传空串 = 沿用已保存的。 @@ -258,7 +274,7 @@ export interface WechatPayConfig { public_key_id: string; // PUB_KEY_ID_ 开头 appid: string; notify_url: string; - has_apiv3_key: boolean; + apiv3_key: string; // 单管理员后台:明文回显 } export async function getWechatPay(): Promise<{ config: WechatPayConfig; enabled: boolean; reason: string }> { diff --git a/sundynix-admin/src/components/TopupChannels.tsx b/sundynix-admin/src/components/TopupChannels.tsx index 2518c8c..1429fc7 100644 --- a/sundynix-admin/src/components/TopupChannels.tsx +++ b/sundynix-admin/src/components/TopupChannels.tsx @@ -27,10 +27,9 @@ export function TopupChannels() { } // ---- 微信支付配置:DB 存储、保存即热生效(不重启 gateway)。 -// APIv3 密钥只写不回显(密文入库,与模型 API Key 同一把密钥加密); -// 商户证书私钥文件放服务器磁盘,这里只填路径。 +// APIv3 密钥密文入库,但单管理员后台明文回显方便核对;证书私钥文件放服务器磁盘,此处只填路径。 function WechatConfigBlock() { - const empty: WechatPayConfig = { mchid: "", cert_serial: "", private_key_path: "", public_key_path: "", public_key_id: "", appid: "", notify_url: "", has_apiv3_key: false }; + const empty: WechatPayConfig = { mchid: "", cert_serial: "", private_key_path: "", public_key_path: "", public_key_id: "", appid: "", notify_url: "", apiv3_key: "" }; const [cfg, setCfg] = useState(empty); const [apiv3, setApiv3] = useState(""); // 留空=沿用已存 const [enabled, setEnabled] = useState(false); @@ -42,6 +41,7 @@ function WechatConfigBlock() { getWechatPay() .then((r) => { setCfg(r.config); + setApiv3(r.config.apiv3_key); setEnabled(r.enabled); setReason(r.reason); }) @@ -65,8 +65,6 @@ function WechatConfigBlock() { }); setEnabled(r.enabled); setReason(r.reason); - setApiv3(""); - if (apiv3) setCfg((c) => ({ ...c, has_apiv3_key: true })); } catch (e) { setErr((e as Error).message); } finally { @@ -108,12 +106,11 @@ function WechatConfigBlock() { {field("公钥 ID(PUB_KEY_ID_ 开头)", "public_key_id", "PUB_KEY_ID_01…")} {field("微信支付公钥文件路径(服务器磁盘)", "public_key_path", "/etc/sundynix/wechat/pub_key.pem", "md:col-span-2")} diff --git a/sundynix-admin/src/pages/LoginConfigPage.tsx b/sundynix-admin/src/pages/LoginConfigPage.tsx index fc0671c..077042c 100644 --- a/sundynix-admin/src/pages/LoginConfigPage.tsx +++ b/sundynix-admin/src/pages/LoginConfigPage.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { getWechatMP, saveWechatMP, GATEWAY, type WechatMPConfig } from "../api"; // 运维 · 登录设置:微信公众号扫码登录(带参二维码 + 关注/扫码事件)。 -// AppSecret 只写不回显(密文入库,与微信支付 APIv3 密钥同一套加密)。 +// AppSecret 明文回显(单管理员后台,方便核对/复制;密文仍加密入库)。 export function LoginConfigPage() { const [cfg, setCfg] = useState(null); const [appid, setAppid] = useState(""); @@ -18,6 +18,7 @@ export function LoginConfigPage() { setCfg(c); setAppid(c.appid); setToken(c.token); + setSecret(c.app_secret); }) .catch((e) => setErr((e as Error).message)); }, []); @@ -29,8 +30,7 @@ export function LoginConfigPage() { setOk(false); try { const r = await saveWechatMP({ appid: appid.trim(), app_secret: secret, token: token.trim() }); - setSecret(""); - setCfg((c) => (c ? { ...c, appid: appid.trim(), token: token.trim(), has_app_secret: c.has_app_secret || !!secret, enabled: r.enabled } : c)); + setCfg((c) => (c ? { ...c, appid: appid.trim(), token: token.trim(), app_secret: secret, enabled: r.enabled } : c)); setOk(true); } catch (e) { setErr((e as Error).message); @@ -47,7 +47,7 @@ export function LoginConfigPage() {

登录设置 · 微信扫码

-

公众号扫码登录(登录即引导关注)。AppSecret 加密入库、只写不回显,随时可换

+

公众号扫码登录(登录即引导关注)。AppSecret 加密入库、后台明文可见,随时可换

@@ -67,9 +67,9 @@ export function LoginConfigPage() { 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" />