Files
BYW-VoiceHub/server/api/admin/users/batch.post.ts
T

211 lines
6.0 KiB
TypeScript

import bcrypt from 'bcryptjs'
import { db } from '~/drizzle/db'
import { users } from '~/drizzle/schema'
import { eq, inArray } from 'drizzle-orm'
import { getAdminPasswordViolation } from '~~/server/utils/admin-password-policy'
interface UserData {
name: string
username: string
password: string
role?: string
status?: 'active' | 'withdrawn' | 'graduate'
grade?: string | null
class?: string | null
}
interface ValidUserData extends UserData {
index: number
}
const normalizeRequiredText = (value: unknown) => String(value || '').trim()
const normalizeOptionalText = (value: unknown) => {
const normalized = String(value || '').trim()
return normalized || null
}
export default defineEventHandler(async (event) => {
// 检查认证和权限
const user = event.context.user
if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
throw createError({
statusCode: 403,
message: '没有权限访问'
})
}
const body = await readBody(event)
if (!body.users || !Array.isArray(body.users) || body.users.length === 0) {
throw createError({
statusCode: 400,
message: '无效的用户数据'
})
}
const results = {
created: 0,
failed: 0,
total: body.users.length,
errors: [] as Array<{ index: number; username?: string; name?: string; reason: string }>
}
// 1. 预处理数据:过滤必填字段,提取要查询的用户名
const validUsers: ValidUserData[] = []
const usernamesToQuery: string[] = []
const usernamesInBatch = new Set<string>()
for (let i = 0; i < body.users.length; i++) {
const userData = body.users[i]
const normalizedUserData = {
...userData,
name: normalizeRequiredText(userData.name),
username: normalizeRequiredText(userData.username),
grade: normalizeOptionalText(userData.grade),
class: normalizeOptionalText(userData.class)
}
if (
!normalizedUserData.name ||
!normalizedUserData.username ||
typeof userData.password !== 'string' ||
!userData.password
) {
results.failed++
results.errors.push({
index: i,
username: userData.username,
name: userData.name,
reason: '缺少必填字段(姓名、账号或密码)'
})
continue
}
const passwordViolation = getAdminPasswordViolation(userData.password)
if (passwordViolation) {
results.failed++
results.errors.push({
index: i,
username: normalizedUserData.username,
name: normalizedUserData.name,
reason: passwordViolation.message
})
continue
}
if (usernamesInBatch.has(normalizedUserData.username)) {
results.failed++
results.errors.push({
index: i,
username: normalizedUserData.username,
name: normalizedUserData.name,
reason: '批处理中存在重复的账号'
})
continue
}
usernamesInBatch.add(normalizedUserData.username)
// 限制单次查询 inArray 的长度,如果需要的话。前端分批已经是 50,所以没问题
validUsers.push({ index: i, ...normalizedUserData })
usernamesToQuery.push(normalizedUserData.username)
}
if (validUsers.length === 0) {
return results
}
// 2. 批量查询已存在的用户名,减少数据库交互
let existingUsernames = new Set<string>()
try {
const existingUsersList = await db
.select({ username: users.username })
.from(users)
.where(inArray(users.username, usernamesToQuery))
existingUsernames = new Set(existingUsersList.map((u) => u.username))
} catch (error: any) {
console.error('批量查询用户失败:', error)
throw createError({
statusCode: 500,
message: '数据库查询错误'
})
}
// 3. 过滤掉已存在的用户
const usersToInsertData: ValidUserData[] = []
for (const userData of validUsers) {
if (existingUsernames.has(userData.username)) {
results.failed++
results.errors.push({
index: userData.index,
username: userData.username,
name: userData.name,
reason: '账号已存在'
})
continue
}
usersToInsertData.push(userData)
}
if (usersToInsertData.length === 0) {
return results
}
try {
// 4. 并发加密密码 (由于使用 bcrypt.hash 异步方法,使用 Promise.all 并发处理可以显著提速)
const hashedPasswords = await Promise.all(
usersToInsertData.map((userData) => bcrypt.hash(userData.password, 10))
)
// 5. 准备批量插入的数据
const insertValues = usersToInsertData.map((userData, idx) => {
// 角色权限控制
let validRole = 'USER' // 默认角色
if (userData.role) {
const rolePermissions: Record<string, string[]> = {
SUPER_ADMIN: ['USER', 'ADMIN', 'SONG_ADMIN', 'SUPER_ADMIN'],
ADMIN: ['USER', 'SONG_ADMIN']
}
const allowedRoles = rolePermissions[user.role]
if (allowedRoles?.includes(userData.role)) {
validRole = userData.role
}
}
// 状态验证
let validStatus = 'active'
if (userData.status && ['active', 'withdrawn', 'graduate'].includes(userData.status)) {
validStatus = userData.status
}
return {
name: userData.name,
username: userData.username,
password: hashedPasswords[idx]!,
role: validRole,
status: validStatus as 'active' | 'withdrawn' | 'graduate',
grade: userData.grade,
class: userData.class
}
})
// 6. 执行批量插入
await db.insert(users).values(insertValues)
results.created += insertValues.length
} catch (error: any) {
console.error(`批量插入用户失败:`, error)
// 如果批量插入整体失败,则全部标记为失败
for (const userData of usersToInsertData) {
results.failed++
results.errors.push({
index: userData.index,
username: userData.username,
name: userData.name,
reason: error.message || '数据库批量写入失败'
})
}
}
return results
})