feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
@@ -0,0 +1,64 @@
import { and, eq } from 'drizzle-orm'
import { db } from '~/drizzle/db'
import { notifications } from '~/drizzle/schema'
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
import { createApiError } from '~~/server/utils/apiError'
import {
canSendSystemNotification,
NOTIFICATION_SOURCES
} from '~~/server/utils/important-notification-policy'
import { resolveNotificationBatchReference } from '~~/server/utils/notification-history-policy'
export default defineEventHandler(async (event) => {
const user = event.context.user
if (!user) {
throw createApiError(401, SERVER_ERROR_CODES.NOTIFICATION_AUTH_REQUIRED, '请先登录后删除通知')
}
if (!canSendSystemNotification(user.role)) {
throw createApiError(
403,
SERVER_ERROR_CODES.NOTIFICATION_ADMIN_REQUIRED,
'只有管理员可以删除通知'
)
}
const batchReference = resolveNotificationBatchReference(getRouterParam(event, 'batchId'))
if (!batchReference) {
throw createApiError(404, SERVER_ERROR_CODES.NOTIFICATION_NOT_FOUND, '通知不存在')
}
const batchCondition = batchReference.batchId
? eq(notifications.batchId, batchReference.batchId)
: eq(notifications.id, batchReference.notificationId!)
let deletedRows
try {
deletedRows = await db
.delete(notifications)
.where(
and(
eq(notifications.type, 'SYSTEM_NOTICE'),
eq(notifications.source, NOTIFICATION_SOURCES.ADMIN_MANUAL),
batchCondition
)
)
.returning({ id: notifications.id })
} catch (error) {
console.error('删除通知失败:', error)
throw createApiError(
500,
SERVER_ERROR_CODES.NOTIFICATION_HISTORY_DELETE_FAILED,
'删除通知失败'
)
}
if (deletedRows.length === 0) {
throw createApiError(404, SERVER_ERROR_CODES.NOTIFICATION_NOT_FOUND, '通知不存在')
}
return {
success: true,
deletedCount: deletedRows.length
}
})
@@ -0,0 +1,157 @@
import { and, count, desc, eq } from 'drizzle-orm'
import { db } from '~/drizzle/db'
import { notifications, users } from '~/drizzle/schema'
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
import { createApiError } from '~~/server/utils/apiError'
import {
canSendSystemNotification,
NOTIFICATION_SOURCES,
serializeNotificationSender
} from '~~/server/utils/important-notification-policy'
import {
resolveNotificationBatchReference,
resolveNotificationHistoryPagination,
resolveNotificationHistoryStatus
} from '~~/server/utils/notification-history-policy'
export default defineEventHandler(async (event) => {
const user = event.context.user
if (!user) {
throw createApiError(
401,
SERVER_ERROR_CODES.NOTIFICATION_AUTH_REQUIRED,
'请先登录后查看通知明细'
)
}
if (!canSendSystemNotification(user.role)) {
throw createApiError(
403,
SERVER_ERROR_CODES.NOTIFICATION_ADMIN_REQUIRED,
'只有管理员可以查看通知明细'
)
}
const batchReference = resolveNotificationBatchReference(getRouterParam(event, 'batchId'))
if (!batchReference) {
throw createApiError(404, SERVER_ERROR_CODES.NOTIFICATION_NOT_FOUND, '通知不存在')
}
const query = getQuery(event)
const status = resolveNotificationHistoryStatus(query.status)
if (!status) {
throw createApiError(
400,
SERVER_ERROR_CODES.NOTIFICATION_HISTORY_STATUS_INVALID,
'通知历史状态筛选值无效'
)
}
const { page, limit, offset } = resolveNotificationHistoryPagination(query.page, query.limit)
const batchCondition = batchReference.batchId
? eq(notifications.batchId, batchReference.batchId)
: eq(notifications.id, batchReference.notificationId!)
const baseCondition = and(
eq(notifications.type, 'SYSTEM_NOTICE'),
eq(notifications.source, NOTIFICATION_SOURCES.ADMIN_MANUAL),
batchCondition
)!
const filteredConditions = [baseCondition]
if (status === 'READ') filteredConditions.push(eq(notifications.read, true))
if (status === 'UNREAD') filteredConditions.push(eq(notifications.read, false))
const filteredCondition = and(...filteredConditions)!
let queryResult
try {
queryResult = await Promise.all([
db
.select({
id: notifications.id,
read: notifications.read,
updatedAt: notifications.updatedAt,
userId: notifications.userId,
username: users.username,
userName: users.name,
grade: users.grade,
className: users.class
})
.from(notifications)
.leftJoin(users, eq(notifications.userId, users.id))
.where(filteredCondition)
.orderBy(desc(notifications.id))
.limit(limit)
.offset(offset),
db.select({ count: count() }).from(notifications).where(filteredCondition),
db
.select({ read: notifications.read, count: count() })
.from(notifications)
.where(baseCondition)
.groupBy(notifications.read),
db
.select({
title: notifications.title,
message: notifications.message,
important: notifications.important,
senderId: notifications.senderId,
senderName: notifications.senderName,
senderUsername: notifications.senderUsername,
createdAt: notifications.createdAt
})
.from(notifications)
.where(baseCondition)
.limit(1)
])
} catch (error) {
console.error('获取通知明细失败:', error)
throw createApiError(
500,
SERVER_ERROR_CODES.NOTIFICATION_HISTORY_FETCH_FAILED,
'获取通知明细失败'
)
}
const [detailRows, totalRows, statsRows, metadataRows] = queryResult
const metadata = metadataRows[0]
if (!metadata) {
throw createApiError(404, SERVER_ERROR_CODES.NOTIFICATION_NOT_FOUND, '通知不存在')
}
const stats = { total: 0, read: 0, unread: 0 }
for (const row of statsRows) {
const value = Number(row.count || 0)
stats.total += value
if (row.read) stats.read = value
else stats.unread = value
}
const total = Number(totalRows[0]?.count || 0)
const { senderId, senderName, senderUsername, ...notificationMetadata } = metadata
return {
notification: {
batchId: getRouterParam(event, 'batchId'),
...notificationMetadata,
sender: serializeNotificationSender({ senderId, senderName, senderUsername })
},
recipients: detailRows.map((row) => ({
id: row.id,
read: row.read,
readAt: row.read ? row.updatedAt : null,
recipient: {
id: row.userId,
username: row.username,
name: row.userName,
grade: row.grade,
class: row.className
}
})),
pagination: {
page,
limit,
total,
totalPages: Math.max(1, Math.ceil(total / limit))
},
stats
}
})
@@ -0,0 +1,181 @@
import { and, eq } from 'drizzle-orm'
import { randomUUID } from 'node:crypto'
import { db } from '~/drizzle/db'
import { notifications } from '~/drizzle/schema'
import { SERVER_ERROR_CODES } from '~~/server/config/constants'
import { createApiError } from '~~/server/utils/apiError'
import {
canSendSystemNotification,
createNotificationSenderSnapshot,
NOTIFICATION_SOURCES,
NOTIFICATION_CONTENT_MAX_LENGTH,
NOTIFICATION_TITLE_MAX_LENGTH,
resolveImportantFlag,
serializeNotificationSender
} from '~~/server/utils/important-notification-policy'
import { resolveNotificationBatchReference } from '~~/server/utils/notification-history-policy'
export default defineEventHandler(async (event) => {
const user = event.context.user
if (!user) {
throw createApiError(401, SERVER_ERROR_CODES.NOTIFICATION_AUTH_REQUIRED, '请先登录后修改通知')
}
if (!canSendSystemNotification(user.role)) {
throw createApiError(
403,
SERVER_ERROR_CODES.NOTIFICATION_ADMIN_REQUIRED,
'只有管理员可以修改通知'
)
}
const batchReference = resolveNotificationBatchReference(getRouterParam(event, 'batchId'))
if (!batchReference) {
throw createApiError(404, SERVER_ERROR_CODES.NOTIFICATION_NOT_FOUND, '通知不存在')
}
const body = await readBody<Record<string, unknown> | null>(event)
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)
if (important === null) {
throw createApiError(
400,
SERVER_ERROR_CODES.NOTIFICATION_IMPORTANT_INVALID,
'重要通知标记必须是布尔值'
)
}
if (!title || !content) {
throw createApiError(
400,
SERVER_ERROR_CODES.NOTIFICATION_TITLE_CONTENT_REQUIRED,
'通知标题和内容不能为空'
)
}
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 batchCondition = batchReference.batchId
? eq(notifications.batchId, batchReference.batchId)
: eq(notifications.id, batchReference.notificationId!)
const baseCondition = and(
eq(notifications.type, 'SYSTEM_NOTICE'),
eq(notifications.source, NOTIFICATION_SOURCES.ADMIN_MANUAL),
batchCondition
)!
if (important) {
try {
const batchId = randomUUID()
const createdAt = new Date()
const senderSnapshot = createNotificationSenderSnapshot({
id: user.id,
name: user.name,
username: user.username
})
// 同一事务内删除旧批次并重新下发,避免旧内容继续弹窗、历史列表残留重复记录
const recipientUserIds = await db.transaction(async (tx) => {
const recipients = await tx
.delete(notifications)
.where(baseCondition)
.returning({ userId: notifications.userId })
if (recipients.length === 0) {
return []
}
const userIds = [...new Set(recipients.map(({ userId }) => userId))]
await tx.insert(notifications).values(
userIds.map((userId) => ({
userId,
type: 'SYSTEM_NOTICE',
batchId,
source: NOTIFICATION_SOURCES.ADMIN_MANUAL,
...senderSnapshot,
title,
message: content,
important: true,
read: false,
userDeleted: false,
createdAt,
updatedAt: createdAt
}))
)
return userIds
})
if (recipientUserIds.length === 0) {
throw createApiError(404, SERVER_ERROR_CODES.NOTIFICATION_NOT_FOUND, '通知不存在')
}
return {
success: true,
createdNewBatch: true,
updatedCount: recipientUserIds.length,
notification: {
batchId,
title,
message: content,
important: true,
sender: serializeNotificationSender(senderSnapshot),
createdAt,
recipientCount: recipientUserIds.length
}
}
} catch (error) {
if (typeof error === 'object' && error && 'statusCode' in error) throw error
console.error('重新发送重要通知失败:', error)
throw createApiError(
500,
SERVER_ERROR_CODES.NOTIFICATION_HISTORY_UPDATE_FAILED,
'修改通知失败'
)
}
}
let updatedRows
try {
updatedRows = await db
.update(notifications)
.set({ title, message: content, important: false })
.where(baseCondition)
.returning({ id: notifications.id })
} catch (error) {
console.error('修改通知失败:', error)
throw createApiError(
500,
SERVER_ERROR_CODES.NOTIFICATION_HISTORY_UPDATE_FAILED,
'修改通知失败'
)
}
if (updatedRows.length === 0) {
throw createApiError(404, SERVER_ERROR_CODES.NOTIFICATION_NOT_FOUND, '通知不存在')
}
return {
success: true,
updatedCount: updatedRows.length,
notification: { title, message: content, important }
}
})