初次提交
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s

This commit is contained in:
2026-09-06 08:19:02 +08:00
commit 4ce4649aaf
1061 changed files with 337258 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
import { db } from '~/drizzle/db'
import { notifications } from '~/drizzle/schema'
import { eq } from 'drizzle-orm'
export default defineEventHandler(async (event) => {
// 检查用户认证
const user = event.context.user
if (!user) {
throw createError({
statusCode: 401,
message: '需要登录才能删除通知'
})
}
// 获取通知ID
const id = parseInt(event.context.params?.id || '0')
if (isNaN(id) || id <= 0) {
throw createError({
statusCode: 400,
message: '无效的通知ID'
})
}
try {
// 检查通知是否属于当前用户
const notificationResult = await db
.select()
.from(notifications)
.where(eq(notifications.id, id))
.limit(1)
const notification = notificationResult[0]
if (!notification) {
throw createError({
statusCode: 404,
message: '通知不存在'
})
}
if (notification.userId !== user.id) {
throw createError({
statusCode: 403,
message: '无权删除此通知'
})
}
// 删除通知
await db.delete(notifications).where(eq(notifications.id, id))
return {
success: true
}
} catch (err) {
console.error('删除通知失败:', err)
throw createError({
statusCode: 500,
message: '删除通知失败'
})
}
})
@@ -0,0 +1,65 @@
import { db } from '~/drizzle/db'
import { notifications } from '~/drizzle/schema'
import { eq } from 'drizzle-orm'
export default defineEventHandler(async (event) => {
// 检查用户认证
const user = event.context.user
if (!user) {
throw createError({
statusCode: 401,
message: '需要登录才能标记通知'
})
}
// 获取通知ID
const id = parseInt(event.context.params?.id || '0')
if (isNaN(id) || id <= 0) {
throw createError({
statusCode: 400,
message: '无效的通知ID'
})
}
try {
// 检查通知是否属于当前用户
const notificationResult = await db
.select()
.from(notifications)
.where(eq(notifications.id, id))
.limit(1)
const notification = notificationResult[0]
if (!notification) {
throw createError({
statusCode: 404,
message: '通知不存在'
})
}
if (notification.userId !== user.id) {
throw createError({
statusCode: 403,
message: '无权标记此通知'
})
}
// 标记为已读
const updatedNotificationResult = await db
.update(notifications)
.set({ read: true })
.where(eq(notifications.id, id))
.returning()
const updatedNotification = updatedNotificationResult[0]
return updatedNotification
} catch (err) {
console.error('标记通知失败:', err)
throw createError({
statusCode: 500,
message: '标记通知失败'
})
}
})
@@ -0,0 +1,31 @@
import { db } from '~/drizzle/db'
import { notifications } from '~/drizzle/schema'
import { eq } from 'drizzle-orm'
export default defineEventHandler(async (event) => {
// 检查用户认证
const user = event.context.user
if (!user) {
throw createError({
statusCode: 401,
message: '需要登录才能清空通知'
})
}
try {
// 删除该用户的所有通知
const result = await db.delete(notifications).where(eq(notifications.userId, user.id))
return {
success: true,
count: result.rowCount || 0
}
} catch (err) {
console.error('清空通知失败:', err)
throw createError({
statusCode: 500,
message: '清空通知失败'
})
}
})
+129
View File
@@ -0,0 +1,129 @@
import { and, count, desc, eq } from 'drizzle-orm'
import { createError, defineEventHandler, getQuery } from 'h3'
import { db } from '~/drizzle/db'
import { notifications, songCollaborators, songs } from '~/drizzle/schema'
export default defineEventHandler(async (event) => {
// 检查用户认证
const user = event.context.user
if (!user) {
throw createError({
statusCode: 401,
message: '需要登录才能获取通知'
})
}
try {
// 获取查询参数
const query = getQuery(event)
const page = Math.max(1, parseInt(query.page as string) || 1)
const limit = Math.min(50, Math.max(1, parseInt(query.limit as string) || 10))
const offset = (page - 1) * limit
// 获取总通知数量
const totalCountResult = await db
.select({ count: count() })
.from(notifications)
.where(eq(notifications.userId, user.id))
const totalCount = totalCountResult[0]?.count || 0
const totalPages = Math.ceil(totalCount / limit)
// 获取分页通知数据
const userNotifications = await db
.select()
.from(notifications)
.where(eq(notifications.userId, user.id))
.orderBy(desc(notifications.createdAt))
.limit(limit)
.offset(offset)
// 丰富通知数据,特别是对于联合投稿邀请
const enrichedNotifications = await Promise.all(
userNotifications.map(async (notification) => {
if (notification.type === 'COLLABORATION_INVITE' && notification.songId) {
// 1. 检查歌曲是否存在
const songExists = await db
.select({ id: songs.id })
.from(songs)
.where(eq(songs.id, notification.songId))
.limit(1)
.then((res) => res.length > 0)
if (!songExists) {
return {
...notification,
handled: true,
status: 'INVALID',
isValid: false,
message: notification.message // 保持原消息,前端会显示已失效
}
}
// 2. 查询对应的邀请状态
const collab = await db
.select()
.from(songCollaborators)
.where(
and(
eq(songCollaborators.songId, notification.songId),
eq(songCollaborators.userId, user.id)
)
)
.limit(1)
if (collab.length > 0) {
return {
...notification,
handled: collab[0].status !== 'PENDING',
status: collab[0].status,
isValid: true,
repliedAt: collab[0].updatedAt || collab[0].createdAt // 假设更新时间为回复时间
}
} else {
// 如果找不到邀请记录,说明可能已经被撤回或删除
return {
...notification,
handled: true,
status: 'INVALID',
isValid: false,
message: notification.message
}
}
}
return {
...notification,
handled: false, // 默认未处理,或者不适用
isValid: true
}
})
)
// 计算未读通知数量
const unreadCountResult = await db
.select({ count: count() })
.from(notifications)
.where(and(eq(notifications.userId, user.id), eq(notifications.read, false)))
const unreadCount = unreadCountResult[0]?.count || 0
return {
notifications: enrichedNotifications,
unreadCount,
pagination: {
currentPage: page,
totalPages,
totalCount,
limit,
hasNextPage: page < totalPages,
hasPrevPage: page > 1
}
}
} catch (err) {
console.error('获取通知失败:', err)
throw createError({
statusCode: 500,
message: '获取通知失败'
})
}
})
@@ -0,0 +1,69 @@
import { createError, defineEventHandler, readBody } from 'h3'
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
const { nickname, verificationCode } = body
if (!nickname || !verificationCode) {
throw createError({
statusCode: 400,
statusMessage: '昵称和验证码不能为空'
})
}
// 验证昵称格式
if (nickname.includes('/')) {
throw createError({
statusCode: 400,
statusMessage: '昵称不能包含斜杠'
})
}
// 发送验证码到 MeoW
const message = `VoiceHub 账号绑定验证码:${verificationCode}`
const title = 'VoiceHub 账号绑定'
// 使用 GET 请求发送验证码
const meowUrl = `https://api.chuckfang.com/${encodeURIComponent(nickname)}/${encodeURIComponent(title)}/${encodeURIComponent(message)}`
const response = await fetch(meowUrl, {
method: 'GET',
headers: {
'User-Agent': 'VoiceHub/1.0'
}
})
if (!response.ok) {
throw createError({
statusCode: 500,
statusMessage: '发送验证码失败,请检查昵称是否正确'
})
}
const result = await response.json()
if (result.status !== 200) {
throw createError({
statusCode: 500,
statusMessage: result.message || '发送验证码失败'
})
}
return {
success: true,
message: '验证码发送成功'
}
} catch (error) {
console.error('发送 MeoW 验证码失败:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: '发送验证码失败,请稍后重试'
})
}
})
@@ -0,0 +1,90 @@
import { createError, defineEventHandler } from 'h3'
import { db } from '~/drizzle/db'
import { users } from '~/drizzle/schema'
import { eq } from 'drizzle-orm'
export default defineEventHandler(async (event) => {
try {
// 使用认证中间件提供的用户信息
const user = event.context.user
if (!user) {
throw createError({
statusCode: 401,
statusMessage: '未授权访问'
})
}
const userId = user.id
// 获取用户的 MeoW 绑定信息
const userDataResult = await db
.select({
id: users.id,
username: users.username,
meowNickname: users.meowNickname
})
.from(users)
.where(eq(users.id, userId))
.limit(1)
const userData = userDataResult[0]
if (!userData) {
throw createError({
statusCode: 404,
statusMessage: '用户不存在'
})
}
if (!userData.meowNickname) {
throw createError({
statusCode: 400,
statusMessage: '尚未绑定 MeoW 账号'
})
}
// 发送测试通知
const message = `这是来自 VoiceHub 的测试通知!您的账号 ${userData.username} 已成功绑定 MeoW。`
const title = 'VoiceHub 测试通知'
const meowUrl = `https://api.chuckfang.com/${encodeURIComponent(userData.meowNickname)}/${encodeURIComponent(title)}/${encodeURIComponent(message)}`
const response = await fetch(meowUrl, {
method: 'GET',
headers: {
'User-Agent': 'VoiceHub/1.0'
}
})
if (!response.ok) {
throw createError({
statusCode: 500,
statusMessage: '发送测试通知失败'
})
}
const result = await response.json()
if (result.status !== 200) {
throw createError({
statusCode: 500,
statusMessage: result.message || '发送测试通知失败'
})
}
return {
success: true,
message: '测试通知发送成功'
}
} catch (error) {
console.error('发送 MeoW 测试通知失败:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: '发送测试通知失败,请稍后重试'
})
}
})
+32
View File
@@ -0,0 +1,32 @@
import { and, db, eq, notifications } from '~/drizzle/db'
export default defineEventHandler(async (event) => {
// 检查用户认证
const user = event.context.user
if (!user) {
throw createError({
statusCode: 401,
message: '需要登录才能标记通知'
})
}
try {
// 标记该用户的所有未读通知为已读
const result = await db
.update(notifications)
.set({ read: true })
.where(and(eq(notifications.userId, user.id), eq(notifications.read, false)))
return {
success: true,
count: result.count
}
} catch (err) {
console.error('标记所有通知失败:', err)
throw createError({
statusCode: 500,
message: '标记所有通知失败'
})
}
})
+113
View File
@@ -0,0 +1,113 @@
import { db } from '~/drizzle/db'
import { notificationSettings } from '~/drizzle/schema'
import { eq } from 'drizzle-orm'
import type { NotificationSettings } from '~~/types'
export default defineEventHandler(async (event) => {
// 检查用户认证
const user = event.context.user
if (!user) {
throw createError({
statusCode: 401,
message: '需要登录才能更新通知设置'
})
}
const body = await readBody(event)
// 验证请求体
if (typeof body !== 'object' || body === null) {
throw createError({
statusCode: 400,
message: '无效的请求数据'
})
}
try {
// 获取用户当前的通知设置
const existingSettingsResult = await db
.select()
.from(notificationSettings)
.where(eq(notificationSettings.userId, user.id))
.limit(1)
let dbSettings: any = existingSettingsResult[0]
if (dbSettings) {
// 更新现有设置
const updatedSettingsResult = await db
.update(notificationSettings)
.set({
enabled: body.systemNotify !== undefined ? body.systemNotify : dbSettings.enabled,
songRequestEnabled:
body.songSelectedNotify !== undefined
? body.songSelectedNotify
: dbSettings.songRequestEnabled,
songPlayedEnabled:
body.songPlayedNotify !== undefined
? body.songPlayedNotify
: dbSettings.songPlayedEnabled,
songVotedEnabled:
body.songVotedNotify !== undefined ? body.songVotedNotify : dbSettings.songVotedEnabled,
songVotedThreshold:
body.songVotedThreshold !== undefined
? Math.max(1, Math.min(10, body.songVotedThreshold))
: dbSettings.songVotedThreshold,
refreshInterval:
body.refreshInterval !== undefined
? Math.max(10, Math.min(300, body.refreshInterval))
: dbSettings.refreshInterval
// 邮件通知总开关移除
})
.where(eq(notificationSettings.userId, user.id))
.returning()
dbSettings = updatedSettingsResult[0]
} else {
// 创建新设置
const newSettingsResult = await db
.insert(notificationSettings)
.values({
userId: user.id,
enabled: body.systemNotify !== undefined ? body.systemNotify : true,
songRequestEnabled:
body.songSelectedNotify !== undefined ? body.songSelectedNotify : true,
songPlayedEnabled: body.songPlayedNotify !== undefined ? body.songPlayedNotify : true,
songVotedEnabled: body.songVotedNotify !== undefined ? body.songVotedNotify : true,
songVotedThreshold:
body.songVotedThreshold !== undefined
? Math.max(1, Math.min(10, body.songVotedThreshold))
: 1,
refreshInterval:
body.refreshInterval !== undefined
? Math.max(10, Math.min(300, body.refreshInterval))
: 60
})
.returning()
dbSettings = newSettingsResult[0]
}
// 转换为前端期望的格式
const settings: NotificationSettings = {
id: dbSettings.id,
userId: dbSettings.userId,
songSelectedNotify: dbSettings.songRequestEnabled,
songPlayedNotify: dbSettings.songPlayedEnabled,
songVotedNotify: dbSettings.songVotedEnabled,
systemNotify: dbSettings.enabled,
refreshInterval: dbSettings.refreshInterval,
songVotedThreshold: dbSettings.songVotedThreshold
}
return {
success: true,
data: settings,
message: '通知设置保存成功'
}
} catch (err) {
console.error('更新通知设置失败:', err)
throw createError({
statusCode: 500,
message: '更新通知设置失败'
})
}
})
+86
View File
@@ -0,0 +1,86 @@
import { createError, defineEventHandler } from 'h3'
import { db } from '~/drizzle/db'
import { notificationSettings, users } from '~/drizzle/schema'
import { eq } from 'drizzle-orm'
export default defineEventHandler(async (event) => {
// 检查用户认证
const user = event.context.user
if (!user) {
throw createError({
statusCode: 401,
message: '需要登录才能获取通知设置'
})
}
try {
// 获取用户信息(包含meowNickname和邮箱信息)
const userInfoResult = await db
.select({
meowNickname: users.meowNickname,
email: users.email,
emailVerified: users.emailVerified
})
.from(users)
.where(eq(users.id, user.id))
.limit(1)
const userInfo = userInfoResult[0]
// 获取用户的通知设置,如果不存在则创建默认设置
const dbSettingsResult = await db
.select()
.from(notificationSettings)
.where(eq(notificationSettings.userId, user.id))
.limit(1)
let dbSettings: any = dbSettingsResult[0]
if (!dbSettings) {
// 创建默认设置
const insertResult = await db
.insert(notificationSettings)
.values({
userId: user.id,
enabled: true,
songRequestEnabled: true,
songVotedEnabled: true,
songPlayedEnabled: true,
refreshInterval: 60,
songVotedThreshold: 1
})
.returning()
dbSettings = insertResult[0]
}
// 转换为前端期望的格式(包含用户邮箱状态)
const settings = {
id: dbSettings.id,
userId: dbSettings.userId,
songSelectedNotify: dbSettings.songRequestEnabled,
songPlayedNotify: dbSettings.songPlayedEnabled,
songVotedNotify: dbSettings.songVotedEnabled,
systemNotify: dbSettings.enabled,
refreshInterval: dbSettings.refreshInterval,
songVotedThreshold: dbSettings.songVotedThreshold,
meowUserId: userInfo?.meowNickname || '',
// 邮件通知总开关移除:以邮箱绑定状态为准
// 用户邮箱信息
userEmail: userInfo?.email || '',
emailVerified: userInfo?.emailVerified || false
}
return {
success: true,
data: settings
}
} catch (err) {
console.error('获取通知设置失败:', err)
throw createError({
statusCode: 500,
message: '获取通知设置失败'
})
}
})