feat: Docker部署与CI/CD集成, 搜索栏修复, 上传目录改为data
This commit is contained in:
373
backend/services/materialService.ts
Normal file
373
backend/services/materialService.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
|
||||
import prisma from '../../lib/prisma';
|
||||
import { MaterialDTO, MaterialType } from '../../types';
|
||||
|
||||
export const MaterialService = {
|
||||
/**
|
||||
* Fetch all materials with relations (Author, Comments)
|
||||
*/
|
||||
async getAllMaterials(filterType?: MaterialType, page = 1, limit = 20, search?: string) {
|
||||
const where: any = filterType ? { type: filterType } : {};
|
||||
if (search && search.trim()) {
|
||||
const q = search.trim();
|
||||
where.OR = [
|
||||
{ title: { contains: q, mode: 'insensitive' } },
|
||||
{ description: { contains: q, mode: 'insensitive' } },
|
||||
{ tags: { some: { name: { contains: q, mode: 'insensitive' } } } }
|
||||
];
|
||||
}
|
||||
|
||||
const [materials, total] = await Promise.all([
|
||||
prisma.material.findMany({
|
||||
where,
|
||||
include: {
|
||||
author: {
|
||||
select: { id: true, username: true, avatarUrl: true, role: true, status: true, createdAt: true, lastLogin: true }
|
||||
},
|
||||
comments: {
|
||||
include: {
|
||||
author: { select: { id: true, username: true, role: true, status: true, createdAt: true, lastLogin: true, avatarUrl: true } }
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
},
|
||||
favorites: true, // To count them
|
||||
tags: true
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit
|
||||
}),
|
||||
prisma.material.count({ where })
|
||||
]);
|
||||
|
||||
// Transform Prisma Result to DTO
|
||||
const items = materials.map(m => ({
|
||||
id: m.id,
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
type: m.type as MaterialType,
|
||||
contentUrl: m.contentUrl || undefined,
|
||||
codeSnippet: m.codeSnippet || undefined,
|
||||
language: m.language || undefined,
|
||||
author: {
|
||||
...m.author,
|
||||
role: m.author.role as any,
|
||||
createdAt: m.author.createdAt.toISOString(),
|
||||
lastLogin: m.author.lastLogin?.toISOString() || '',
|
||||
avatarUrl: m.author.avatarUrl || ''
|
||||
},
|
||||
stats: {
|
||||
views: m.views,
|
||||
downloads: m.downloads,
|
||||
favorites: m.favorites.length
|
||||
},
|
||||
tags: m.tags.map(t => t.name),
|
||||
createdAt: m.createdAt.toISOString(),
|
||||
comments: m.comments.map(c => ({
|
||||
id: c.id,
|
||||
content: c.content,
|
||||
createdAt: c.createdAt.toISOString(),
|
||||
author: {
|
||||
...c.author,
|
||||
role: c.author.role as any,
|
||||
createdAt: c.author.createdAt.toISOString(),
|
||||
lastLogin: c.author.lastLogin?.toISOString() || '',
|
||||
avatarUrl: c.author.avatarUrl || ''
|
||||
}
|
||||
}))
|
||||
}));
|
||||
return { items, total };
|
||||
},
|
||||
|
||||
async getMaterialsByAuthor(userId: string, page = 1, limit = 20) {
|
||||
const [materials, total] = await Promise.all([
|
||||
prisma.material.findMany({
|
||||
where: { authorId: userId },
|
||||
include: {
|
||||
author: {
|
||||
select: { id: true, username: true, avatarUrl: true, role: true, status: true, createdAt: true, lastLogin: true }
|
||||
},
|
||||
comments: {
|
||||
include: {
|
||||
author: { select: { id: true, username: true, role: true, status: true, createdAt: true, lastLogin: true, avatarUrl: true } }
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
},
|
||||
favorites: true,
|
||||
tags: true
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit
|
||||
}),
|
||||
prisma.material.count({ where: { authorId: userId } })
|
||||
]);
|
||||
|
||||
const items = materials.map(m => ({
|
||||
id: m.id,
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
type: m.type as MaterialType,
|
||||
contentUrl: m.contentUrl || undefined,
|
||||
codeSnippet: m.codeSnippet || undefined,
|
||||
language: m.language || undefined,
|
||||
author: {
|
||||
...m.author,
|
||||
role: m.author.role as any,
|
||||
createdAt: m.author.createdAt.toISOString(),
|
||||
lastLogin: m.author.lastLogin?.toISOString() || '',
|
||||
avatarUrl: m.author.avatarUrl || ''
|
||||
},
|
||||
stats: {
|
||||
views: m.views,
|
||||
downloads: m.downloads,
|
||||
favorites: m.favorites.length
|
||||
},
|
||||
tags: m.tags.map(t => t.name),
|
||||
createdAt: m.createdAt.toISOString(),
|
||||
comments: m.comments.map(c => ({
|
||||
id: c.id,
|
||||
content: c.content,
|
||||
createdAt: c.createdAt.toISOString(),
|
||||
author: {
|
||||
...c.author,
|
||||
role: c.author.role as any,
|
||||
createdAt: c.author.createdAt.toISOString(),
|
||||
lastLogin: c.author.lastLogin?.toISOString() || '',
|
||||
avatarUrl: c.author.avatarUrl || ''
|
||||
}
|
||||
}))
|
||||
}));
|
||||
return { items, total };
|
||||
},
|
||||
|
||||
async getFavoritedMaterialsByUser(userId: string, page = 1, limit = 20) {
|
||||
const where = { favorites: { some: { userId } } };
|
||||
const [materials, total] = await Promise.all([
|
||||
prisma.material.findMany({
|
||||
where: { favorites: { some: { userId } } },
|
||||
include: {
|
||||
author: {
|
||||
select: { id: true, username: true, avatarUrl: true, role: true, status: true, createdAt: true, lastLogin: true }
|
||||
},
|
||||
comments: {
|
||||
include: {
|
||||
author: { select: { id: true, username: true, role: true, status: true, createdAt: true, lastLogin: true, avatarUrl: true } }
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
},
|
||||
favorites: true,
|
||||
tags: true
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit
|
||||
}),
|
||||
prisma.material.count({ where })
|
||||
]);
|
||||
|
||||
const items = materials.map(m => ({
|
||||
id: m.id,
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
type: m.type as MaterialType,
|
||||
contentUrl: m.contentUrl || undefined,
|
||||
codeSnippet: m.codeSnippet || undefined,
|
||||
language: m.language || undefined,
|
||||
author: {
|
||||
...m.author,
|
||||
role: m.author.role as any,
|
||||
createdAt: m.author.createdAt.toISOString(),
|
||||
lastLogin: m.author.lastLogin?.toISOString() || '',
|
||||
avatarUrl: m.author.avatarUrl || ''
|
||||
},
|
||||
stats: {
|
||||
views: m.views,
|
||||
downloads: m.downloads,
|
||||
favorites: m.favorites.length
|
||||
},
|
||||
tags: m.tags.map(t => t.name),
|
||||
createdAt: m.createdAt.toISOString(),
|
||||
comments: m.comments.map(c => ({
|
||||
id: c.id,
|
||||
content: c.content,
|
||||
createdAt: c.createdAt.toISOString(),
|
||||
author: {
|
||||
...c.author,
|
||||
role: c.author.role as any,
|
||||
createdAt: c.author.createdAt.toISOString(),
|
||||
lastLogin: c.author.lastLogin?.toISOString() || '',
|
||||
avatarUrl: c.author.avatarUrl || ''
|
||||
}
|
||||
}))
|
||||
}));
|
||||
return { items, total };
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new Material
|
||||
*/
|
||||
async createMaterial(userId: string, data: any) {
|
||||
return prisma.material.create({
|
||||
data: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
type: data.type,
|
||||
codeSnippet: data.codeSnippet,
|
||||
contentUrl: data.contentUrl,
|
||||
language: data.language || 'text',
|
||||
authorId: userId,
|
||||
tags: {
|
||||
connectOrCreate: data.tags.map((tag: string) => ({
|
||||
where: { name: tag },
|
||||
create: { name: tag }
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async deleteMaterial(id: string) {
|
||||
return prisma.material.delete({ where: { id } });
|
||||
},
|
||||
|
||||
async incrementView(id: string) {
|
||||
return prisma.material.update({
|
||||
where: { id },
|
||||
data: { views: { increment: 1 } }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get material by ID with all relations
|
||||
*/
|
||||
async getMaterialById(id: string): Promise<MaterialDTO | null> {
|
||||
const material = await prisma.material.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
author: {
|
||||
select: { id: true, username: true, avatarUrl: true, role: true, status: true, createdAt: true, lastLogin: true }
|
||||
},
|
||||
comments: {
|
||||
include: {
|
||||
author: { select: { id: true, username: true, role: true, status: true, createdAt: true, lastLogin: true, avatarUrl: true } }
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
},
|
||||
favorites: true,
|
||||
tags: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!material) return null;
|
||||
|
||||
// Also increment view count
|
||||
await this.incrementView(id);
|
||||
|
||||
// Transform to DTO
|
||||
return {
|
||||
id: material.id,
|
||||
title: material.title,
|
||||
description: material.description,
|
||||
type: material.type as MaterialType,
|
||||
contentUrl: material.contentUrl || undefined,
|
||||
codeSnippet: material.codeSnippet || undefined,
|
||||
language: material.language || undefined,
|
||||
author: {
|
||||
...material.author,
|
||||
role: material.author.role as any,
|
||||
createdAt: material.author.createdAt.toISOString(),
|
||||
lastLogin: material.author.lastLogin?.toISOString() || '',
|
||||
avatarUrl: material.author.avatarUrl || ''
|
||||
},
|
||||
stats: {
|
||||
views: material.views + 1, // Include the increment we just did
|
||||
downloads: material.downloads,
|
||||
favorites: material.favorites.length
|
||||
},
|
||||
tags: material.tags.map(t => t.name),
|
||||
createdAt: material.createdAt.toISOString(),
|
||||
comments: material.comments.map(c => ({
|
||||
id: c.id,
|
||||
content: c.content,
|
||||
createdAt: c.createdAt.toISOString(),
|
||||
author: {
|
||||
...c.author,
|
||||
role: c.author.role as any,
|
||||
createdAt: c.author.createdAt.toISOString(),
|
||||
lastLogin: c.author.lastLogin?.toISOString() || '',
|
||||
avatarUrl: c.author.avatarUrl || ''
|
||||
}
|
||||
}))
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a comment to a material
|
||||
*/
|
||||
async addComment(materialId: string, userId: string, content: string) {
|
||||
return prisma.comment.create({
|
||||
data: {
|
||||
content,
|
||||
authorId: userId,
|
||||
materialId
|
||||
},
|
||||
include: {
|
||||
author: { select: { id: true, username: true, avatarUrl: true, role: true, status: true, createdAt: true, lastLogin: true } }
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle favorite status for a material
|
||||
* Returns the new favorite count
|
||||
*/
|
||||
async toggleFavorite(materialId: string, userId: string): Promise<number> {
|
||||
// Check if already favorited
|
||||
const existing = await prisma.favorite.findUnique({
|
||||
where: {
|
||||
userId_materialId: {
|
||||
userId,
|
||||
materialId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Remove favorite
|
||||
await prisma.favorite.delete({
|
||||
where: {
|
||||
userId_materialId: {
|
||||
userId,
|
||||
materialId
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Add favorite
|
||||
await prisma.favorite.create({
|
||||
data: {
|
||||
userId,
|
||||
materialId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Return new favorite count
|
||||
const count = await prisma.favorite.count({
|
||||
where: { materialId }
|
||||
});
|
||||
|
||||
return count;
|
||||
},
|
||||
|
||||
/**
|
||||
* Increment download count
|
||||
*/
|
||||
async incrementDownload(id: string) {
|
||||
return prisma.material.update({
|
||||
where: { id },
|
||||
data: { downloads: { increment: 1 } }
|
||||
});
|
||||
}
|
||||
};
|
||||
144
backend/services/userService.ts
Normal file
144
backend/services/userService.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
|
||||
import prisma from '../../lib/prisma';
|
||||
import { UserDTO, UserRole } from '../../types';
|
||||
import { hashPassword, verifyPassword } from '../../lib/auth';
|
||||
|
||||
export const UserService = {
|
||||
/**
|
||||
* Retrieves a user by ID with specific fields selected.
|
||||
*/
|
||||
async getUserById(id: string): Promise<UserDTO | null> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
avatarUrl: true,
|
||||
role: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
lastLogin: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return {
|
||||
...user,
|
||||
role: user.role as UserRole, // Cast Prisma enum to TS enum
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
lastLogin: user.lastLogin?.toISOString() || new Date().toISOString(),
|
||||
avatarUrl: user.avatarUrl || ''
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get user by username (for login)
|
||||
*/
|
||||
async getUserByUsername(username: string) {
|
||||
return prisma.user.findUnique({
|
||||
where: { username }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new user with hashed password
|
||||
*/
|
||||
async createUser(username: string, password: string, email?: string) {
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
// Generate a random avatar URL using DiceBear or similar service
|
||||
const avatarUrl = `https://api.dicebear.com/7.x/avataaars/svg?seed=${username}`;
|
||||
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
password: hashedPassword,
|
||||
avatarUrl,
|
||||
role: 'CREATOR',
|
||||
status: 'ACTIVE'
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Authenticate user with username and password
|
||||
* Returns user if credentials are valid, null otherwise
|
||||
*/
|
||||
async authenticateUser(username: string, password: string): Promise<UserDTO | null> {
|
||||
const user = await this.getUserByUsername(username);
|
||||
|
||||
if (!user || !user.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValid = await verifyPassword(password, user.password);
|
||||
if (!isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update last login time
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLogin: new Date() }
|
||||
});
|
||||
|
||||
// Return user DTO (without password)
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
avatarUrl: user.avatarUrl || '',
|
||||
role: user.role as UserRole,
|
||||
status: user.status as any,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
lastLogin: new Date().toISOString()
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Admin: Get all users
|
||||
*/
|
||||
async getAllUsers() {
|
||||
return prisma.user.findMany({
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates user profile settings
|
||||
*/
|
||||
async updateUser(id: string, data: { username?: string; avatarUrl?: string }) {
|
||||
return prisma.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
username: data.username,
|
||||
avatarUrl: data.avatarUrl
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Admin: Update user role
|
||||
*/
|
||||
async updateUserRole(id: string, role: 'USER' | 'CREATOR' | 'MANAGER' | 'ADMIN') {
|
||||
return prisma.user.update({
|
||||
where: { id },
|
||||
data: { role }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Admin: Toggle Ban Status
|
||||
*/
|
||||
async toggleUserStatus(id: string) {
|
||||
const user = await prisma.user.findUnique({ where: { id } });
|
||||
if (!user) throw new Error('User not found');
|
||||
|
||||
const newStatus = user.status === 'ACTIVE' ? 'BANNED' : 'ACTIVE';
|
||||
|
||||
return prisma.user.update({
|
||||
where: { id },
|
||||
data: { status: newStatus }
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user