feat: 支持注销用户并展示普通用户使用情况

- 新增管理员注销用户接口,要求输入用户名二次确认
- 注销时清理用户当前图片、视频、文件、标签、会话和自身日志
- 保留备份任务、管理员审计数据和全局配置
- 用户列表展示图片、视频、文件占用,管理员行隐藏使用情况和操作按钮
- 补充注销流程与用量统计测试
This commit is contained in:
tianmo committed 2026-10-19 17:48:44 +08:00
1 parent dd0005b7ba
commit ccfdac2e1b
8 files changed
+394 -24

No files matched your search

+42
View File
@@ -459,6 +459,12 @@ export interface AdminUserRow {
password_reset: number
}
export interface AdminUserUsage {
imageBytes: number
videoBytes: number
fileBytes: number
}
/** 管理员用户列表 */
export function listAdminUsers(): AdminUserRow[] {
ensurePasswordResetColumn()
@@ -468,6 +474,42 @@ export function listAdminUsers(): AdminUserRow[] {
`).all() as unknown as AdminUserRow[]
}
/** 管理员用户列表,附带图片、视频、文件占用 */
export function listAdminUsersWithUsage(): Array<AdminUserRow & { usage: AdminUserUsage }> {
const usage = new Map<number, AdminUserUsage>()
const rows = getDb().prepare(`
SELECT user_id, media_type, COALESCE(SUM(size), 0) AS bytes
FROM images
WHERE user_id IS NOT NULL
GROUP BY user_id, media_type
`).all() as Array<{ user_id: number, media_type: string, bytes: number }>
for (const row of rows) {
const entry = usage.get(row.user_id) ?? {
imageBytes: 0,
videoBytes: 0,
fileBytes: 0
}
if (row.media_type === 'video') {
entry.videoBytes = Number(row.bytes)
} else if (row.media_type === 'file') {
entry.fileBytes = Number(row.bytes)
} else {
entry.imageBytes = Number(row.bytes)
}
usage.set(row.user_id, entry)
}
return listAdminUsers().map(user => ({
...user,
usage: usage.get(user.id) ?? {
imageBytes: 0,
videoBytes: 0,
fileBytes: 0
}
}))
}
/** 清空密码并标记需要重置 */
export function clearUserPassword(userId: number): void {
ensurePasswordResetColumn()