feat: admin back-office system + AI assistant with action commands
- Admin analytics (recharts charts, overview stats, trend analysis, time range selector) - Admin permissions (AdminRole/AdminUser models, role CRUD, permission catalog) - Admin system config (site/AI/member category tabs, per-key save) - Admin operations (Banner CRUD, push notification send/delete) - Admin user management (table, search, create/edit, ban/delete) - Admin layout (custom top bar with branding + sidebar, admin AI assistant) - Admin login (adminToken localStorage, adminInfo display) - Admin AI assistant (purple '运营助手', context-aware prompts, action commands) - Public AI assistant Phase B (action commands: navigate, setModel, startChat, openSkill, setParameter) - Assistant context mapping + action executor - Skills API fix: tags parsing fallback (JSON.parse → split) - Sandbox crash fix: curScene fallback system prompt - JWT token expiration: access 2h→7d, refresh 7d→30d, admin 8h→7d - i18n: assistant-related translations (zh/en) - PM2 ecosystem config for process management - AI assistant login prompt fix: admin uses getAdminToken(), public uses apiFetch with token refresh
This commit is contained in:
@@ -337,16 +337,30 @@ model Subscription {
|
||||
@@map("subscriptions")
|
||||
}
|
||||
|
||||
model AdminRole {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
description String?
|
||||
permissions Json @default("[]")
|
||||
status String @default("ACTIVE")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
admins AdminUser[]
|
||||
|
||||
@@map("admin_roles")
|
||||
}
|
||||
|
||||
model AdminUser {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
passwordHash String
|
||||
nickname String?
|
||||
role String @default("editor")
|
||||
status String @default("ACTIVE")
|
||||
roleId String?
|
||||
role AdminRole? @relation(fields: [roleId], references: [id])
|
||||
status String @default("ACTIVE")
|
||||
lastLoginAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("admin_users")
|
||||
}
|
||||
@@ -529,3 +543,48 @@ model Notification {
|
||||
@@index([userId, isRead])
|
||||
@@map("notifications")
|
||||
}
|
||||
|
||||
model Banner {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
image String
|
||||
link String?
|
||||
position String @default("home")
|
||||
sortOrder Int @default(0)
|
||||
status String @default("DRAFT")
|
||||
startAt DateTime?
|
||||
endAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("banners")
|
||||
}
|
||||
|
||||
model SystemNotification {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
content String
|
||||
type String @default("system")
|
||||
target String @default("all")
|
||||
targetIds String?
|
||||
status String @default("DRAFT")
|
||||
scheduledAt DateTime?
|
||||
sentAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("system_notifications")
|
||||
}
|
||||
|
||||
model SystemConfig {
|
||||
id String @id @default(cuid())
|
||||
category String
|
||||
key String @unique
|
||||
value String
|
||||
description String?
|
||||
status String @default("ACTIVE")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("system_configs")
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@ import { JwtModule } from '@nestjs/jwt';
|
||||
import { AdminController } from './admin.controller';
|
||||
import { AdminService } from './admin.service';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
import { AnalyticsController } from './analytics.controller';
|
||||
import { SettingsController } from './settings.controller';
|
||||
import { OperationsController } from './operations.controller';
|
||||
import { UsersController } from './users.controller';
|
||||
import { CoursesModule } from '../courses/courses.module';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [CoursesModule, AuthModule],
|
||||
controllers: [AdminController],
|
||||
controllers: [AdminController, AnalyticsController, SettingsController, OperationsController, UsersController],
|
||||
providers: [AdminService, AdminGuard],
|
||||
exports: [AdminService],
|
||||
})
|
||||
|
||||
@@ -11,7 +11,10 @@ export class AdminService {
|
||||
) {}
|
||||
|
||||
async login(username: string, password: string) {
|
||||
const admin = await this.prisma.adminUser.findUnique({ where: { username } });
|
||||
const admin = await this.prisma.adminUser.findUnique({
|
||||
where: { username },
|
||||
include: { role: true },
|
||||
});
|
||||
if (!admin || admin.status !== 'ACTIVE') {
|
||||
throw new UnauthorizedException('管理员账号不可用');
|
||||
}
|
||||
@@ -28,10 +31,10 @@ export class AdminService {
|
||||
|
||||
const token = this.jwtService.sign(
|
||||
{ sub: admin.id, type: 'admin' },
|
||||
{ expiresIn: '8h' },
|
||||
{ expiresIn: '7d' },
|
||||
);
|
||||
|
||||
return { token, id: admin.id, username: admin.username, role: admin.role };
|
||||
return { token, id: admin.id, username: admin.username, role: admin.role?.name };
|
||||
}
|
||||
|
||||
async getDashboard() {
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
@ApiTags('管理后台 - 数据分析')
|
||||
@Controller('admin/analytics')
|
||||
@UseGuards(AuthGuard('jwt'), AdminGuard)
|
||||
@ApiBearerAuth()
|
||||
export class AnalyticsController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
@Get('overview')
|
||||
async overview(@Query('range') range: string = '7d') {
|
||||
const now = new Date();
|
||||
let startDate: Date;
|
||||
let prevStartDate: Date;
|
||||
let prevEndDate: Date;
|
||||
|
||||
switch (range) {
|
||||
case 'today':
|
||||
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
prevStartDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
||||
prevEndDate = startDate;
|
||||
break;
|
||||
case '30d':
|
||||
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
prevStartDate = new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000);
|
||||
prevEndDate = startDate;
|
||||
break;
|
||||
case '7d':
|
||||
default:
|
||||
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
prevStartDate = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000);
|
||||
prevEndDate = startDate;
|
||||
}
|
||||
|
||||
const [
|
||||
totalUsers,
|
||||
activeUsers,
|
||||
newUsers,
|
||||
prevNewUsers,
|
||||
totalOrders,
|
||||
todayOrders,
|
||||
prevTodayOrders,
|
||||
totalRevenue,
|
||||
todayRevenue,
|
||||
totalCourses,
|
||||
totalSkills,
|
||||
] = await Promise.all([
|
||||
this.prisma.user.count({ where: { deletedAt: null } }),
|
||||
this.prisma.user.count({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
updatedAt: { gte: startDate },
|
||||
},
|
||||
}),
|
||||
this.prisma.user.count({
|
||||
where: { createdAt: { gte: startDate }, deletedAt: null },
|
||||
}),
|
||||
this.prisma.user.count({
|
||||
where: {
|
||||
createdAt: { gte: prevStartDate, lt: prevEndDate },
|
||||
deletedAt: null,
|
||||
},
|
||||
}),
|
||||
this.prisma.order.count({ where: { status: 'PAID' } }),
|
||||
this.prisma.order.count({
|
||||
where: {
|
||||
status: 'PAID',
|
||||
createdAt: { gte: new Date(now.getFullYear(), now.getMonth(), now.getDate()) },
|
||||
},
|
||||
}),
|
||||
this.prisma.order.count({
|
||||
where: {
|
||||
status: 'PAID',
|
||||
createdAt: {
|
||||
gte: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1),
|
||||
lt: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
|
||||
},
|
||||
},
|
||||
}),
|
||||
this.prisma.order.aggregate({
|
||||
_sum: { amount: true },
|
||||
where: { status: 'PAID' },
|
||||
}),
|
||||
this.prisma.order.aggregate({
|
||||
_sum: { amount: true },
|
||||
where: {
|
||||
status: 'PAID',
|
||||
createdAt: { gte: new Date(now.getFullYear(), now.getMonth(), now.getDate()) },
|
||||
},
|
||||
}),
|
||||
this.prisma.course.count({ where: { deletedAt: null } }),
|
||||
this.prisma.skill.count({ where: { status: 'ACTIVE' } }),
|
||||
]);
|
||||
|
||||
const revenue = totalRevenue._sum.amount || 0;
|
||||
const todayRev = todayRevenue._sum.amount || 0;
|
||||
const prevRev = todayRev * (prevNewUsers > 0 ? newUsers / prevNewUsers : 1);
|
||||
|
||||
const userGrowth = prevNewUsers > 0 ? ((newUsers - prevNewUsers) / prevNewUsers * 100) : 0;
|
||||
const orderGrowth = prevTodayOrders > 0 ? ((todayOrders - prevTodayOrders) / prevTodayOrders * 100) : 0;
|
||||
const revGrowth = prevRev > 0 ? ((todayRev - prevRev) / prevRev * 100) : 0;
|
||||
|
||||
return {
|
||||
users: {
|
||||
total: totalUsers,
|
||||
active: activeUsers,
|
||||
new: newUsers,
|
||||
growth: Math.round(userGrowth * 10) / 10,
|
||||
},
|
||||
orders: {
|
||||
total: totalOrders,
|
||||
today: todayOrders,
|
||||
growth: Math.round(orderGrowth * 10) / 10,
|
||||
},
|
||||
revenue: {
|
||||
total: revenue,
|
||||
today: todayRev,
|
||||
growth: Math.round(revGrowth * 10) / 10,
|
||||
},
|
||||
courses: {
|
||||
total: totalCourses,
|
||||
},
|
||||
skills: {
|
||||
total: totalSkills,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('trend')
|
||||
async trend(@Query('type') type: string = 'users', @Query('days') days: number = 30) {
|
||||
const now = new Date();
|
||||
const startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
|
||||
|
||||
if (type === 'users') {
|
||||
const data = await this.prisma.$queryRaw<{ date: string; count: bigint }[]>`
|
||||
SELECT DATE(created_at) as date, COUNT(*) as count
|
||||
FROM User
|
||||
WHERE deleted_at IS NULL AND created_at >= ${startDate}
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date
|
||||
`;
|
||||
return data.map(d => ({ date: d.date, count: Number(d.count) }));
|
||||
}
|
||||
|
||||
if (type === 'orders') {
|
||||
const data = await this.prisma.$queryRaw<{ date: string; count: bigint; revenue: bigint }[]>`
|
||||
SELECT DATE(created_at) as date, COUNT(*) as count, COALESCE(SUM(amount), 0) as revenue
|
||||
FROM \`Order\`
|
||||
WHERE status = 'PAID' AND created_at >= ${startDate}
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date
|
||||
`;
|
||||
return data.map(d => ({ date: d.date, count: Number(d.count), revenue: Number(d.revenue) }));
|
||||
}
|
||||
|
||||
if (type === 'sessions') {
|
||||
const data = await this.prisma.$queryRaw<{ date: string; count: bigint }[]>`
|
||||
SELECT DATE(created_at) as date, COUNT(*) as count
|
||||
FROM sandbox_session
|
||||
WHERE created_at >= ${startDate}
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date
|
||||
`;
|
||||
return data.map(d => ({ date: d.date, count: Number(d.count) }));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@Get('top')
|
||||
async top(@Query('type') type: string = 'courses') {
|
||||
if (type === 'courses') {
|
||||
return this.prisma.course.findMany({
|
||||
where: { deletedAt: null, status: 'PUBLISHED' },
|
||||
select: { id: true, title: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'skills') {
|
||||
return this.prisma.skill.findMany({
|
||||
where: { status: 'ACTIVE' },
|
||||
select: { id: true, name: true, category: true },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
take: 10,
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
@ApiTags('管理后台 - 运营管理')
|
||||
@Controller('admin')
|
||||
@UseGuards(AuthGuard('jwt'), AdminGuard)
|
||||
@ApiBearerAuth()
|
||||
export class OperationsController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
// Banners
|
||||
@Get('banners')
|
||||
async banners() {
|
||||
return { items: await this.prisma.banner.findMany({ orderBy: { sortOrder: 'asc' } }) };
|
||||
}
|
||||
|
||||
@Post('banners')
|
||||
async createBanner(@Body() body: { title: string; image: string; link?: string; position?: string; sortOrder?: number; status?: string }) {
|
||||
return this.prisma.banner.create({
|
||||
data: {
|
||||
title: body.title,
|
||||
image: body.image,
|
||||
link: body.link || '',
|
||||
position: body.position || 'home',
|
||||
sortOrder: body.sortOrder || 0,
|
||||
status: body.status || 'DRAFT',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Put('banners/:id')
|
||||
async updateBanner(@Param('id') id: string, @Body() body: any) {
|
||||
return this.prisma.banner.update({ where: { id: parseInt(id) }, data: body });
|
||||
}
|
||||
|
||||
@Delete('banners/:id')
|
||||
async deleteBanner(@Param('id') id: string) {
|
||||
return this.prisma.banner.delete({ where: { id: parseInt(id) } });
|
||||
}
|
||||
|
||||
// Notifications
|
||||
@Get('notifications')
|
||||
async notifications() {
|
||||
return { items: await this.prisma.systemNotification.findMany({ orderBy: { createdAt: 'desc' } }) };
|
||||
}
|
||||
|
||||
@Post('notifications')
|
||||
async createNotification(@Body() body: { title: string; content: string; type?: string; target?: string; status?: string }) {
|
||||
return this.prisma.systemNotification.create({
|
||||
data: {
|
||||
title: body.title,
|
||||
content: body.content,
|
||||
type: body.type || 'system',
|
||||
target: body.target || 'all',
|
||||
status: body.status || 'DRAFT',
|
||||
sentAt: body.status === 'SENT' ? new Date() : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Delete('notifications/:id')
|
||||
async deleteNotification(@Param('id') id: string) {
|
||||
return this.prisma.systemNotification.delete({ where: { id: parseInt(id) } });
|
||||
}
|
||||
|
||||
// Config
|
||||
@Get('config')
|
||||
async configs() {
|
||||
return { items: await this.prisma.systemConfig.findMany() };
|
||||
}
|
||||
|
||||
@Get('config/:category')
|
||||
async configByCategory(@Param('category') category: string) {
|
||||
return { items: await this.prisma.systemConfig.findMany({ where: { category } }) };
|
||||
}
|
||||
|
||||
@Put('config/:key')
|
||||
async updateConfig(@Param('key') key: string, @Body() body: { value: string }) {
|
||||
const existing = await this.prisma.systemConfig.findUnique({ where: { key } });
|
||||
if (existing) {
|
||||
return this.prisma.systemConfig.update({ where: { key }, data: { value: body.value } });
|
||||
}
|
||||
return this.prisma.systemConfig.create({
|
||||
data: { key, value: body.value, category: 'other' },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
@ApiTags('管理后台 - 角色权限')
|
||||
@Controller('admin/settings')
|
||||
@UseGuards(AuthGuard('jwt'), AdminGuard)
|
||||
@ApiBearerAuth()
|
||||
export class SettingsController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
@Get('roles')
|
||||
async roles() {
|
||||
return {
|
||||
items: await this.prisma.adminRole.findMany({
|
||||
where: { status: 'ACTIVE' },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('roles')
|
||||
async createRole(@Body() body: { name: string; description?: string; permissions?: string[] }) {
|
||||
return this.prisma.adminRole.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
description: body.description || '',
|
||||
permissions: body.permissions || [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Put('roles/:id')
|
||||
async updateRole(@Param('id') id: string, @Body() body: { name?: string; description?: string; permissions?: string[] }) {
|
||||
const data: any = {};
|
||||
if (body.name) data.name = body.name;
|
||||
if (body.description !== undefined) data.description = body.description;
|
||||
if (body.permissions) data.permissions = body.permissions;
|
||||
return this.prisma.adminRole.update({ where: { id }, data });
|
||||
}
|
||||
|
||||
@Delete('roles/:id')
|
||||
async deleteRole(@Param('id') id: string) {
|
||||
return this.prisma.adminRole.update({ where: { id }, data: { status: 'DISABLED' } });
|
||||
}
|
||||
|
||||
@Get('admins')
|
||||
async admins() {
|
||||
return {
|
||||
items: await this.prisma.adminUser.findMany({
|
||||
where: { status: 'ACTIVE' },
|
||||
include: { role: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('admins')
|
||||
async createAdmin(@Body() body: { username: string; password: string; nickname?: string; roleId?: string }) {
|
||||
const bcrypt = require('bcryptjs');
|
||||
const passwordHash = await bcrypt.hash(body.password, 10);
|
||||
return this.prisma.adminUser.create({
|
||||
data: {
|
||||
username: body.username,
|
||||
passwordHash,
|
||||
nickname: body.nickname || '',
|
||||
roleId: body.roleId || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Put('admins/:id')
|
||||
async updateAdmin(@Param('id') id: string, @Body() body: { nickname?: string; roleId?: string; status?: string }) {
|
||||
const data: any = {};
|
||||
if (body.nickname !== undefined) data.nickname = body.nickname;
|
||||
if (body.roleId !== undefined) data.roleId = body.roleId;
|
||||
if (body.status) data.status = body.status;
|
||||
return this.prisma.adminUser.update({ where: { id: parseInt(id) }, data });
|
||||
}
|
||||
|
||||
@Delete('admins/:id')
|
||||
async deleteAdmin(@Param('id') id: string) {
|
||||
return this.prisma.adminUser.update({ where: { id: parseInt(id) }, data: { status: 'DISABLED' } });
|
||||
}
|
||||
|
||||
@Get('permissions')
|
||||
permissions() {
|
||||
return {
|
||||
items: [
|
||||
{ key: 'dashboard', name: '仪表盘', category: '首页' },
|
||||
{ key: 'users', name: '用户管理', category: '用户' },
|
||||
{ key: 'courses', name: '课程管理', category: '内容' },
|
||||
{ key: 'prompts', name: '提示词管理', category: '内容' },
|
||||
{ key: 'contents', name: '内容管理', category: '内容' },
|
||||
{ key: 'tools', name: '工具管理', category: '内容' },
|
||||
{ key: 'orders', name: '订单管理', category: '交易' },
|
||||
{ key: 'enterprise', name: '企业版管理', category: '交易' },
|
||||
{ key: 'comments', name: '评论审核', category: '社区' },
|
||||
{ key: 'analytics', name: '数据分析', category: '统计' },
|
||||
{ key: 'roles', name: '角色权限', category: '设置' },
|
||||
{ key: 'admins', name: '管理员', category: '设置' },
|
||||
{ key: 'config', name: '系统配置', category: '设置' },
|
||||
{ key: 'banners', name: 'Banner管理', category: '运营' },
|
||||
{ key: 'notifications', name: '推送管理', category: '运营' },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { AdminGuard } from './admin.guard';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
@ApiTags('管理后台 - 用户管理')
|
||||
@Controller('admin/users')
|
||||
@UseGuards(AuthGuard('jwt'), AdminGuard)
|
||||
@ApiBearerAuth()
|
||||
export class UsersController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
@Get()
|
||||
async list(@Query('search') search?: string) {
|
||||
const where: any = {};
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nickname: { contains: search } },
|
||||
{ phone: { contains: search } },
|
||||
{ email: { contains: search } },
|
||||
];
|
||||
}
|
||||
return {
|
||||
items: await this.prisma.user.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true, phone: true, nickname: true, email: true,
|
||||
status: true, memberPlan: true, createdAt: true,
|
||||
},
|
||||
orderBy: { id: 'desc' },
|
||||
take: 100,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() body: { phone: string; nickname?: string; email?: string; password: string }) {
|
||||
const bcrypt = require('bcryptjs');
|
||||
const passwordHash = await bcrypt.hash(body.password, 10);
|
||||
return this.prisma.user.create({
|
||||
data: {
|
||||
phone: body.phone,
|
||||
nickname: body.nickname || '',
|
||||
email: body.email || '',
|
||||
passwordHash,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
async update(@Param('id') id: string, @Body() body: { nickname?: string; email?: string; status?: string }) {
|
||||
const data: any = {};
|
||||
if (body.nickname !== undefined) data.nickname = body.nickname;
|
||||
if (body.email !== undefined) data.email = body.email;
|
||||
if (body.status) data.status = body.status;
|
||||
return this.prisma.user.update({ where: { id: parseInt(id) }, data });
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async delete(@Param('id') id: string) {
|
||||
return this.prisma.user.update({
|
||||
where: { id: parseInt(id) },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { JwtStrategy } from './jwt.strategy';
|
||||
JwtModule.registerAsync({
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.get('JWT_SECRET'),
|
||||
signOptions: { expiresIn: config.get('JWT_EXPIRES_IN') || '2h' },
|
||||
signOptions: { expiresIn: config.get('JWT_EXPIRES_IN') || '7d' },
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
|
||||
@@ -84,8 +84,8 @@ export class AuthService {
|
||||
private generateTokens(userId: number) {
|
||||
const payload = { sub: userId };
|
||||
return {
|
||||
accessToken: this.jwtService.sign(payload, { expiresIn: '2h' }),
|
||||
refreshToken: this.jwtService.sign(payload, { expiresIn: '7d' }),
|
||||
accessToken: this.jwtService.sign(payload, { expiresIn: '7d' }),
|
||||
refreshToken: this.jwtService.sign(payload, { expiresIn: '30d' }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { SkillsService, Skill } from './skills.service';
|
||||
import { SkillsService } from './skills.service';
|
||||
|
||||
@ApiTags('技能')
|
||||
@Controller('skills')
|
||||
|
||||
@@ -5,6 +5,16 @@ import { PrismaService } from '../../prisma/prisma.service';
|
||||
export class SkillsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
private parseTags(tags: string): string[] {
|
||||
if (!tags) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(tags);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return tags.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(params: { category?: string; difficulty?: string; search?: string; tag?: string }) {
|
||||
const where: any = { status: 'ACTIVE' };
|
||||
if (params.category) where.category = params.category;
|
||||
@@ -29,6 +39,7 @@ export class SkillsService {
|
||||
...s,
|
||||
starters: JSON.parse(s.starters),
|
||||
tasks: JSON.parse(s.tasks),
|
||||
tags: this.parseTags(s.tags),
|
||||
})),
|
||||
total: items.length,
|
||||
};
|
||||
@@ -41,6 +52,7 @@ export class SkillsService {
|
||||
...skill,
|
||||
starters: JSON.parse(skill.starters),
|
||||
tasks: JSON.parse(skill.tasks),
|
||||
tags: this.parseTags(skill.tags),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user