用户删除
This commit is contained in:
1 parent
0483b14e9b
commit
b75ce5b490
5 files changed
+155
-2
No files matched your search
@@ -16,6 +16,7 @@ interface UserRow {
|
|||||||
registered_at: string;
|
registered_at: string;
|
||||||
last_login_at: string | null;
|
last_login_at: string | null;
|
||||||
entitlement_count: number;
|
entitlement_count: number;
|
||||||
|
order_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EntitlementRow {
|
interface EntitlementRow {
|
||||||
@@ -52,11 +53,14 @@ export default function UsersPage() {
|
|||||||
const [sortKey, setSortKey] = useState('id');
|
const [sortKey, setSortKey] = useState('id');
|
||||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [notice, setNotice] = useState('');
|
||||||
const [editing, setEditing] = useState<UserRow | null>(null);
|
const [editing, setEditing] = useState<UserRow | null>(null);
|
||||||
const [form, setForm] = useState({ nickname: '', headicon_id: '0', status: '1' });
|
const [form, setForm] = useState({ nickname: '', headicon_id: '0', status: '1' });
|
||||||
const [courses, setCourses] = useState<CourseRow[]>([]);
|
const [courses, setCourses] = useState<CourseRow[]>([]);
|
||||||
const [entitlements, setEntitlements] = useState<EntitlementRow[]>([]);
|
const [entitlements, setEntitlements] = useState<EntitlementRow[]>([]);
|
||||||
const [grantCourseId, setGrantCourseId] = useState('');
|
const [grantCourseId, setGrantCourseId] = useState('');
|
||||||
|
/** 待确认删除的用户 */
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<UserRow | null>(null);
|
||||||
|
|
||||||
const load = useCallback((nextPage: number, nextKeyword: string) => {
|
const load = useCallback((nextPage: number, nextKeyword: string) => {
|
||||||
setError('');
|
setError('');
|
||||||
@@ -100,6 +104,7 @@ export default function UsersPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openEdit = (user: UserRow) => {
|
const openEdit = (user: UserRow) => {
|
||||||
|
setError('');
|
||||||
setEditing(user);
|
setEditing(user);
|
||||||
setForm({
|
setForm({
|
||||||
nickname: user.nickname ?? '',
|
nickname: user.nickname ?? '',
|
||||||
@@ -110,6 +115,25 @@ export default function UsersPage() {
|
|||||||
loadEntitlements(user.id);
|
loadEntitlements(user.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 删除用户,连带它的订单和权益一起删(服务端在同一个事务里做)。 */
|
||||||
|
const removeUser = () => {
|
||||||
|
if (!deleteTarget) return;
|
||||||
|
const target = deleteTarget;
|
||||||
|
setError('');
|
||||||
|
setNotice('');
|
||||||
|
api
|
||||||
|
.deleteUser(target.id)
|
||||||
|
.then((res) => {
|
||||||
|
setDeleteTarget(null);
|
||||||
|
setNotice(
|
||||||
|
`已删除用户 #${target.id}` +
|
||||||
|
(res ? `(连带 ${res.orders} 笔订单、${res.entitlements} 条权益)` : ''),
|
||||||
|
);
|
||||||
|
load(page, keyword);
|
||||||
|
})
|
||||||
|
.catch((err: { message: string }) => setError(err.message));
|
||||||
|
};
|
||||||
|
|
||||||
const saveUser = () => {
|
const saveUser = () => {
|
||||||
if (!editing) return;
|
if (!editing) return;
|
||||||
setError('');
|
setError('');
|
||||||
@@ -161,6 +185,7 @@ export default function UsersPage() {
|
|||||||
「主动登录」指用户自己点过登录(下单的前置条件),跟打开小程序时的静默登录无关,也跟有没有昵称无关。
|
「主动登录」指用户自己点过登录(下单的前置条件),跟打开小程序时的静默登录无关,也跟有没有昵称无关。
|
||||||
</p>
|
</p>
|
||||||
<Notice message={error} />
|
<Notice message={error} />
|
||||||
|
{notice && <Notice message={notice} kind="success" />}
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
title={`共 ${total} 位用户`}
|
title={`共 ${total} 位用户`}
|
||||||
@@ -253,6 +278,15 @@ export default function UsersPage() {
|
|||||||
<td className="px-2 py-2 text-right">
|
<td className="px-2 py-2 text-right">
|
||||||
<Button variant="ghost" onClick={() => openEdit(u)}>
|
<Button variant="ghost" onClick={() => openEdit(u)}>
|
||||||
编辑 / 权益
|
编辑 / 权益
|
||||||
|
</Button>{' '}
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => {
|
||||||
|
setError('');
|
||||||
|
setDeleteTarget(u);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -277,6 +311,38 @@ export default function UsersPage() {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={deleteTarget !== null}
|
||||||
|
title={`删除用户 #${deleteTarget?.id ?? ''}`}
|
||||||
|
onClose={() => setDeleteTarget(null)}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="ghost" onClick={() => setDeleteTarget(null)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button variant="danger" onClick={removeUser}>
|
||||||
|
确认删除
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* 删除失败(比如还有订单/权益)时把原因显示在弹窗里,否则提示会被弹窗挡住 */}
|
||||||
|
<Notice message={error} />
|
||||||
|
<div className="space-y-3 text-sm text-gray-600">
|
||||||
|
<p>
|
||||||
|
{deleteTarget?.nickname || '(无昵称)'}
|
||||||
|
<span className="ml-2 font-mono text-xs text-gray-400">{deleteTarget?.openid}</span>
|
||||||
|
</p>
|
||||||
|
<p className="rounded-md bg-red-50 px-3 py-2 text-red-700">
|
||||||
|
确认后会<span className="font-medium">连带删除</span>该用户的<b>全部订单和权益</b>
|
||||||
|
(该用户当前有 {deleteTarget?.order_count ?? 0} 笔订单、
|
||||||
|
{deleteTarget?.entitlement_count ?? 0} 条权益),然后删掉用户记录。
|
||||||
|
<span className="font-medium">此操作不可撤销。</span>
|
||||||
|
</p>
|
||||||
|
<p>删除后,对方用同一个微信再次打开小程序会重新注册成新用户(昵称要重新设置)。</p>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={editing !== null}
|
open={editing !== null}
|
||||||
title={`用户 #${editing?.id ?? ''}`}
|
title={`用户 #${editing?.id ?? ''}`}
|
||||||
@@ -290,6 +356,8 @@ export default function UsersPage() {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{/* 保存 / 发放 / 回收失败的原因也显示在弹窗里,不然会被弹窗挡住看不到 */}
|
||||||
|
<Notice message={error} />
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -138,6 +138,10 @@ export const api = {
|
|||||||
updateUser: (id: number, payload: Record<string, unknown>) =>
|
updateUser: (id: number, payload: Record<string, unknown>) =>
|
||||||
request<unknown>(`/admin/v1/users/${id}`, { method: 'PATCH', body: JSON.stringify(payload) }),
|
request<unknown>(`/admin/v1/users/${id}`, { method: 'PATCH', body: JSON.stringify(payload) }),
|
||||||
|
|
||||||
|
/** 删除用户,连带它的订单和权益一起删(返回连带删掉的行数) */
|
||||||
|
deleteUser: (id: number) =>
|
||||||
|
request<{ orders: number; entitlements: number }>(`/admin/v1/users/${id}`, { method: 'DELETE' }),
|
||||||
|
|
||||||
listResource: (resource: string, params: Record<string, string | number | undefined>) =>
|
listResource: (resource: string, params: Record<string, string | number | undefined>) =>
|
||||||
request<Paged<Record<string, unknown>>>(`/admin/v1/data/${resource}${qs(params)}`),
|
request<Paged<Record<string, unknown>>>(`/admin/v1/data/${resource}${qs(params)}`),
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"fileNames":["./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts",".Line truncated
|
{"fileNames":["./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts",".Line truncated
|
||||||
@@ -114,6 +114,12 @@ export class AdminController {
|
|||||||
return this.admin.updateUser(env, id, parsed.data);
|
return this.admin.updateUser(env, id, parsed.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 删除用户记录。只会删 users 这一行,其他表不动。 */
|
||||||
|
@Delete('users/:id')
|
||||||
|
deleteUser(@AdminEnvParam() env: DataSourceEnv, @Param('id', ParseIntPipe) id: number) {
|
||||||
|
return this.admin.deleteUser(env, id);
|
||||||
|
}
|
||||||
|
|
||||||
// ── 通用内容 CRUD ──────────────────────────────────────────────────────
|
// ── 通用内容 CRUD ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Get('data/:resource')
|
@Get('data/:resource')
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const NOTIFY_EVENT_LABEL: Record<string, string> = {
|
|||||||
xpay_complaint_notify: '用户投诉',
|
xpay_complaint_notify: '用户投诉',
|
||||||
xpay_wxpay_callback_notify: '微信支付风控事件',
|
xpay_wxpay_callback_notify: '微信支付风控事件',
|
||||||
user_enter_tempsession: '用户进入客服会话',
|
user_enter_tempsession: '用户进入客服会话',
|
||||||
|
admin_delete_user: '后台删除用户',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 客服消息在日志里的 event 形如 cs_message:text,按前缀归成一类展示。 */
|
/** 客服消息在日志里的 event 形如 cs_message:text,按前缀归成一类展示。 */
|
||||||
@@ -223,7 +224,7 @@ export class AdminService {
|
|||||||
orderBy,
|
orderBy,
|
||||||
skip: (current - 1) * size,
|
skip: (current - 1) * size,
|
||||||
take: size,
|
take: size,
|
||||||
include: { _count: { select: { entitlements: true } } },
|
include: { _count: { select: { entitlements: true, orders: true } } },
|
||||||
}),
|
}),
|
||||||
db.user.count({ where }),
|
db.user.count({ where }),
|
||||||
]);
|
]);
|
||||||
@@ -240,6 +241,7 @@ export class AdminService {
|
|||||||
registered_at: u.registered_at,
|
registered_at: u.registered_at,
|
||||||
last_login_at: u.last_login_at,
|
last_login_at: u.last_login_at,
|
||||||
entitlement_count: u._count.entitlements,
|
entitlement_count: u._count.entitlements,
|
||||||
|
order_count: u._count.orders,
|
||||||
})),
|
})),
|
||||||
total,
|
total,
|
||||||
page: current,
|
page: current,
|
||||||
@@ -251,6 +253,79 @@ export class AdminService {
|
|||||||
return this.prisma.clientFor(env).user.update({ where: { id }, data: input });
|
return this.prisma.clientFor(env).user.update({ where: { id }, data: input });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户,**连带它的订单和权益一起删**。
|
||||||
|
*
|
||||||
|
* 为什么要连带:外键是 RESTRICT,不先删子表就删不掉主表。
|
||||||
|
* 三个写操作放同一个事务,要么全成功要么都不动。
|
||||||
|
*
|
||||||
|
* 删掉之后,对方用同一个微信再次打开小程序会重新注册成新用户(openid 唯一,自动新建)。
|
||||||
|
*
|
||||||
|
* 注:refund_requests 对 orders 的外键是 SET NULL,所以那些申请会留着、
|
||||||
|
* 但指向的订单没了(后台显示「未匹配到订单」)。
|
||||||
|
*/
|
||||||
|
async deleteUser(
|
||||||
|
env: DataSourceEnv,
|
||||||
|
id: number,
|
||||||
|
): Promise<{ orders: number; entitlements: number }> {
|
||||||
|
const db = this.prisma.clientFor(env);
|
||||||
|
|
||||||
|
// 先把要删的东西连明细一起查出来——删完就查不到了,日志要靠这份数据
|
||||||
|
const [user, orders, entitlements] = await Promise.all([
|
||||||
|
db.user.findUnique({ where: { id } }),
|
||||||
|
db.order.findMany({
|
||||||
|
where: { user_id: id },
|
||||||
|
include: { course: { select: { title: true } } },
|
||||||
|
orderBy: { created_at: 'asc' },
|
||||||
|
}),
|
||||||
|
db.entitlement.findMany({
|
||||||
|
where: { user_id: id },
|
||||||
|
include: { course: { select: { title: true } } },
|
||||||
|
orderBy: { granted_at: 'asc' },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 记进「通知日志」:点开原始报文能看到删了哪些单、哪些权益。
|
||||||
|
// 和删除放同一个事务——删除不可撤销,日志是唯一凭据,不能出现「删了但没记上」。
|
||||||
|
const logData = {
|
||||||
|
event: 'admin_delete_user',
|
||||||
|
processed: 1,
|
||||||
|
err_msg: `后台删除用户 #${id}(连带 ${orders.length} 笔订单、${entitlements.length} 条权益)`,
|
||||||
|
raw_body: JSON.stringify(
|
||||||
|
{
|
||||||
|
用户: { id, openid: user?.openid ?? null, nickname: user?.nickname ?? null },
|
||||||
|
订单: orders.map((o) => ({
|
||||||
|
订单号: o.out_trade_no,
|
||||||
|
课程: o.course?.title ?? null,
|
||||||
|
金额分: o.amount_fen,
|
||||||
|
状态: o.status,
|
||||||
|
终端: o.terminal,
|
||||||
|
下单时间: o.created_at,
|
||||||
|
已退款时间: o.refunded_at,
|
||||||
|
})),
|
||||||
|
权益: entitlements.map((e) => ({
|
||||||
|
课程: e.course?.title ?? null,
|
||||||
|
来源: e.source,
|
||||||
|
授予时间: e.granted_at,
|
||||||
|
到期时间: e.expire_at,
|
||||||
|
备注: e.note,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.$transaction([
|
||||||
|
db.entitlement.deleteMany({ where: { user_id: id } }),
|
||||||
|
db.order.deleteMany({ where: { user_id: id } }),
|
||||||
|
db.user.delete({ where: { id } }),
|
||||||
|
db.paymentNotifyLog.create({ data: logData }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { orders: orders.length, entitlements: entitlements.length };
|
||||||
|
}
|
||||||
|
|
||||||
// ── 通用内容 CRUD ──────────────────────────────────────────────────────
|
// ── 通用内容 CRUD ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
async listResource(env: DataSourceEnv, name: string, page = 1, pageSize = 20, keyword = '', filter: Record<string, unknown> = {}) {
|
async listResource(env: DataSourceEnv, name: string, page = 1, pageSize = 20, keyword = '', filter: Record<string, unknown> = {}) {
|
||||||
|
|||||||
Reference in new issue
Block a user