538de50bb1
- Prisma User 模型新增 username 字段(唯一索引) - 注册先查重复再创建,返回友好中文提示(非 500) - 登录支持用户名/手机号/邮箱三种方式 - 前端注册表单增加用户名输入框,预校验 2-20 位格式 - 新增 scripts/deploy.sh:一键构建并部署主站+镜像站+重启后端+重载 Nginx - 镜像站 www.yuzhiran.com.cn Nginx 配置与主站同步 - AI 助手架构升级:用户端/管理后台均采用完整 Tool Calling 架构 - 新增 UserAiAssistantService(18 工具)+ AiAssistantController - admin 助手新增 search + mark-all-notifications-read 工具 - 修复注册 500 错误:catch Prisma P2002 → BadRequestException - Baidu Analytics Script 注入 root layout
236 lines
8.9 KiB
TypeScript
Executable File
236 lines
8.9 KiB
TypeScript
Executable File
import { Controller, Post, Get, Put, Body, UseGuards, Req, Param, Query, HttpException, HttpStatus } 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 };
|
|
}
|
|
}
|
|
}
|