注册支持用户名/手机号/邮箱 + 镜像站部署脚本 + AI 助手 Tool Calling 重构

- 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
This commit is contained in:
yuzhiran-dev
2026-06-01 23:14:42 +08:00
parent 6f3fe50ee0
commit 538de50bb1
329 changed files with 8777 additions and 868 deletions
+120 -7
View File
@@ -1,4 +1,4 @@
import { Controller, Post, Get, Put, Body, UseGuards, Req, Param, HttpException, HttpStatus } from '@nestjs/common';
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';
@@ -7,6 +7,7 @@ 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')
@@ -17,6 +18,7 @@ export class AdminController {
private prisma: PrismaService,
private aiGateway: AIGatewayService,
private aiAssistant: AdminAiAssistantService,
private gatewayPay: GatewayPayService,
) {}
@Post('login')
@@ -34,13 +36,124 @@ export class AdminController {
@Get('orders')
@UseGuards(AuthGuard('jwt'), AdminGuard)
@ApiBearerAuth()
async orders(@Req() req: any) {
const items = await this.prisma.order.findMany({
orderBy: { createdAt: 'desc' },
take: 100,
include: { user: { select: { id: true, nickname: true, phone: true } } },
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 } } },
});
return { items };
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')