Initial commit
This commit is contained in:
510
lib/services/recording.service.ts
Normal file
510
lib/services/recording.service.ts
Normal file
@ -0,0 +1,510 @@
|
||||
import { prisma } from "../database";
|
||||
import { Recording, Prisma } from "@prisma/client";
|
||||
import { writeFile, unlink } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { cache } from "../cache";
|
||||
import { logger } from "../utils/logger";
|
||||
import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||
|
||||
export interface CreateRecordingData {
|
||||
title: string;
|
||||
audioUrl: string;
|
||||
duration: number;
|
||||
fileSize: number;
|
||||
mimeType: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface UpdateRecordingData {
|
||||
title?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface RecordingWithUser {
|
||||
id: string;
|
||||
title: string;
|
||||
audioUrl: string;
|
||||
duration: number;
|
||||
fileSize: number;
|
||||
mimeType: string;
|
||||
createdAt: Date;
|
||||
user: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
// S3 删除工具函数
|
||||
async function deleteS3File(audioUrl: string) {
|
||||
try {
|
||||
const s3 = new S3Client({
|
||||
region: process.env.AWS_REGION || "us-east-1",
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
||||
},
|
||||
});
|
||||
const url = new URL(audioUrl);
|
||||
const bucket = url.hostname.split(".")[0];
|
||||
const key = url.pathname.slice(1); // 去掉开头的 /
|
||||
await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
||||
console.log(`S3 文件删除成功: ${bucket}/${key}`);
|
||||
} catch (err) {
|
||||
console.warn("S3 文件删除失败(忽略):", err);
|
||||
}
|
||||
}
|
||||
|
||||
export class RecordingService {
|
||||
/**
|
||||
* 创建新录音
|
||||
*/
|
||||
static async createRecording(data: CreateRecordingData): Promise<Recording> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const recording = await prisma.recording.create({
|
||||
data,
|
||||
});
|
||||
|
||||
// 清除相关缓存
|
||||
cache.delete(`recordings:user:${data.userId}`);
|
||||
cache.delete(`stats:user:${data.userId}`);
|
||||
|
||||
// 清除可能的缓存变体
|
||||
cache.delete(`recordings:user:${data.userId}:0:20:{"createdAt":"desc"}`);
|
||||
cache.delete(`recordings:user:${data.userId}:0:50:{"createdAt":"desc"}`);
|
||||
|
||||
logger.logDbOperation("create", "recording", Date.now() - startTime);
|
||||
logger.logUserAction(data.userId, "create_recording", {
|
||||
recordingId: recording.id,
|
||||
});
|
||||
|
||||
return recording;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Failed to create recording",
|
||||
{ userId: data.userId },
|
||||
error as Error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取录音
|
||||
*/
|
||||
static async getRecordingById(id: string): Promise<Recording | null> {
|
||||
const cacheKey = `recording:${id}`;
|
||||
const cached = cache.get<Recording>(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const recording = await prisma.recording.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (recording) {
|
||||
cache.set(cacheKey, recording, 10 * 60 * 1000); // 缓存10分钟
|
||||
}
|
||||
|
||||
logger.logDbOperation("findUnique", "recording", Date.now() - startTime);
|
||||
return recording;
|
||||
} catch (error) {
|
||||
logger.error("Failed to get recording by ID", { id }, error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID获取录音列表
|
||||
*/
|
||||
static async getRecordingsByUserId(
|
||||
userId: string,
|
||||
options?: {
|
||||
skip?: number;
|
||||
take?: number;
|
||||
orderBy?: Prisma.RecordingOrderByWithRelationInput;
|
||||
}
|
||||
): Promise<RecordingWithUser[]> {
|
||||
const {
|
||||
skip = 0,
|
||||
take = 50,
|
||||
orderBy = { createdAt: "desc" },
|
||||
} = options || {};
|
||||
const cacheKey = `recordings:user:${userId}:${skip}:${take}:${JSON.stringify(
|
||||
orderBy
|
||||
)}`;
|
||||
|
||||
const cached = cache.get<RecordingWithUser[]>(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const recordings = await prisma.recording.findMany({
|
||||
where: { userId },
|
||||
skip,
|
||||
take,
|
||||
orderBy,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cache.set(cacheKey, recordings, 5 * 60 * 1000); // 缓存5分钟
|
||||
logger.logDbOperation("findMany", "recording", Date.now() - startTime);
|
||||
|
||||
return recordings;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Failed to get recordings by user ID",
|
||||
{ userId },
|
||||
error as Error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新录音信息
|
||||
*/
|
||||
static async updateRecording(
|
||||
id: string,
|
||||
userId: string,
|
||||
data: UpdateRecordingData
|
||||
): Promise<Recording> {
|
||||
const startTime = Date.now();
|
||||
|
||||
console.log(
|
||||
`RecordingService.updateRecording - 开始更新录音: ${id}, 用户: ${userId}, 数据:`,
|
||||
data
|
||||
);
|
||||
|
||||
try {
|
||||
// 验证录音所有权
|
||||
const recording = await prisma.recording.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
|
||||
console.log(
|
||||
`RecordingService.updateRecording - 查找录音结果:`,
|
||||
recording ? "找到" : "未找到"
|
||||
);
|
||||
|
||||
if (!recording) {
|
||||
throw new Error("录音不存在或无权限");
|
||||
}
|
||||
|
||||
console.log(
|
||||
`RecordingService.updateRecording - 开始更新数据库, 当前标题: "${recording.title}"`
|
||||
);
|
||||
|
||||
const updatedRecording = await prisma.recording.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`RecordingService.updateRecording - 数据库更新成功, 新标题: "${updatedRecording.title}"`
|
||||
);
|
||||
|
||||
// 清除相关缓存
|
||||
cache.delete(`recording:${id}`);
|
||||
cache.delete(`recordings:user:${userId}`);
|
||||
|
||||
// 清除可能的缓存变体
|
||||
cache.delete(`recordings:user:${userId}:0:20:{"createdAt":"desc"}`);
|
||||
cache.delete(`recordings:user:${userId}:0:50:{"createdAt":"desc"}`);
|
||||
|
||||
// 清除所有可能的录音列表缓存
|
||||
const commonSkipValues = [0, 20, 50, 100];
|
||||
const commonTakeValues = [20, 50, 100];
|
||||
const commonOrderBy = ['{"createdAt":"desc"}', '{"createdAt":"asc"}'];
|
||||
|
||||
for (const skip of commonSkipValues) {
|
||||
for (const take of commonTakeValues) {
|
||||
for (const orderBy of commonOrderBy) {
|
||||
cache.delete(
|
||||
`recordings:user:${userId}:${skip}:${take}:${orderBy}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.logDbOperation("update", "recording", Date.now() - startTime);
|
||||
logger.logUserAction(userId, "update_recording", {
|
||||
recordingId: id,
|
||||
data,
|
||||
});
|
||||
|
||||
return updatedRecording;
|
||||
} catch (error) {
|
||||
console.error(`RecordingService.updateRecording - 更新失败:`, error);
|
||||
logger.error(
|
||||
"Failed to update recording",
|
||||
{ id, userId },
|
||||
error as Error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除录音
|
||||
*/
|
||||
static async deleteRecording(id: string, userId: string): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`RecordingService: Attempting to delete recording ${id} for user ${userId}`
|
||||
);
|
||||
|
||||
// 验证录音所有权
|
||||
const recording = await prisma.recording.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
|
||||
console.log(
|
||||
`RecordingService: Found recording:`,
|
||||
recording ? "Yes" : "No"
|
||||
);
|
||||
|
||||
if (!recording) {
|
||||
// 检查录音是否存在,但属于其他用户
|
||||
const otherRecording = await prisma.recording.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (otherRecording) {
|
||||
console.log(
|
||||
`RecordingService: Recording exists but belongs to user ${otherRecording.userId}, not ${userId}`
|
||||
);
|
||||
throw new Error("录音不存在或无权限");
|
||||
} else {
|
||||
console.log(`RecordingService: Recording ${id} does not exist`);
|
||||
throw new Error("录音不存在");
|
||||
}
|
||||
}
|
||||
|
||||
// 先删除 S3 文件
|
||||
await deleteS3File(recording.audioUrl);
|
||||
|
||||
console.log(`RecordingService: Deleting recording from database`);
|
||||
|
||||
// 删除数据库记录
|
||||
await prisma.recording.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
// 清除相关缓存 - 更彻底的清除
|
||||
cache.delete(`recording:${id}`);
|
||||
cache.delete(`recordings:user:${userId}`);
|
||||
cache.delete(`stats:user:${userId}`);
|
||||
|
||||
// 清除可能的缓存变体
|
||||
cache.delete(`recordings:user:${userId}:0:20:{"createdAt":"desc"}`);
|
||||
cache.delete(`recordings:user:${userId}:0:50:{"createdAt":"desc"}`);
|
||||
|
||||
console.log(`RecordingService: Cache cleared`);
|
||||
|
||||
logger.logDbOperation("delete", "recording", Date.now() - startTime);
|
||||
logger.logUserAction(userId, "delete_recording", { recordingId: id });
|
||||
|
||||
console.log(`RecordingService: Recording ${id} deleted successfully`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`RecordingService: Failed to delete recording ${id}:`,
|
||||
error
|
||||
);
|
||||
logger.error(
|
||||
"Failed to delete recording",
|
||||
{ id, userId },
|
||||
error as Error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存录音文件
|
||||
*/
|
||||
static async saveRecordingFile(
|
||||
file: Buffer,
|
||||
filename: string
|
||||
): Promise<string> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 验证文件大小 (50MB 限制)
|
||||
const maxSize = 50 * 1024 * 1024;
|
||||
if (file.length > maxSize) {
|
||||
throw new Error("文件大小不能超过 50MB");
|
||||
}
|
||||
|
||||
// 验证文件名格式
|
||||
const filenameRegex = /^recording-\d+\.webm$/;
|
||||
if (!filenameRegex.test(filename)) {
|
||||
throw new Error("文件名格式不正确");
|
||||
}
|
||||
|
||||
// 验证文件内容类型 - 更宽松的 WebM 验证
|
||||
if (file.length < 4) {
|
||||
throw new Error("文件太小,无法验证格式");
|
||||
}
|
||||
|
||||
const webmHeader = file.slice(0, 4);
|
||||
const webmSignature = Buffer.from([0x1a, 0x45, 0xdf, 0xa3]);
|
||||
|
||||
// 检查是否为 WebM 格式,但也允许其他音频格式
|
||||
const isValidWebM = webmHeader.equals(webmSignature);
|
||||
const hasAudioExtension =
|
||||
filename.toLowerCase().endsWith(".webm") ||
|
||||
filename.toLowerCase().endsWith(".mp3") ||
|
||||
filename.toLowerCase().endsWith(".wav");
|
||||
|
||||
if (!isValidWebM && !hasAudioExtension) {
|
||||
logger.warn("File format validation failed", {
|
||||
filename,
|
||||
header: webmHeader.toString("hex"),
|
||||
expected: webmSignature.toString("hex"),
|
||||
});
|
||||
// 不抛出错误,允许上传,但记录警告
|
||||
}
|
||||
|
||||
const uploadDir = join(process.cwd(), "public", "recordings");
|
||||
const filePath = join(uploadDir, filename);
|
||||
|
||||
await writeFile(filePath, file);
|
||||
|
||||
logger.info("Recording file saved", {
|
||||
filename,
|
||||
size: file.length,
|
||||
duration: Date.now() - startTime,
|
||||
});
|
||||
|
||||
return `/recordings/${filename}`;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Failed to save recording file",
|
||||
{ filename },
|
||||
error as Error
|
||||
);
|
||||
throw new Error("保存录音文件失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户录音统计
|
||||
*/
|
||||
static async getUserRecordingStats(userId: string): Promise<{
|
||||
totalRecordings: number;
|
||||
totalDuration: number;
|
||||
totalFileSize: number;
|
||||
}> {
|
||||
const cacheKey = `stats:user:${userId}`;
|
||||
const cached = cache.get<{
|
||||
totalRecordings: number;
|
||||
totalDuration: number;
|
||||
totalFileSize: number;
|
||||
}>(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const stats = await prisma.recording.aggregate({
|
||||
where: { userId },
|
||||
_count: {
|
||||
id: true,
|
||||
},
|
||||
_sum: {
|
||||
duration: true,
|
||||
fileSize: true,
|
||||
},
|
||||
});
|
||||
|
||||
const result = {
|
||||
totalRecordings: stats._count.id,
|
||||
totalDuration: stats._sum.duration || 0,
|
||||
totalFileSize: stats._sum.fileSize || 0,
|
||||
};
|
||||
|
||||
cache.set(cacheKey, result, 10 * 60 * 1000); // 缓存10分钟
|
||||
logger.logDbOperation("aggregate", "recording", Date.now() - startTime);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Failed to get user recording stats",
|
||||
{ userId },
|
||||
error as Error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除用户录音
|
||||
*/
|
||||
static async deleteUserRecordings(userId: string): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const recordings = await prisma.recording.findMany({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
// 删除所有录音文件
|
||||
for (const recording of recordings) {
|
||||
try {
|
||||
const filePath = join(process.cwd(), "public", recording.audioUrl);
|
||||
await unlink(filePath);
|
||||
} catch {
|
||||
// 忽略文件删除错误
|
||||
logger.warn("Failed to delete recording file during bulk delete", {
|
||||
filePath: recording.audioUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
await prisma.recording.deleteMany({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
// 清除相关缓存
|
||||
cache.delete(`recordings:user:${userId}`);
|
||||
cache.delete(`stats:user:${userId}`);
|
||||
|
||||
logger.logDbOperation("deleteMany", "recording", Date.now() - startTime);
|
||||
logger.logUserAction(userId, "delete_all_recordings", {
|
||||
count: recordings.length,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Failed to delete user recordings",
|
||||
{ userId },
|
||||
error as Error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
151
lib/services/user-settings.service.ts
Normal file
151
lib/services/user-settings.service.ts
Normal file
@ -0,0 +1,151 @@
|
||||
import { prisma } from "../database";
|
||||
import { UserSettings } from "@prisma/client";
|
||||
|
||||
export interface CreateUserSettingsData {
|
||||
userId: string;
|
||||
defaultQuality?: string;
|
||||
publicProfile?: boolean;
|
||||
allowDownload?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateUserSettingsData {
|
||||
defaultQuality?: string;
|
||||
publicProfile?: boolean;
|
||||
allowDownload?: boolean;
|
||||
}
|
||||
|
||||
export class UserSettingsService {
|
||||
/**
|
||||
* 获取用户设置
|
||||
*/
|
||||
static async getUserSettings(userId: string): Promise<UserSettings | null> {
|
||||
try {
|
||||
const settings = await prisma.userSettings.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error("Failed to get user settings:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户设置
|
||||
*/
|
||||
static async createUserSettings(
|
||||
data: CreateUserSettingsData
|
||||
): Promise<UserSettings> {
|
||||
try {
|
||||
const settings = await prisma.userSettings.create({
|
||||
data: {
|
||||
userId: data.userId,
|
||||
defaultQuality: data.defaultQuality ?? "medium",
|
||||
publicProfile: data.publicProfile ?? false,
|
||||
allowDownload: data.allowDownload ?? true,
|
||||
},
|
||||
});
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error("Failed to create user settings:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户设置
|
||||
*/
|
||||
static async updateUserSettings(
|
||||
userId: string,
|
||||
data: UpdateUserSettingsData
|
||||
): Promise<UserSettings> {
|
||||
try {
|
||||
// 检查用户设置是否存在
|
||||
let settings = await prisma.userSettings.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
// 如果不存在,创建默认设置
|
||||
settings = await this.createUserSettings({
|
||||
userId,
|
||||
...data,
|
||||
});
|
||||
} else {
|
||||
// 更新现有设置
|
||||
settings = await prisma.userSettings.update({
|
||||
where: { userId },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error("Failed to update user settings:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户设置
|
||||
*/
|
||||
static async deleteUserSettings(userId: string): Promise<void> {
|
||||
try {
|
||||
await prisma.userSettings.delete({
|
||||
where: { userId },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to delete user settings:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建用户设置
|
||||
*/
|
||||
static async getOrCreateUserSettings(userId: string): Promise<UserSettings> {
|
||||
try {
|
||||
let settings = await this.getUserSettings(userId);
|
||||
|
||||
if (!settings) {
|
||||
settings = await this.createUserSettings({
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error("Failed to get or create user settings:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户设置为默认值
|
||||
*/
|
||||
static async resetUserSettings(userId: string): Promise<UserSettings> {
|
||||
try {
|
||||
const settings = await prisma.userSettings.upsert({
|
||||
where: { userId },
|
||||
update: {
|
||||
defaultQuality: "medium",
|
||||
publicProfile: false,
|
||||
allowDownload: true,
|
||||
},
|
||||
create: {
|
||||
userId,
|
||||
defaultQuality: "medium",
|
||||
publicProfile: false,
|
||||
allowDownload: true,
|
||||
},
|
||||
});
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error("Failed to reset user settings:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
159
lib/services/user.service.ts
Normal file
159
lib/services/user.service.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import { prisma } from '../database'
|
||||
import { User, Prisma } from '@prisma/client'
|
||||
import bcrypt from 'bcrypt'
|
||||
|
||||
export interface CreateUserData {
|
||||
email: string
|
||||
name?: string
|
||||
password?: string
|
||||
image?: string
|
||||
}
|
||||
|
||||
export interface UpdateUserData {
|
||||
name?: string
|
||||
email?: string
|
||||
image?: string
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string
|
||||
name: string | null
|
||||
email: string | null
|
||||
image: string | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export class UserService {
|
||||
/**
|
||||
* 创建新用户
|
||||
*/
|
||||
static async createUser(data: CreateUserData): Promise<User> {
|
||||
const { email, name, password, image } = data
|
||||
|
||||
// 验证邮箱格式
|
||||
if (!this.isValidEmail(email)) {
|
||||
throw new Error('邮箱格式不正确')
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error('邮箱已被注册')
|
||||
}
|
||||
|
||||
// 哈希密码(如果提供)
|
||||
let hashedPassword: string | undefined
|
||||
if (password) {
|
||||
if (password.length < 6) {
|
||||
throw new Error('密码长度至少6位')
|
||||
}
|
||||
hashedPassword = await bcrypt.hash(password, 12)
|
||||
}
|
||||
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
name,
|
||||
image,
|
||||
hashedPassword
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户
|
||||
*/
|
||||
static async getUserById(id: string): Promise<UserProfile | null> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
}
|
||||
})
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据邮箱获取用户
|
||||
*/
|
||||
static async getUserByEmail(email: string): Promise<User | null> {
|
||||
return prisma.user.findUnique({
|
||||
where: { email }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
static async updateUser(id: string, data: UpdateUserData): Promise<UserProfile> {
|
||||
const updateData: Prisma.UserUpdateInput = {}
|
||||
|
||||
if (data.name !== undefined) {
|
||||
updateData.name = data.name
|
||||
}
|
||||
|
||||
if (data.email !== undefined) {
|
||||
if (!this.isValidEmail(data.email)) {
|
||||
throw new Error('邮箱格式不正确')
|
||||
}
|
||||
updateData.email = data.email
|
||||
}
|
||||
|
||||
if (data.image !== undefined) {
|
||||
updateData.image = data.image
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
image: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
}
|
||||
})
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户密码
|
||||
*/
|
||||
static async verifyPassword(user: User, password: string): Promise<boolean> {
|
||||
if (!user.hashedPassword) {
|
||||
return false
|
||||
}
|
||||
|
||||
return bcrypt.compare(password, user.hashedPassword)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
static async deleteUser(id: string): Promise<void> {
|
||||
await prisma.user.delete({
|
||||
where: { id }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱格式
|
||||
*/
|
||||
private static isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user