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
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Controller, Post, Get, Put, Body, UseGuards, Req, Param, Query, HttpException, HttpStatus } from '@nestjs/common';
|
||||
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';
|
||||
@@ -232,4 +232,253 @@ export class AdminController {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,17 @@ import { PrismaService } from '../../prisma/prisma.service';
|
||||
export class PublicController {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
@Get('stats')
|
||||
async getStats() {
|
||||
const [courses, prompts, tools, users] = await Promise.all([
|
||||
this.prisma.course.count({ where: { status: 'PUBLISHED', deletedAt: null } }),
|
||||
this.prisma.prompt.count({ where: { status: 'PUBLISHED' } }),
|
||||
this.prisma.tool.count({ where: { status: 'PUBLISHED' } }),
|
||||
this.prisma.user.count({ where: { status: 'ACTIVE' } }),
|
||||
]);
|
||||
return { courses, prompts, tools, users };
|
||||
}
|
||||
|
||||
@Get('banners')
|
||||
async banners(@Query('position') position?: string) {
|
||||
const now = new Date();
|
||||
|
||||
Reference in New Issue
Block a user