feat: 迁移自定义功能至 1.5.9.9 版本
包含音乐源限制、CI 防 OOM 优化、数据库连接池优化、管理端权限及 UI 细节定制等功能
This commit is contained in:
@@ -1,220 +1,179 @@
|
||||
import { and, eq, or } from 'drizzle-orm'
|
||||
import { db } from '~/drizzle/db'
|
||||
import { users } from '~/drizzle/schema'
|
||||
import { and, eq, or } from 'drizzle-orm'
|
||||
import {
|
||||
createBatchSystemNotifications,
|
||||
createSystemNotification
|
||||
} from '~~/server/services/notificationService'
|
||||
import { createApiError } from '~~/server/utils/apiError'
|
||||
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
|
||||
import {
|
||||
canSendSystemNotification,
|
||||
NOTIFICATION_CONTENT_MAX_LENGTH,
|
||||
NOTIFICATION_TITLE_MAX_LENGTH,
|
||||
resolveImportantFlag
|
||||
} from '~~/server/utils/important-notification-policy'
|
||||
|
||||
type NotificationFilter = {
|
||||
grade?: unknown
|
||||
class?: unknown
|
||||
classes?: unknown
|
||||
userIds?: unknown
|
||||
}
|
||||
|
||||
const supportedScopes = ['ALL', 'GRADE', 'CLASS', 'MULTI_CLASS', 'SPECIFIC_USERS'] as const
|
||||
} from '../../../services/notificationService'
|
||||
import { getClientIP } from '~~/server/utils/ip-utils'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// 检查用户是否为管理员
|
||||
const user = event.context.user
|
||||
|
||||
if (!user) {
|
||||
throw createApiError(401, SERVER_ERROR_CODES.NOTIFICATION_AUTH_REQUIRED, '需要登录才能发送通知')
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: '未授权访问'
|
||||
})
|
||||
}
|
||||
|
||||
if (!canSendSystemNotification(user.role)) {
|
||||
throw createApiError(
|
||||
403,
|
||||
SERVER_ERROR_CODES.NOTIFICATION_ADMIN_REQUIRED,
|
||||
'只有管理员可以发送系统通知'
|
||||
)
|
||||
if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
message: '只有管理员可以发送系统通知'
|
||||
})
|
||||
}
|
||||
|
||||
const body = await readBody<Record<string, unknown> | null>(event)
|
||||
const sender = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username
|
||||
}
|
||||
const title = typeof body?.title === 'string' ? body.title.trim() : ''
|
||||
const rawContent = typeof body?.content === 'string' ? body.content : body?.message
|
||||
const content = typeof rawContent === 'string' ? rawContent.trim() : ''
|
||||
const important = resolveImportantFlag(body?.important)
|
||||
try {
|
||||
// 获取客户端IP地址
|
||||
const clientIP = getClientIP(event)
|
||||
|
||||
if (important === null) {
|
||||
throw createApiError(
|
||||
400,
|
||||
SERVER_ERROR_CODES.NOTIFICATION_IMPORTANT_INVALID,
|
||||
'重要通知标记必须是布尔值'
|
||||
)
|
||||
}
|
||||
// 获取请求数据
|
||||
const body = await readBody(event)
|
||||
const { title, message, content, scope, filter, userId, type } = body
|
||||
|
||||
if (!title || !content) {
|
||||
throw createApiError(
|
||||
400,
|
||||
SERVER_ERROR_CODES.NOTIFICATION_TITLE_CONTENT_REQUIRED,
|
||||
'通知标题和内容不能为空'
|
||||
)
|
||||
}
|
||||
// 处理单个用户通知(用于权限变更等系统通知)
|
||||
if (userId && title && (message || content)) {
|
||||
const notificationContent = message || content
|
||||
const result = await createSystemNotification(userId, title, notificationContent, clientIP)
|
||||
|
||||
if (title.length > NOTIFICATION_TITLE_MAX_LENGTH) {
|
||||
throw createApiError(
|
||||
400,
|
||||
SERVER_ERROR_CODES.NOTIFICATION_TITLE_TOO_LONG,
|
||||
`通知标题不能超过 ${NOTIFICATION_TITLE_MAX_LENGTH} 个字符`,
|
||||
{ params: [NOTIFICATION_TITLE_MAX_LENGTH] }
|
||||
)
|
||||
}
|
||||
|
||||
if (content.length > NOTIFICATION_CONTENT_MAX_LENGTH) {
|
||||
throw createApiError(
|
||||
400,
|
||||
SERVER_ERROR_CODES.NOTIFICATION_CONTENT_TOO_LONG,
|
||||
`通知正文不能超过 ${NOTIFICATION_CONTENT_MAX_LENGTH} 个字符`,
|
||||
{ params: [NOTIFICATION_CONTENT_MAX_LENGTH] }
|
||||
)
|
||||
}
|
||||
|
||||
const targetUserId = body?.userId
|
||||
if (targetUserId !== undefined) {
|
||||
// 兼容旧调用方传入数字字符串的情况
|
||||
const normalizedUserId =
|
||||
typeof targetUserId === 'string' && targetUserId.trim()
|
||||
? Number(targetUserId.trim())
|
||||
: targetUserId
|
||||
if (!Number.isInteger(normalizedUserId) || Number(normalizedUserId) <= 0) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.NOTIFICATION_USER_ID_INVALID, '无效的用户 ID')
|
||||
if (result) {
|
||||
return {
|
||||
success: true,
|
||||
message: '通知发送成功',
|
||||
sentCount: 1,
|
||||
totalUsers: 1
|
||||
}
|
||||
} else {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: '发送通知失败'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const result = await createSystemNotification(
|
||||
Number(normalizedUserId),
|
||||
title,
|
||||
content,
|
||||
important,
|
||||
sender
|
||||
)
|
||||
// 验证批量通知请求数据
|
||||
if (!title || !content) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '通知标题和内容不能为空'
|
||||
})
|
||||
}
|
||||
|
||||
if (!scope || !['ALL', 'GRADE', 'CLASS', 'MULTI_CLASS', 'SPECIFIC_USERS'].includes(scope)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '无效的通知范围'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询符合条件的用户IDs
|
||||
let userIds: number[] = []
|
||||
|
||||
if (scope === 'ALL') {
|
||||
// 查询所有用户
|
||||
const allUsers = await db.select({ id: users.id }).from(users)
|
||||
userIds = allUsers.map((user) => user.id)
|
||||
} else if (scope === 'GRADE') {
|
||||
// 按年级查询
|
||||
if (!filter?.grade) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '年级不能为空'
|
||||
})
|
||||
}
|
||||
|
||||
const gradeUsers = await db
|
||||
.select({ id: users.id })
|
||||
.from(users)
|
||||
.where(eq(users.grade, filter.grade))
|
||||
userIds = gradeUsers.map((user) => user.id)
|
||||
} else if (scope === 'CLASS') {
|
||||
// 按单个班级查询
|
||||
if (!filter?.grade || !filter?.class) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '年级和班级不能为空'
|
||||
})
|
||||
}
|
||||
|
||||
const classUsers = await db
|
||||
.select({ id: users.id })
|
||||
.from(users)
|
||||
.where(and(eq(users.grade, filter.grade), eq(users.class, filter.class)))
|
||||
userIds = classUsers.map((user) => user.id)
|
||||
} else if (scope === 'MULTI_CLASS') {
|
||||
// 按多个班级查询
|
||||
if (!filter?.classes || !Array.isArray(filter.classes) || filter.classes.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '未选择任何班级'
|
||||
})
|
||||
}
|
||||
|
||||
// 构建查询条件,使用OR连接多个班级条件
|
||||
const whereConditions = filter.classes.map((cls: { grade: string; class: string }) =>
|
||||
and(eq(users.grade, cls.grade), eq(users.class, cls.class))
|
||||
)
|
||||
|
||||
const multiClassUsers = await db
|
||||
.select({ id: users.id })
|
||||
.from(users)
|
||||
.where(or(...whereConditions))
|
||||
userIds = multiClassUsers.map((user) => user.id)
|
||||
} else if (scope === 'SPECIFIC_USERS') {
|
||||
// 指定用户
|
||||
if (!filter?.userIds || !Array.isArray(filter.userIds) || filter.userIds.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '未选择任何用户'
|
||||
})
|
||||
}
|
||||
|
||||
userIds = filter.userIds
|
||||
}
|
||||
|
||||
if (userIds.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: '没有找到符合条件的用户',
|
||||
sentCount: 0,
|
||||
totalUsers: 0
|
||||
}
|
||||
}
|
||||
|
||||
// 使用通知服务批量发送通知
|
||||
const result = await createBatchSystemNotifications(userIds, title, content, clientIP)
|
||||
|
||||
if (!result) {
|
||||
throw createApiError(500, SERVER_ERROR_CODES.NOTIFICATION_SEND_FAILED, '发送通知失败')
|
||||
throw new Error('发送通知失败')
|
||||
}
|
||||
|
||||
// 处理结果
|
||||
let sentCount = 0
|
||||
let totalUsers = userIds.length
|
||||
|
||||
// 判断result类型
|
||||
if (Array.isArray(result)) {
|
||||
sentCount = result.length
|
||||
} else if (result && typeof result === 'object' && 'count' in result) {
|
||||
sentCount = result.count
|
||||
totalUsers = result.total || userIds.length
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '通知发送成功',
|
||||
sentCount: 1,
|
||||
totalUsers: 1
|
||||
sentCount,
|
||||
totalUsers
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('发送通知失败:', error)
|
||||
|
||||
const scope = typeof body?.scope === 'string' ? body.scope : ''
|
||||
if (!supportedScopes.includes(scope as (typeof supportedScopes)[number])) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.NOTIFICATION_SCOPE_INVALID, '无效的通知范围')
|
||||
}
|
||||
|
||||
const filter = (
|
||||
body?.filter && typeof body.filter === 'object' ? body.filter : {}
|
||||
) as NotificationFilter
|
||||
let userIds: number[]
|
||||
|
||||
// 仅向活跃状态的用户发送通知,排除已退学/毕业用户
|
||||
const activeUserCondition = eq(users.status, 'active')
|
||||
|
||||
if (scope === 'ALL') {
|
||||
const allUsers = await db.select({ id: users.id }).from(users).where(activeUserCondition)
|
||||
userIds = allUsers.map((target) => target.id)
|
||||
} else if (scope === 'GRADE') {
|
||||
const grade = typeof filter.grade === 'string' ? filter.grade.trim() : ''
|
||||
if (!grade) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.NOTIFICATION_GRADE_REQUIRED, '年级不能为空')
|
||||
}
|
||||
|
||||
const gradeUsers = await db.select({ id: users.id }).from(users).where(and(eq(users.grade, grade), activeUserCondition))
|
||||
userIds = gradeUsers.map((target) => target.id)
|
||||
} else if (scope === 'CLASS') {
|
||||
const grade = typeof filter.grade === 'string' ? filter.grade.trim() : ''
|
||||
const className = typeof filter.class === 'string' ? filter.class.trim() : ''
|
||||
if (!grade || !className) {
|
||||
throw createApiError(
|
||||
400,
|
||||
SERVER_ERROR_CODES.NOTIFICATION_GRADE_CLASS_REQUIRED,
|
||||
'年级和班级不能为空'
|
||||
)
|
||||
}
|
||||
|
||||
const classUsers = await db
|
||||
.select({ id: users.id })
|
||||
.from(users)
|
||||
.where(and(eq(users.grade, grade), eq(users.class, className), activeUserCondition))
|
||||
userIds = classUsers.map((target) => target.id)
|
||||
} else if (scope === 'MULTI_CLASS') {
|
||||
if (!Array.isArray(filter.classes) || filter.classes.length === 0) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.NOTIFICATION_CLASSES_REQUIRED, '未选择任何班级')
|
||||
}
|
||||
|
||||
const classes = filter.classes.map((entry) => {
|
||||
const candidate = entry && typeof entry === 'object' ? (entry as Record<string, unknown>) : {}
|
||||
return {
|
||||
grade: typeof candidate.grade === 'string' ? candidate.grade.trim() : '',
|
||||
className: typeof candidate.class === 'string' ? candidate.class.trim() : ''
|
||||
}
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
message: error.message || '发送通知失败'
|
||||
})
|
||||
|
||||
if (classes.some((entry) => !entry.grade || !entry.className)) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.NOTIFICATION_CLASSES_REQUIRED, '班级信息不完整')
|
||||
}
|
||||
|
||||
const whereConditions = classes.map((entry) =>
|
||||
and(eq(users.grade, entry.grade), eq(users.class, entry.className), activeUserCondition)
|
||||
)
|
||||
const multiClassUsers = await db
|
||||
.select({ id: users.id })
|
||||
.from(users)
|
||||
.where(or(...whereConditions))
|
||||
userIds = multiClassUsers.map((target) => target.id)
|
||||
} else {
|
||||
if (!Array.isArray(filter.userIds) || filter.userIds.length === 0) {
|
||||
throw createApiError(400, SERVER_ERROR_CODES.NOTIFICATION_USERS_REQUIRED, '未选择任何用户')
|
||||
}
|
||||
|
||||
if (filter.userIds.some((id) => !Number.isInteger(id) || Number(id) <= 0)) {
|
||||
throw createApiError(
|
||||
400,
|
||||
SERVER_ERROR_CODES.NOTIFICATION_USER_ID_INVALID,
|
||||
'用户 ID 列表包含无效值'
|
||||
)
|
||||
}
|
||||
|
||||
userIds = [...new Set(filter.userIds.map(Number))]
|
||||
}
|
||||
|
||||
if (userIds.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: '没有找到符合条件的用户',
|
||||
sentCount: 0,
|
||||
totalUsers: 0
|
||||
}
|
||||
}
|
||||
|
||||
const result = await createBatchSystemNotifications(userIds, title, content, important, sender)
|
||||
if (!result) {
|
||||
throw createApiError(500, SERVER_ERROR_CODES.NOTIFICATION_SEND_FAILED, '发送通知失败')
|
||||
}
|
||||
|
||||
const sentCount = Array.isArray(result) ? result.length : result.count
|
||||
const totalUsers = Array.isArray(result) ? userIds.length : result.total || userIds.length
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '通知发送成功',
|
||||
sentCount,
|
||||
totalUsers
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user