feat: add server-side admin user pagination and align traffic report accounting
This commit is contained in:
@@ -886,6 +886,199 @@ const UserDB = {
|
||||
return db.prepare(query).all(...params);
|
||||
},
|
||||
|
||||
queryAdminUsers(options = {}) {
|
||||
const parsedPage = Number.parseInt(options.page, 10);
|
||||
const parsedPageSize = Number.parseInt(options.pageSize, 10);
|
||||
const safePage = Number.isFinite(parsedPage) && parsedPage > 0 ? parsedPage : 1;
|
||||
const safePageSize = Number.isFinite(parsedPageSize)
|
||||
? Math.min(200, Math.max(1, parsedPageSize))
|
||||
: 20;
|
||||
|
||||
const keyword = typeof options.keyword === 'string' ? options.keyword.trim() : '';
|
||||
const role = ['all', 'admin', 'user'].includes(options.role) ? options.role : 'all';
|
||||
const status = ['all', 'active', 'banned', 'unverified', 'download_blocked'].includes(options.status)
|
||||
? options.status
|
||||
: 'all';
|
||||
const storage = ['all', 'local', 'oss', 'local_only', 'oss_only', 'user_choice'].includes(options.storage)
|
||||
? options.storage
|
||||
: 'all';
|
||||
const sort = [
|
||||
'created_desc',
|
||||
'created_asc',
|
||||
'username_asc',
|
||||
'username_desc',
|
||||
'storage_usage_desc',
|
||||
'download_usage_desc'
|
||||
].includes(options.sort)
|
||||
? options.sort
|
||||
: 'created_desc';
|
||||
|
||||
const whereClauses = ['1=1'];
|
||||
const params = [];
|
||||
|
||||
if (keyword) {
|
||||
const likeKeyword = `%${keyword}%`;
|
||||
whereClauses.push('(CAST(id AS TEXT) LIKE ? OR username LIKE ? OR email LIKE ?)');
|
||||
params.push(likeKeyword, likeKeyword, likeKeyword);
|
||||
}
|
||||
|
||||
if (role === 'admin') {
|
||||
whereClauses.push('is_admin = 1');
|
||||
} else if (role === 'user') {
|
||||
whereClauses.push('is_admin = 0');
|
||||
}
|
||||
|
||||
if (status === 'banned') {
|
||||
whereClauses.push('is_banned = 1');
|
||||
} else if (status === 'unverified') {
|
||||
whereClauses.push('is_banned = 0 AND is_verified = 0');
|
||||
} else if (status === 'download_blocked') {
|
||||
whereClauses.push(`
|
||||
is_banned = 0
|
||||
AND is_verified = 1
|
||||
AND COALESCE(download_traffic_quota, 0) >= 0
|
||||
AND (
|
||||
COALESCE(download_traffic_quota, 0) = 0
|
||||
OR COALESCE(download_traffic_used, 0) >= COALESCE(download_traffic_quota, 0)
|
||||
)
|
||||
`);
|
||||
} else if (status === 'active') {
|
||||
whereClauses.push(`
|
||||
is_banned = 0
|
||||
AND is_verified = 1
|
||||
AND (
|
||||
COALESCE(download_traffic_quota, -1) < 0
|
||||
OR (
|
||||
COALESCE(download_traffic_quota, 0) > 0
|
||||
AND COALESCE(download_traffic_used, 0) < COALESCE(download_traffic_quota, 0)
|
||||
)
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
if (storage === 'local' || storage === 'oss') {
|
||||
whereClauses.push("COALESCE(current_storage_type, 'oss') = ?");
|
||||
params.push(storage);
|
||||
} else if (storage !== 'all') {
|
||||
whereClauses.push("COALESCE(storage_permission, 'oss_only') = ?");
|
||||
params.push(storage);
|
||||
}
|
||||
|
||||
const defaultOssQuota = 1024 * 1024 * 1024; // 1GB
|
||||
const storageUsageExpr = `
|
||||
CASE
|
||||
WHEN COALESCE(current_storage_type, 'oss') = 'local' THEN
|
||||
CASE
|
||||
WHEN COALESCE(local_storage_quota, 0) > 0
|
||||
THEN (COALESCE(local_storage_used, 0) * 100.0 / COALESCE(local_storage_quota, 1))
|
||||
ELSE 0
|
||||
END
|
||||
ELSE
|
||||
CASE
|
||||
WHEN COALESCE(oss_storage_quota, 0) > 0
|
||||
THEN (COALESCE(storage_used, 0) * 100.0 / COALESCE(oss_storage_quota, ${defaultOssQuota}))
|
||||
ELSE 0
|
||||
END
|
||||
END
|
||||
`;
|
||||
const downloadUsageExpr = `
|
||||
CASE
|
||||
WHEN COALESCE(download_traffic_quota, 0) < 0 THEN 0
|
||||
WHEN COALESCE(download_traffic_quota, 0) = 0 THEN 100
|
||||
ELSE MIN(
|
||||
100,
|
||||
COALESCE(download_traffic_used, 0) * 100.0 / MAX(COALESCE(download_traffic_quota, 0), 1)
|
||||
)
|
||||
END
|
||||
`;
|
||||
|
||||
const orderByMap = {
|
||||
created_desc: 'datetime(created_at) DESC, id DESC',
|
||||
created_asc: 'datetime(created_at) ASC, id ASC',
|
||||
username_asc: "LOWER(COALESCE(username, '')) ASC, id ASC",
|
||||
username_desc: "LOWER(COALESCE(username, '')) DESC, id DESC",
|
||||
storage_usage_desc: `${storageUsageExpr} DESC, id DESC`,
|
||||
download_usage_desc: `${downloadUsageExpr} DESC, id DESC`
|
||||
};
|
||||
const orderBySql = orderByMap[sort] || orderByMap.created_desc;
|
||||
const whereSql = whereClauses.join(' AND ');
|
||||
|
||||
const globalTotalRow = db.prepare('SELECT COUNT(*) AS total FROM users').get();
|
||||
const globalTotal = Number(globalTotalRow?.total || 0);
|
||||
|
||||
const totalRow = db.prepare(`
|
||||
SELECT COUNT(*) AS total
|
||||
FROM users
|
||||
WHERE ${whereSql}
|
||||
`).get(...params);
|
||||
const filteredTotal = Number(totalRow?.total || 0);
|
||||
|
||||
const summaryRow = db.prepare(`
|
||||
SELECT
|
||||
COUNT(*) AS filtered_total,
|
||||
SUM(CASE WHEN is_banned = 1 THEN 1 ELSE 0 END) AS banned,
|
||||
SUM(CASE WHEN is_banned = 0 AND is_verified = 0 THEN 1 ELSE 0 END) AS unverified,
|
||||
SUM(
|
||||
CASE
|
||||
WHEN is_banned = 0
|
||||
AND is_verified = 1
|
||||
AND COALESCE(download_traffic_quota, 0) >= 0
|
||||
AND (
|
||||
COALESCE(download_traffic_quota, 0) = 0
|
||||
OR COALESCE(download_traffic_used, 0) >= COALESCE(download_traffic_quota, 0)
|
||||
)
|
||||
THEN 1 ELSE 0
|
||||
END
|
||||
) AS download_blocked,
|
||||
SUM(
|
||||
CASE
|
||||
WHEN is_banned = 0
|
||||
AND is_verified = 1
|
||||
AND (
|
||||
COALESCE(download_traffic_quota, -1) < 0
|
||||
OR (
|
||||
COALESCE(download_traffic_quota, 0) > 0
|
||||
AND COALESCE(download_traffic_used, 0) < COALESCE(download_traffic_quota, 0)
|
||||
)
|
||||
)
|
||||
THEN 1 ELSE 0
|
||||
END
|
||||
) AS active
|
||||
FROM users
|
||||
WHERE ${whereSql}
|
||||
`).get(...params);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filteredTotal / safePageSize));
|
||||
const page = Math.min(Math.max(1, safePage), totalPages);
|
||||
const offset = (page - 1) * safePageSize;
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE ${whereSql}
|
||||
ORDER BY ${orderBySql}
|
||||
LIMIT ? OFFSET ?
|
||||
`).all(...params, safePageSize, offset);
|
||||
|
||||
return {
|
||||
rows,
|
||||
pagination: {
|
||||
page,
|
||||
pageSize: safePageSize,
|
||||
total: filteredTotal,
|
||||
totalPages
|
||||
},
|
||||
summary: {
|
||||
global_total: Number.isFinite(globalTotal) ? globalTotal : 0,
|
||||
filtered_total: Number(summaryRow?.filtered_total || 0),
|
||||
active: Number(summaryRow?.active || 0),
|
||||
banned: Number(summaryRow?.banned || 0),
|
||||
unverified: Number(summaryRow?.unverified || 0),
|
||||
download_blocked: Number(summaryRow?.download_blocked || 0)
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// 删除用户
|
||||
delete(id) {
|
||||
return db.prepare('DELETE FROM users WHERE id = ?').run(id);
|
||||
|
||||
Reference in New Issue
Block a user