import { Controller, Post, Get, Put, Delete, Body, UseGuards, Req, Param, Query, HttpException, HttpStatus, ParseIntPipe } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { AuthGuard } from '@nestjs/passport'; import { AdminService } from './admin.service'; import { AdminGuard } from './admin.guard'; import { CoursesService } from '../courses/courses.service'; import { PrismaService } from '../../prisma/prisma.service'; import { AIGatewayService } from '../ai/ai-gateway.service'; import { AdminAiAssistantService } from './ai-assistant.service'; import { GatewayPayService } from '../payment/gateway-pay.service'; @ApiTags('管理后台') @Controller('admin') export class AdminController { constructor( private adminService: AdminService, private coursesService: CoursesService, private prisma: PrismaService, private aiGateway: AIGatewayService, private aiAssistant: AdminAiAssistantService, private gatewayPay: GatewayPayService, ) {} @Post('login') async login(@Body() body: { username: string; password: string }) { return this.adminService.login(body.username, body.password); } @Get('dashboard') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async dashboard() { return this.adminService.getDashboard(); } @Get('orders') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async orders( @Query('page') page?: string, @Query('pageSize') pageSize?: string, @Query('search') search?: string, @Query('status') status?: string, @Query('payChannel') payChannel?: string, @Query('planType') planType?: string, ) { const p = Math.max(1, Number(page) || 1); const ps = Math.min(100, Math.max(1, Number(pageSize) || 20)); const skip = (p - 1) * ps; const where: any = {}; if (status) where.status = status; if (payChannel) where.payChannel = payChannel; if (planType) where.planType = planType; if (search) { where.OR = [ { orderNo: { contains: search } }, { user: { nickname: { contains: search } } }, { user: { phone: { contains: search } } }, ]; } const [items, total] = await Promise.all([ this.prisma.order.findMany({ where, orderBy: { createdAt: 'desc' }, skip, take: ps, include: { user: { select: { id: true, nickname: true, phone: true } } }, }), this.prisma.order.count({ where }), ]); return { items, total, page: p, pageSize: ps }; } @Get('orders/:orderNo') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async orderDetail(@Param('orderNo') orderNo: string) { const order = await this.prisma.order.findUnique({ where: { orderNo }, include: { user: { select: { id: true, nickname: true, phone: true, email: true, memberPlan: true, memberExpire: true } } }, }); if (!order) throw new HttpException('订单不存在', HttpStatus.NOT_FOUND); return order; } @Post('orders/:orderNo/refund') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async orderRefund(@Param('orderNo') orderNo: string, @Body() body: { amount?: number; reason?: string }) { const order = await this.prisma.order.findUnique({ where: { orderNo } }); if (!order) throw new HttpException('订单不存在', HttpStatus.NOT_FOUND); if (order.status !== 'PAID') throw new HttpException('订单未支付,无法退款', HttpStatus.BAD_REQUEST); return this.gatewayPay.refund(orderNo, body.amount || order.amount, body.reason); } @Post('orders/:orderNo/mark-paid') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async markOrderPaid(@Param('orderNo') orderNo: string) { const order = await this.prisma.order.findUnique({ where: { orderNo } }); if (!order) throw new HttpException('订单不存在', HttpStatus.NOT_FOUND); if (order.status !== 'PENDING') throw new HttpException('只能标记待支付订单为已支付', HttpStatus.BAD_REQUEST); await this.prisma.order.update({ where: { id: order.id }, data: { status: 'PAID', paidAt: new Date(), transactionId: `MANUAL_${Date.now()}` }, }); // 激活订阅 const mockOrder = { planType: order.planType, amount: order.amount }; if (mockOrder.planType === 'MONTHLY' || mockOrder.planType === 'YEARLY') { const durationDays = mockOrder.planType === 'MONTHLY' ? 30 : 365; const now = new Date(); let endDate: Date; const existingSub = await this.prisma.subscription.findFirst({ where: { userId: order.userId, status: 'ACTIVE', endDate: { gt: now } }, }); if (existingSub) { endDate = new Date(existingSub.endDate.getTime() + durationDays * 24 * 60 * 60 * 1000); await this.prisma.subscription.update({ where: { id: existingSub.id }, data: { endDate } }); } else { endDate = new Date(now); endDate.setDate(endDate.getDate() + durationDays); await this.prisma.subscription.create({ data: { userId: order.userId, plan: order.planType as any, startDate: now, endDate, status: 'ACTIVE' }, }); } await this.prisma.user.update({ where: { id: order.userId }, data: { memberPlan: order.planType as any, memberExpire: endDate }, }); } return { ok: true, message: '订单已标记为已支付' }; } @Get('orders/:orderNo/query') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async queryOrderPayment(@Param('orderNo') orderNo: string) { const order = await this.prisma.order.findUnique({ where: { orderNo } }); if (!order) throw new HttpException('订单不存在', HttpStatus.NOT_FOUND); if (order.gatewayOrderId && !order.gatewayOrderId.startsWith('mock_')) { return this.gatewayPay.queryOrder(order.gatewayOrderId); } return { outTradeNo: orderNo, localStatus: order.status, amount: order.amount, planType: order.planType, }; } @Put('courses/:id/chapters') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async updateCourseChapters(@Param('id') id: string, @Body() body: any) { return this.coursesService.updateWithChapters(+id, body); } @Get('comments/pending') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async pendingComments() { const items = await this.prisma.comment.findMany({ where: { status: 'PENDING_REVIEW' }, include: { user: { select: { id: true, nickname: true, avatar: true } }, post: { select: { id: true, title: true } }, }, orderBy: { createdAt: 'desc' }, take: 50, }); return { items }; } @Put('comments/:id/approve') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async approveComment(@Param('id') id: string) { await this.prisma.comment.update({ where: { id: parseInt(id) }, data: { status: 'PUBLISHED', reviewedAt: new Date() }, }); return { ok: true }; } @Put('comments/:id/reject') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async rejectComment(@Param('id') id: string, @Body() body: { reason?: string }) { await this.prisma.comment.update({ where: { id: parseInt(id) }, data: { status: 'REJECTED', reviewNote: body.reason || '违规内容', reviewedAt: new Date() }, }); return { ok: true }; } @Post('ai-assistant/chat') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async aiAssistantChat(@Body() body: { messages: { role: string; content: string }[] }) { try { const { messages } = body; const apiMessages = messages.map(m => ({ role: m.role as 'user' | 'assistant' | 'system', content: m.content })); const reply = await this.aiGateway.chat('general', apiMessages, { temperature: 0.7, max_tokens: 2000 }); return { reply }; } catch (err: any) { throw new HttpException(err.message || 'AI 助手请求失败', HttpStatus.INTERNAL_SERVER_ERROR); } } @Post('ai-assistant/action') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async aiAssistantAction(@Body() body: { tool: string; params: Record; messages: { role: string; content: string }[] }) { const result = await this.aiAssistant.execute({ tool: body.tool, params: body.params }); try { const apiMessages = [ { role: 'system' as const, content: '你是一个管理后台助手。下面是对用户请求的执行结果,请用自然语言总结给用户,语言简洁。' }, ...body.messages.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content })), { role: 'assistant' as const, content: `【工具执行结果】\n工具: ${body.tool}\n参数: ${JSON.stringify(body.params)}\n结果: ${result.summary}\n数据: ${JSON.stringify(result.data)}` }, ]; const reply = await this.aiGateway.chat('general', apiMessages, { temperature: 0.3, max_tokens: 1000 }); return { reply, result }; } catch { return { reply: result.summary, result }; } } // --- Practice Questions Admin --- @Get('practices') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async adminPractices(@Query('page') page?: string, @Query('pageSize') pageSize?: string, @Query('search') search?: string) { const p = Math.max(1, Number(page) || 1); const ps = Math.min(100, Math.max(1, Number(pageSize) || 20)); const where: any = {}; if (search) where.title = { contains: search }; const [items, total] = await Promise.all([ this.prisma.practiceQuestion.findMany({ where, orderBy: { sortOrder: 'asc' }, skip: (p - 1) * ps, take: ps }), this.prisma.practiceQuestion.count({ where }), ]); return { items, total, page: p, pageSize: ps }; } @Get('practices/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async adminPractice(@Param('id', ParseIntPipe) id: number) { const item = await this.prisma.practiceQuestion.findUnique({ where: { id } }); if (!item) throw new HttpException('练习题目不存在', HttpStatus.NOT_FOUND); return item; } @Post('practices') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async createPractice(@Body() body: any) { const item = await this.prisma.practiceQuestion.create({ data: body }); return item; } @Put('practices/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async updatePractice(@Param('id', ParseIntPipe) id: number, @Body() body: any) { const { id: _id, ...data } = body; const item = await this.prisma.practiceQuestion.update({ where: { id }, data }); return item; } @Delete('practices/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async deletePractice(@Param('id', ParseIntPipe) id: number) { await this.prisma.practiceQuestion.delete({ where: { id } }); return { ok: true }; } // --- Skills Admin --- @Get('skills') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async adminSkills(@Query('page') page?: string, @Query('pageSize') pageSize?: string) { const p = Math.max(1, Number(page) || 1); const ps = Math.min(100, Math.max(1, Number(pageSize) || 20)); const [items, total] = await Promise.all([ this.prisma.skill.findMany({ orderBy: { sortOrder: 'asc' }, skip: (p - 1) * ps, take: ps }), this.prisma.skill.count(), ]); return { items, total, page: p, pageSize: ps }; } @Get('skills/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async adminSkill(@Param('id') id: string) { const item = await this.prisma.skill.findUnique({ where: { id } }); if (!item) throw new HttpException('技能不存在', HttpStatus.NOT_FOUND); return item; } @Put('skills/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async updateSkill(@Param('id') id: string, @Body() body: any) { const { id: _id, purchasedBy, ...data } = body; const item = await this.prisma.skill.update({ where: { id }, data }); return item; } // --- Circles Admin --- @Get('circles') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async adminCircles(@Query('page') page?: string, @Query('pageSize') pageSize?: string) { const p = Math.max(1, Number(page) || 1); const ps = Math.min(100, Math.max(1, Number(pageSize) || 20)); const [items, total] = await Promise.all([ this.prisma.circle.findMany({ orderBy: { createdAt: 'desc' }, skip: (p - 1) * ps, take: ps, include: { creator: { select: { id: true, nickname: true } }, _count: { select: { members: true, posts: true } } } }), this.prisma.circle.count(), ]); return { items, total, page: p, pageSize: ps }; } @Delete('circles/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async deleteCircle(@Param('id', ParseIntPipe) id: number) { await this.prisma.circle.delete({ where: { id } }); return { ok: true }; } // --- Community Posts Admin --- @Get('posts') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async adminPosts(@Query('page') page?: string, @Query('pageSize') pageSize?: string, @Query('status') status?: string) { const p = Math.max(1, Number(page) || 1); const ps = Math.min(100, Math.max(1, Number(pageSize) || 20)); const where: any = {}; if (status) where.status = status; const [items, total] = await Promise.all([ this.prisma.post.findMany({ where, orderBy: { createdAt: 'desc' }, skip: (p - 1) * ps, take: ps, include: { user: { select: { id: true, nickname: true } }, _count: { select: { comments: true, likes: true } } }, }), this.prisma.post.count({ where }), ]); return { items, total, page: p, pageSize: ps }; } @Get('posts/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async adminPost(@Param('id', ParseIntPipe) id: number) { const item = await this.prisma.post.findUnique({ where: { id }, include: { user: { select: { id: true, nickname: true } }, _count: { select: { comments: true, likes: true } } }, }); if (!item) throw new HttpException('帖子不存在', HttpStatus.NOT_FOUND); return item; } @Put('posts/:id/status') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async updatePostStatus(@Param('id', ParseIntPipe) id: number, @Body() body: { status: string }) { const item = await this.prisma.post.update({ where: { id }, data: { status: body.status } }); return item; } @Delete('posts/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async deletePost(@Param('id', ParseIntPipe) id: number) { await this.prisma.post.delete({ where: { id } }); return { ok: true }; } // --- AI Tools Admin --- @Get('tools') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async adminTools(@Query('page') page?: string, @Query('pageSize') pageSize?: string, @Query('search') search?: string) { const p = Math.max(1, Number(page) || 1); const ps = Math.min(100, Math.max(1, Number(pageSize) || 20)); const where: any = {}; if (search) where.name = { contains: search }; const [items, total] = await Promise.all([ this.prisma.tool.findMany({ where, orderBy: { createdAt: 'desc' }, skip: (p - 1) * ps, take: ps }), this.prisma.tool.count({ where }), ]); return { items, total, page: p, pageSize: ps }; } @Get('tools/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async adminTool(@Param('id', ParseIntPipe) id: number) { const item = await this.prisma.tool.findUnique({ where: { id } }); if (!item) throw new HttpException('工具不存在', HttpStatus.NOT_FOUND); return item; } @Post('tools') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async createTool(@Body() body: any) { const item = await this.prisma.tool.create({ data: body }); return item; } @Put('tools/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async updateTool(@Param('id', ParseIntPipe) id: number, @Body() body: any) { const { id: _id, ...data } = body; const item = await this.prisma.tool.update({ where: { id }, data }); return item; } @Delete('tools/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async deleteTool(@Param('id', ParseIntPipe) id: number) { await this.prisma.tool.delete({ where: { id } }); return { ok: true }; } // --- AI Models Admin --- @Get('models') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async adminModels(@Query('page') page?: string, @Query('pageSize') pageSize?: string) { const p = Math.max(1, Number(page) || 1); const ps = Math.min(100, Math.max(1, Number(pageSize) || 20)); const [items, total] = await Promise.all([ this.prisma.aiModel.findMany({ orderBy: { sortOrder: 'asc' }, skip: (p - 1) * ps, take: ps }), this.prisma.aiModel.count(), ]); return { items, total, page: p, pageSize: ps }; } @Get('models/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async adminModel(@Param('id', ParseIntPipe) id: number) { const item = await this.prisma.aiModel.findUnique({ where: { id } }); if (!item) throw new HttpException('模型不存在', HttpStatus.NOT_FOUND); return item; } @Post('models') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async createModel(@Body() body: any) { const item = await this.prisma.aiModel.create({ data: body }); return item; } @Put('models/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async updateModel(@Param('id', ParseIntPipe) id: number, @Body() body: any) { const { id: _id, ...data } = body; const item = await this.prisma.aiModel.update({ where: { id }, data }); return item; } @Delete('models/:id') @UseGuards(AuthGuard('jwt'), AdminGuard) @ApiBearerAuth() async deleteModel(@Param('id', ParseIntPipe) id: number) { await this.prisma.aiModel.delete({ where: { id } }); return { ok: true }; } }