用户删除

This commit is contained in:
Yeomai committed 2026-10-04 18:10:28 +08:00
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>
+4
View File
@@ -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
View File
@@ -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> = {}) {