Files
sundynix-agentix/sundynix-admin/src/pages/LoginConfigPage.tsx
T
Blizzard 0f2afdaac1 feat(admin): 微信用户观测页 + 唯一昵称 + 密钥明文回显(单管理员后台)
按需求四项:
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 <noreply@anthropic.com>
2026-07-21 10:36:27 +08:00

107 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { getWechatMP, saveWechatMP, GATEWAY, type WechatMPConfig } from "../api";
// 运维 · 登录设置:微信公众号扫码登录(带参二维码 + 关注/扫码事件)。
// AppSecret 明文回显(单管理员后台,方便核对/复制;密文仍加密入库)。
export function LoginConfigPage() {
const [cfg, setCfg] = useState<WechatMPConfig | null>(null);
const [appid, setAppid] = useState("");
const [token, setToken] = useState("");
const [secret, setSecret] = useState(""); // 留空=沿用已存
const [busy, setBusy] = useState(false);
const [err, setErr] = useState("");
const [ok, setOk] = useState(false);
useEffect(() => {
getWechatMP()
.then((c) => {
setCfg(c);
setAppid(c.appid);
setToken(c.token);
setSecret(c.app_secret);
})
.catch((e) => setErr((e as Error).message));
}, []);
const save = async () => {
if (busy) return;
setBusy(true);
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));
setOk(true);
} catch (e) {
setErr((e as Error).message);
} finally {
setBusy(false);
}
};
// 回调 URL 用当前后端地址推断(生产 GATEWAY 为空串=同源,显示成本域名)
const origin = GATEWAY || (typeof window !== "undefined" ? window.location.origin : "");
const callbackURL = origin.replace(/:\d+$/, "") + "/wx/mp/callback";
return (
<div className="space-y-5">
<div className="border-b border-gray-200 pb-4">
<h3 className="text-base font-semibold text-gray-800"> · </h3>
<p className="text-xs text-gray-400">AppSecret </p>
</div>
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center gap-2">
<h4 className="text-sm font-semibold text-gray-700"></h4>
{cfg?.enabled ? (
<span className="rounded bg-emerald-50 px-2 py-0.5 text-[10px] font-medium text-emerald-600"></span>
) : (
<span className="rounded bg-gray-100 px-2 py-0.5 text-[10px] text-gray-500"></span>
)}
</div>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<label className="text-xs text-gray-500">
AppID
<input value={appid} onChange={(e) => setAppid(e.target.value)} placeholder="wx..."
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" />
</label>
<label className="text-xs text-gray-500">
AppSecret
<input value={secret} onChange={(e) => setSecret(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" />
</label>
<label className="text-xs text-gray-500 md:col-span-2">
Token
<input value={token} onChange={(e) => 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" />
</label>
</div>
{err && <p className="mt-2 text-xs text-rose-500">{err}</p>}
{ok && <p className="mt-2 text-xs text-emerald-600"></p>}
<div className="mt-3">
<button onClick={() => void save()} disabled={busy}
className="rounded-lg bg-violet-600 px-3.5 py-1.5 text-sm text-white hover:bg-violet-700 disabled:opacity-40">
{busy ? "保存中…" : "保存"}
</button>
</div>
</div>
{/* 配置清单:把「必须在公众平台做什么」写在眼前,省得来回翻文档 */}
<div className="rounded-xl border border-amber-100 bg-amber-50/40 p-5 text-xs leading-relaxed text-gray-600">
<h4 className="mb-2 text-sm font-semibold text-gray-700"></h4>
<ol className="list-decimal space-y-1.5 pl-4">
<li>(URL) <code className="break-all rounded bg-white px-1">{callbackURL}</code></li>
<li>(Token) <b></b></li>
<li> <b></b></li>
<li>IP IP access_token 40164</li>
<li></li>
</ol>
</div>
</div>
);
}