0f2afdaac1
按需求四项: 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>
107 lines
5.2 KiB
TypeScript
107 lines
5.2 KiB
TypeScript
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>
|
||
);
|
||
}
|