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>
77 lines
3.1 KiB
TypeScript
77 lines
3.1 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { adminWechatUsers, type WechatUserRow } from "../api";
|
||
|
||
// 平台 · 微信用户:所有微信扫码登录的用户,集中查看。
|
||
// 每个用户仍是独立租户/独立积分(各买各的),这里只做统一观测。
|
||
const MICRO = 1_000_000;
|
||
const credits = (m: number) => (m / MICRO).toLocaleString("zh-CN", { maximumFractionDigits: 2 });
|
||
|
||
export function WechatUsersPage() {
|
||
const [rows, setRows] = useState<WechatUserRow[]>([]);
|
||
const [err, setErr] = useState("");
|
||
const [loading, setLoading] = useState(true);
|
||
const [copied, setCopied] = useState("");
|
||
|
||
useEffect(() => {
|
||
adminWechatUsers()
|
||
.then(setRows)
|
||
.catch((e) => setErr((e as Error).message))
|
||
.finally(() => setLoading(false));
|
||
}, []);
|
||
|
||
const copy = (s: string) => {
|
||
void navigator.clipboard?.writeText(s);
|
||
setCopied(s);
|
||
setTimeout(() => setCopied(""), 1200);
|
||
};
|
||
|
||
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">
|
||
扫码登录的用户 · 共 {rows.length} 人 · 每人独立租户与积分(各买各的)
|
||
</p>
|
||
</div>
|
||
|
||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||
{err && <p className="mb-2 text-xs text-rose-500">{err}</p>}
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b border-gray-100 text-left text-[11px] uppercase tracking-wide text-gray-400">
|
||
<th className="py-2 pr-3 font-medium">昵称</th>
|
||
<th className="py-2 pr-3 font-medium">OpenID</th>
|
||
<th className="py-2 pr-3 text-right font-medium">积分余额</th>
|
||
<th className="py-2 font-medium">加入时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{rows.map((u) => (
|
||
<tr key={u.id} className="border-b border-gray-50 last:border-0">
|
||
<td className="py-2 pr-3 text-gray-800">{u.name || "—"}</td>
|
||
<td className="py-2 pr-3">
|
||
{/* openid 长且要复制,点一下复制全串 */}
|
||
<button
|
||
onClick={() => copy(u.wechat_openid)}
|
||
title="点击复制"
|
||
className="font-mono text-[11px] text-gray-500 hover:text-violet-600"
|
||
>
|
||
{copied === u.wechat_openid ? "已复制 ✓" : `${u.wechat_openid.slice(0, 10)}…${u.wechat_openid.slice(-6)}`}
|
||
</button>
|
||
</td>
|
||
<td className="py-2 pr-3 text-right tabular-nums text-gray-700">{credits(u.balance_micro)}</td>
|
||
<td className="py-2 text-xs text-gray-500">{new Date(u.created_at).toLocaleString("zh-CN")}</td>
|
||
</tr>
|
||
))}
|
||
{rows.length === 0 && !loading && (
|
||
<tr>
|
||
<td colSpan={4} className="py-8 text-center text-xs text-gray-400">还没有微信用户</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|