注册支持用户名/手机号/邮箱 + 镜像站部署脚本 + 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:
Regular → Executable
+120
-7
@@ -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')
|
||||
|
||||
Regular → Executable
Regular → Executable
+2
-1
@@ -12,9 +12,10 @@ import { PublicController } from './public.controller';
|
||||
import { CoursesModule } from '../courses/courses.module';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { AIModule } from '../ai/ai.module';
|
||||
import { PaymentModule } from '../payment/payment.module';
|
||||
|
||||
@Module({
|
||||
imports: [CoursesModule, AuthModule, AIModule],
|
||||
imports: [CoursesModule, AuthModule, AIModule, PaymentModule],
|
||||
controllers: [AdminController, AnalyticsController, SettingsController, OperationsController, UsersController, PublicController],
|
||||
providers: [AdminService, AdminGuard, AdminAiAssistantService],
|
||||
exports: [AdminService],
|
||||
|
||||
Regular → Executable
Regular → Executable
+35
@@ -208,6 +208,15 @@ export class AdminAiAssistantService {
|
||||
{
|
||||
name: 'delete-admin', description: '禁用一个管理员账号', parameters: { id: { type: 'number', description: '管理员ID' } },
|
||||
},
|
||||
// ---- 全局搜索 ----
|
||||
{
|
||||
name: 'search', description: '全局搜索用户、订单、课程、内容等',
|
||||
parameters: { q: { type: 'string', description: '搜索关键词(必填)' }, type: { type: 'string', enum: ['all', 'users', 'orders', 'courses', 'contents'], description: '搜索范围(可选,默认全部)' } },
|
||||
},
|
||||
// ---- 通知批量操作 ----
|
||||
{
|
||||
name: 'mark-all-notifications-read', description: '将所有系统通知标记为已读', parameters: {},
|
||||
},
|
||||
// ---- 企业版 ----
|
||||
{
|
||||
name: 'get-enterprise-orgs', description: '查看所有企业组织', parameters: {},
|
||||
@@ -383,6 +392,32 @@ export class AdminAiAssistantService {
|
||||
return { success: true, data: { id: call.params.id, status: newPromptStatus }, summary: `提示词 ${call.params.id} 状态已切换为 ${newPromptStatus}` };
|
||||
}
|
||||
|
||||
case 'search': {
|
||||
const q = call.params.q;
|
||||
if (!q) throw new Error('请输入搜索关键词');
|
||||
const results: any[] = [];
|
||||
if (call.params.type === 'all' || call.params.type === 'users') {
|
||||
const users = await this.prisma.user.findMany({ where: { deletedAt: null, OR: [{ nickname: { contains: q } }, { phone: { contains: q } }, { email: { contains: q } }] }, select: { id: true, nickname: true, phone: true, email: true, status: true }, take: 20 });
|
||||
results.push(...users.map(u => ({ ...u, _type: 'user' })));
|
||||
}
|
||||
if (call.params.type === 'all' || call.params.type === 'orders') {
|
||||
const orders = await this.prisma.order.findMany({ where: { OR: [{ orderNo: { contains: q } }] }, take: 20, orderBy: { createdAt: 'desc' } });
|
||||
results.push(...orders.map(o => ({ ...o, _type: 'order' })));
|
||||
}
|
||||
if (call.params.type === 'all' || call.params.type === 'courses') {
|
||||
const courses = await this.prisma.course.findMany({ where: { deletedAt: null, OR: [{ title: { contains: q } }, { description: { contains: q } }] }, select: { id: true, title: true, status: true }, take: 20 });
|
||||
results.push(...courses.map(c => ({ ...c, _type: 'course' })));
|
||||
}
|
||||
if (call.params.type === 'all' || call.params.type === 'contents') {
|
||||
const contents = await this.prisma.content.findMany({ where: { deletedAt: null, OR: [{ title: { contains: q } }, { summary: { contains: q } }] }, select: { id: true, title: true, status: true }, take: 20 });
|
||||
results.push(...contents.map(c => ({ ...c, _type: 'content' })));
|
||||
}
|
||||
return { success: true, data: { results, total: results.length }, summary: `找到 ${results.length} 条结果,共 ${results.filter(r => r._type === 'user').length} 个用户、${results.filter(r => r._type === 'order').length} 个订单、${results.filter(r => r._type === 'course').length} 个课程、${results.filter(r => r._type === 'content').length} 个内容` };
|
||||
}
|
||||
case 'mark-all-notifications-read': {
|
||||
await this.prisma.notification.updateMany({ where: { isRead: false }, data: { isRead: true } });
|
||||
return { success: true, data: {}, summary: '所有通知已标记为已读' };
|
||||
}
|
||||
case 'get-enterprise-orgs': {
|
||||
const items = await this.prisma.organization.findMany({ take: 50, orderBy: { createdAt: 'desc' } });
|
||||
return { success: true, data: { items, total: items.length }, summary: `共 ${items.length} 个企业组织` };
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Reference in New Issue
Block a user