From e5e6a646055b538d358930eca80219b4cea568bc Mon Sep 17 00:00:00 2001 From: no_gen_mu Date: Sat, 4 Jul 2026 15:04:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(users):=20=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=A0=E6=B3=A8=E9=94=80=E8=B4=A6=E5=8F=B7=E5=85=A5?= =?UTF-8?q?=E5=8F=A3(super=5Fadmin)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配合后端注销级联清理 + 删除端点(app-server PR #115),后台加删除入口。 - 新增 DeleteUserModal:受控弹窗, 必填「注销原因(入审计)」, 红字说明删什么/保留什么, api.delete 带 body 调 DELETE /admin/api/users/{id}, 后端 409(有未提现钱/在审提现)经 errMsg 原样透出 - 用户详情页:顶部加红色「注销账号」按钮(canDo(['super_admin']), deleted 状态隐藏) - 用户列表页:操作列加红色「注销」内联链接(同门控), 列宽 300→340 前端 super_admin 门控与后端 require_role("super_admin") 对齐, 门控被绕过后端亦兜底。 next build ✓ Compiled successfully(tsc + eslint + 21 路由)。 Co-Authored-By: Claude Opus 4.8 --- src/app/(main)/users/[id]/page.tsx | 14 +++++++ src/app/(main)/users/page.tsx | 18 ++++++++- src/components/DeleteUserModal.tsx | 61 ++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 src/components/DeleteUserModal.tsx diff --git a/src/app/(main)/users/[id]/page.tsx b/src/app/(main)/users/[id]/page.tsx index 2c1c1dc..47e2c11 100644 --- a/src/app/(main)/users/[id]/page.tsx +++ b/src/app/(main)/users/[id]/page.tsx @@ -23,6 +23,7 @@ import { canDo } from '@/lib/auth'; import { formatUtcTime, formatWallTime, yuan } from '@/lib/format'; import { useCursorList } from '@/lib/useCursorList'; import { AdjustCashModal, AdjustCoinModal } from '@/components/AdjustBalanceModals'; +import { DeleteUserModal } from '@/components/DeleteUserModal'; import type { CashTxn, CoinTxn, UserOverview, WithdrawOrder } from '@/lib/types'; export default function UserDetailPage() { @@ -33,6 +34,7 @@ export default function UserDetailPage() { const [err, setErr] = useState(null); const [coinOpen, setCoinOpen] = useState(false); const [cashOpen, setCashOpen] = useState(false); + const [delOpen, setDelOpen] = useState(false); const loadData = useCallback(() => { setErr(null); @@ -52,6 +54,7 @@ export default function UserDetailPage() { const canCoins = canDo(['finance']); const canStatus = canDo(['operator']); + const canDelete = canDo(['super_admin']); // 注销账号最高危,仅 super_admin(与后端 require_role 对齐) const toggleStatus = () => { if (!data) return; @@ -154,6 +157,11 @@ export default function UserDetailPage() { {data.user.status === 'active' ? '封禁' : '解封'} )} + {canDelete && data.user.status !== 'deleted' && ( + + )} @@ -272,6 +280,12 @@ export default function UserDetailPage() { cash.reload(); }} /> + setDelOpen(false)} + onDone={loadData} + /> ); } diff --git a/src/app/(main)/users/page.tsx b/src/app/(main)/users/page.tsx index d53269b..3f5d532 100644 --- a/src/app/(main)/users/page.tsx +++ b/src/app/(main)/users/page.tsx @@ -12,6 +12,7 @@ import { canDo } from '@/lib/auth'; import { formatUtcTime } from '@/lib/format'; import { usePagedList } from '@/lib/usePagedList'; import { AdjustCashModal, AdjustCoinModal } from '@/components/AdjustBalanceModals'; +import { DeleteUserModal } from '@/components/DeleteUserModal'; import type { UserListItem } from '@/lib/types'; const { RangePicker } = DatePicker; @@ -43,9 +44,11 @@ export default function UsersPage() { const canCoins = canDo(['finance']); const canStatus = canDo(['operator']); + const canDelete = canDo(['super_admin']); // 注销账号最高危,仅 super_admin(与后端 require_role 对齐) const [coinUser, setCoinUser] = useState(null); const [cashUser, setCashUser] = useState(null); + const [delUser, setDelUser] = useState(null); const [selectedRowKeys, setSelectedRowKeys] = useState([]); // 筛选/排序变化后清空已选(避免选中项跨筛选错位) @@ -246,7 +249,7 @@ export default function UsersPage() { title: '操作', key: 'op', fixed: 'right', - width: 300, + width: 340, render: (_: unknown, u: UserListItem) => ( e.stopPropagation()}> router.push(`/users/${u.id}`)}>详情 @@ -269,6 +272,11 @@ export default function UsersPage() { ) : ( setForceOnboarding(u, true)}>开启新手引导 ))} + {canDelete && u.status !== 'deleted' && ( + setDelUser(u)}> + 注销 + + )} ), }, @@ -377,12 +385,18 @@ export default function UsersPage() { showTotal: (t) => `共 ${t} 个用户`, onChange: onPageChange, }} - scroll={{ x: 1160 }} + scroll={{ x: 1200 }} onChange={onTableChange} /> setCoinUser(null)} /> setCashUser(null)} /> + setDelUser(null)} + onDone={reload} + /> ); } diff --git a/src/components/DeleteUserModal.tsx b/src/components/DeleteUserModal.tsx new file mode 100644 index 0000000..7b92709 --- /dev/null +++ b/src/components/DeleteUserModal.tsx @@ -0,0 +1,61 @@ +'use client'; + +// 注销/删除账号弹窗(受控)。列表页与用户详情页共用,避免两处各写一套(同 AdjustBalanceModals)。 +// 复用 C 端自助注销同一套后端清理:物理删除个人内容/行为/PII 表 + 清零余额 + 匿名化 user 行, +// 保留资金流水/邀请关系/广告幂等+对账表。仅 super_admin 可见入口;后端亦 require_role("super_admin") 兜底。 +// 后端资金前置闸(有未提现现金/邀请金 / 在审提现单 → 409)带明确 detail,errMsg 原样透出。 +import { App, Form, Input, Modal } from 'antd'; +import { api, errMsg } from '@/lib/api'; + +export type DeleteTargetUser = { id: number; phone: string }; + +interface Props { + user: DeleteTargetUser | null; + open: boolean; + onClose: () => void; + onDone?: () => void; // 删除成功后回调(刷新详情/列表) +} + +export function DeleteUserModal({ user, open, onClose, onDone }: Props) { + const { message } = App.useApp(); + const [form] = Form.useForm(); + + const submit = async () => { + if (!user) return; + const v = await form.validateFields(); // 原因必填,缺失即红字阻断 + try { + // axios 的 DELETE 带 body 要放在 config.data 里 + await api.delete(`/admin/api/users/${user.id}`, { data: { reason: v.reason } }); + message.success('账号已注销'); + onClose(); + onDone?.(); + } catch (e) { + message.error(errMsg(e)); + } + }; + + return ( + { + if (o) form.resetFields(); + }} + destroyOnHidden + > +

+ 将永久删除该账号的个人数据(比价/省钱/签到/领券/埋点/设备/邀请指纹等)并清零余额、匿名化手机号, + 不可恢复。资金流水/邀请关系/广告对账记录按合规保留。 +

+
+ + + +
+
+ ); +}