Files
ai-learning-platform/backend/src/modules/admin/admin.controller.ts
T
yuzhiran-dev 0f215d2aad P8 平台轻量化改造 + SSG修复 + 编程导师 + 文档完善
- Prisma: Tool 模型加 affiliateLink;免费用户沙盒 10→5 次/日
- 后端: Tools API + /admin/tools CRUD 5 端点;Practices 完整模块
- 导航: 主菜单隐藏企业版/社区(URL 可访问)
- 首页: 重定位为 AI 工具指南;新增精选工具区块;Feature 重写
- 工具页: affiliateLink 绿色推荐 Badge
- SSG 修复: config.ts 构建时直连 localhost:4000,页面 108→127
- 沙盒: 新增编程导师场景(苏格拉底教学法)
- 练习系统: Practices 多场景练习(含结构化评分)
- 技能广场: 6 个付费 Skill(标题大师/回款助手等)
- 管理后台: Models/Posts/Practices CRUD 页面
- 文档: README + progress.md 全面更新;AGENTS.md 同步定位
- 清理: .env.example 移除;tsbuildinfo gitignore
2026-06-18 18:14:07 +08:00

485 lines
18 KiB
TypeScript
Executable File

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<string, any>; 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 };
}
}