From bead1cf4dce50b0e07130f6146a09e4f01260bc1 Mon Sep 17 00:00:00 2001 From: aid Date: Thu, 2 Apr 2026 14:04:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=A5=E7=A6=BB=E5=AD=A6=E6=A0=A1=E7=AB=AF/?= =?UTF-8?q?=E6=95=99=E5=B8=88=E7=AB=AF/=E5=AD=A6=E7=94=9F=E7=AB=AF/3D?= =?UTF-8?q?=E5=BB=BA=E6=A8=A1=E6=A8=A1=E5=9D=97=EF=BC=8C=E6=B8=85=E7=90=86?= =?UTF-8?q?=E8=B7=A8=E6=A8=A1=E5=9D=97=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 backend: school/ (schools, departments, grades, classes, teachers, students) - 移除 backend: ai-3d/ (controller, service, providers, utils) - 移除 frontend: views/school/, views/workbench/ai-3d/, views/model/ - 移除 prisma schema: School, Grade, Department, Class, Teacher, Student, StudentInterestClass, AI3DTask 共8个模型 - 移除 app.module.ts: SchoolModule, AI3DModule 导入 - 移除 router/index.ts: 3D建模4条路由 - 移除 menu.ts: componentMap 中学校/3D映射 - 修复 registrations.service.ts: 教师判断从 Teacher 模型改为角色判断 - 修复 results.service.ts: 移除 student include - 修复 homework services: 移除 student/class/grade 相关 Prisma 查询 - 保留 students.ts/teachers.ts/ai-3d.ts 最小类型存根供赛事组件引用 - 原始代码备份至 competition-management-system-stripped-modules/ Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/prisma/schema.prisma | 230 -- backend/src/ai-3d/ai-3d.controller.ts | 375 --- backend/src/ai-3d/ai-3d.module.ts | 39 - backend/src/ai-3d/ai-3d.service.ts | 525 --- backend/src/ai-3d/dto/create-task.dto.ts | 63 - backend/src/ai-3d/dto/query-task.dto.ts | 23 - .../providers/ai-3d-provider.interface.ts | 63 - .../src/ai-3d/providers/hunyuan.provider.ts | 269 -- backend/src/ai-3d/providers/mock.provider.ts | 179 -- backend/src/ai-3d/utils/tencent-cloud-sign.ts | 101 - backend/src/ai-3d/utils/zip-handler.ts | 176 - backend/src/app.module.ts | 4 - .../registrations/registrations.service.ts | 60 +- .../src/contests/results/results.service.ts | 14 - .../homework/homeworks/homeworks.service.ts | 29 +- .../submissions/submissions.service.ts | 93 +- .../src/school/classes/classes.controller.ts | 74 - backend/src/school/classes/classes.module.ts | 13 - backend/src/school/classes/classes.service.ts | 282 -- .../school/classes/dto/create-class.dto.ts | 32 - .../school/classes/dto/update-class.dto.ts | 36 - .../departments/departments.controller.ts | 78 - .../school/departments/departments.module.ts | 13 - .../school/departments/departments.service.ts | 361 --- .../departments/dto/create-department.dto.ts | 28 - .../departments/dto/update-department.dto.ts | 30 - .../src/school/grades/dto/create-grade.dto.ts | 23 - .../src/school/grades/dto/update-grade.dto.ts | 26 - .../src/school/grades/grades.controller.ts | 70 - backend/src/school/grades/grades.module.ts | 13 - backend/src/school/grades/grades.service.ts | 203 -- backend/src/school/school.module.ts | 28 - .../school/schools/dto/create-school.dto.ts | 37 - .../school/schools/dto/update-school.dto.ts | 37 - .../src/school/schools/schools.controller.ts | 59 - backend/src/school/schools/schools.module.ts | 13 - backend/src/school/schools/schools.service.ts | 112 - .../school/students/dto/create-student.dto.ts | 81 - .../school/students/dto/update-student.dto.ts | 79 - .../school/students/students.controller.ts | 78 - .../src/school/students/students.module.ts | 13 - .../src/school/students/students.service.ts | 542 ---- .../school/teachers/dto/create-teacher.dto.ts | 71 - .../school/teachers/dto/update-teacher.dto.ts | 69 - .../school/teachers/teachers.controller.ts | 82 - .../src/school/teachers/teachers.module.ts | 13 - .../src/school/teachers/teachers.service.ts | 395 --- frontend/src/api/ai-3d.ts | 158 +- frontend/src/api/schools.ts | 73 - frontend/src/api/students.ts | 210 +- frontend/src/api/teachers.ts | 172 +- frontend/src/router/index.ts | 44 - frontend/src/utils/menu.ts | 9 - frontend/src/views/model/ModelViewer.vue | 2360 -------------- frontend/src/views/school/classes/Index.vue | 369 --- .../src/views/school/departments/Index.vue | 351 -- frontend/src/views/school/grades/Index.vue | 280 -- frontend/src/views/school/schools/Index.vue | 307 -- frontend/src/views/school/students/Index.vue | 552 ---- frontend/src/views/school/teachers/Index.vue | 498 --- .../src/views/workbench/ai-3d/Generate.vue | 1081 ------- .../src/views/workbench/ai-3d/History.vue | 961 ------ frontend/src/views/workbench/ai-3d/Index.vue | 2822 ----------------- 63 files changed, 176 insertions(+), 15305 deletions(-) delete mode 100644 backend/src/ai-3d/ai-3d.controller.ts delete mode 100644 backend/src/ai-3d/ai-3d.module.ts delete mode 100644 backend/src/ai-3d/ai-3d.service.ts delete mode 100644 backend/src/ai-3d/dto/create-task.dto.ts delete mode 100644 backend/src/ai-3d/dto/query-task.dto.ts delete mode 100644 backend/src/ai-3d/providers/ai-3d-provider.interface.ts delete mode 100644 backend/src/ai-3d/providers/hunyuan.provider.ts delete mode 100644 backend/src/ai-3d/providers/mock.provider.ts delete mode 100644 backend/src/ai-3d/utils/tencent-cloud-sign.ts delete mode 100644 backend/src/ai-3d/utils/zip-handler.ts delete mode 100644 backend/src/school/classes/classes.controller.ts delete mode 100644 backend/src/school/classes/classes.module.ts delete mode 100644 backend/src/school/classes/classes.service.ts delete mode 100644 backend/src/school/classes/dto/create-class.dto.ts delete mode 100644 backend/src/school/classes/dto/update-class.dto.ts delete mode 100644 backend/src/school/departments/departments.controller.ts delete mode 100644 backend/src/school/departments/departments.module.ts delete mode 100644 backend/src/school/departments/departments.service.ts delete mode 100644 backend/src/school/departments/dto/create-department.dto.ts delete mode 100644 backend/src/school/departments/dto/update-department.dto.ts delete mode 100644 backend/src/school/grades/dto/create-grade.dto.ts delete mode 100644 backend/src/school/grades/dto/update-grade.dto.ts delete mode 100644 backend/src/school/grades/grades.controller.ts delete mode 100644 backend/src/school/grades/grades.module.ts delete mode 100644 backend/src/school/grades/grades.service.ts delete mode 100644 backend/src/school/school.module.ts delete mode 100644 backend/src/school/schools/dto/create-school.dto.ts delete mode 100644 backend/src/school/schools/dto/update-school.dto.ts delete mode 100644 backend/src/school/schools/schools.controller.ts delete mode 100644 backend/src/school/schools/schools.module.ts delete mode 100644 backend/src/school/schools/schools.service.ts delete mode 100644 backend/src/school/students/dto/create-student.dto.ts delete mode 100644 backend/src/school/students/dto/update-student.dto.ts delete mode 100644 backend/src/school/students/students.controller.ts delete mode 100644 backend/src/school/students/students.module.ts delete mode 100644 backend/src/school/students/students.service.ts delete mode 100644 backend/src/school/teachers/dto/create-teacher.dto.ts delete mode 100644 backend/src/school/teachers/dto/update-teacher.dto.ts delete mode 100644 backend/src/school/teachers/teachers.controller.ts delete mode 100644 backend/src/school/teachers/teachers.module.ts delete mode 100644 backend/src/school/teachers/teachers.service.ts delete mode 100644 frontend/src/api/schools.ts delete mode 100644 frontend/src/views/model/ModelViewer.vue delete mode 100644 frontend/src/views/school/classes/Index.vue delete mode 100644 frontend/src/views/school/departments/Index.vue delete mode 100644 frontend/src/views/school/grades/Index.vue delete mode 100644 frontend/src/views/school/schools/Index.vue delete mode 100644 frontend/src/views/school/students/Index.vue delete mode 100644 frontend/src/views/school/teachers/Index.vue delete mode 100644 frontend/src/views/workbench/ai-3d/Generate.vue delete mode 100644 frontend/src/views/workbench/ai-3d/History.vue delete mode 100644 frontend/src/views/workbench/ai-3d/Index.vue diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index af288d3..3bf375c 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -31,12 +31,6 @@ model Tenant { permissions Permission[] dicts Dict[] configs Config[] - school School? /// 学校信息(一对一) - grades Grade[] /// 年级 - departments Department[] /// 部门 - classes Class[] /// 班级 - teachers Teacher[] /// 教师 - students Student[] /// 学生 contestTeams ContestTeam[] /// 赛事团队 contestTeamMembers ContestTeamMember[] /// 团队成员 contestRegistrations ContestRegistration[] /// 赛事报名 @@ -50,8 +44,6 @@ model Tenant { homeworkSubmissions HomeworkSubmission[] /// 作业提交记录 homeworkReviewRules HomeworkReviewRule[] /// 作业评审规则 homeworkScores HomeworkScore[] /// 作业评分 - // AI 3D 生成关联 - ai3dTasks AI3DTask[] /// AI 3D 生成任务 creatorUser User? @relation("TenantCreator", fields: [creator], references: [id], onDelete: SetNull) modifierUser User? @relation("TenantModifier", fields: [modifier], references: [id], onDelete: SetNull) @@ -105,20 +97,6 @@ model User { modifiedConfigs Config[] @relation("ConfigModifier") createdTenants Tenant[] @relation("TenantCreator") modifiedTenants Tenant[] @relation("TenantModifier") - teacher Teacher? /// 教师信息(一对一) - student Student? /// 学生信息(一对一) - createdSchools School[] @relation("SchoolCreator") - modifiedSchools School[] @relation("SchoolModifier") - createdGrades Grade[] @relation("GradeCreator") - modifiedGrades Grade[] @relation("GradeModifier") - createdDepartments Department[] @relation("DepartmentCreator") - modifiedDepartments Department[] @relation("DepartmentModifier") - createdClasses Class[] @relation("ClassCreator") - modifiedClasses Class[] @relation("ClassModifier") - createdTeachers Teacher[] @relation("TeacherCreator") - modifiedTeachers Teacher[] @relation("TeacherModifier") - createdStudents Student[] @relation("StudentCreator") - modifiedStudents Student[] @relation("StudentModifier") // 赛事相关关联 createdContests Contest[] @relation("ContestCreator") modifiedContests Contest[] @relation("ContestModifier") @@ -164,8 +142,6 @@ model User { homeworkScoresAsReviewer HomeworkScore[] @relation("HomeworkScoreReviewer") createdHomeworkScores HomeworkScore[] @relation("HomeworkScoreCreator") modifiedHomeworkScores HomeworkScore[] @relation("HomeworkScoreModifier") - // AI 3D 生成关联 - ai3dTasks AI3DTask[] @relation("AI3DTaskUser") /// 用户的 AI 3D 生成任务 // 预设评语关联 presetComments PresetComment[] @relation("PresetCommentJudge") /// 评委的预设评语 // 子女关联 @@ -378,183 +354,6 @@ model Log { @@map("logs") } -/// 学校信息表(扩展租户信息) -model School { - id Int @id @default(autoincrement()) - tenantId Int @unique @map("tenant_id") /// 租户ID(一对一) - address String? /// 学校地址 - phone String? /// 联系电话 - principal String? /// 校长姓名 - established DateTime? /// 建校时间 - description String? @db.Text /// 学校描述 - logo String? /// 学校Logo URL - website String? /// 学校网站 - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 修改时间 - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - creatorUser User? @relation("SchoolCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("SchoolModifier", fields: [modifier], references: [id], onDelete: SetNull) - - @@map("schools") -} - -/// 年级表 -model Grade { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 租户ID - name String /// 年级名称(如:一年级、二年级) - code String /// 年级编码(在租户内唯一,如:grade_1, grade_2) - level Int /// 年级级别(用于排序,如:1, 2, 3) - description String? /// 年级描述 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 修改时间 - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - classes Class[] /// 班级 - creatorUser User? @relation("GradeCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("GradeModifier", fields: [modifier], references: [id], onDelete: SetNull) - - @@unique([tenantId, code]) - @@unique([tenantId, level]) - @@map("grades") -} - -/// 部门表(支持树形结构) -model Department { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 租户ID - name String /// 部门名称 - code String /// 部门编码(在租户内唯一) - parentId Int? @map("parent_id") /// 父部门ID(支持树形结构) - description String? /// 部门描述 - sort Int @default(0) /// 排序 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 修改时间 - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - parent Department? @relation("DepartmentTree", fields: [parentId], references: [id]) - children Department[] @relation("DepartmentTree") - teachers Teacher[] /// 教师 - creatorUser User? @relation("DepartmentCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("DepartmentModifier", fields: [modifier], references: [id], onDelete: SetNull) - - @@unique([tenantId, code]) - @@map("departments") -} - -/// 班级表 -model Class { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 租户ID - gradeId Int @map("grade_id") /// 年级ID - name String /// 班级名称(如:一年级1班、二年级2班) - code String /// 班级编码(在租户内唯一) - type Int @default(1) @map("type") /// 班级类型:1-行政班级(教学班级),2-兴趣班 - capacity Int? /// 班级容量(可选) - description String? /// 班级描述 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 修改时间 - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - grade Grade @relation(fields: [gradeId], references: [id], onDelete: Cascade) - students Student[] /// 学生(行政班级) - studentInterestClasses StudentInterestClass[] /// 学生兴趣班关联 - creatorUser User? @relation("ClassCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("ClassModifier", fields: [modifier], references: [id], onDelete: SetNull) - - @@unique([tenantId, code]) - @@map("classes") -} - -/// 教师表 -model Teacher { - id Int @id @default(autoincrement()) - userId Int @unique @map("user_id") /// 用户ID(一对一) - tenantId Int @map("tenant_id") /// 租户ID - departmentId Int @map("department_id") /// 部门ID - employeeNo String? @map("employee_no") /// 工号(在租户内唯一) - phone String? /// 联系电话 - idCard String? @map("id_card") /// 身份证号 - gender Int? /// 性别:1-男,2-女 - birthDate DateTime? @map("birth_date") /// 出生日期 - hireDate DateTime? @map("hire_date") /// 入职日期 - subject String? /// 任教科目(可选,如:语文、数学) - title String? /// 职称(可选,如:高级教师、一级教师) - description String? @db.Text /// 教师描述 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 修改时间 - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - department Department @relation(fields: [departmentId], references: [id], onDelete: Restrict) - creatorUser User? @relation("TeacherCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("TeacherModifier", fields: [modifier], references: [id], onDelete: SetNull) - - @@unique([tenantId, employeeNo]) - @@map("teachers") -} - -/// 学生表 -model Student { - id Int @id @default(autoincrement()) - userId Int @unique @map("user_id") /// 用户ID(一对一) - tenantId Int @map("tenant_id") /// 租户ID - classId Int @map("class_id") /// 行政班级ID - studentNo String? @map("student_no") /// 学号(在租户内唯一) - phone String? /// 联系电话 - idCard String? @map("id_card") /// 身份证号 - gender Int? /// 性别:1-男,2-女 - birthDate DateTime? @map("birth_date") /// 出生日期 - enrollmentDate DateTime? @map("enrollment_date") /// 入学日期 - parentName String? @map("parent_name") /// 家长姓名 - parentPhone String? @map("parent_phone") /// 家长电话 - address String? /// 家庭地址 - description String? @db.Text /// 学生描述 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 修改时间 - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - class Class @relation(fields: [classId], references: [id], onDelete: Restrict) - interestClasses StudentInterestClass[] /// 兴趣班关联 - creatorUser User? @relation("StudentCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("StudentModifier", fields: [modifier], references: [id], onDelete: SetNull) - - @@unique([tenantId, studentNo]) - @@map("students") -} - -/// 学生兴趣班关联表(多对多) -model StudentInterestClass { - id Int @id @default(autoincrement()) - studentId Int @map("student_id") /// 学生ID - classId Int @map("class_id") /// 兴趣班ID - - student Student @relation(fields: [studentId], references: [id], onDelete: Cascade) - class Class @relation(fields: [classId], references: [id], onDelete: Cascade) - - @@unique([studentId, classId]) - @@map("student_interest_classes") -} - // ============================================ // 赛事管理模块 // ============================================ @@ -1107,35 +906,6 @@ model HomeworkScore { // AI 3D 模型生成模块 // ============================================ -/// AI 3D 生成任务表 -model AI3DTask { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 租户ID - userId Int @map("user_id") /// 用户ID(任务归属用户) - inputType String @map("input_type") /// 输入类型:text | image - inputContent String @map("input_content") @db.Text /// 输入内容:文字描述或图片URL - generateType String @default("Normal") @map("generate_type") /// 生成类型:Normal(带纹理) | Geometry(白模) | LowPoly(低多边形) | Sketch(草图) - status String @default("pending") /// 任务状态:pending | processing | completed | failed | timeout - resultUrl String? @map("result_url") @db.Text /// 生成的3D模型URL(单个结果,兼容旧数据) - previewUrl String? @map("preview_url") @db.Text /// 预览图URL(单个结果,兼容旧数据) - resultUrls Json? @map("result_urls") /// 生成的3D模型URL数组(多个结果,文生3D生成4个) - previewUrls Json? @map("preview_urls") /// 预览图URL数组(多个结果) - errorMessage String? @map("error_message") @db.Text /// 失败时的错误信息 - externalTaskId String? @map("external_task_id") /// 外部AI服务的任务ID - retryCount Int @default(0) @map("retry_count") /// 已重试次数 - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - completeTime DateTime? @map("complete_time") /// 完成时间 - - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - user User @relation("AI3DTaskUser", fields: [userId], references: [id], onDelete: Cascade) - - @@index([userId]) - @@index([tenantId]) - @@index([status]) - @@index([createTime]) - @@map("t_ai_3d_task") -} - /// 预设评语表 model PresetComment { id Int @id @default(autoincrement()) diff --git a/backend/src/ai-3d/ai-3d.controller.ts b/backend/src/ai-3d/ai-3d.controller.ts deleted file mode 100644 index fc3430f..0000000 --- a/backend/src/ai-3d/ai-3d.controller.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { - Controller, - Get, - Post, - Delete, - Body, - Param, - Query, - UseGuards, - Request, - ParseIntPipe, - Res, - HttpException, - HttpStatus, - Logger, -} from '@nestjs/common'; -import { Response } from 'express'; -import axios from 'axios'; -import AdmZip from 'adm-zip'; -import { AI3DService } from './ai-3d.service'; -import { CreateTaskDto } from './dto/create-task.dto'; -import { QueryTaskDto } from './dto/query-task.dto'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { CurrentTenantId } from '../auth/decorators/current-tenant-id.decorator'; -import { Public } from '../auth/decorators/public.decorator'; - -@Controller('ai-3d') -@UseGuards(JwtAuthGuard) -export class AI3DController { - private readonly logger = new Logger(AI3DController.name); - - constructor(private readonly ai3dService: AI3DService) {} - - /** - * 创建生成任务 - * POST /api/ai-3d/generate - */ - @Post('generate') - createTask( - @Body() createTaskDto: CreateTaskDto, - @CurrentTenantId() tenantId: number, - @Request() req, - ) { - const userId = req?.user?.userId; - return this.ai3dService.createTask(userId, tenantId, createTaskDto); - } - - /** - * 获取任务列表 - * GET /api/ai-3d/tasks - */ - @Get('tasks') - getTasks(@Query() queryDto: QueryTaskDto, @Request() req) { - const userId = req?.user?.userId; - return this.ai3dService.getTasks(userId, queryDto); - } - - /** - * 获取任务详情 - * GET /api/ai-3d/tasks/:id - */ - @Get('tasks/:id') - getTask(@Param('id', ParseIntPipe) id: number, @Request() req) { - const userId = req?.user?.userId; - return this.ai3dService.getTask(userId, id); - } - - /** - * 重试任务 - * POST /api/ai-3d/tasks/:id/retry - */ - @Post('tasks/:id/retry') - retryTask(@Param('id', ParseIntPipe) id: number, @Request() req) { - const userId = req?.user?.userId; - return this.ai3dService.retryTask(userId, id); - } - - /** - * 删除任务 - * DELETE /api/ai-3d/tasks/:id - */ - @Delete('tasks/:id') - deleteTask(@Param('id', ParseIntPipe) id: number, @Request() req) { - const userId = req?.user?.userId; - return this.ai3dService.deleteTask(userId, id); - } - - /** - * 代理模型文件(解决CORS问题) - * GET /api/ai-3d/proxy-model - */ - @Get('proxy-model') - @Public() // 允许公开访问,因为模型URL已经包含签名 - async proxyModel(@Query('url') url: string, @Res() res: Response) { - if (!url) { - throw new HttpException('URL参数不能为空', HttpStatus.BAD_REQUEST); - } - - this.logger.log(`[proxy-model] 开始处理请求,URL长度: ${url.length}`); - - try { - // URL解码(处理URL编码) - let decodedUrl: string; - try { - decodedUrl = decodeURIComponent(url); - this.logger.log(`[proxy-model] URL解码成功`); - } catch (e) { - // 如果解码失败,使用原始URL - decodedUrl = url; - this.logger.warn(`[proxy-model] URL解码失败,使用原始URL`); - } - - // 验证URL是否为腾讯云COS链接(安全验证) - if ( - !decodedUrl.includes('tencentcos.cn') && - !decodedUrl.includes('qcloud.com') - ) { - throw new HttpException( - '不支持的URL来源,仅支持腾讯云COS链接', - HttpStatus.BAD_REQUEST, - ); - } - - // 验证URL格式 - try { - new URL(decodedUrl); - } catch (e) { - throw new HttpException('URL格式无效', HttpStatus.BAD_REQUEST); - } - - this.logger.log(`[proxy-model] 开始下载文件...`); - - // 从源URL获取文件 - const response = await axios.get(decodedUrl, { - responseType: 'arraybuffer', // 使用arraybuffer以便处理ZIP - timeout: 120000, // 120秒超时(ZIP文件可能较大) - maxContentLength: 100 * 1024 * 1024, // 最大100MB - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', - Accept: '*/*', - }, - }); - - this.logger.log( - `[proxy-model] 文件下载成功,大小: ${response.data.byteLength} bytes`, - ); - - let fileData: Buffer = Buffer.from(response.data); - let contentType = - response.headers['content-type'] || 'application/octet-stream'; - let contentLength = fileData.length; - - this.logger.log(`[proxy-model] Content-Type: ${contentType}`); - - // 如果是ZIP文件,解压并提取3D模型文件 - if ( - decodedUrl.toLowerCase().includes('.zip') || - contentType.includes('zip') || - contentType.includes('application/zip') - ) { - this.logger.log(`[proxy-model] 检测到ZIP文件,开始解压...`); - try { - const zip = new AdmZip(fileData); - const zipEntries = zip.getEntries(); - - this.logger.log( - `[proxy-model] ZIP解压成功,包含 ${zipEntries.length} 个文件`, - ); - - // 列出所有文件便于调试 - const allFiles = zipEntries.map((e) => e.entryName); - this.logger.log(`[proxy-model] ZIP文件列表: ${allFiles.join(', ')}`); - - // 按优先级查找3D模型文件: GLB > GLTF > OBJ - let modelEntry = zipEntries.find((entry) => - entry.entryName.toLowerCase().endsWith('.glb'), - ); - - if (!modelEntry) { - modelEntry = zipEntries.find((entry) => - entry.entryName.toLowerCase().endsWith('.gltf'), - ); - } - - if (!modelEntry) { - modelEntry = zipEntries.find((entry) => - entry.entryName.toLowerCase().endsWith('.obj'), - ); - } - - if (modelEntry) { - this.logger.log( - `[proxy-model] 找到模型文件: ${modelEntry.entryName}`, - ); - fileData = modelEntry.getData(); - const entryName = modelEntry.entryName.toLowerCase(); - let modelType = 'glb'; // 默认类型 - if (entryName.endsWith('.glb')) { - contentType = 'model/gltf-binary'; - modelType = 'glb'; - } else if (entryName.endsWith('.gltf')) { - contentType = 'model/gltf+json'; - modelType = 'gltf'; - } else if (entryName.endsWith('.obj')) { - contentType = 'text/plain'; // OBJ 是文本格式 - modelType = 'obj'; - } - contentLength = fileData.length; - this.logger.log( - `[proxy-model] 模型类型: ${modelType}, 大小: ${contentLength} bytes`, - ); - // 添加自定义头部,告知前端实际的模型类型 - res.setHeader('X-Model-Type', modelType); - } else { - // 列出ZIP中的所有文件,便于调试 - const fileList = zipEntries.map((e) => e.entryName).join(', '); - this.logger.error( - `[proxy-model] ZIP中未找到3D模型文件。ZIP内容: ${fileList}`, - ); - throw new HttpException( - `ZIP文件中未找到3D模型文件(GLB/GLTF/OBJ)。ZIP内容: ${fileList}`, - HttpStatus.BAD_REQUEST, - ); - } - } catch (zipError: any) { - this.logger.error( - `[proxy-model] ZIP处理失败: ${zipError.message}`, - zipError.stack, - ); - if (zipError instanceof HttpException) { - throw zipError; - } - throw new HttpException( - `ZIP解压失败: ${zipError.message}`, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - // 设置响应头 - res.setHeader('Content-Type', contentType); - res.setHeader('Content-Length', contentLength); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - res.setHeader('Cache-Control', 'public, max-age=3600'); // 缓存1小时 - - // 发送文件数据 - this.logger.log(`[proxy-model] 发送响应,大小: ${contentLength} bytes`); - res.send(fileData); - } catch (error: any) { - this.logger.error( - `[proxy-model] 请求处理失败: ${error.message}`, - error.stack, - ); - - if (error instanceof HttpException) { - throw error; - } - if (error.response) { - res.status(error.response.status).json({ - message: `代理请求失败: ${error.response.statusText}`, - status: error.response.status, - }); - } else if (error.code === 'ECONNABORTED') { - res.status(HttpStatus.REQUEST_TIMEOUT).json({ - message: '请求超时,请稍后重试', - }); - } else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') { - res.status(HttpStatus.BAD_GATEWAY).json({ - message: `无法连接到目标服务器: ${error.message}`, - }); - } else { - res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ - message: `代理请求失败: ${error.message}`, - error: - process.env.NODE_ENV === 'development' ? error.stack : undefined, - }); - } - } - } - - /** - * 代理预览图(解决CORS问题) - * GET /api/ai-3d/proxy-preview - */ - @Get('proxy-preview') - @Public() // 允许公开访问,因为预览图URL已经包含签名 - async proxyPreview(@Query('url') url: string, @Res() res: Response) { - if (!url) { - throw new HttpException('URL参数不能为空', HttpStatus.BAD_REQUEST); - } - - try { - // URL解码(处理URL编码) - let decodedUrl: string; - try { - decodedUrl = decodeURIComponent(url); - } catch (e) { - // 如果解码失败,使用原始URL - decodedUrl = url; - } - - // 验证URL是否为腾讯云COS链接(安全验证) - if ( - !decodedUrl.includes('tencentcos.cn') && - !decodedUrl.includes('qcloud.com') - ) { - throw new HttpException( - '不支持的URL来源,仅支持腾讯云COS链接', - HttpStatus.BAD_REQUEST, - ); - } - - // 验证URL格式 - try { - new URL(decodedUrl); - } catch (e) { - throw new HttpException('URL格式无效', HttpStatus.BAD_REQUEST); - } - - // 从源URL获取图片 - const response = await axios.get(decodedUrl, { - responseType: 'arraybuffer', - timeout: 30000, // 30秒超时 - maxContentLength: 10 * 1024 * 1024, // 最大10MB - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', - Accept: 'image/*', - }, - }); - - const imageData = Buffer.from(response.data); - const contentType = response.headers['content-type'] || 'image/jpeg'; - const contentLength = imageData.length; - - // 设置响应头 - res.setHeader('Content-Type', contentType); - res.setHeader('Content-Length', contentLength); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - res.setHeader('Cache-Control', 'public, max-age=86400'); // 缓存24小时 - - // 发送图片数据 - res.send(imageData); - } catch (error: any) { - if (error instanceof HttpException) { - throw error; - } - if (error.response) { - res.status(error.response.status).json({ - message: `代理请求失败: ${error.response.statusText}`, - status: error.response.status, - }); - } else if (error.code === 'ECONNABORTED') { - res.status(HttpStatus.REQUEST_TIMEOUT).json({ - message: '请求超时,请稍后重试', - }); - } else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') { - res.status(HttpStatus.BAD_GATEWAY).json({ - message: `无法连接到目标服务器: ${error.message}`, - }); - } else { - res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ - message: `代理请求失败: ${error.message}`, - error: - process.env.NODE_ENV === 'development' ? error.stack : undefined, - }); - } - } - } -} diff --git a/backend/src/ai-3d/ai-3d.module.ts b/backend/src/ai-3d/ai-3d.module.ts deleted file mode 100644 index dcbb6e7..0000000 --- a/backend/src/ai-3d/ai-3d.module.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { AI3DController } from './ai-3d.controller'; -import { AI3DService } from './ai-3d.service'; -import { MockAI3DProvider } from './providers/mock.provider'; -import { HunyuanAI3DProvider } from './providers/hunyuan.provider'; -import { AI3D_PROVIDER } from './providers/ai-3d-provider.interface'; -import { PrismaModule } from '../prisma/prisma.module'; - -@Module({ - imports: [PrismaModule, ConfigModule], - controllers: [AI3DController], - providers: [ - AI3DService, - MockAI3DProvider, - HunyuanAI3DProvider, - { - provide: AI3D_PROVIDER, - useFactory: ( - configService: ConfigService, - mockProvider: MockAI3DProvider, - hunyuanProvider: HunyuanAI3DProvider, - ) => { - const provider = configService.get('AI_3D_PROVIDER') || 'mock'; - - switch (provider.toLowerCase()) { - case 'hunyuan': - return hunyuanProvider; - case 'mock': - default: - return mockProvider; - } - }, - inject: [ConfigService, MockAI3DProvider, HunyuanAI3DProvider], - }, - ], - exports: [AI3DService], -}) -export class AI3DModule {} diff --git a/backend/src/ai-3d/ai-3d.service.ts b/backend/src/ai-3d/ai-3d.service.ts deleted file mode 100644 index cc6f659..0000000 --- a/backend/src/ai-3d/ai-3d.service.ts +++ /dev/null @@ -1,525 +0,0 @@ -import { - Injectable, - Inject, - NotFoundException, - BadRequestException, - Logger, -} from '@nestjs/common'; -import { PrismaService } from '../prisma/prisma.service'; -import { OssService } from '../oss/oss.service'; -import { CreateTaskDto } from './dto/create-task.dto'; -import { QueryTaskDto } from './dto/query-task.dto'; -import { AI3DProvider, AI3D_PROVIDER } from './providers/ai-3d-provider.interface'; - -// 配置常量 -const MAX_USER_TASKS = 3; // 每用户最大任务数(pending + processing) -const API_MAX_CONCURRENT = 3; // 混元API全局最大并发数(所有用户共享) -const TASK_TIMEOUT_MS = 10 * 60 * 1000; // 10分钟超时 -const MAX_RETRY_COUNT = 3; // 最大重试次数 -const QUEUE_CHECK_INTERVAL = 3000; // 队列检查间隔(毫秒) - -@Injectable() -export class AI3DService { - private readonly logger = new Logger(AI3DService.name); - private queueCheckTimer: NodeJS.Timeout | null = null; - private isProcessingQueue = false; // 防止并发处理队列 - - constructor( - private prisma: PrismaService, - private ossService: OssService, - @Inject(AI3D_PROVIDER) private ai3dProvider: AI3DProvider, - ) { - // 启动队列检查定时器 - this.startQueueChecker(); - } - - /** - * 启动队列检查定时器 - */ - private startQueueChecker() { - if (this.queueCheckTimer) return; - - this.queueCheckTimer = setInterval(async () => { - await this.processQueuedTasks(); - }, QUEUE_CHECK_INTERVAL); - - this.logger.log('队列检查器已启动'); - } - - /** - * 获取当前正在API执行的任务数(全局,所有用户) - */ - private async getGlobalProcessingCount(): Promise { - return this.prisma.aI3DTask.count({ - where: { - status: 'processing', // 只统计已提交到API的任务 - }, - }); - } - - /** - * 处理排队中的任务 - */ - private async processQueuedTasks() { - // 防止并发处理 - if (this.isProcessingQueue) return; - this.isProcessingQueue = true; - - try { - // 检查当前全局并发数 - const processingCount = await this.getGlobalProcessingCount(); - const availableSlots = API_MAX_CONCURRENT - processingCount; - - if (availableSlots <= 0) { - return; // 没有可用槽位 - } - - // 获取等待中的任务(按创建时间排序,先进先出) - const pendingTasks = await this.prisma.aI3DTask.findMany({ - where: { status: 'pending' }, - orderBy: { createTime: 'asc' }, - take: availableSlots, - }); - - if (pendingTasks.length === 0) return; - - this.logger.log( - `队列处理: ${pendingTasks.length} 个任务待提交,可用槽位: ${availableSlots}`, - ); - - // 逐个提交任务 - for (const task of pendingTasks) { - // 再次检查并发数(防止并发提交) - const currentProcessing = await this.getGlobalProcessingCount(); - if (currentProcessing >= API_MAX_CONCURRENT) { - this.logger.log('全局并发已满,停止提交'); - break; - } - - await this.submitTaskToAPI(task); - } - } catch (error) { - this.logger.error(`处理队列任务出错: ${error.message}`); - } finally { - this.isProcessingQueue = false; - } - } - - /** - * 提交任务到混元API - */ - private async submitTaskToAPI(task: any) { - try { - // 构建生成选项 - const options: any = { - generateType: task.generateType, - }; - - const externalTaskId = await this.ai3dProvider.submitTask( - task.inputType as 'text' | 'image', - task.inputContent, - options, - ); - - // 更新状态为处理中 - await this.prisma.aI3DTask.update({ - where: { id: task.id }, - data: { - status: 'processing', - externalTaskId, - }, - }); - - // 启动轮询检查任务状态 - this.pollTaskStatus(task.id, externalTaskId, Date.now()); - - this.logger.log(`任务 ${task.id} 已提交到API,外部ID: ${externalTaskId}`); - } catch (error) { - // 提交失败,标记为失败 - await this.prisma.aI3DTask.update({ - where: { id: task.id }, - data: { - status: 'failed', - errorMessage: error.message || 'AI服务提交失败', - completeTime: new Date(), - }, - }); - this.logger.error(`任务 ${task.id} 提交API失败: ${error.message}`); - } - } - - /** - * 创建生成任务 - */ - async createTask( - userId: number, - tenantId: number, - dto: CreateTaskDto, - ) { - // 1. 检查用户当前任务数量(pending + processing) - const userTaskCount = await this.prisma.aI3DTask.count({ - where: { - userId, - status: { in: ['pending', 'processing'] }, - }, - }); - - if (userTaskCount >= MAX_USER_TASKS) { - throw new BadRequestException( - `您当前有 ${userTaskCount} 个任务正在排队或处理中,最多同时 ${MAX_USER_TASKS} 个任务,请等待完成后再提交`, - ); - } - - // 2. 创建数据库记录(初始状态为 pending,表示排队中) - const task = await this.prisma.aI3DTask.create({ - data: { - userId, - tenantId, - inputType: dto.inputType, - inputContent: dto.inputContent, - generateType: dto.generateType || 'Normal', - status: 'pending', - }, - }); - - this.logger.log(`任务 ${task.id} 已创建,进入队列`); - - // 3. 检查全局并发数,决定是立即提交还是等待队列处理 - const processingCount = await this.getGlobalProcessingCount(); - - if (processingCount < API_MAX_CONCURRENT) { - // 有空闲槽位,立即提交 - try { - // 构建生成选项 - const options: any = { - generateType: dto.generateType, - faceCount: dto.faceCount, - }; - - // 处理多视图图片(图生3D支持) - if (dto.inputType === 'image' && dto.multiViewImages) { - const viewKeyMap: Record = { - left: 'left', - right: 'right', - back: 'back', - top: 'top', - bottom: 'bottom', - left45: 'left_front', - right45: 'right_front', - }; - - const multiViewImages: { viewType: string; imageUrl: string }[] = []; - for (const [key, url] of Object.entries(dto.multiViewImages)) { - if (url && key !== 'front' && viewKeyMap[key]) { - multiViewImages.push({ - viewType: viewKeyMap[key], - imageUrl: url, - }); - } - } - - if (multiViewImages.length > 0) { - options.multiViewImages = multiViewImages; - this.logger.log(`多视图模式: ${multiViewImages.length} 张额外视图`); - } - } - - const externalTaskId = await this.ai3dProvider.submitTask( - dto.inputType, - dto.inputContent, - options, - ); - - // 更新状态为处理中 - await this.prisma.aI3DTask.update({ - where: { id: task.id }, - data: { - status: 'processing', - externalTaskId, - }, - }); - - // 启动轮询检查任务状态 - this.pollTaskStatus(task.id, externalTaskId, Date.now()); - - this.logger.log(`任务 ${task.id} 已提交到API,外部ID: ${externalTaskId}`); - } catch (error) { - // 提交失败,标记为失败 - await this.prisma.aI3DTask.update({ - where: { id: task.id }, - data: { - status: 'failed', - errorMessage: error.message || 'AI服务提交失败', - completeTime: new Date(), - }, - }); - this.logger.error(`任务 ${task.id} 提交失败: ${error.message}`); - throw error; - } - } else { - // 全局并发已满,任务保持 pending 状态,等待队列调度 - this.logger.log( - `全局并发已满 (${processingCount}/${API_MAX_CONCURRENT}),任务 ${task.id} 进入排队`, - ); - } - - return this.getTask(userId, task.id); - } - - /** - * 获取任务列表 - */ - async getTasks(userId: number, query: QueryTaskDto) { - const { page = 1, pageSize = 10, status } = query; - - const where: any = { userId }; - if (status) { - where.status = status; - } - - const [list, total] = await Promise.all([ - this.prisma.aI3DTask.findMany({ - where, - skip: (page - 1) * pageSize, - take: pageSize, - orderBy: { createTime: 'desc' }, - }), - this.prisma.aI3DTask.count({ where }), - ]); - - return { - list, - total, - page, - pageSize, - }; - } - - /** - * 获取任务详情 - */ - async getTask(userId: number, id: number) { - const task = await this.prisma.aI3DTask.findFirst({ - where: { id, userId }, - }); - - if (!task) { - throw new NotFoundException('任务不存在'); - } - - // 如果任务在排队中,计算队列位置 - if (task.status === 'pending') { - const queuePosition = await this.getQueuePosition(task.id, task.createTime); - return { - ...task, - queuePosition, - }; - } - - return task; - } - - /** - * 获取任务在队列中的位置 - */ - private async getQueuePosition(taskId: number, createTime: Date): Promise { - // 统计在当前任务之前创建的、仍在排队的任务数量 - const position = await this.prisma.aI3DTask.count({ - where: { - status: 'pending', - createTime: { lte: createTime }, - }, - }); - return position; - } - - /** - * 删除任务 - */ - async deleteTask(userId: number, id: number) { - const task = await this.getTask(userId, id); - - await this.prisma.aI3DTask.delete({ - where: { id: task.id }, - }); - - this.logger.log(`任务 ${id} 已删除`); - - return null; - } - - /** - * 重试任务 - */ - async retryTask(userId: number, id: number) { - const task = await this.prisma.aI3DTask.findFirst({ - where: { id, userId }, - }); - - if (!task) { - throw new NotFoundException('任务不存在'); - } - - // 只有失败或超时的任务可以重试 - if (!['failed', 'timeout'].includes(task.status)) { - throw new BadRequestException('只有失败或超时的任务可以重试'); - } - - // 检查重试次数 - if (task.retryCount >= MAX_RETRY_COUNT) { - throw new BadRequestException( - `已达到最大重试次数 ${MAX_RETRY_COUNT} 次,请创建新任务`, - ); - } - - // 检查用户任务数限制 - const userTaskCount = await this.prisma.aI3DTask.count({ - where: { - userId, - status: { in: ['pending', 'processing'] }, - }, - }); - - if (userTaskCount >= MAX_USER_TASKS) { - throw new BadRequestException( - `您当前有 ${userTaskCount} 个任务正在排队或处理中,请等待完成后再重试`, - ); - } - - // 重置任务状态为 pending(进入队列) - await this.prisma.aI3DTask.update({ - where: { id }, - data: { - status: 'pending', - errorMessage: null, - completeTime: null, - externalTaskId: null, - retryCount: { increment: 1 }, - }, - }); - - this.logger.log(`任务 ${id} 已重新加入队列,等待处理`); - - // 检查是否可以立即提交 - const processingCount = await this.getGlobalProcessingCount(); - if (processingCount < API_MAX_CONCURRENT) { - // 有空闲槽位,立即提交 - const updatedTask = await this.prisma.aI3DTask.findUnique({ - where: { id }, - }); - if (updatedTask) { - await this.submitTaskToAPI(updatedTask); - } - } - - return this.getTask(userId, id); - } - - /** - * 轮询检查任务状态 - */ - private async pollTaskStatus( - taskId: number, - externalTaskId: string, - startTime: number, - ) { - const checkStatus = async () => { - // 1. 检查是否超时 - if (Date.now() - startTime > TASK_TIMEOUT_MS) { - await this.prisma.aI3DTask.update({ - where: { id: taskId }, - data: { - status: 'timeout', - errorMessage: '任务处理超时,请重试', - completeTime: new Date(), - }, - }); - this.logger.warn(`任务 ${taskId} 超时`); - return; - } - - // 2. 查询外部任务状态 - try { - const result = await this.ai3dProvider.queryTask(externalTaskId); - - if (result.status === 'completed' || result.status === 'failed') { - let finalResultUrl = result.resultUrl; - let finalPreviewUrl = result.previewUrl; - let finalResultUrls = result.resultUrls || null; - let finalPreviewUrls = result.previewUrls || null; - - // 3. 如果任务成功且COS已启用,转存文件到自己的COS - if (result.status === 'completed' && this.ossService.isEnabled()) { - try { - // 转存所有模型文件 - if (result.resultUrls && result.resultUrls.length > 0) { - this.logger.log(`开始转存 ${result.resultUrls.length} 个模型文件到COS: ${taskId}`); - const uploadedModelUrls: string[] = []; - for (let i = 0; i < result.resultUrls.length; i++) { - const modelResult = await this.ossService.uploadAI3DModel( - result.resultUrls[i], - taskId, - i, - ); - uploadedModelUrls.push(modelResult.modelUrl); - this.logger.log(`模型文件 ${i + 1} 转存完成: ${modelResult.modelUrl}`); - } - finalResultUrl = uploadedModelUrls[0]; - finalResultUrls = uploadedModelUrls; - } - - // 转存所有预览图 - if (result.previewUrls && result.previewUrls.length > 0) { - this.logger.log(`开始转存 ${result.previewUrls.length} 个预览图到COS: ${taskId}`); - const uploadedPreviewUrls: string[] = []; - for (let i = 0; i < result.previewUrls.length; i++) { - const previewResult = await this.ossService.uploadAI3DPreview( - result.previewUrls[i], - taskId, - i, - ); - uploadedPreviewUrls.push(previewResult.previewUrl); - this.logger.log(`预览图 ${i + 1} 转存完成: ${previewResult.previewUrl}`); - } - finalPreviewUrl = uploadedPreviewUrls[0]; - finalPreviewUrls = uploadedPreviewUrls; - } - } catch (transferError) { - // 转存失败不影响任务完成,只记录日志 - this.logger.error( - `文件转存失败,使用原始URL: ${transferError.message}`, - ); - } - } - - await this.prisma.aI3DTask.update({ - where: { id: taskId }, - data: { - status: result.status, - resultUrl: finalResultUrl, - previewUrl: finalPreviewUrl, - resultUrls: finalResultUrls, - previewUrls: finalPreviewUrls, - errorMessage: result.errorMessage, - completeTime: new Date(), - }, - }); - - this.logger.log( - `任务 ${taskId} ${result.status === 'completed' ? '完成' : '失败'}`, - ); - } else { - // 继续轮询,每2秒检查一次 - setTimeout(checkStatus, 2000); - } - } catch (error) { - this.logger.error(`轮询任务 ${taskId} 状态出错: ${error.message}`); - // 出错后延长轮询间隔,每5秒重试 - setTimeout(checkStatus, 5000); - } - }; - - // 首次检查延迟2秒 - setTimeout(checkStatus, 2000); - } -} diff --git a/backend/src/ai-3d/dto/create-task.dto.ts b/backend/src/ai-3d/dto/create-task.dto.ts deleted file mode 100644 index 8cf91f1..0000000 --- a/backend/src/ai-3d/dto/create-task.dto.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - IsString, - IsIn, - IsNotEmpty, - MaxLength, - IsOptional, - IsInt, - Min, - Max, - IsObject, -} from 'class-validator'; - -/** - * 模型生成类型 - * Normal: 带纹理 - * LowPoly: 低多边形 - * Geometry: 白模 - * Sketch: 草图 - */ -export type GenerateType = 'Normal' | 'LowPoly' | 'Geometry' | 'Sketch'; - -/** - * 多视图图片类型 - * 支持的视角:left, right, back, top, bottom, left_front (左前45°), right_front (右前45°) - */ -export type MultiViewImages = { - front?: string; // 正图(作为主图) - left?: string; // 左视图 - right?: string; // 右视图 - back?: string; // 后视图 - top?: string; // 顶视图 - bottom?: string; // 底视图 - left45?: string; // 左前45°视图 -> left_front - right45?: string; // 右前45°视图 -> right_front -}; - -export class CreateTaskDto { - @IsString() - @IsIn(['text', 'image'], { message: '输入类型必须是 text 或 image' }) - inputType: 'text' | 'image'; - - @IsString() - @IsNotEmpty({ message: '输入内容不能为空' }) - @MaxLength(2000, { message: '输入内容最多2000个字符' }) - inputContent: string; - - @IsOptional() - @IsString() - @IsIn(['Normal', 'LowPoly', 'Geometry', 'Sketch'], { - message: '模型类型必须是 Normal、LowPoly、Geometry 或 Sketch', - }) - generateType?: GenerateType; - - @IsOptional() - @IsInt({ message: '模型面数必须是整数' }) - @Min(10000, { message: '模型面数最小为10000' }) - @Max(1500000, { message: '模型面数最大为1500000' }) - faceCount?: number; - - @IsOptional() - @IsObject({ message: '多视图图片必须是对象' }) - multiViewImages?: MultiViewImages; -} diff --git a/backend/src/ai-3d/dto/query-task.dto.ts b/backend/src/ai-3d/dto/query-task.dto.ts deleted file mode 100644 index 326a41c..0000000 --- a/backend/src/ai-3d/dto/query-task.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IsOptional, IsString, IsInt, Min, IsIn } from 'class-validator'; -import { Type } from 'class-transformer'; - -export class QueryTaskDto { - @IsOptional() - @Type(() => Number) - @IsInt() - @Min(1) - page?: number = 1; - - @IsOptional() - @Type(() => Number) - @IsInt() - @Min(1) - pageSize?: number = 10; - - @IsOptional() - @IsString() - @IsIn(['pending', 'processing', 'completed', 'failed', 'timeout'], { - message: '状态必须是 pending、processing、completed、failed 或 timeout', - }) - status?: string; -} diff --git a/backend/src/ai-3d/providers/ai-3d-provider.interface.ts b/backend/src/ai-3d/providers/ai-3d-provider.interface.ts deleted file mode 100644 index 4873d2c..0000000 --- a/backend/src/ai-3d/providers/ai-3d-provider.interface.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * AI 3D 生成结果 - */ -export interface AI3DGenerateResult { - taskId: string; // 外部任务ID - status: 'pending' | 'processing' | 'completed' | 'failed'; - resultUrl?: string; // 3D模型URL(单个结果,兼容旧数据) - previewUrl?: string; // 预览图URL(单个结果,兼容旧数据) - resultUrls?: string[]; // 3D模型URL数组(多个结果,文生3D生成4个) - previewUrls?: string[]; // 预览图URL数组(多个结果) - errorMessage?: string; // 错误信息 -} - -/** - * 多视图图片 - */ -export interface MultiViewImage { - viewType: string; // left, right, back, top, bottom, left_front, right_front - imageUrl: string; -} - -/** - * 模型生成配置选项 - */ -export interface AI3DGenerateOptions { - /** 模型生成类型:Normal-带纹理, LowPoly-低多边形, Geometry-白模, Sketch-草图 */ - generateType?: 'Normal' | 'LowPoly' | 'Geometry' | 'Sketch'; - /** 模型面数:10000-1500000,默认500000 */ - faceCount?: number; - /** 多视图图片(图生3D支持) */ - multiViewImages?: MultiViewImage[]; -} - -/** - * AI 3D 服务提供者接口 - * 支持 Mock、腾讯混元、Meshy 等实现 - */ -export interface AI3DProvider { - /** - * 提交生成任务 - * @param inputType 输入类型:text | image - * @param inputContent 输入内容:文字描述或图片URL - * @param options 可选配置项(仅文生3D支持) - * @returns 外部任务ID - */ - submitTask( - inputType: 'text' | 'image', - inputContent: string, - options?: AI3DGenerateOptions, - ): Promise; - - /** - * 查询任务状态 - * @param taskId 外部任务ID - * @returns 任务状态和结果 - */ - queryTask(taskId: string): Promise; -} - -/** - * AI 3D Provider 注入令牌 - */ -export const AI3D_PROVIDER = 'AI3D_PROVIDER'; diff --git a/backend/src/ai-3d/providers/hunyuan.provider.ts b/backend/src/ai-3d/providers/hunyuan.provider.ts deleted file mode 100644 index 7a5f030..0000000 --- a/backend/src/ai-3d/providers/hunyuan.provider.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import axios from 'axios'; -import { - AI3DProvider, - AI3DGenerateResult, - AI3DGenerateOptions, -} from './ai-3d-provider.interface'; -import { TencentCloudSigner } from '../utils/tencent-cloud-sign'; - -/** - * 腾讯混元 3D Provider - * 文档:https://cloud.tencent.com/document/api/1804/123447 - * API概览:https://cloud.tencent.com/document/product/1804/120838 - * - * 重要说明: - * - 默认提供3个并发任务,最多同时处理3个任务 - * - 每个任务会消耗一定积分(根据资源包计费) - * - 如果遇到"资源不足"错误,可能是:并发数达到上限、积分不足、或服务暂时不可用 - */ -@Injectable() -export class HunyuanAI3DProvider implements AI3DProvider { - private readonly logger = new Logger(HunyuanAI3DProvider.name); - private readonly apiHost = 'ai3d.tencentcloudapi.com'; - private readonly apiVersion = '2025-05-13'; // 使用正确的API版本 - private readonly secretId: string; - private readonly secretKey: string; - private readonly region: string; - - constructor(private configService: ConfigService) { - this.secretId = this.configService.get('TENCENT_SECRET_ID'); - this.secretKey = this.configService.get('TENCENT_SECRET_KEY'); - this.region = - this.configService.get('TENCENT_REGION') || 'ap-guangzhou'; - - if (!this.secretId || !this.secretKey) { - this.logger.warn( - '未配置腾讯云密钥,请设置 TENCENT_SECRET_ID 和 TENCENT_SECRET_KEY 环境变量', - ); - } - } - - /** - * 提交生成任务 - */ - async submitTask( - inputType: 'text' | 'image', - inputContent: string, - options?: AI3DGenerateOptions, - ): Promise { - try { - // 构造请求参数 - const payload: any = {}; - - if (inputType === 'text') { - // 文生3D:使用 Prompt - payload.Prompt = inputContent; - - // 文生3D支持额外参数 - if (options?.generateType) { - payload.GenerateType = options.generateType; - } - if (options?.faceCount) { - payload.FaceCount = options.faceCount; - } - - this.logger.log( - `提交文生3D任务: ${inputContent.substring(0, 50)}... ` + - `[类型: ${options?.generateType || 'Normal'}, 面数: ${options?.faceCount || 500000}]`, - ); - } else { - // 图生3D:使用 ImageUrl 或 ImageBase64 - if ( - inputContent.startsWith('http://') || - inputContent.startsWith('https://') - ) { - payload.ImageUrl = inputContent; - } else { - // 假设是 Base64 编码的图片 - payload.ImageBase64 = inputContent; - } - - // 图生3D也支持模型类型 - if (options?.generateType) { - payload.GenerateType = options.generateType; - } - - // 多视图图片支持 - if (options?.multiViewImages && options.multiViewImages.length > 0) { - // 检查是否包含扩展视角(需要 v3.1) - const extendedViewTypes = ['top', 'bottom', 'left_front', 'right_front']; - const hasExtendedViews = options.multiViewImages.some( - (img) => extendedViewTypes.includes(img.viewType) - ); - - // 如果有扩展视角,使用模型版本 3.1 - if (hasExtendedViews) { - payload.ModelVersion = '3.1'; - } - - payload.MultiViewImages = options.multiViewImages.map((img) => ({ - ViewType: img.viewType, - ViewImageUrl: img.imageUrl, - })); - this.logger.log( - `提交图生3D任务(多视图): ${options.multiViewImages.length} 张图片 ` + - `[类型: ${options?.generateType || 'Normal'}]` + - (hasExtendedViews ? ' [模型版本: 3.1]' : ''), - ); - } else { - this.logger.log( - `提交图生3D任务: ${inputContent.substring(0, 50)}... ` + - `[类型: ${options?.generateType || 'Normal'}]`, - ); - } - } - - // 生成签名和请求头 - const headers = TencentCloudSigner.sign({ - secretId: this.secretId, - secretKey: this.secretKey, - service: 'ai3d', - host: this.apiHost, - region: this.region, - action: 'SubmitHunyuanTo3DProJob', - version: this.apiVersion, - payload, - }); - - // 发送请求 - const response = await axios.post(`https://${this.apiHost}`, payload, { - headers, - }); - - // 检查响应 - if (response.data.Response?.Error) { - const error = response.data.Response.Error; - this.logger.error(`混元3D API错误: ${error.Code} - ${error.Message}`); - - // 对特定错误提供更友好的提示 - if (error.Code === 'ResourceInsufficient') { - const friendlyMessage = - '资源不足。可能原因:1) 并发任务数已达到上限(默认3个),请等待其他任务完成;' + - '2) 积分余额不足,请检查腾讯云控制台的积分余额;' + - '3) 服务暂时不可用,请稍后重试。'; - throw new Error(`混元3D API错误: ${friendlyMessage}`); - } - - throw new Error(`混元3D API错误: ${error.Message}`); - } - - const jobId = response.data.Response?.JobId; - if (!jobId) { - this.logger.error('混元3D API未返回JobId'); - throw new Error('混元3D API未返回任务ID'); - } - - this.logger.log(`混元3D任务创建成功: ${jobId}`); - return jobId; - } catch (error) { - this.logger.error(`提交混元3D任务失败: ${error.message}`, error.stack); - throw error; - } - } - - /** - * 查询任务状态 - */ - async queryTask(taskId: string): Promise { - try { - // 构造请求参数 - const payload = { - JobId: taskId, - }; - - // 生成签名和请求头 - const headers = TencentCloudSigner.sign({ - secretId: this.secretId, - secretKey: this.secretKey, - service: 'ai3d', - host: this.apiHost, - region: this.region, - action: 'QueryHunyuanTo3DProJob', - version: this.apiVersion, - payload, - }); - - // 发送请求 - const response = await axios.post(`https://${this.apiHost}`, payload, { - headers, - }); - - // 检查响应 - if (response.data.Response?.Error) { - const error = response.data.Response.Error; - this.logger.error(`混元3D查询错误: ${error.Code} - ${error.Message}`); - return { - taskId, - status: 'failed', - errorMessage: error.Message, - }; - } - - const result = response.data.Response; - - // 映射任务状态 - // 混元状态: WAIT(等待中)| RUN(运行中)| FAIL(失败)| DONE(完成) - const statusMap: Record< - string, - 'pending' | 'processing' | 'completed' | 'failed' - > = { - WAIT: 'processing', - RUN: 'processing', - FAIL: 'failed', - DONE: 'completed', - }; - - const status = statusMap[result.Status] || 'processing'; - - // 构造返回结果 - const generateResult: AI3DGenerateResult = { - taskId, - status, - }; - - // 如果任务完成,提取模型URL - // 根据API文档,返回的是 ResultFile3Ds 数组 - // 注意:这里只返回原始URL,COS转存由AI3DService统一处理 - if (status === 'completed' && result.ResultFile3Ds?.length > 0) { - const file3Ds = result.ResultFile3Ds; - // 提取所有模型URL和预览图URL - const urls = file3Ds.map((file: any) => file.Url).filter(Boolean); - const previewUrls = file3Ds - .map((file: any) => file.PreviewImageUrl) - .filter(Boolean); - - if (urls.length > 0) { - generateResult.resultUrl = urls[0]; - generateResult.resultUrls = urls; - } - - if (previewUrls.length > 0) { - generateResult.previewUrl = previewUrls[0]; - generateResult.previewUrls = previewUrls; - } - - this.logger.log( - `混元3D任务 ${taskId} 完成: ${urls.length} 个模型文件, ${previewUrls.length} 个预览图`, - ); - } else if (status === 'failed') { - // 失败原因:根据文档,错误信息在 ErrorMessage 字段 - generateResult.errorMessage = - result.ErrorMessage || result.ErrorCode || '生成失败'; - this.logger.warn( - `混元3D任务 ${taskId} 失败: ${generateResult.errorMessage}`, - ); - } - - return generateResult; - } catch (error) { - this.logger.error(`查询混元3D任务失败: ${error.message}`, error.stack); - return { - taskId, - status: 'failed', - errorMessage: `查询任务失败: ${error.message}`, - }; - } - } -} diff --git a/backend/src/ai-3d/providers/mock.provider.ts b/backend/src/ai-3d/providers/mock.provider.ts deleted file mode 100644 index 91385b8..0000000 --- a/backend/src/ai-3d/providers/mock.provider.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { AI3DProvider, AI3DGenerateResult } from './ai-3d-provider.interface'; -import { v4 as uuidv4 } from 'uuid'; - -interface MockTask { - status: 'pending' | 'processing' | 'completed' | 'failed'; - startTime: number; - inputType: string; - inputContent: string; - resultUrl?: string; - previewUrl?: string; - resultUrls?: string[]; - previewUrls?: string[]; - errorMessage?: string; -} - -/** - * Mock AI 3D Provider - * 用于开发阶段模拟 AI 3D 生成服务 - */ -@Injectable() -export class MockAI3DProvider implements AI3DProvider { - private readonly logger = new Logger(MockAI3DProvider.name); - private tasks = new Map(); - - // 模拟完成时间范围(毫秒) - private readonly MIN_COMPLETION_TIME = 5000; // 5秒 - private readonly MAX_COMPLETION_TIME = 15000; // 15秒 - - // 模拟成功率 - private readonly SUCCESS_RATE = 0.9; // 90% 成功率 - - // 示例 3D 模型 URL(使用公开可访问的 GLB 文件) - private readonly SAMPLE_MODELS = [ - // three.js 官方示例模型 - 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', - 'https://threejs.org/examples/models/gltf/LittlestTokyo.glb', - 'https://threejs.org/examples/models/gltf/Soldier.glb', - 'https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb', - ]; - - // 示例预览图(使用占位图服务) - private readonly SAMPLE_PREVIEWS = [ - 'https://picsum.photos/seed/model1/400/300', - 'https://picsum.photos/seed/model2/400/300', - 'https://picsum.photos/seed/model3/400/300', - 'https://picsum.photos/seed/model4/400/300', - ]; - - async submitTask( - inputType: 'text' | 'image', - inputContent: string, - ): Promise { - const taskId = uuidv4(); - - this.logger.log( - `Mock: 创建任务 ${taskId}, 类型: ${inputType}, 内容: ${inputContent.substring(0, 50)}...`, - ); - - // 创建任务记录 - this.tasks.set(taskId, { - status: 'processing', - startTime: Date.now(), - inputType, - inputContent, - }); - - // 模拟异步完成 - const completionTime = - this.MIN_COMPLETION_TIME + - Math.random() * (this.MAX_COMPLETION_TIME - this.MIN_COMPLETION_TIME); - - setTimeout(() => { - this.completeTask(taskId); - }, completionTime); - - return taskId; - } - - async queryTask(taskId: string): Promise { - const task = this.tasks.get(taskId); - - if (!task) { - this.logger.warn(`Mock: 任务 ${taskId} 不存在`); - return { - taskId, - status: 'failed', - errorMessage: '任务不存在', - }; - } - - return { - taskId, - status: task.status, - resultUrl: task.resultUrl, - previewUrl: task.previewUrl, - resultUrls: task.resultUrls, - previewUrls: task.previewUrls, - errorMessage: task.errorMessage, - }; - } - - /** - * 模拟任务完成 - */ - private completeTask(taskId: string): void { - const task = this.tasks.get(taskId); - if (!task) return; - - // 根据成功率决定是否成功 - const isSuccess = Math.random() < this.SUCCESS_RATE; - - if (isSuccess) { - // 文生3D生成4个不同角度的模型 - if (task.inputType === 'text') { - const resultUrls: string[] = []; - const previewUrls: string[] = []; - - // 生成4个模型结果,使用不同的示例模型 - for (let i = 0; i < 4; i++) { - const modelIndex = i % this.SAMPLE_MODELS.length; - const previewIndex = i % this.SAMPLE_PREVIEWS.length; - resultUrls.push(this.SAMPLE_MODELS[modelIndex]); - previewUrls.push(this.SAMPLE_PREVIEWS[previewIndex]); - } - - task.status = 'completed'; - task.resultUrls = resultUrls; - task.previewUrls = previewUrls; - // 兼容旧字段,使用第一个结果 - task.resultUrl = resultUrls[0]; - task.previewUrl = previewUrls[0]; - - this.logger.log( - `Mock: 文生3D任务 ${taskId} 完成, 生成 ${resultUrls.length} 个模型`, - ); - } else { - // 图生3D只生成1个模型 - const modelIndex = Math.floor( - Math.random() * this.SAMPLE_MODELS.length, - ); - const modelUrl = this.SAMPLE_MODELS[modelIndex]; - const previewUrl = this.SAMPLE_PREVIEWS[modelIndex % this.SAMPLE_PREVIEWS.length]; - - task.status = 'completed'; - task.resultUrl = modelUrl; - task.previewUrl = previewUrl; - task.resultUrls = [modelUrl]; - task.previewUrls = [previewUrl]; - - this.logger.log(`Mock: 图生3D任务 ${taskId} 完成, 模型: ${modelUrl}`); - } - } else { - task.status = 'failed'; - task.errorMessage = '模拟生成失败:AI 服务暂时不可用'; - - this.logger.warn(`Mock: 任务 ${taskId} 失败`); - } - - this.tasks.set(taskId, task); - - // 清理过期任务(保留1小时) - this.cleanupOldTasks(); - } - - /** - * 清理超过1小时的任务记录 - */ - private cleanupOldTasks(): void { - const oneHourAgo = Date.now() - 60 * 60 * 1000; - - for (const [taskId, task] of this.tasks.entries()) { - if (task.startTime < oneHourAgo) { - this.tasks.delete(taskId); - this.logger.debug(`Mock: 清理过期任务 ${taskId}`); - } - } - } -} diff --git a/backend/src/ai-3d/utils/tencent-cloud-sign.ts b/backend/src/ai-3d/utils/tencent-cloud-sign.ts deleted file mode 100644 index a4f53b0..0000000 --- a/backend/src/ai-3d/utils/tencent-cloud-sign.ts +++ /dev/null @@ -1,101 +0,0 @@ -import * as crypto from 'crypto'; - -export interface TencentCloudSignOptions { - secretId: string; - secretKey: string; - service: string; - host: string; - region?: string; - action: string; - version: string; - payload?: any; - timestamp?: number; -} - -/** - * 腾讯云 API 签名 v3 - * 文档:https://cloud.tencent.com/document/api/213/30654 - */ -export class TencentCloudSigner { - private static readonly ALGORITHM = 'TC3-HMAC-SHA256'; - private static readonly SIGNED_HEADERS = 'content-type;host;x-tc-action'; - - /** - * 生成签名和请求头 - */ - static sign(options: TencentCloudSignOptions): Record { - const timestamp = options.timestamp || Math.floor(Date.now() / 1000); - const date = new Date(timestamp * 1000) - .toISOString() - .substr(0, 10); - - // 1. 拼接规范请求串 - const payload = options.payload ? JSON.stringify(options.payload) : ''; - const hashedRequestPayload = this.sha256Hex(payload); - const canonicalRequest = [ - 'POST', - '/', - '', - `content-type:application/json`, - `host:${options.host}`, - `x-tc-action:${options.action.toLowerCase()}`, - '', - this.SIGNED_HEADERS, - hashedRequestPayload, - ].join('\n'); - - // 2. 拼接待签名字符串 - const hashedCanonicalRequest = this.sha256Hex(canonicalRequest); - const credentialScope = `${date}/${options.service}/tc3_request`; - const stringToSign = [ - this.ALGORITHM, - timestamp.toString(), - credentialScope, - hashedCanonicalRequest, - ].join('\n'); - - // 3. 计算签名 - const secretDate = this.hmacSha256( - `TC3${options.secretKey}`, - date, - ); - const secretService = this.hmacSha256(secretDate, options.service); - const secretSigning = this.hmacSha256(secretService, 'tc3_request'); - const signature = this.hmacSha256Hex(secretSigning, stringToSign); - - // 4. 拼接 Authorization - const authorization = `${this.ALGORITHM} Credential=${options.secretId}/${credentialScope}, SignedHeaders=${this.SIGNED_HEADERS}, Signature=${signature}`; - - // 5. 返回请求头 - return { - 'Content-Type': 'application/json', - 'Host': options.host, - 'X-TC-Action': options.action, - 'X-TC-Version': options.version, - 'X-TC-Timestamp': timestamp.toString(), - 'X-TC-Region': options.region || 'ap-guangzhou', - 'Authorization': authorization, - }; - } - - /** - * SHA256 哈希(十六进制) - */ - private static sha256Hex(data: string): string { - return crypto.createHash('sha256').update(data, 'utf8').digest('hex'); - } - - /** - * HMAC-SHA256(Buffer) - */ - private static hmacSha256(key: string | Buffer, data: string): Buffer { - return crypto.createHmac('sha256', key).update(data, 'utf8').digest(); - } - - /** - * HMAC-SHA256(十六进制) - */ - private static hmacSha256Hex(key: Buffer, data: string): string { - return crypto.createHmac('sha256', key).update(data, 'utf8').digest('hex'); - } -} diff --git a/backend/src/ai-3d/utils/zip-handler.ts b/backend/src/ai-3d/utils/zip-handler.ts deleted file mode 100644 index ab67116..0000000 --- a/backend/src/ai-3d/utils/zip-handler.ts +++ /dev/null @@ -1,176 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import AdmZip from 'adm-zip'; -import axios from 'axios'; -import { Logger } from '@nestjs/common'; - -export class ZipHandler { - private static readonly logger = new Logger(ZipHandler.name); - - /** - * 下载并解压.zip文件,提取3D模型文件 - * @param zipUrl ZIP文件的URL - * @param outputDir 输出目录(默认为系统临时目录) - * @returns 提取的3D模型文件路径、预览图路径和文件Buffer - */ - static async downloadAndExtract( - zipUrl: string, - outputDir?: string, - ): Promise<{ - modelPath: string; - previewPath?: string; - modelBuffer: Buffer; - previewBuffer?: Buffer; - }> { - // 使用系统临时目录 - const baseDir = - outputDir || - path.join(os.tmpdir(), 'ai-3d', Date.now().toString()); - - try { - if (!fs.existsSync(baseDir)) { - fs.mkdirSync(baseDir, { recursive: true }); - } - - // 1. 下载ZIP文件 - this.logger.log(`开始下载ZIP文件: ${zipUrl}`); - const zipPath = path.join(baseDir, 'model.zip'); - await this.downloadFile(zipUrl, zipPath); - this.logger.log(`ZIP文件下载完成: ${zipPath}`); - - // 2. 解压ZIP文件 - this.logger.log(`开始解压ZIP文件`); - const extractDir = path.join(baseDir, 'extracted'); - await this.extractZip(zipPath, extractDir); - this.logger.log(`ZIP文件解压完成: ${extractDir}`); - - // 3. 查找3D模型文件和预览图 - const files = this.getAllFiles(extractDir); - const modelFile = this.findModelFile(files); - const previewFile = this.findPreviewImage(files); - - if (!modelFile) { - throw new Error('在ZIP文件中未找到3D模型文件(.glb, .gltf)'); - } - - this.logger.log(`找到3D模型文件: ${modelFile}`); - if (previewFile) { - this.logger.log(`找到预览图: ${previewFile}`); - } - - // 4. 读取文件Buffer(用于上传到COS) - const modelBuffer = fs.readFileSync(modelFile); - const previewBuffer = previewFile ? fs.readFileSync(previewFile) : undefined; - - return { - modelPath: modelFile, - previewPath: previewFile, - modelBuffer, - previewBuffer, - }; - } catch (error) { - this.logger.error(`处理ZIP文件失败: ${error.message}`, error.stack); - throw error; - } finally { - // 清理临时目录 - try { - if (fs.existsSync(baseDir)) { - fs.rmSync(baseDir, { recursive: true, force: true }); - this.logger.log(`已清理临时目录: ${baseDir}`); - } - } catch (err) { - this.logger.warn(`清理临时目录失败: ${err.message}`); - } - } - } - - /** - * 下载文件 - */ - private static async downloadFile( - url: string, - outputPath: string, - ): Promise { - const response = await axios.get(url, { - responseType: 'arraybuffer', - timeout: 60000, // 60秒超时 - }); - - fs.writeFileSync(outputPath, response.data); - } - - /** - * 解压ZIP文件 - */ - private static async extractZip( - zipPath: string, - outputDir: string, - ): Promise { - const zip = new AdmZip(zipPath); - zip.extractAllTo(outputDir, true); - } - - /** - * 递归获取目录下的所有文件 - */ - private static getAllFiles(dir: string): string[] { - const files: string[] = []; - - const traverse = (currentDir: string) => { - const items = fs.readdirSync(currentDir); - - for (const item of items) { - const fullPath = path.join(currentDir, item); - const stat = fs.statSync(fullPath); - - if (stat.isDirectory()) { - traverse(fullPath); - } else { - files.push(fullPath); - } - } - }; - - traverse(dir); - return files; - } - - /** - * 查找3D模型文件(.glb, .gltf) - */ - private static findModelFile(files: string[]): string | undefined { - // 优先查找.glb文件(二进制格式,更常用) - const glbFile = files.find((file) => file.toLowerCase().endsWith('.glb')); - if (glbFile) return glbFile; - - // 其次查找.gltf文件 - const gltfFile = files.find((file) => - file.toLowerCase().endsWith('.gltf'), - ); - if (gltfFile) return gltfFile; - - // 其他可能的3D格式 - const otherFormats = ['.obj', '.fbx', '.stl']; - for (const format of otherFormats) { - const file = files.find((f) => f.toLowerCase().endsWith(format)); - if (file) return file; - } - - return undefined; - } - - /** - * 查找预览图(.jpg, .jpeg, .png) - */ - private static findPreviewImage(files: string[]): string | undefined { - const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp']; - - for (const ext of imageExtensions) { - const imageFile = files.find((file) => file.toLowerCase().endsWith(ext)); - if (imageFile) return imageFile; - } - - return undefined; - } -} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 72b2d5d..41cca23 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -11,14 +11,12 @@ import { DictModule } from './dict/dict.module'; import { ConfigModule as SystemConfigModule } from './config/config.module'; import { LogsModule } from './logs/logs.module'; import { TenantsModule } from './tenants/tenants.module'; -import { SchoolModule } from './school/school.module'; import { ContestsModule } from './contests/contests.module'; import { AnalyticsModule } from './contests/analytics/analytics.module'; import { JudgesManagementModule } from './judges-management/judges-management.module'; import { UploadModule } from './upload/upload.module'; import { HomeworkModule } from './homework/homework.module'; import { OssModule } from './oss/oss.module'; -import { AI3DModule } from './ai-3d/ai-3d.module'; import { PublicModule } from './public/public.module'; import { JwtAuthGuard } from './auth/guards/jwt-auth.guard'; import { RolesGuard } from './auth/guards/roles.guard'; @@ -46,14 +44,12 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter'; SystemConfigModule, LogsModule, TenantsModule, - SchoolModule, ContestsModule, AnalyticsModule, JudgesManagementModule, UploadModule, HomeworkModule, OssModule, - AI3DModule, PublicModule, ], providers: [ diff --git a/backend/src/contests/registrations/registrations.service.ts b/backend/src/contests/registrations/registrations.service.ts index 7801b69..155e41c 100644 --- a/backend/src/contests/registrations/registrations.service.ts +++ b/backend/src/contests/registrations/registrations.service.ts @@ -104,16 +104,19 @@ export class RegistrationsService { // 检查当前登录用户是否是老师,如果是则设置为指导老师(registrant) let registrantId = createRegistrationDto.userId; if (creatorId) { - // 检查创建者是否是老师 - const creator = await this.prisma.user.findUnique({ + // 检查创建者是否是老师(通过角色判断) + const creatorWithRoles = await this.prisma.user.findUnique({ where: { id: creatorId }, include: { - teacher: true, + roles: { include: { role: true } }, }, }); - + const isTeacher = creatorWithRoles?.roles?.some( + (ur) => ur.role.code === 'teacher', + ); + // 如果创建者是老师,则设置为指导老师 - if (creator?.teacher) { + if (isTeacher) { registrantId = creatorId; } } @@ -148,10 +151,12 @@ export class RegistrationsService { if (creatorId) { const creator = await tx.user.findUnique({ where: { id: creatorId }, - include: { teacher: true }, + include: { roles: { include: { role: true } } }, }); - - if (creator?.teacher) { + const isTeacher = creator?.roles?.some( + (ur) => ur.role.code === 'teacher', + ); + if (isTeacher) { shouldCreateDefaultTeacher = true; } } @@ -411,20 +416,6 @@ export class RegistrationsService { name: true, }, }, - student: { - include: { - class: { - include: { - grade: { - select: { - id: true, - name: true, - }, - }, - }, - }, - }, - }, }, }, child: { @@ -510,20 +501,6 @@ export class RegistrationsService { name: true, }, }, - student: { - include: { - class: { - include: { - grade: { - select: { - id: true, - name: true, - }, - }, - }, - }, - }, - }, }, }, team: { @@ -642,13 +619,16 @@ export class RegistrationsService { throw new NotFoundException('报名记录不存在'); } - // 验证用户是否是老师 - const teacher = await this.prisma.user.findUnique({ + // 验证用户是否是老师(通过角色判断) + const teacherUser = await this.prisma.user.findUnique({ where: { id: teacherUserId }, - include: { teacher: true }, + include: { roles: { include: { role: true } } }, }); + const isTeacher = teacherUser?.roles?.some( + (ur) => ur.role.code === 'teacher', + ); - if (!teacher?.teacher) { + if (!isTeacher) { throw new BadRequestException('该用户不是老师'); } diff --git a/backend/src/contests/results/results.service.ts b/backend/src/contests/results/results.service.ts index 514de8e..6f9764c 100644 --- a/backend/src/contests/results/results.service.ts +++ b/backend/src/contests/results/results.service.ts @@ -491,20 +491,6 @@ export class ResultsService { name: true, }, }, - student: { - include: { - class: { - include: { - grade: { - select: { - id: true, - name: true, - }, - }, - }, - }, - }, - }, }, }, team: { diff --git a/backend/src/homework/homeworks/homeworks.service.ts b/backend/src/homework/homeworks/homeworks.service.ts index c704621..81866c6 100644 --- a/backend/src/homework/homeworks/homeworks.service.ts +++ b/backend/src/homework/homeworks/homeworks.service.ts @@ -303,18 +303,9 @@ export class HomeworksService { ? JSON.parse(homework.publishScope) : homework.publishScope; + // 注意:学校模块已剥离,班级名称暂不可用 if (Array.isArray(classIds) && classIds.length > 0) { - const classes = await this.prisma.class.findMany({ - where: { - id: { in: classIds }, - validState: 1, - }, - select: { - id: true, - name: true, - }, - }); - publishScopeNames = classes.map((c) => c.name); + publishScopeNames = classIds.map((id) => `班级${id}`); } } catch (e) { // 解析失败,保持空数组 @@ -343,20 +334,8 @@ export class HomeworksService { const { page = 1, pageSize = 10, name } = queryDto; const skip = (page - 1) * pageSize; - // 获取学生信息,包括班级 - const user = await this.prisma.user.findUnique({ - where: { id: userId }, - include: { - student: { - select: { - id: true, - classId: true, - }, - }, - }, - }); - - const studentClassId = user?.student?.classId; + // 注意: 学校模块已剥离,班级过滤暂不可用 + const studentClassId: number | undefined = undefined; // 构建查询条件 const where: any = { diff --git a/backend/src/homework/submissions/submissions.service.ts b/backend/src/homework/submissions/submissions.service.ts index db8f145..d9a4ffb 100644 --- a/backend/src/homework/submissions/submissions.service.ts +++ b/backend/src/homework/submissions/submissions.service.ts @@ -43,24 +43,6 @@ export class SubmissionsService { id: true, username: true, nickname: true, - student: { - select: { - id: true, - studentNo: true, - class: { - select: { - id: true, - name: true, - grade: { - select: { - id: true, - name: true, - }, - }, - }, - }, - }, - }, }, }, scores: { @@ -126,7 +108,7 @@ export class SubmissionsService { }; } - if (studentAccount || studentName || classIds || gradeId) { + if (studentAccount || studentName) { where.student = {}; if (studentAccount) { @@ -140,20 +122,6 @@ export class SubmissionsService { contains: studentName, }; } - - if (classIds && classIds.length > 0) { - where.student.student = { - classId: { - in: classIds, - }, - }; - } else if (gradeId) { - where.student.student = { - class: { - gradeId: gradeId, - }, - }; - } } if (status) { @@ -177,23 +145,6 @@ export class SubmissionsService { id: true, username: true, nickname: true, - student: { - select: { - studentNo: true, - class: { - select: { - id: true, - name: true, - grade: { - select: { - id: true, - name: true, - }, - }, - }, - }, - }, - }, }, }, _count: { @@ -219,46 +170,10 @@ export class SubmissionsService { /** * 获取班级树结构(用于左侧筛选) + * 注意:学校模块已剥离,班级树暂不可用 */ - async getClassTree(tenantId: number) { - const grades = await this.prisma.grade.findMany({ - where: { - tenantId, - validState: 1, - }, - include: { - classes: { - where: { - validState: 1, - type: 1, // 只获取行政班级 - }, - select: { - id: true, - name: true, - code: true, - }, - orderBy: { - code: 'asc', - }, - }, - }, - orderBy: { - level: 'asc', - }, - }); - - return grades.map((grade) => ({ - id: `grade_${grade.id}`, - name: grade.name, - type: 'grade', - gradeId: grade.id, - children: grade.classes.map((cls) => ({ - id: cls.id, - name: cls.name, - type: 'class', - classId: cls.id, - })), - })); + async getClassTree(_tenantId: number) { + return []; } /** diff --git a/backend/src/school/classes/classes.controller.ts b/backend/src/school/classes/classes.controller.ts deleted file mode 100644 index fdd48f8..0000000 --- a/backend/src/school/classes/classes.controller.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, - Query, - UseGuards, - Request, -} from '@nestjs/common'; -import { ClassesService } from './classes.service'; -import { CreateClassDto } from './dto/create-class.dto'; -import { UpdateClassDto } from './dto/update-class.dto'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; - -@Controller('classes') -@UseGuards(JwtAuthGuard) -export class ClassesController { - constructor(private readonly classesService: ClassesService) {} - - @Post() - create(@Body() createClassDto: CreateClassDto, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - if (!tenantId) { - throw new Error('无法确定租户信息'); - } - const creatorId = req.user?.id; - return this.classesService.create(createClassDto, tenantId, creatorId); - } - - @Get() - findAll( - @Query('page') page?: string, - @Query('pageSize') pageSize?: string, - @Query('gradeId') gradeId?: string, - @Query('type') type?: string, - @Request() req?: any, - ) { - const tenantId = req?.tenantId || req?.user?.tenantId; - return this.classesService.findAll( - page ? parseInt(page) : 1, - pageSize ? parseInt(pageSize) : 10, - tenantId, - gradeId ? parseInt(gradeId) : undefined, - type ? parseInt(type) : undefined, - ); - } - - @Get(':id') - findOne(@Param('id') id: string, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - return this.classesService.findOne(+id, tenantId); - } - - @Patch(':id') - update( - @Param('id') id: string, - @Body() updateClassDto: UpdateClassDto, - @Request() req, - ) { - const tenantId = req.tenantId || req.user?.tenantId; - const modifierId = req.user?.id; - return this.classesService.update(+id, updateClassDto, tenantId, modifierId); - } - - @Delete(':id') - remove(@Param('id') id: string, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - return this.classesService.remove(+id, tenantId); - } -} - diff --git a/backend/src/school/classes/classes.module.ts b/backend/src/school/classes/classes.module.ts deleted file mode 100644 index 924b36c..0000000 --- a/backend/src/school/classes/classes.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ClassesService } from './classes.service'; -import { ClassesController } from './classes.controller'; -import { PrismaModule } from '../../prisma/prisma.module'; - -@Module({ - imports: [PrismaModule], - controllers: [ClassesController], - providers: [ClassesService], - exports: [ClassesService], -}) -export class ClassesModule {} - diff --git a/backend/src/school/classes/classes.service.ts b/backend/src/school/classes/classes.service.ts deleted file mode 100644 index ea7e509..0000000 --- a/backend/src/school/classes/classes.service.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common'; -import { PrismaService } from '../../prisma/prisma.service'; -import { CreateClassDto } from './dto/create-class.dto'; -import { UpdateClassDto } from './dto/update-class.dto'; - -@Injectable() -export class ClassesService { - constructor(private prisma: PrismaService) {} - - async create(createClassDto: CreateClassDto, tenantId: number, creatorId?: number) { - // 验证年级是否存在且属于该租户 - const grade = await this.prisma.grade.findFirst({ - where: { - id: createClassDto.gradeId, - tenantId, - validState: 1, - }, - }); - - if (!grade) { - throw new NotFoundException('年级不存在或不属于该租户'); - } - - // 检查班级编码是否已存在 - const existingByCode = await this.prisma.class.findFirst({ - where: { - tenantId, - code: createClassDto.code, - }, - }); - - if (existingByCode) { - throw new ConflictException('班级编码已存在'); - } - - const data: any = { - ...createClassDto, - tenantId, - }; - - if (creatorId) { - data.creator = creatorId; - } - - return this.prisma.class.create({ - data, - include: { - grade: true, - _count: { - select: { - students: { - where: { - validState: 1, - }, - }, - studentInterestClasses: true, - }, - }, - }, - }); - } - - async findAll( - page: number = 1, - pageSize: number = 10, - tenantId?: number, - gradeId?: number, - type?: number, - ) { - const skip = (page - 1) * pageSize; - const where: any = { validState: 1 }; - - if (tenantId) { - where.tenantId = tenantId; - } - - if (gradeId) { - where.gradeId = gradeId; - } - - if (type !== undefined) { - where.type = type; - } - - const [list, total] = await Promise.all([ - this.prisma.class.findMany({ - where, - skip, - take: pageSize, - orderBy: [ - { grade: { level: 'asc' } }, - { name: 'asc' }, - ], - include: { - grade: true, - _count: { - select: { - students: { - where: { - validState: 1, - }, - }, - studentInterestClasses: true, - }, - }, - }, - }), - this.prisma.class.count({ where }), - ]); - - return { - list, - total, - page, - pageSize, - }; - } - - async findOne(id: number, tenantId?: number) { - const where: any = { id, validState: 1 }; - if (tenantId) { - where.tenantId = tenantId; - } - - const classEntity = await this.prisma.class.findFirst({ - where, - include: { - grade: true, - students: { - where: { - validState: 1, - }, - include: { - user: true, - }, - }, - studentInterestClasses: { - include: { - student: { - include: { - user: true, - }, - }, - }, - }, - }, - }); - - if (!classEntity) { - throw new NotFoundException('班级不存在'); - } - - return classEntity; - } - - async update(id: number, updateClassDto: UpdateClassDto, tenantId?: number, modifierId?: number) { - // 验证班级是否存在 - const existingClass = await this.findOne(id, tenantId); - - const data: any = {}; - - // 如果更新年级,验证年级是否存在 - if (updateClassDto.gradeId && updateClassDto.gradeId !== existingClass.gradeId) { - const grade = await this.prisma.grade.findFirst({ - where: { - id: updateClassDto.gradeId, - tenantId: tenantId || existingClass.tenantId, - validState: 1, - }, - }); - - if (!grade) { - throw new NotFoundException('年级不存在或不属于该租户'); - } - data.gradeId = updateClassDto.gradeId; - } - - // 如果更新编码,检查是否冲突 - if (updateClassDto.code && updateClassDto.code !== existingClass.code) { - const existingByCode = await this.prisma.class.findFirst({ - where: { - tenantId: tenantId || existingClass.tenantId, - code: updateClassDto.code, - id: { not: id }, - }, - }); - - if (existingByCode) { - throw new ConflictException('班级编码已存在'); - } - data.code = updateClassDto.code; - } - - if (updateClassDto.name) { - data.name = updateClassDto.name; - } - - if (updateClassDto.type !== undefined) { - // 如果从行政班级改为兴趣班,需要检查是否有学生 - if (existingClass.type === 1 && updateClassDto.type === 2) { - const studentCount = await this.prisma.student.count({ - where: { - classId: id, - validState: 1, - }, - }); - - if (studentCount > 0) { - throw new BadRequestException('无法将包含学生的行政班级改为兴趣班'); - } - } - data.type = updateClassDto.type; - } - - if (updateClassDto.capacity !== undefined) { - data.capacity = updateClassDto.capacity; - } - - if (updateClassDto.description !== undefined) { - data.description = updateClassDto.description; - } - - if (modifierId) { - data.modifier = modifierId; - } - - return this.prisma.class.update({ - where: { id }, - data, - include: { - grade: true, - _count: { - select: { - students: { - where: { - validState: 1, - }, - }, - studentInterestClasses: true, - }, - }, - }, - }); - } - - async remove(id: number, tenantId?: number) { - // 验证班级是否存在 - const classEntity = await this.findOne(id, tenantId); - - // 如果是行政班级,检查是否有学生 - if (classEntity.type === 1) { - const studentCount = await this.prisma.student.count({ - where: { - classId: id, - validState: 1, - }, - }); - - if (studentCount > 0) { - throw new BadRequestException('删除班级前需先转移或删除所有学生'); - } - } - - // 如果是兴趣班,删除关联关系 - if (classEntity.type === 2) { - await this.prisma.studentInterestClass.deleteMany({ - where: { - classId: id, - }, - }); - } - - // 软删除 - return this.prisma.class.update({ - where: { id }, - data: { - validState: 2, - }, - }); - } -} - diff --git a/backend/src/school/classes/dto/create-class.dto.ts b/backend/src/school/classes/dto/create-class.dto.ts deleted file mode 100644 index f94e700..0000000 --- a/backend/src/school/classes/dto/create-class.dto.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - IsString, - IsInt, - IsOptional, - Min, - IsIn, -} from 'class-validator'; - -export class CreateClassDto { - @IsInt() - gradeId: number; - - @IsString() - name: string; - - @IsString() - code: string; - - @IsInt() - @IsIn([1, 2]) - type: number; // 1-行政班级,2-兴趣班 - - @IsInt() - @Min(1) - @IsOptional() - capacity?: number; - - @IsString() - @IsOptional() - description?: string; -} - diff --git a/backend/src/school/classes/dto/update-class.dto.ts b/backend/src/school/classes/dto/update-class.dto.ts deleted file mode 100644 index dddff26..0000000 --- a/backend/src/school/classes/dto/update-class.dto.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { - IsString, - IsInt, - IsOptional, - Min, - IsIn, -} from 'class-validator'; - -export class UpdateClassDto { - @IsInt() - @IsOptional() - gradeId?: number; - - @IsString() - @IsOptional() - name?: string; - - @IsString() - @IsOptional() - code?: string; - - @IsInt() - @IsIn([1, 2]) - @IsOptional() - type?: number; // 1-行政班级,2-兴趣班 - - @IsInt() - @Min(1) - @IsOptional() - capacity?: number; - - @IsString() - @IsOptional() - description?: string; -} - diff --git a/backend/src/school/departments/departments.controller.ts b/backend/src/school/departments/departments.controller.ts deleted file mode 100644 index 6520bd7..0000000 --- a/backend/src/school/departments/departments.controller.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, - Query, - UseGuards, - Request, -} from '@nestjs/common'; -import { DepartmentsService } from './departments.service'; -import { CreateDepartmentDto } from './dto/create-department.dto'; -import { UpdateDepartmentDto } from './dto/update-department.dto'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; - -@Controller('departments') -@UseGuards(JwtAuthGuard) -export class DepartmentsController { - constructor(private readonly departmentsService: DepartmentsService) {} - - @Post() - create(@Body() createDepartmentDto: CreateDepartmentDto, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - if (!tenantId) { - throw new Error('无法确定租户信息'); - } - const creatorId = req.user?.id; - return this.departmentsService.create(createDepartmentDto, tenantId, creatorId); - } - - @Get() - findAll( - @Query('page') page?: string, - @Query('pageSize') pageSize?: string, - @Query('parentId') parentId?: string, - @Request() req?: any, - ) { - const tenantId = req?.tenantId || req?.user?.tenantId; - return this.departmentsService.findAll( - page ? parseInt(page) : 1, - pageSize ? parseInt(pageSize) : 10, - tenantId, - parentId ? parseInt(parentId) : undefined, - ); - } - - @Get('tree') - findTree(@Request() req?: any) { - const tenantId = req?.tenantId || req?.user?.tenantId; - return this.departmentsService.findTree(tenantId); - } - - @Get(':id') - findOne(@Param('id') id: string, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - return this.departmentsService.findOne(+id, tenantId); - } - - @Patch(':id') - update( - @Param('id') id: string, - @Body() updateDepartmentDto: UpdateDepartmentDto, - @Request() req, - ) { - const tenantId = req.tenantId || req.user?.tenantId; - const modifierId = req.user?.id; - return this.departmentsService.update(+id, updateDepartmentDto, tenantId, modifierId); - } - - @Delete(':id') - remove(@Param('id') id: string, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - return this.departmentsService.remove(+id, tenantId); - } -} - diff --git a/backend/src/school/departments/departments.module.ts b/backend/src/school/departments/departments.module.ts deleted file mode 100644 index 87b279a..0000000 --- a/backend/src/school/departments/departments.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { DepartmentsService } from './departments.service'; -import { DepartmentsController } from './departments.controller'; -import { PrismaModule } from '../../prisma/prisma.module'; - -@Module({ - imports: [PrismaModule], - controllers: [DepartmentsController], - providers: [DepartmentsService], - exports: [DepartmentsService], -}) -export class DepartmentsModule {} - diff --git a/backend/src/school/departments/departments.service.ts b/backend/src/school/departments/departments.service.ts deleted file mode 100644 index 7f91f78..0000000 --- a/backend/src/school/departments/departments.service.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common'; -import { PrismaService } from '../../prisma/prisma.service'; -import { CreateDepartmentDto } from './dto/create-department.dto'; -import { UpdateDepartmentDto } from './dto/update-department.dto'; - -@Injectable() -export class DepartmentsService { - constructor(private prisma: PrismaService) {} - - async create(createDepartmentDto: CreateDepartmentDto, tenantId: number, creatorId?: number) { - // 检查部门编码是否已存在 - const existingByCode = await this.prisma.department.findFirst({ - where: { - tenantId, - code: createDepartmentDto.code, - }, - }); - - if (existingByCode) { - throw new ConflictException('部门编码已存在'); - } - - // 如果指定了父部门,验证父部门是否存在且属于该租户 - if (createDepartmentDto.parentId) { - const parent = await this.prisma.department.findFirst({ - where: { - id: createDepartmentDto.parentId, - tenantId, - validState: 1, - }, - }); - - if (!parent) { - throw new NotFoundException('父部门不存在或不属于该租户'); - } - } - - const data: any = { - ...createDepartmentDto, - tenantId, - }; - - if (creatorId) { - data.creator = creatorId; - } - - return this.prisma.department.create({ - data, - include: { - parent: true, - _count: { - select: { - teachers: { - where: { - validState: 1, - }, - }, - children: { - where: { - validState: 1, - }, - }, - }, - }, - }, - }); - } - - async findAll(page: number = 1, pageSize: number = 10, tenantId?: number, parentId?: number) { - const skip = (page - 1) * pageSize; - const where: any = { validState: 1 }; - - if (tenantId) { - where.tenantId = tenantId; - } - - if (parentId !== undefined) { - where.parentId = parentId; - } - - const [list, total] = await Promise.all([ - this.prisma.department.findMany({ - where, - skip, - take: pageSize, - orderBy: [ - { sort: 'desc' }, - { name: 'asc' }, - ], - include: { - parent: true, - _count: { - select: { - teachers: { - where: { - validState: 1, - }, - }, - children: { - where: { - validState: 1, - }, - }, - }, - }, - }, - }), - this.prisma.department.count({ where }), - ]); - - return { - list, - total, - page, - pageSize, - }; - } - - async findTree(tenantId?: number) { - const where: any = { validState: 1 }; - if (tenantId) { - where.tenantId = tenantId; - } - - const departments = await this.prisma.department.findMany({ - where, - orderBy: [ - { sort: 'desc' }, - { name: 'asc' }, - ], - include: { - _count: { - select: { - teachers: { - where: { - validState: 1, - }, - }, - children: { - where: { - validState: 1, - }, - }, - }, - }, - }, - }); - - // 构建树形结构 - const buildTree = (items: any[], parentId: number | null = null): any[] => { - return items - .filter(item => item.parentId === parentId) - .map(item => ({ - ...item, - children: buildTree(items, item.id), - })); - }; - - return buildTree(departments); - } - - async findOne(id: number, tenantId?: number) { - const where: any = { id, validState: 1 }; - if (tenantId) { - where.tenantId = tenantId; - } - - const department = await this.prisma.department.findFirst({ - where, - include: { - parent: true, - children: { - where: { - validState: 1, - }, - }, - teachers: { - where: { - validState: 1, - }, - include: { - user: true, - }, - }, - }, - }); - - if (!department) { - throw new NotFoundException('部门不存在'); - } - - return department; - } - - async update(id: number, updateDepartmentDto: UpdateDepartmentDto, tenantId?: number, modifierId?: number) { - // 验证部门是否存在 - const existingDepartment = await this.findOne(id, tenantId); - - const data: any = {}; - - // 如果更新编码,检查是否冲突 - if (updateDepartmentDto.code && updateDepartmentDto.code !== existingDepartment.code) { - const existingByCode = await this.prisma.department.findFirst({ - where: { - tenantId: tenantId || existingDepartment.tenantId, - code: updateDepartmentDto.code, - id: { not: id }, - }, - }); - - if (existingByCode) { - throw new ConflictException('部门编码已存在'); - } - data.code = updateDepartmentDto.code; - } - - // 如果更新父部门,验证父部门是否存在且不会造成循环引用 - if (updateDepartmentDto.parentId !== undefined) { - if (updateDepartmentDto.parentId === id) { - throw new BadRequestException('不能将部门设置为自己的父部门'); - } - - if (updateDepartmentDto.parentId !== null) { - const parent = await this.prisma.department.findFirst({ - where: { - id: updateDepartmentDto.parentId, - tenantId: tenantId || existingDepartment.tenantId, - validState: 1, - }, - }); - - if (!parent) { - throw new NotFoundException('父部门不存在或不属于该租户'); - } - - // 检查是否会造成循环引用(父部门不能是当前部门的子部门) - const isDescendant = await this.checkIsDescendant( - updateDepartmentDto.parentId, - id, - tenantId || existingDepartment.tenantId, - ); - - if (isDescendant) { - throw new BadRequestException('不能将部门设置为其子部门的父部门'); - } - } - data.parentId = updateDepartmentDto.parentId; - } - - if (updateDepartmentDto.name) { - data.name = updateDepartmentDto.name; - } - - if (updateDepartmentDto.description !== undefined) { - data.description = updateDepartmentDto.description; - } - - if (updateDepartmentDto.sort !== undefined) { - data.sort = updateDepartmentDto.sort; - } - - if (modifierId) { - data.modifier = modifierId; - } - - return this.prisma.department.update({ - where: { id }, - data, - include: { - parent: true, - _count: { - select: { - teachers: { - where: { - validState: 1, - }, - }, - children: { - where: { - validState: 1, - }, - }, - }, - }, - }, - }); - } - - async remove(id: number, tenantId?: number) { - // 验证部门是否存在 - const department = await this.findOne(id, tenantId); - - // 检查是否有子部门 - const childrenCount = await this.prisma.department.count({ - where: { - parentId: id, - validState: 1, - }, - }); - - if (childrenCount > 0) { - throw new BadRequestException('删除部门前需先删除或转移所有子部门'); - } - - // 检查是否有教师 - const teacherCount = await this.prisma.teacher.count({ - where: { - departmentId: id, - validState: 1, - }, - }); - - if (teacherCount > 0) { - throw new BadRequestException('删除部门前需先转移或删除该部门下的所有教师'); - } - - // 软删除 - return this.prisma.department.update({ - where: { id }, - data: { - validState: 2, - }, - }); - } - - private async checkIsDescendant(parentId: number, childId: number, tenantId: number): Promise { - const parent = await this.prisma.department.findFirst({ - where: { - id: parentId, - tenantId, - validState: 1, - }, - include: { - children: { - where: { - validState: 1, - }, - }, - }, - }); - - if (!parent) { - return false; - } - - // 检查直接子部门 - if (parent.children.some(child => child.id === childId)) { - return true; - } - - // 递归检查所有子部门 - for (const child of parent.children) { - if (await this.checkIsDescendant(child.id, childId, tenantId)) { - return true; - } - } - - return false; - } -} - diff --git a/backend/src/school/departments/dto/create-department.dto.ts b/backend/src/school/departments/dto/create-department.dto.ts deleted file mode 100644 index ab9e383..0000000 --- a/backend/src/school/departments/dto/create-department.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - IsString, - IsInt, - IsOptional, - Min, -} from 'class-validator'; - -export class CreateDepartmentDto { - @IsString() - name: string; - - @IsString() - code: string; - - @IsInt() - @IsOptional() - parentId?: number; - - @IsString() - @IsOptional() - description?: string; - - @IsInt() - @Min(0) - @IsOptional() - sort?: number; -} - diff --git a/backend/src/school/departments/dto/update-department.dto.ts b/backend/src/school/departments/dto/update-department.dto.ts deleted file mode 100644 index 3ad292e..0000000 --- a/backend/src/school/departments/dto/update-department.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - IsString, - IsInt, - IsOptional, - Min, -} from 'class-validator'; - -export class UpdateDepartmentDto { - @IsString() - @IsOptional() - name?: string; - - @IsString() - @IsOptional() - code?: string; - - @IsInt() - @IsOptional() - parentId?: number | null; - - @IsString() - @IsOptional() - description?: string; - - @IsInt() - @Min(0) - @IsOptional() - sort?: number; -} - diff --git a/backend/src/school/grades/dto/create-grade.dto.ts b/backend/src/school/grades/dto/create-grade.dto.ts deleted file mode 100644 index 7409a5a..0000000 --- a/backend/src/school/grades/dto/create-grade.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { - IsString, - IsInt, - IsOptional, - Min, -} from 'class-validator'; - -export class CreateGradeDto { - @IsString() - name: string; - - @IsString() - code: string; - - @IsInt() - @Min(1) - level: number; - - @IsString() - @IsOptional() - description?: string; -} - diff --git a/backend/src/school/grades/dto/update-grade.dto.ts b/backend/src/school/grades/dto/update-grade.dto.ts deleted file mode 100644 index 0755304..0000000 --- a/backend/src/school/grades/dto/update-grade.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - IsString, - IsInt, - IsOptional, - Min, -} from 'class-validator'; - -export class UpdateGradeDto { - @IsString() - @IsOptional() - name?: string; - - @IsString() - @IsOptional() - code?: string; - - @IsInt() - @Min(1) - @IsOptional() - level?: number; - - @IsString() - @IsOptional() - description?: string; -} - diff --git a/backend/src/school/grades/grades.controller.ts b/backend/src/school/grades/grades.controller.ts deleted file mode 100644 index 97a33cb..0000000 --- a/backend/src/school/grades/grades.controller.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, - Query, - UseGuards, - Request, -} from '@nestjs/common'; -import { GradesService } from './grades.service'; -import { CreateGradeDto } from './dto/create-grade.dto'; -import { UpdateGradeDto } from './dto/update-grade.dto'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; - -@Controller('grades') -@UseGuards(JwtAuthGuard) -export class GradesController { - constructor(private readonly gradesService: GradesService) {} - - @Post() - create(@Body() createGradeDto: CreateGradeDto, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - if (!tenantId) { - throw new Error('无法确定租户信息'); - } - const creatorId = req.user?.id; - return this.gradesService.create(createGradeDto, tenantId, creatorId); - } - - @Get() - findAll( - @Query('page') page?: string, - @Query('pageSize') pageSize?: string, - @Request() req?: any, - ) { - const tenantId = req?.tenantId || req?.user?.tenantId; - return this.gradesService.findAll( - page ? parseInt(page) : 1, - pageSize ? parseInt(pageSize) : 10, - tenantId, - ); - } - - @Get(':id') - findOne(@Param('id') id: string, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - return this.gradesService.findOne(+id, tenantId); - } - - @Patch(':id') - update( - @Param('id') id: string, - @Body() updateGradeDto: UpdateGradeDto, - @Request() req, - ) { - const tenantId = req.tenantId || req.user?.tenantId; - const modifierId = req.user?.id; - return this.gradesService.update(+id, updateGradeDto, tenantId, modifierId); - } - - @Delete(':id') - remove(@Param('id') id: string, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - return this.gradesService.remove(+id, tenantId); - } -} - diff --git a/backend/src/school/grades/grades.module.ts b/backend/src/school/grades/grades.module.ts deleted file mode 100644 index 82b7565..0000000 --- a/backend/src/school/grades/grades.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { GradesService } from './grades.service'; -import { GradesController } from './grades.controller'; -import { PrismaModule } from '../../prisma/prisma.module'; - -@Module({ - imports: [PrismaModule], - controllers: [GradesController], - providers: [GradesService], - exports: [GradesService], -}) -export class GradesModule {} - diff --git a/backend/src/school/grades/grades.service.ts b/backend/src/school/grades/grades.service.ts deleted file mode 100644 index ee4939b..0000000 --- a/backend/src/school/grades/grades.service.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common'; -import { PrismaService } from '../../prisma/prisma.service'; -import { CreateGradeDto } from './dto/create-grade.dto'; -import { UpdateGradeDto } from './dto/update-grade.dto'; - -@Injectable() -export class GradesService { - constructor(private prisma: PrismaService) {} - - async create(createGradeDto: CreateGradeDto, tenantId: number, creatorId?: number) { - // 检查年级编码是否已存在 - const existingByCode = await this.prisma.grade.findFirst({ - where: { - tenantId, - code: createGradeDto.code, - }, - }); - - if (existingByCode) { - throw new ConflictException('年级编码已存在'); - } - - // 检查年级级别是否已存在 - const existingByLevel = await this.prisma.grade.findFirst({ - where: { - tenantId, - level: createGradeDto.level, - }, - }); - - if (existingByLevel) { - throw new ConflictException('年级级别已存在'); - } - - const data: any = { - ...createGradeDto, - tenantId, - }; - - if (creatorId) { - data.creator = creatorId; - } - - return this.prisma.grade.create({ - data, - include: { - classes: { - where: { - validState: 1, - }, - }, - }, - }); - } - - async findAll(page: number = 1, pageSize: number = 10, tenantId?: number) { - const skip = (page - 1) * pageSize; - const where = tenantId ? { tenantId, validState: 1 } : { validState: 1 }; - - const [list, total] = await Promise.all([ - this.prisma.grade.findMany({ - where, - skip, - take: pageSize, - orderBy: { - level: 'asc', - }, - include: { - _count: { - select: { - classes: { - where: { - validState: 1, - }, - }, - }, - }, - }, - }), - this.prisma.grade.count({ where }), - ]); - - return { - list, - total, - page, - pageSize, - }; - } - - async findOne(id: number, tenantId?: number) { - const where: any = { id, validState: 1 }; - if (tenantId) { - where.tenantId = tenantId; - } - - const grade = await this.prisma.grade.findFirst({ - where, - include: { - classes: { - where: { - validState: 1, - }, - }, - }, - }); - - if (!grade) { - throw new NotFoundException('年级不存在'); - } - - return grade; - } - - async update(id: number, updateGradeDto: UpdateGradeDto, tenantId?: number, modifierId?: number) { - // 验证年级是否存在 - const existingGrade = await this.findOne(id, tenantId); - - const data: any = {}; - - // 如果更新编码,检查是否冲突 - if (updateGradeDto.code && updateGradeDto.code !== existingGrade.code) { - const existingByCode = await this.prisma.grade.findFirst({ - where: { - tenantId: tenantId || existingGrade.tenantId, - code: updateGradeDto.code, - id: { not: id }, - }, - }); - - if (existingByCode) { - throw new ConflictException('年级编码已存在'); - } - data.code = updateGradeDto.code; - } - - // 如果更新级别,检查是否冲突 - if (updateGradeDto.level && updateGradeDto.level !== existingGrade.level) { - const existingByLevel = await this.prisma.grade.findFirst({ - where: { - tenantId: tenantId || existingGrade.tenantId, - level: updateGradeDto.level, - id: { not: id }, - }, - }); - - if (existingByLevel) { - throw new ConflictException('年级级别已存在'); - } - data.level = updateGradeDto.level; - } - - if (updateGradeDto.name) { - data.name = updateGradeDto.name; - } - - if (updateGradeDto.description !== undefined) { - data.description = updateGradeDto.description; - } - - if (modifierId) { - data.modifier = modifierId; - } - - return this.prisma.grade.update({ - where: { id }, - data, - include: { - classes: { - where: { - validState: 1, - }, - }, - }, - }); - } - - async remove(id: number, tenantId?: number) { - // 验证年级是否存在 - const grade = await this.findOne(id, tenantId); - - // 检查是否有班级关联 - const classCount = await this.prisma.class.count({ - where: { - gradeId: id, - validState: 1, - }, - }); - - if (classCount > 0) { - throw new BadRequestException('删除年级前需先删除所有班级'); - } - - // 软删除 - return this.prisma.grade.update({ - where: { id }, - data: { - validState: 2, - }, - }); - } -} - diff --git a/backend/src/school/school.module.ts b/backend/src/school/school.module.ts deleted file mode 100644 index 93b0c98..0000000 --- a/backend/src/school/school.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Module } from '@nestjs/common'; -import { SchoolsModule } from './schools/schools.module'; -import { GradesModule } from './grades/grades.module'; -import { ClassesModule } from './classes/classes.module'; -import { DepartmentsModule } from './departments/departments.module'; -import { TeachersModule } from './teachers/teachers.module'; -import { StudentsModule } from './students/students.module'; - -@Module({ - imports: [ - SchoolsModule, - GradesModule, - ClassesModule, - DepartmentsModule, - TeachersModule, - StudentsModule, - ], - exports: [ - SchoolsModule, - GradesModule, - ClassesModule, - DepartmentsModule, - TeachersModule, - StudentsModule, - ], -}) -export class SchoolModule {} - diff --git a/backend/src/school/schools/dto/create-school.dto.ts b/backend/src/school/schools/dto/create-school.dto.ts deleted file mode 100644 index ca5a62e..0000000 --- a/backend/src/school/schools/dto/create-school.dto.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - IsString, - IsOptional, - IsDateString, - IsUrl, -} from 'class-validator'; - -export class CreateSchoolDto { - @IsString() - @IsOptional() - address?: string; - - @IsString() - @IsOptional() - phone?: string; - - @IsString() - @IsOptional() - principal?: string; - - @IsDateString() - @IsOptional() - established?: string; - - @IsString() - @IsOptional() - description?: string; - - @IsUrl() - @IsOptional() - logo?: string; - - @IsUrl() - @IsOptional() - website?: string; -} - diff --git a/backend/src/school/schools/dto/update-school.dto.ts b/backend/src/school/schools/dto/update-school.dto.ts deleted file mode 100644 index 17a70ad..0000000 --- a/backend/src/school/schools/dto/update-school.dto.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - IsString, - IsOptional, - IsDateString, - IsUrl, -} from 'class-validator'; - -export class UpdateSchoolDto { - @IsString() - @IsOptional() - address?: string; - - @IsString() - @IsOptional() - phone?: string; - - @IsString() - @IsOptional() - principal?: string; - - @IsDateString() - @IsOptional() - established?: string; - - @IsString() - @IsOptional() - description?: string; - - @IsUrl() - @IsOptional() - logo?: string; - - @IsUrl() - @IsOptional() - website?: string; -} - diff --git a/backend/src/school/schools/schools.controller.ts b/backend/src/school/schools/schools.controller.ts deleted file mode 100644 index 469803b..0000000 --- a/backend/src/school/schools/schools.controller.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Delete, - UseGuards, - Request, -} from '@nestjs/common'; -import { SchoolsService } from './schools.service'; -import { CreateSchoolDto } from './dto/create-school.dto'; -import { UpdateSchoolDto } from './dto/update-school.dto'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; - -@Controller('schools') -@UseGuards(JwtAuthGuard) -export class SchoolsController { - constructor(private readonly schoolsService: SchoolsService) {} - - @Post() - create(@Body() createSchoolDto: CreateSchoolDto, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - if (!tenantId) { - throw new Error('无法确定租户信息'); - } - const creatorId = req.user?.id; - return this.schoolsService.create(createSchoolDto, tenantId, creatorId); - } - - @Get() - findOne(@Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - if (!tenantId) { - throw new Error('无法确定租户信息'); - } - return this.schoolsService.findOne(tenantId); - } - - @Patch() - update(@Body() updateSchoolDto: UpdateSchoolDto, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - if (!tenantId) { - throw new Error('无法确定租户信息'); - } - const modifierId = req.user?.id; - return this.schoolsService.update(tenantId, updateSchoolDto, modifierId); - } - - @Delete() - remove(@Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - if (!tenantId) { - throw new Error('无法确定租户信息'); - } - return this.schoolsService.remove(tenantId); - } -} - diff --git a/backend/src/school/schools/schools.module.ts b/backend/src/school/schools/schools.module.ts deleted file mode 100644 index 760db9f..0000000 --- a/backend/src/school/schools/schools.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { SchoolsService } from './schools.service'; -import { SchoolsController } from './schools.controller'; -import { PrismaModule } from '../../prisma/prisma.module'; - -@Module({ - imports: [PrismaModule], - controllers: [SchoolsController], - providers: [SchoolsService], - exports: [SchoolsService], -}) -export class SchoolsModule {} - diff --git a/backend/src/school/schools/schools.service.ts b/backend/src/school/schools/schools.service.ts deleted file mode 100644 index e687d9b..0000000 --- a/backend/src/school/schools/schools.service.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - Injectable, - NotFoundException, - ConflictException, -} from '@nestjs/common'; -import { PrismaService } from '../../prisma/prisma.service'; -import { CreateSchoolDto } from './dto/create-school.dto'; -import { UpdateSchoolDto } from './dto/update-school.dto'; - -@Injectable() -export class SchoolsService { - constructor(private prisma: PrismaService) {} - - async create( - createSchoolDto: CreateSchoolDto, - tenantId: number, - creatorId?: number, - ) { - // 检查租户是否已有学校信息 - const existingSchool = await this.prisma.school.findUnique({ - where: { tenantId }, - }); - - if (existingSchool) { - throw new ConflictException('该租户已存在学校信息'); - } - - // 检查租户是否存在 - const tenant = await this.prisma.tenant.findUnique({ - where: { id: tenantId }, - }); - - if (!tenant) { - throw new NotFoundException('租户不存在'); - } - - const data: any = { - ...createSchoolDto, - tenantId, - }; - - if (createSchoolDto.established) { - data.established = new Date(createSchoolDto.established); - } - - if (creatorId) { - data.creator = creatorId; - } - - return this.prisma.school.create({ - data, - include: { - tenant: true, - }, - }); - } - - async findOne(tenantId: number) { - const school = await this.prisma.school.findUnique({ - where: { tenantId }, - include: { - tenant: true, - }, - }); - - // 学校信息不存在时返回 null,而不是抛出异常 - // 因为这是一个正常的业务状态(学校信息可能还没有创建) - return school || null; - } - - async update( - tenantId: number, - updateSchoolDto: UpdateSchoolDto, - modifierId?: number, - ) { - // 验证学校是否存在 - const school = await this.findOne(tenantId); - if (!school) { - throw new NotFoundException('学校信息不存在'); - } - - const data: any = { ...updateSchoolDto }; - - if (updateSchoolDto.established) { - data.established = new Date(updateSchoolDto.established); - } - - if (modifierId) { - data.modifier = modifierId; - } - - return this.prisma.school.update({ - where: { tenantId }, - data, - include: { - tenant: true, - }, - }); - } - - async remove(tenantId: number) { - // 验证学校是否存在 - const school = await this.findOne(tenantId); - if (!school) { - throw new NotFoundException('学校信息不存在'); - } - - return this.prisma.school.delete({ - where: { tenantId }, - }); - } -} diff --git a/backend/src/school/students/dto/create-student.dto.ts b/backend/src/school/students/dto/create-student.dto.ts deleted file mode 100644 index 4ca2d6e..0000000 --- a/backend/src/school/students/dto/create-student.dto.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - IsString, - IsInt, - IsOptional, - IsEmail, - IsDateString, - IsIn, - IsArray, -} from 'class-validator'; - -export class CreateStudentDto { - @IsString() - username: string; - - @IsString() - password: string; - - @IsString() - nickname: string; - - @IsEmail() - @IsOptional() - email?: string; - - @IsString() - @IsOptional() - avatar?: string; - - @IsInt() - classId: number; // 行政班级ID - - @IsString() - @IsOptional() - studentNo?: string; - - @IsString() - @IsOptional() - phone?: string; - - @IsString() - @IsOptional() - idCard?: string; - - @IsInt() - @IsIn([1, 2]) - @IsOptional() - gender?: number; // 1-男,2-女 - - @IsDateString() - @IsOptional() - birthDate?: string; - - @IsDateString() - @IsOptional() - enrollmentDate?: string; - - @IsString() - @IsOptional() - parentName?: string; - - @IsString() - @IsOptional() - parentPhone?: string; - - @IsString() - @IsOptional() - address?: string; - - @IsString() - @IsOptional() - description?: string; - - @IsArray() - @IsInt({ each: true }) - @IsOptional() - interestClassIds?: number[]; // 兴趣班ID数组 - - @IsInt() - @IsOptional() - validState?: number; // 有效状态:1-有效,0-无效 -} diff --git a/backend/src/school/students/dto/update-student.dto.ts b/backend/src/school/students/dto/update-student.dto.ts deleted file mode 100644 index 7011c1a..0000000 --- a/backend/src/school/students/dto/update-student.dto.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - IsString, - IsInt, - IsOptional, - IsEmail, - IsDateString, - IsIn, - IsArray, -} from 'class-validator'; - -export class UpdateStudentDto { - @IsString() - @IsOptional() - nickname?: string; - - @IsEmail() - @IsOptional() - email?: string; - - @IsString() - @IsOptional() - avatar?: string; - - @IsInt() - @IsOptional() - classId?: number; // 行政班级ID - - @IsString() - @IsOptional() - studentNo?: string; - - @IsString() - @IsOptional() - phone?: string; - - @IsString() - @IsOptional() - idCard?: string; - - @IsInt() - @IsIn([1, 2]) - @IsOptional() - gender?: number; - - @IsDateString() - @IsOptional() - birthDate?: string; - - @IsDateString() - @IsOptional() - enrollmentDate?: string; - - @IsString() - @IsOptional() - parentName?: string; - - @IsString() - @IsOptional() - parentPhone?: string; - - @IsString() - @IsOptional() - address?: string; - - @IsString() - @IsOptional() - description?: string; - - @IsInt() - @IsIn([1, 2]) - @IsOptional() - validState?: number; // 1-有效,2-失效 - - @IsArray() - @IsInt({ each: true }) - @IsOptional() - interestClassIds?: number[]; // 兴趣班ID数组 -} - diff --git a/backend/src/school/students/students.controller.ts b/backend/src/school/students/students.controller.ts deleted file mode 100644 index fd0fffc..0000000 --- a/backend/src/school/students/students.controller.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, - Query, - UseGuards, - Request, -} from '@nestjs/common'; -import { StudentsService } from './students.service'; -import { CreateStudentDto } from './dto/create-student.dto'; -import { UpdateStudentDto } from './dto/update-student.dto'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; - -@Controller('students') -@UseGuards(JwtAuthGuard) -export class StudentsController { - constructor(private readonly studentsService: StudentsService) {} - - @Post() - create(@Body() createStudentDto: CreateStudentDto, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - if (!tenantId) { - throw new Error('无法确定租户信息'); - } - const creatorId = req.user?.id; - return this.studentsService.create(createStudentDto, tenantId, creatorId); - } - - @Get() - findAll( - @Query('page') page?: string, - @Query('pageSize') pageSize?: string, - @Query('classId') classId?: string, - @Request() req?: any, - ) { - const tenantId = req?.tenantId || req?.user?.tenantId; - return this.studentsService.findAll( - page ? parseInt(page) : 1, - pageSize ? parseInt(pageSize) : 10, - tenantId, - classId ? parseInt(classId) : undefined, - ); - } - - @Get('user/:userId') - findByUserId(@Param('userId') userId: string, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - return this.studentsService.findByUserId(+userId, tenantId); - } - - @Get(':id') - findOne(@Param('id') id: string, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - return this.studentsService.findOne(+id, tenantId); - } - - @Patch(':id') - update( - @Param('id') id: string, - @Body() updateStudentDto: UpdateStudentDto, - @Request() req, - ) { - const tenantId = req.tenantId || req.user?.tenantId; - const modifierId = req.user?.id; - return this.studentsService.update(+id, updateStudentDto, tenantId, modifierId); - } - - @Delete(':id') - remove(@Param('id') id: string, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - return this.studentsService.remove(+id, tenantId); - } -} - diff --git a/backend/src/school/students/students.module.ts b/backend/src/school/students/students.module.ts deleted file mode 100644 index 55b843a..0000000 --- a/backend/src/school/students/students.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { StudentsService } from './students.service'; -import { StudentsController } from './students.controller'; -import { PrismaModule } from '../../prisma/prisma.module'; - -@Module({ - imports: [PrismaModule], - controllers: [StudentsController], - providers: [StudentsService], - exports: [StudentsService], -}) -export class StudentsModule {} - diff --git a/backend/src/school/students/students.service.ts b/backend/src/school/students/students.service.ts deleted file mode 100644 index e417d35..0000000 --- a/backend/src/school/students/students.service.ts +++ /dev/null @@ -1,542 +0,0 @@ -import { - Injectable, - NotFoundException, - ConflictException, - BadRequestException, -} from '@nestjs/common'; -import { PrismaService } from '../../prisma/prisma.service'; -import { CreateStudentDto } from './dto/create-student.dto'; -import { UpdateStudentDto } from './dto/update-student.dto'; -import * as bcrypt from 'bcrypt'; - -@Injectable() -export class StudentsService { - constructor(private prisma: PrismaService) {} - - async create( - createStudentDto: CreateStudentDto, - tenantId: number, - creatorId?: number, - ) { - // 验证行政班级是否存在且属于该租户,且是行政班级 - const classEntity = await this.prisma.class.findFirst({ - where: { - id: createStudentDto.classId, - tenantId, - type: 1, // 必须是行政班级 - validState: 1, - }, - }); - - if (!classEntity) { - throw new BadRequestException('行政班级不存在或不属于该租户'); - } - - // 检查用户名是否已存在 - const existingUser = await this.prisma.user.findFirst({ - where: { - tenantId, - username: createStudentDto.username, - }, - }); - - if (existingUser) { - throw new ConflictException('用户名已存在'); - } - - // 如果提供了学号,检查学号是否已存在 - if (createStudentDto.studentNo) { - const existingStudent = await this.prisma.student.findFirst({ - where: { - tenantId, - studentNo: createStudentDto.studentNo, - }, - }); - - if (existingStudent) { - throw new ConflictException('学号已存在'); - } - } - - // 如果提供了兴趣班,验证兴趣班是否存在且是兴趣班类型 - if ( - createStudentDto.interestClassIds && - createStudentDto.interestClassIds.length > 0 - ) { - const interestClasses = await this.prisma.class.findMany({ - where: { - id: { in: createStudentDto.interestClassIds }, - tenantId, - type: 2, // 必须是兴趣班 - validState: 1, - }, - }); - - if (interestClasses.length !== createStudentDto.interestClassIds.length) { - throw new NotFoundException('部分兴趣班不存在或不属于该租户'); - } - } - - // 加密密码 - const hashedPassword = await bcrypt.hash(createStudentDto.password, 10); - - // 创建 User 记录 - const userData: any = { - username: createStudentDto.username, - password: hashedPassword, - nickname: createStudentDto.nickname, - tenantId, - }; - - if (createStudentDto.email) { - userData.email = createStudentDto.email; - } - - if (createStudentDto.avatar) { - userData.avatar = createStudentDto.avatar; - } - - if (creatorId) { - userData.creator = creatorId; - } - - // 创建 Student 记录 - const studentData: any = { - classId: createStudentDto.classId, - tenantId, - }; - - if (createStudentDto.studentNo) { - studentData.studentNo = createStudentDto.studentNo; - } - - if (createStudentDto.phone) { - studentData.phone = createStudentDto.phone; - } - - if (createStudentDto.idCard) { - studentData.idCard = createStudentDto.idCard; - } - - if (createStudentDto.gender) { - studentData.gender = createStudentDto.gender; - } - - if (createStudentDto.birthDate) { - studentData.birthDate = new Date(createStudentDto.birthDate); - } - - if (createStudentDto.enrollmentDate) { - studentData.enrollmentDate = new Date(createStudentDto.enrollmentDate); - } - - if (createStudentDto.parentName) { - studentData.parentName = createStudentDto.parentName; - } - - if (createStudentDto.parentPhone) { - studentData.parentPhone = createStudentDto.parentPhone; - } - - if (createStudentDto.address) { - studentData.address = createStudentDto.address; - } - - if (createStudentDto.description) { - studentData.description = createStudentDto.description; - } - - if (creatorId) { - studentData.creator = creatorId; - } - - // 使用事务创建 User、Student 和兴趣班关联 - return this.prisma.$transaction(async (tx) => { - const user = await tx.user.create({ - data: userData, - }); - - const student = await tx.student.create({ - data: { - ...studentData, - userId: user.id, - }, - include: { - user: true, - class: { - include: { - grade: true, - }, - }, - }, - }); - - // 创建兴趣班关联 - if ( - createStudentDto.interestClassIds && - createStudentDto.interestClassIds.length > 0 - ) { - await tx.studentInterestClass.createMany({ - data: createStudentDto.interestClassIds.map((classId) => ({ - studentId: student.id, - classId, - })), - }); - } - - // 重新查询以包含兴趣班信息 - return tx.student.findUnique({ - where: { id: student.id }, - include: { - user: true, - class: { - include: { - grade: true, - }, - }, - interestClasses: { - include: { - class: { - include: { - grade: true, - }, - }, - }, - }, - }, - }); - }); - } - - async findAll( - page: number = 1, - pageSize: number = 10, - tenantId?: number, - classId?: number, - ) { - const skip = (page - 1) * pageSize; - const where: any = { validState: 1 }; - - if (tenantId) { - where.tenantId = tenantId; - } - - if (classId) { - where.classId = classId; - } - - const [list, total] = await Promise.all([ - this.prisma.student.findMany({ - where, - skip, - take: pageSize, - orderBy: { - createTime: 'desc', - }, - include: { - user: true, - class: { - include: { - grade: true, - }, - }, - interestClasses: { - include: { - class: { - include: { - grade: true, - }, - }, - }, - }, - }, - }), - this.prisma.student.count({ where }), - ]); - - return { - list, - total, - page, - pageSize, - }; - } - - async findOne(id: number, tenantId?: number) { - const where: any = { id, validState: 1 }; - if (tenantId) { - where.tenantId = tenantId; - } - - const student = await this.prisma.student.findFirst({ - where, - include: { - user: true, - class: { - include: { - grade: true, - }, - }, - interestClasses: { - include: { - class: { - include: { - grade: true, - }, - }, - }, - }, - }, - }); - - if (!student) { - throw new NotFoundException('学生不存在'); - } - - return student; - } - - async findByUserId(userId: number, tenantId?: number) { - const where: any = { userId, validState: 1 }; - if (tenantId) { - where.tenantId = tenantId; - } - - const student = await this.prisma.student.findFirst({ - where, - include: { - user: true, - class: { - include: { - grade: true, - }, - }, - interestClasses: { - include: { - class: { - include: { - grade: true, - }, - }, - }, - }, - }, - }); - - if (!student) { - throw new NotFoundException('学生不存在'); - } - - return student; - } - - async update( - id: number, - updateStudentDto: UpdateStudentDto, - tenantId?: number, - modifierId?: number, - ) { - // 验证学生是否存在 - const existingStudent = await this.findOne(id, tenantId); - - // 如果更新行政班级,验证班级是否存在且是行政班级 - if ( - updateStudentDto.classId && - updateStudentDto.classId !== existingStudent.classId - ) { - const classEntity = await this.prisma.class.findFirst({ - where: { - id: updateStudentDto.classId, - tenantId: tenantId || existingStudent.tenantId, - type: 1, // 必须是行政班级 - validState: 1, - }, - }); - - if (!classEntity) { - throw new NotFoundException('行政班级不存在或不属于该租户'); - } - } - - // 如果更新学号,检查是否冲突 - if ( - updateStudentDto.studentNo && - updateStudentDto.studentNo !== existingStudent.studentNo - ) { - const existingByStudentNo = await this.prisma.student.findFirst({ - where: { - tenantId: tenantId || existingStudent.tenantId, - studentNo: updateStudentDto.studentNo, - id: { not: id }, - }, - }); - - if (existingByStudentNo) { - throw new ConflictException('学号已存在'); - } - } - - // 更新 Student 记录 - const studentData: any = {}; - - if (updateStudentDto.classId !== undefined) { - studentData.classId = updateStudentDto.classId; - } - - if (updateStudentDto.studentNo !== undefined) { - studentData.studentNo = updateStudentDto.studentNo; - } - - if (updateStudentDto.phone !== undefined) { - studentData.phone = updateStudentDto.phone; - } - - if (updateStudentDto.idCard !== undefined) { - studentData.idCard = updateStudentDto.idCard; - } - - if (updateStudentDto.gender !== undefined) { - studentData.gender = updateStudentDto.gender; - } - - if (updateStudentDto.birthDate !== undefined) { - studentData.birthDate = new Date(updateStudentDto.birthDate); - } - - if (updateStudentDto.enrollmentDate !== undefined) { - studentData.enrollmentDate = new Date(updateStudentDto.enrollmentDate); - } - - if (updateStudentDto.parentName !== undefined) { - studentData.parentName = updateStudentDto.parentName; - } - - if (updateStudentDto.parentPhone !== undefined) { - studentData.parentPhone = updateStudentDto.parentPhone; - } - - if (updateStudentDto.address !== undefined) { - studentData.address = updateStudentDto.address; - } - - if (updateStudentDto.description !== undefined) { - studentData.description = updateStudentDto.description; - } - - if (updateStudentDto.validState !== undefined) { - studentData.validState = updateStudentDto.validState; - } - - if (modifierId) { - studentData.modifier = modifierId; - } - - // 更新 User 记录 - const userData: any = {}; - - if (updateStudentDto.nickname !== undefined) { - userData.nickname = updateStudentDto.nickname; - } - - if (updateStudentDto.email !== undefined) { - userData.email = updateStudentDto.email; - } - - if (updateStudentDto.avatar !== undefined) { - userData.avatar = updateStudentDto.avatar; - } - - if (updateStudentDto.validState !== undefined) { - userData.validState = updateStudentDto.validState; - } - - if (modifierId) { - userData.modifier = modifierId; - } - - // 使用事务更新 User、Student 和兴趣班关联 - return this.prisma.$transaction(async (tx) => { - if (Object.keys(userData).length > 0) { - await tx.user.update({ - where: { id: existingStudent.userId }, - data: userData, - }); - } - - const student = await tx.student.update({ - where: { id }, - data: studentData, - }); - - // 如果提供了兴趣班ID数组,更新兴趣班关联 - if (updateStudentDto.interestClassIds !== undefined) { - // 验证兴趣班是否存在且是兴趣班类型 - if (updateStudentDto.interestClassIds.length > 0) { - const interestClasses = await tx.class.findMany({ - where: { - id: { in: updateStudentDto.interestClassIds }, - tenantId: tenantId || existingStudent.tenantId, - type: 2, // 必须是兴趣班 - validState: 1, - }, - }); - - if ( - interestClasses.length !== updateStudentDto.interestClassIds.length - ) { - throw new NotFoundException('部分兴趣班不存在或不属于该租户'); - } - } - - // 删除现有关联 - await tx.studentInterestClass.deleteMany({ - where: { - studentId: id, - }, - }); - - // 创建新关联 - if (updateStudentDto.interestClassIds.length > 0) { - await tx.studentInterestClass.createMany({ - data: updateStudentDto.interestClassIds.map((classId) => ({ - studentId: id, - classId, - })), - }); - } - } - - // 重新查询以包含所有关联信息 - return tx.student.findUnique({ - where: { id }, - include: { - user: true, - class: { - include: { - grade: true, - }, - }, - interestClasses: { - include: { - class: { - include: { - grade: true, - }, - }, - }, - }, - }, - }); - }); - } - - async remove(id: number, tenantId?: number) { - // 验证学生是否存在 - await this.findOne(id, tenantId); - - // 删除学生会级联删除 User 和兴趣班关联(通过 Prisma schema 的 onDelete: Cascade) - return this.prisma.student.delete({ - where: { id }, - }); - } -} diff --git a/backend/src/school/teachers/dto/create-teacher.dto.ts b/backend/src/school/teachers/dto/create-teacher.dto.ts deleted file mode 100644 index 84297ab..0000000 --- a/backend/src/school/teachers/dto/create-teacher.dto.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - IsString, - IsInt, - IsOptional, - IsEmail, - IsDateString, - IsIn, -} from 'class-validator'; - -export class CreateTeacherDto { - @IsString() - username: string; - - @IsString() - password: string; - - @IsString() - nickname: string; - - @IsEmail() - @IsOptional() - email?: string; - - @IsString() - @IsOptional() - avatar?: string; - - @IsInt() - departmentId: number; - - @IsString() - @IsOptional() - employeeNo?: string; - - @IsString() - @IsOptional() - phone?: string; - - @IsString() - @IsOptional() - idCard?: string; - - @IsInt() - @IsIn([1, 2]) - @IsOptional() - gender?: number; // 1-男,2-女 - - @IsDateString() - @IsOptional() - birthDate?: string; - - @IsDateString() - @IsOptional() - hireDate?: string; - - @IsString() - @IsOptional() - subject?: string; - - @IsString() - @IsOptional() - title?: string; - - @IsString() - @IsOptional() - description?: string; - - @IsInt() - @IsOptional() - validState?: number; // 有效状态:1-有效,0-无效 -} diff --git a/backend/src/school/teachers/dto/update-teacher.dto.ts b/backend/src/school/teachers/dto/update-teacher.dto.ts deleted file mode 100644 index 091b42c..0000000 --- a/backend/src/school/teachers/dto/update-teacher.dto.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - IsString, - IsInt, - IsOptional, - IsEmail, - IsDateString, - IsIn, -} from 'class-validator'; - -export class UpdateTeacherDto { - @IsString() - @IsOptional() - nickname?: string; - - @IsEmail() - @IsOptional() - email?: string; - - @IsString() - @IsOptional() - avatar?: string; - - @IsInt() - @IsOptional() - departmentId?: number; - - @IsString() - @IsOptional() - employeeNo?: string; - - @IsString() - @IsOptional() - phone?: string; - - @IsString() - @IsOptional() - idCard?: string; - - @IsInt() - @IsIn([1, 2]) - @IsOptional() - gender?: number; - - @IsDateString() - @IsOptional() - birthDate?: string; - - @IsDateString() - @IsOptional() - hireDate?: string; - - @IsString() - @IsOptional() - subject?: string; - - @IsString() - @IsOptional() - title?: string; - - @IsString() - @IsOptional() - description?: string; - - @IsInt() - @IsIn([1, 2]) - @IsOptional() - validState?: number; // 1-有效,2-失效 -} - diff --git a/backend/src/school/teachers/teachers.controller.ts b/backend/src/school/teachers/teachers.controller.ts deleted file mode 100644 index e069ca7..0000000 --- a/backend/src/school/teachers/teachers.controller.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, - Query, - UseGuards, - Request, -} from '@nestjs/common'; -import { TeachersService } from './teachers.service'; -import { CreateTeacherDto } from './dto/create-teacher.dto'; -import { UpdateTeacherDto } from './dto/update-teacher.dto'; -import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; - -@Controller('teachers') -@UseGuards(JwtAuthGuard) -export class TeachersController { - constructor(private readonly teachersService: TeachersService) {} - - @Post() - create(@Body() createTeacherDto: CreateTeacherDto, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - if (!tenantId) { - throw new Error('无法确定租户信息'); - } - const creatorId = req.user?.id; - return this.teachersService.create(createTeacherDto, tenantId, creatorId); - } - - @Get() - findAll( - @Query('page') page?: string, - @Query('pageSize') pageSize?: string, - @Query('departmentId') departmentId?: string, - @Query('nickname') nickname?: string, - @Query('username') username?: string, - @Request() req?: any, - ) { - const tenantId = req?.tenantId || req?.user?.tenantId; - return this.teachersService.findAll( - page ? parseInt(page) : 1, - pageSize ? parseInt(pageSize) : 10, - tenantId, - departmentId ? parseInt(departmentId) : undefined, - nickname, - username, - ); - } - - @Get('user/:userId') - findByUserId(@Param('userId') userId: string, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - return this.teachersService.findByUserId(+userId, tenantId); - } - - @Get(':id') - findOne(@Param('id') id: string, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - return this.teachersService.findOne(+id, tenantId); - } - - @Patch(':id') - update( - @Param('id') id: string, - @Body() updateTeacherDto: UpdateTeacherDto, - @Request() req, - ) { - const tenantId = req.tenantId || req.user?.tenantId; - const modifierId = req.user?.id; - return this.teachersService.update(+id, updateTeacherDto, tenantId, modifierId); - } - - @Delete(':id') - remove(@Param('id') id: string, @Request() req) { - const tenantId = req.tenantId || req.user?.tenantId; - return this.teachersService.remove(+id, tenantId); - } -} - diff --git a/backend/src/school/teachers/teachers.module.ts b/backend/src/school/teachers/teachers.module.ts deleted file mode 100644 index a552f2c..0000000 --- a/backend/src/school/teachers/teachers.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TeachersService } from './teachers.service'; -import { TeachersController } from './teachers.controller'; -import { PrismaModule } from '../../prisma/prisma.module'; - -@Module({ - imports: [PrismaModule], - controllers: [TeachersController], - providers: [TeachersService], - exports: [TeachersService], -}) -export class TeachersModule {} - diff --git a/backend/src/school/teachers/teachers.service.ts b/backend/src/school/teachers/teachers.service.ts deleted file mode 100644 index 9adb36a..0000000 --- a/backend/src/school/teachers/teachers.service.ts +++ /dev/null @@ -1,395 +0,0 @@ -import { - Injectable, - NotFoundException, - ConflictException, -} from '@nestjs/common'; -import { PrismaService } from '../../prisma/prisma.service'; -import { CreateTeacherDto } from './dto/create-teacher.dto'; -import { UpdateTeacherDto } from './dto/update-teacher.dto'; -import * as bcrypt from 'bcrypt'; - -@Injectable() -export class TeachersService { - constructor(private prisma: PrismaService) {} - - async create( - createTeacherDto: CreateTeacherDto, - tenantId: number, - creatorId?: number, - ) { - // 验证部门是否存在且属于该租户 - const department = await this.prisma.department.findFirst({ - where: { - id: createTeacherDto.departmentId, - tenantId, - validState: 1, - }, - }); - - if (!department) { - throw new NotFoundException('部门不存在或不属于该租户'); - } - - // 检查用户名是否已存在 - const existingUser = await this.prisma.user.findFirst({ - where: { - tenantId, - username: createTeacherDto.username, - }, - }); - - if (existingUser) { - throw new ConflictException('用户名已存在'); - } - - // 如果提供了工号,检查工号是否已存在 - if (createTeacherDto.employeeNo) { - const existingTeacher = await this.prisma.teacher.findFirst({ - where: { - tenantId, - employeeNo: createTeacherDto.employeeNo, - }, - }); - - if (existingTeacher) { - throw new ConflictException('工号已存在'); - } - } - - // 加密密码 - const hashedPassword = await bcrypt.hash(createTeacherDto.password, 10); - - // 创建 User 记录 - const userData: any = { - username: createTeacherDto.username, - password: hashedPassword, - nickname: createTeacherDto.nickname, - tenantId, - }; - - if (createTeacherDto.email) { - userData.email = createTeacherDto.email; - } - - if (createTeacherDto.avatar) { - userData.avatar = createTeacherDto.avatar; - } - - if (creatorId) { - userData.creator = creatorId; - } - - // 创建 Teacher 记录 - const teacherData: any = { - departmentId: createTeacherDto.departmentId, - tenantId, - }; - - if (createTeacherDto.employeeNo) { - teacherData.employeeNo = createTeacherDto.employeeNo; - } - - if (createTeacherDto.phone) { - teacherData.phone = createTeacherDto.phone; - } - - if (createTeacherDto.idCard) { - teacherData.idCard = createTeacherDto.idCard; - } - - if (createTeacherDto.gender) { - teacherData.gender = createTeacherDto.gender; - } - - if (createTeacherDto.birthDate) { - teacherData.birthDate = new Date(createTeacherDto.birthDate); - } - - if (createTeacherDto.hireDate) { - teacherData.hireDate = new Date(createTeacherDto.hireDate); - } - - if (createTeacherDto.subject) { - teacherData.subject = createTeacherDto.subject; - } - - if (createTeacherDto.title) { - teacherData.title = createTeacherDto.title; - } - - if (createTeacherDto.description) { - teacherData.description = createTeacherDto.description; - } - - if (creatorId) { - teacherData.creator = creatorId; - } - - // 使用事务创建 User 和 Teacher - return this.prisma.$transaction(async (tx) => { - const user = await tx.user.create({ - data: userData, - }); - - const teacher = await tx.teacher.create({ - data: { - ...teacherData, - userId: user.id, - }, - include: { - user: true, - department: true, - }, - }); - - return teacher; - }); - } - - async findAll( - page: number = 1, - pageSize: number = 10, - tenantId?: number, - departmentId?: number, - nickname?: string, - username?: string, - ) { - const skip = (page - 1) * pageSize; - const where: any = { validState: 1 }; - - if (tenantId) { - where.tenantId = tenantId; - } - - if (departmentId) { - where.departmentId = departmentId; - } - - // 支持通过用户昵称和用户名搜索 - if (nickname || username) { - where.user = {}; - if (nickname) { - where.user.nickname = { contains: nickname }; - } - if (username) { - where.user.username = { contains: username }; - } - } - - const [list, total] = await Promise.all([ - this.prisma.teacher.findMany({ - where, - skip, - take: pageSize, - orderBy: { - createTime: 'desc', - }, - include: { - user: true, - department: true, - }, - }), - this.prisma.teacher.count({ where }), - ]); - - return { - list, - total, - page, - pageSize, - }; - } - - async findOne(id: number, tenantId?: number) { - const where: any = { id, validState: 1 }; - if (tenantId) { - where.tenantId = tenantId; - } - - const teacher = await this.prisma.teacher.findFirst({ - where, - include: { - user: true, - department: true, - }, - }); - - if (!teacher) { - throw new NotFoundException('教师不存在'); - } - - return teacher; - } - - async findByUserId(userId: number, tenantId?: number) { - const where: any = { userId, validState: 1 }; - if (tenantId) { - where.tenantId = tenantId; - } - - const teacher = await this.prisma.teacher.findFirst({ - where, - include: { - user: true, - department: true, - }, - }); - - if (!teacher) { - throw new NotFoundException('教师不存在'); - } - - return teacher; - } - - async update( - id: number, - updateTeacherDto: UpdateTeacherDto, - tenantId?: number, - modifierId?: number, - ) { - // 验证教师是否存在 - const existingTeacher = await this.findOne(id, tenantId); - - // 如果更新部门,验证部门是否存在 - if ( - updateTeacherDto.departmentId && - updateTeacherDto.departmentId !== existingTeacher.departmentId - ) { - const department = await this.prisma.department.findFirst({ - where: { - id: updateTeacherDto.departmentId, - tenantId: tenantId || existingTeacher.tenantId, - validState: 1, - }, - }); - - if (!department) { - throw new NotFoundException('部门不存在或不属于该租户'); - } - } - - // 如果更新工号,检查是否冲突 - if ( - updateTeacherDto.employeeNo && - updateTeacherDto.employeeNo !== existingTeacher.employeeNo - ) { - const existingByEmployeeNo = await this.prisma.teacher.findFirst({ - where: { - tenantId: tenantId || existingTeacher.tenantId, - employeeNo: updateTeacherDto.employeeNo, - id: { not: id }, - }, - }); - - if (existingByEmployeeNo) { - throw new ConflictException('工号已存在'); - } - } - - // 更新 Teacher 记录 - const teacherData: any = {}; - - if (updateTeacherDto.departmentId !== undefined) { - teacherData.departmentId = updateTeacherDto.departmentId; - } - - if (updateTeacherDto.employeeNo !== undefined) { - teacherData.employeeNo = updateTeacherDto.employeeNo; - } - - if (updateTeacherDto.phone !== undefined) { - teacherData.phone = updateTeacherDto.phone; - } - - if (updateTeacherDto.idCard !== undefined) { - teacherData.idCard = updateTeacherDto.idCard; - } - - if (updateTeacherDto.gender !== undefined) { - teacherData.gender = updateTeacherDto.gender; - } - - if (updateTeacherDto.birthDate !== undefined) { - teacherData.birthDate = new Date(updateTeacherDto.birthDate); - } - - if (updateTeacherDto.hireDate !== undefined) { - teacherData.hireDate = new Date(updateTeacherDto.hireDate); - } - - if (updateTeacherDto.subject !== undefined) { - teacherData.subject = updateTeacherDto.subject; - } - - if (updateTeacherDto.title !== undefined) { - teacherData.title = updateTeacherDto.title; - } - - if (updateTeacherDto.description !== undefined) { - teacherData.description = updateTeacherDto.description; - } - - if (updateTeacherDto.validState !== undefined) { - teacherData.validState = updateTeacherDto.validState; - } - - if (modifierId) { - teacherData.modifier = modifierId; - } - - // 更新 User 记录 - const userData: any = {}; - - if (updateTeacherDto.nickname !== undefined) { - userData.nickname = updateTeacherDto.nickname; - } - - if (updateTeacherDto.email !== undefined) { - userData.email = updateTeacherDto.email; - } - - if (updateTeacherDto.avatar !== undefined) { - userData.avatar = updateTeacherDto.avatar; - } - - if (updateTeacherDto.validState !== undefined) { - userData.validState = updateTeacherDto.validState; - } - - if (modifierId) { - userData.modifier = modifierId; - } - - // 使用事务更新 User 和 Teacher - return this.prisma.$transaction(async (tx) => { - if (Object.keys(userData).length > 0) { - await tx.user.update({ - where: { id: existingTeacher.userId }, - data: userData, - }); - } - - const teacher = await tx.teacher.update({ - where: { id }, - data: teacherData, - include: { - user: true, - department: true, - }, - }); - - return teacher; - }); - } - - async remove(id: number, tenantId?: number) { - // 验证教师是否存在 - const teacher = await this.findOne(id, tenantId); - - // 删除教师会级联删除 User(通过 Prisma schema 的 onDelete: Cascade) - return this.prisma.teacher.delete({ - where: { id }, - }); - } -} diff --git a/frontend/src/api/ai-3d.ts b/frontend/src/api/ai-3d.ts index 808f7e5..27ee713 100644 --- a/frontend/src/api/ai-3d.ts +++ b/frontend/src/api/ai-3d.ts @@ -1,119 +1,39 @@ -import request from "@/utils/request"; -import type { PaginationParams } from "@/types/api"; - -// ==================== AI 3D 任务相关类型 ==================== - -/** - * AI 3D 任务状态 - */ -export type AI3DTaskStatus = - | "pending" - | "processing" - | "completed" - | "failed" - | "timeout"; - -/** - * AI 3D 任务输入类型 - */ -export type AI3DInputType = "text" | "image"; - -/** - * AI 3D 任务 - */ -export interface AI3DTask { - id: number; - tenantId: number; - userId: number; - inputType: AI3DInputType; - inputContent: string; - status: AI3DTaskStatus; - resultUrl?: string; - previewUrl?: string; - // 多结果支持(文生3D会生成4个不同角度的模型) - resultUrls?: string[]; - previewUrls?: string[]; - errorMessage?: string; - externalTaskId?: string; - retryCount: number; - createTime: string; - completeTime?: string; - // 队列位置(仅 pending 状态时返回) - queuePosition?: number; -} - -/** - * 模型生成类型 - */ -export type AI3DGenerateType = "Normal" | "LowPoly" | "Geometry" | "Sketch"; - -/** - * 创建任务参数 - */ -export interface CreateAI3DTaskParams { - inputType: AI3DInputType; - inputContent: string; - /** 模型生成类型:Normal-带纹理, LowPoly-低多边形, Geometry-白模, Sketch-草图 */ - generateType?: AI3DGenerateType; - /** 模型面数:10000-1500000,默认500000 */ - faceCount?: number; -} - -/** - * 查询任务参数 - */ -export interface QueryAI3DTaskParams extends PaginationParams { - status?: AI3DTaskStatus; -} - -/** - * 任务列表响应 - */ -export interface AI3DTaskListResponse { - list: AI3DTask[]; - total: number; - page: number; - pageSize: number; -} - -// ==================== API 接口 ==================== - -/** - * 创建生成任务 - * POST /api/ai-3d/generate - */ -export function createAI3DTask(data: CreateAI3DTaskParams) { - return request.post("/ai-3d/generate", data); -} - -/** - * 获取任务列表 - * GET /api/ai-3d/tasks - */ -export function getAI3DTasks(params?: QueryAI3DTaskParams) { - return request.get("/ai-3d/tasks", { params }); -} - -/** - * 获取任务详情 - * GET /api/ai-3d/tasks/:id - */ -export function getAI3DTask(id: number) { - return request.get(`/ai-3d/tasks/${id}`); -} - -/** - * 重试任务 - * POST /api/ai-3d/tasks/:id/retry - */ -export function retryAI3DTask(id: number) { - return request.post(`/ai-3d/tasks/${id}/retry`); -} - -/** - * 删除任务 - * DELETE /api/ai-3d/tasks/:id - */ -export function deleteAI3DTask(id: number) { - return request.delete(`/ai-3d/tasks/${id}`); -} +/** + * AI 3D 模块 API 存根文件 + * 原模块已剥离至 competition-management-system-stripped-modules/ + * 仅保留被赛事模块引用的类型和接口定义 + */ +import request from "@/utils/request"; +import type { PaginationParams } from "@/types/api"; + +export type AI3DTaskStatus = "pending" | "processing" | "completed" | "failed" | "timeout"; +export type AI3DInputType = "text" | "image"; + +export interface AI3DTask { + id: number; + tenantId: number; + userId: number; + inputType: AI3DInputType; + inputContent: string; + status: AI3DTaskStatus; + resultUrl?: string; + previewUrl?: string; + resultUrls?: string[]; + previewUrls?: string[]; + errorMessage?: string; + retryCount: number; + createTime: string; + completeTime?: string; + queuePosition?: number; +} + +export interface AI3DTaskListResponse { + list: AI3DTask[]; + total: number; + page: number; + pageSize: number; +} + +export function getAI3DTasks(params?: PaginationParams & { status?: AI3DTaskStatus }) { + return request.get("/ai-3d/tasks", { params }); +} diff --git a/frontend/src/api/schools.ts b/frontend/src/api/schools.ts deleted file mode 100644 index a42455a..0000000 --- a/frontend/src/api/schools.ts +++ /dev/null @@ -1,73 +0,0 @@ -import request from "@/utils/request"; - -export interface School { - id: number; - tenantId: number; - address?: string; - phone?: string; - principal?: string; - established?: string; - description?: string; - logo?: string; - website?: string; - creator?: number; - modifier?: number; - createTime?: string; - modifyTime?: string; - tenant?: { - id: number; - name: string; - code: string; - }; -} - -export interface CreateSchoolForm { - address?: string; - phone?: string; - principal?: string; - established?: string; - description?: string; - logo?: string; - website?: string; -} - -export interface UpdateSchoolForm { - address?: string; - phone?: string; - principal?: string; - established?: string; - description?: string; - logo?: string; - website?: string; -} - -// 获取学校信息 -export async function getSchool(): Promise { - const response = await request.get("/schools"); - return response; -} - -// 创建学校信息 -export async function createSchool(data: CreateSchoolForm): Promise { - const response = await request.post("/schools", data); - return response; -} - -// 更新学校信息 -export async function updateSchool(data: UpdateSchoolForm): Promise { - const response = await request.patch("/schools", data); - return response; -} - -// 删除学校信息 -export async function deleteSchool(): Promise { - return await request.delete("/schools"); -} - -export const schoolsApi = { - get: getSchool, - create: createSchool, - update: updateSchool, - delete: deleteSchool, -}; - diff --git a/frontend/src/api/students.ts b/frontend/src/api/students.ts index 1b2e97d..fa376b2 100644 --- a/frontend/src/api/students.ts +++ b/frontend/src/api/students.ts @@ -1,150 +1,60 @@ -import request from "@/utils/request"; -import type { PaginationParams, PaginationResponse } from "@/types/api"; - -export interface Student { - id: number; - userId: number; - tenantId: number; - classId: number; - studentNo?: string; - phone?: string; - idCard?: string; - gender?: number; // 1-男,2-女 - birthDate?: string; - enrollmentDate?: string; - parentName?: string; - parentPhone?: string; - address?: string; - description?: string; - validState: number; - creator?: number; - modifier?: number; - createTime?: string; - modifyTime?: string; - user?: { - id: number; - username: string; - nickname: string; - email?: string; - avatar?: string; - validState: number; - }; - class?: { - id: number; - name: string; - code: string; - type: number; - grade?: { - id: number; - name: string; - code: string; - level: number; - }; - }; - interestClasses?: Array<{ - id: number; - class: { - id: number; - name: string; - code: string; - type: number; - grade?: { - id: number; - name: string; - code: string; - level: number; - }; - }; - }>; -} - -export interface CreateStudentForm { - username: string; - password: string; - nickname: string; - email?: string; - avatar?: string; - classId: number; - studentNo?: string; - phone?: string; - idCard?: string; - gender?: number; - birthDate?: string; - enrollmentDate?: string; - parentName?: string; - parentPhone?: string; - address?: string; - description?: string; - interestClassIds?: number[]; -} - -export interface UpdateStudentForm { - nickname?: string; - email?: string; - avatar?: string; - classId?: number; - studentNo?: string; - phone?: string; - idCard?: string; - gender?: number; - birthDate?: string; - enrollmentDate?: string; - parentName?: string; - parentPhone?: string; - address?: string; - description?: string; - validState?: number; - interestClassIds?: number[]; -} - -// 获取学生列表 -export async function getStudentsList( - params: PaginationParams & { classId?: number } -): Promise> { - const response = await request.get>("/students", { - params, - }); - return response; -} - -// 获取单个学生详情 -export async function getStudentDetail(id: number): Promise { - const response = await request.get(`/students/${id}`); - return response; -} - -// 根据用户ID获取学生信息 -export async function getStudentByUserId(userId: number): Promise { - const response = await request.get(`/students/user/${userId}`); - return response; -} - -// 创建学生 -export async function createStudent(data: CreateStudentForm): Promise { - const response = await request.post("/students", data); - return response; -} - -// 更新学生 -export async function updateStudent( - id: number, - data: UpdateStudentForm -): Promise { - const response = await request.patch(`/students/${id}`, data); - return response; -} - -// 删除学生 -export async function deleteStudent(id: number): Promise { - return await request.delete(`/students/${id}`); -} - -export const studentsApi = { - getList: getStudentsList, - getDetail: getStudentDetail, - getByUserId: getStudentByUserId, - create: createStudent, - update: updateStudent, - delete: deleteStudent, -}; - +/** + * 学生模块 API 存根文件 + * 原模块已剥离至 competition-management-system-stripped-modules/ + * 仅保留被赛事模块引用的类型和接口定义 + */ +import request from "@/utils/request"; +import type { PaginationParams, PaginationResponse } from "@/types/api"; + +export interface Student { + id: number; + userId: number; + tenantId: number; + classId: number; + studentNo?: string; + phone?: string; + idCard?: string; + gender?: number; + birthDate?: string; + enrollmentDate?: string; + parentName?: string; + parentPhone?: string; + address?: string; + description?: string; + validState: number; + user?: { + id: number; + username: string; + nickname: string; + email?: string; + avatar?: string; + validState: number; + }; + class?: { + id: number; + name: string; + code: string; + type: number; + grade?: { + id: number; + name: string; + code: string; + level: number; + }; + }; +} + +export async function getStudentsList( + params: PaginationParams & { classId?: number } +): Promise> { + return request.get>("/students", { params }); +} + +export async function getStudentByUserId(userId: number): Promise { + return request.get(`/students/user/${userId}`); +} + +export const studentsApi = { + getList: getStudentsList, + getByUserId: getStudentByUserId, +}; diff --git a/frontend/src/api/teachers.ts b/frontend/src/api/teachers.ts index 16b3e6e..e34eed9 100644 --- a/frontend/src/api/teachers.ts +++ b/frontend/src/api/teachers.ts @@ -1,123 +1,49 @@ -import request from "@/utils/request"; -import type { PaginationParams, PaginationResponse } from "@/types/api"; - -export interface Teacher { - id: number; - userId: number; - tenantId: number; - departmentId: number; - employeeNo?: string; - phone?: string; - idCard?: string; - gender?: number; // 1-男,2-女 - birthDate?: string; - hireDate?: string; - subject?: string; - title?: string; - description?: string; - validState: number; - creator?: number; - modifier?: number; - createTime?: string; - modifyTime?: string; - user?: { - id: number; - username: string; - nickname: string; - email?: string; - avatar?: string; - validState: number; - }; - department?: { - id: number; - name: string; - code: string; - }; -} - -export interface CreateTeacherForm { - username: string; - password: string; - nickname: string; - email?: string; - avatar?: string; - departmentId: number; - employeeNo?: string; - phone?: string; - idCard?: string; - gender?: number; - birthDate?: string; - hireDate?: string; - subject?: string; - title?: string; - description?: string; -} - -export interface UpdateTeacherForm { - nickname?: string; - email?: string; - avatar?: string; - departmentId?: number; - employeeNo?: string; - phone?: string; - idCard?: string; - gender?: number; - birthDate?: string; - hireDate?: string; - subject?: string; - title?: string; - description?: string; - validState?: number; -} - -// 获取教师列表 -export async function getTeachersList( - params: PaginationParams & { departmentId?: number; nickname?: string; username?: string } -): Promise> { - const response = await request.get>("/teachers", { - params, - }); - return response; -} - -// 获取单个教师详情 -export async function getTeacherDetail(id: number): Promise { - const response = await request.get(`/teachers/${id}`); - return response; -} - -// 根据用户ID获取教师信息 -export async function getTeacherByUserId(userId: number): Promise { - const response = await request.get(`/teachers/user/${userId}`); - return response; -} - -// 创建教师 -export async function createTeacher(data: CreateTeacherForm): Promise { - const response = await request.post("/teachers", data); - return response; -} - -// 更新教师 -export async function updateTeacher( - id: number, - data: UpdateTeacherForm -): Promise { - const response = await request.patch(`/teachers/${id}`, data); - return response; -} - -// 删除教师 -export async function deleteTeacher(id: number): Promise { - return await request.delete(`/teachers/${id}`); -} - -export const teachersApi = { - getList: getTeachersList, - getDetail: getTeacherDetail, - getByUserId: getTeacherByUserId, - create: createTeacher, - update: updateTeacher, - delete: deleteTeacher, -}; - +/** + * 教师模块 API 存根文件 + * 原模块已剥离至 competition-management-system-stripped-modules/ + * 仅保留被赛事模块引用的类型和接口定义 + */ +import request from "@/utils/request"; +import type { PaginationParams, PaginationResponse } from "@/types/api"; + +export interface Teacher { + id: number; + userId: number; + tenantId: number; + departmentId: number; + employeeNo?: string; + phone?: string; + gender?: number; + subject?: string; + title?: string; + description?: string; + validState: number; + user?: { + id: number; + username: string; + nickname: string; + email?: string; + avatar?: string; + validState: number; + }; + department?: { + id: number; + name: string; + code: string; + }; +} + +export async function getTeachersList( + params: PaginationParams & { departmentId?: number; nickname?: string; username?: string } +): Promise> { + return request.get>("/teachers", { params }); +} + +export async function getTeacherByUserId(userId: number): Promise { + return request.get(`/teachers/user/${userId}`); +} + +export const teachersApi = { + getList: getTeachersList, + getByUserId: getTeacherByUserId, +}; diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index ed7aa3f..536b254 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -310,50 +310,6 @@ const baseRoutes: RouteRecordRaw[] = [ requiresAuth: true, }, }, - // 3D建模实验室路由(工作台模块下) - { - path: "workbench/3d-lab", - name: "3DModelingLab", - component: () => import("@/views/workbench/ai-3d/Index.vue"), - meta: { - title: "3D建模实验室", - requiresAuth: true, - hideSidebar: true, - }, - }, - // 3D模型生成页面 - { - path: "workbench/3d-lab/generate/:taskId", - name: "AI3DGenerate", - component: () => import("@/views/workbench/ai-3d/Generate.vue"), - meta: { - title: "3D模型生成", - requiresAuth: true, - hideSidebar: true, - }, - }, - // 3D创作历史页面 - { - path: "workbench/3d-lab/history", - name: "AI3DHistory", - component: () => import("@/views/workbench/ai-3d/History.vue"), - meta: { - title: "创作历史", - requiresAuth: true, - hideSidebar: true, - }, - }, - // 3D模型预览页面 - { - path: "workbench/model-viewer", - name: "ModelViewer", - component: () => import("@/views/model/ModelViewer.vue"), - meta: { - title: "3D模型预览", - requiresAuth: true, - hideSidebar: true, - }, - }, // 动态路由将在这里添加 ], }, diff --git a/frontend/src/utils/menu.ts b/frontend/src/utils/menu.ts index 9937da4..199887c 100644 --- a/frontend/src/utils/menu.ts +++ b/frontend/src/utils/menu.ts @@ -19,15 +19,6 @@ const componentMap: Record Promise> = { "analytics/Overview": () => import("@/views/analytics/Overview.vue"), "analytics/Review": () => import("@/views/analytics/Review.vue"), "system/tenant-info/Index": () => import("@/views/system/tenant-info/Index.vue"), - "workbench/ai-3d/Index": () => import("@/views/workbench/ai-3d/Index.vue"), - // 学校管理模块 - "school/schools/Index": () => import("@/views/school/schools/Index.vue"), - "school/departments/Index": () => - import("@/views/school/departments/Index.vue"), - "school/grades/Index": () => import("@/views/school/grades/Index.vue"), - "school/classes/Index": () => import("@/views/school/classes/Index.vue"), - "school/teachers/Index": () => import("@/views/school/teachers/Index.vue"), - "school/students/Index": () => import("@/views/school/students/Index.vue"), // 活动管理模块 "contests/Index": () => import("@/views/contests/Index.vue"), "contests/Activities": () => import("@/views/contests/Activities.vue"), diff --git a/frontend/src/views/model/ModelViewer.vue b/frontend/src/views/model/ModelViewer.vue deleted file mode 100644 index 215d848..0000000 --- a/frontend/src/views/model/ModelViewer.vue +++ /dev/null @@ -1,2360 +0,0 @@ - - - - - diff --git a/frontend/src/views/school/classes/Index.vue b/frontend/src/views/school/classes/Index.vue deleted file mode 100644 index 4b60bfc..0000000 --- a/frontend/src/views/school/classes/Index.vue +++ /dev/null @@ -1,369 +0,0 @@ - - - - - diff --git a/frontend/src/views/school/departments/Index.vue b/frontend/src/views/school/departments/Index.vue deleted file mode 100644 index de3eccf..0000000 --- a/frontend/src/views/school/departments/Index.vue +++ /dev/null @@ -1,351 +0,0 @@ - - - - - diff --git a/frontend/src/views/school/grades/Index.vue b/frontend/src/views/school/grades/Index.vue deleted file mode 100644 index affd5d3..0000000 --- a/frontend/src/views/school/grades/Index.vue +++ /dev/null @@ -1,280 +0,0 @@ - - - - - diff --git a/frontend/src/views/school/schools/Index.vue b/frontend/src/views/school/schools/Index.vue deleted file mode 100644 index 3b09967..0000000 --- a/frontend/src/views/school/schools/Index.vue +++ /dev/null @@ -1,307 +0,0 @@ - - - - - diff --git a/frontend/src/views/school/students/Index.vue b/frontend/src/views/school/students/Index.vue deleted file mode 100644 index f1367f5..0000000 --- a/frontend/src/views/school/students/Index.vue +++ /dev/null @@ -1,552 +0,0 @@ - - - - - diff --git a/frontend/src/views/school/teachers/Index.vue b/frontend/src/views/school/teachers/Index.vue deleted file mode 100644 index 27b0169..0000000 --- a/frontend/src/views/school/teachers/Index.vue +++ /dev/null @@ -1,498 +0,0 @@ - - - - - diff --git a/frontend/src/views/workbench/ai-3d/Generate.vue b/frontend/src/views/workbench/ai-3d/Generate.vue deleted file mode 100644 index f33308b..0000000 --- a/frontend/src/views/workbench/ai-3d/Generate.vue +++ /dev/null @@ -1,1081 +0,0 @@ - - - - - diff --git a/frontend/src/views/workbench/ai-3d/History.vue b/frontend/src/views/workbench/ai-3d/History.vue deleted file mode 100644 index 4560de5..0000000 --- a/frontend/src/views/workbench/ai-3d/History.vue +++ /dev/null @@ -1,961 +0,0 @@ - - - - - diff --git a/frontend/src/views/workbench/ai-3d/Index.vue b/frontend/src/views/workbench/ai-3d/Index.vue deleted file mode 100644 index 420a0c7..0000000 --- a/frontend/src/views/workbench/ai-3d/Index.vue +++ /dev/null @@ -1,2822 +0,0 @@ - - - - - - - -