diff --git a/backend/data/menus.json b/backend/data/menus.json index b6bd846..fe0368f 100644 --- a/backend/data/menus.json +++ b/backend/data/menus.json @@ -3,10 +3,20 @@ "name": "工作台", "path": "/workbench", "icon": "DashboardOutlined", - "component": "workbench/Index", + "component": null, "parentId": null, "sort": 1, - "permission": "workbench:read" + "permission": "ai-3d:read", + "children": [ + { + "name": "3D建模实验室", + "path": "/workbench/3d-lab", + "icon": "ExperimentOutlined", + "component": "workbench/ai-3d/Index", + "sort": 1, + "permission": "ai-3d:read" + } + ] }, { "name": "学校管理", diff --git a/backend/data/permissions.json b/backend/data/permissions.json index e04ae9a..4abfc13 100644 --- a/backend/data/permissions.json +++ b/backend/data/permissions.json @@ -1,10 +1,17 @@ [ { - "code": "workbench:read", - "resource": "workbench", + "code": "ai-3d:read", + "resource": "ai-3d", "action": "read", - "name": "查看工作台", - "description": "允许查看工作台" + "name": "使用3D建模实验室", + "description": "允许使用AI 3D建模实验室" + }, + { + "code": "ai-3d:create", + "resource": "ai-3d", + "action": "create", + "name": "创建3D模型任务", + "description": "允许创建AI 3D模型生成任务" }, { "code": "user:create", diff --git a/backend/package-lock.json b/backend/package-lock.json index 9422d3a..d9bf31c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -25,7 +25,8 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "reflect-metadata": "^0.2.1", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "uuid": "^13.0.0" }, "devDependencies": { "@nestjs/cli": "^10.3.2", @@ -37,6 +38,7 @@ "@types/node": "^20.11.5", "@types/passport-jwt": "^4.0.1", "@types/passport-local": "^1.0.36", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", "dotenv": "^17.2.3", @@ -2513,6 +2515,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.15.10", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", @@ -8927,6 +8936,16 @@ "node": ">=0.6" } }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -10454,13 +10473,16 @@ } }, "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/backend/package.json b/backend/package.json index f6795a6..8bdb175 100644 --- a/backend/package.json +++ b/backend/package.json @@ -61,7 +61,8 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "reflect-metadata": "^0.2.1", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "uuid": "^13.0.0" }, "devDependencies": { "@nestjs/cli": "^10.3.2", @@ -73,6 +74,7 @@ "@types/node": "^20.11.5", "@types/passport-jwt": "^4.0.1", "@types/passport-local": "^1.0.36", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", "dotenv": "^17.2.3", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 0fbe119..b880921 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -24,33 +24,35 @@ model Tenant { createTime DateTime @default(now()) @map("create_time") /// 创建时间 modifyTime DateTime @updatedAt @map("modify_time") /// 修改时间 - users User[] - roles Role[] - menus TenantMenu[] - permissions Permission[] - dicts Dict[] - configs Config[] - school School? /// 学校信息(一对一) - grades Grade[] /// 年级 - departments Department[] /// 部门 - classes Class[] /// 班级 - teachers Teacher[] /// 教师 - students Student[] /// 学生 - contestTeams ContestTeam[] /// 赛事团队 - contestTeamMembers ContestTeamMember[] /// 团队成员 - contestRegistrations ContestRegistration[] /// 赛事报名 + users User[] + roles Role[] + menus TenantMenu[] + permissions Permission[] + dicts Dict[] + configs Config[] + school School? /// 学校信息(一对一) + grades Grade[] /// 年级 + departments Department[] /// 部门 + classes Class[] /// 班级 + teachers Teacher[] /// 教师 + students Student[] /// 学生 + contestTeams ContestTeam[] /// 赛事团队 + contestTeamMembers ContestTeamMember[] /// 团队成员 + contestRegistrations ContestRegistration[] /// 赛事报名 contestRegistrationTeachers ContestRegistrationTeacher[] /// 报名指导老师关联 - contestWorks ContestWork[] /// 参赛作品 - contestWorkAttachments ContestWorkAttachment[] /// 作品附件 - contestWorkScores ContestWorkScore[] /// 作品评分 - contestReviewRules ContestReviewRule[] /// 评审规则 + contestWorks ContestWork[] /// 参赛作品 + contestWorkAttachments ContestWorkAttachment[] /// 作品附件 + contestWorkScores ContestWorkScore[] /// 作品评分 + contestReviewRules ContestReviewRule[] /// 评审规则 // 作业管理关联 - homeworks Homework[] /// 作业 - homeworkSubmissions HomeworkSubmission[] /// 作业提交记录 - homeworkReviewRules HomeworkReviewRule[] /// 作业评审规则 - homeworkScores HomeworkScore[] /// 作业评分 - creatorUser User? @relation("TenantCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("TenantModifier", fields: [modifier], references: [id], onDelete: SetNull) + homeworks Homework[] /// 作业 + 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) @@map("tenants") } @@ -69,91 +71,93 @@ model User { organization String? /// 所属单位(用于评委等独立用户) status String @default("enabled") /// 账号状态:enabled-启用,disabled-停用 validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - creator Int? @map("creator") /// 创建人ID - modifier Int? @map("modifier") /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 修改时间 + creator Int? @map("creator") /// 创建人ID + modifier Int? @map("modifier") /// 修改人ID + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 修改时间 - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - roles UserRole[] - logs Log[] - createdBy User? @relation("UserCreator", fields: [creator], references: [id], onDelete: SetNull) - modifiedBy User? @relation("UserModifier", fields: [modifier], references: [id], onDelete: SetNull) - createdUsers User[] @relation("UserCreator") - modifiedUsers User[] @relation("UserModifier") - createdRoles Role[] @relation("RoleCreator") - modifiedRoles Role[] @relation("RoleModifier") - createdPermissions Permission[] @relation("PermissionCreator") - modifiedPermissions Permission[] @relation("PermissionModifier") - createdMenus Menu[] @relation("MenuCreator") - modifiedMenus Menu[] @relation("MenuModifier") - createdDicts Dict[] @relation("DictCreator") - modifiedDicts Dict[] @relation("DictModifier") - createdDictItems DictItem[] @relation("DictItemCreator") - modifiedDictItems DictItem[] @relation("DictItemModifier") - createdConfigs Config[] @relation("ConfigCreator") - 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") + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + roles UserRole[] + logs Log[] + createdBy User? @relation("UserCreator", fields: [creator], references: [id], onDelete: SetNull) + modifiedBy User? @relation("UserModifier", fields: [modifier], references: [id], onDelete: SetNull) + createdUsers User[] @relation("UserCreator") + modifiedUsers User[] @relation("UserModifier") + createdRoles Role[] @relation("RoleCreator") + modifiedRoles Role[] @relation("RoleModifier") + createdPermissions Permission[] @relation("PermissionCreator") + modifiedPermissions Permission[] @relation("PermissionModifier") + createdMenus Menu[] @relation("MenuCreator") + modifiedMenus Menu[] @relation("MenuModifier") + createdDicts Dict[] @relation("DictCreator") + modifiedDicts Dict[] @relation("DictModifier") + createdDictItems DictItem[] @relation("DictItemCreator") + modifiedDictItems DictItem[] @relation("DictItemModifier") + createdConfigs Config[] @relation("ConfigCreator") + 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") - createdContestAttachments ContestAttachment[] @relation("ContestAttachmentCreator") - modifiedContestAttachments ContestAttachment[] @relation("ContestAttachmentModifier") - createdContestReviewRules ContestReviewRule[] @relation("ContestReviewRuleCreator") - modifiedContestReviewRules ContestReviewRule[] @relation("ContestReviewRuleModifier") - createdContestTeams ContestTeam[] @relation("ContestTeamCreator") - modifiedContestTeams ContestTeam[] @relation("ContestTeamModifier") - ledContestTeams ContestTeam[] @relation("ContestTeamLeader") - createdContestTeamMembers ContestTeamMember[] @relation("ContestTeamMemberCreator") - modifiedContestTeamMembers ContestTeamMember[] @relation("ContestTeamMemberModifier") - contestTeamMembers ContestTeamMember[] @relation("ContestTeamMemberUser") - createdContestRegistrations ContestRegistration[] @relation("ContestRegistrationCreator") - modifiedContestRegistrations ContestRegistration[] @relation("ContestRegistrationModifier") - contestRegistrations ContestRegistration[] @relation("ContestRegistrationUser") - createdContestWorks ContestWork[] @relation("ContestWorkCreator") - modifiedContestWorks ContestWork[] @relation("ContestWorkModifier") - createdContestWorkAttachments ContestWorkAttachment[] @relation("ContestWorkAttachmentCreator") - modifiedContestWorkAttachments ContestWorkAttachment[] @relation("ContestWorkAttachmentModifier") - createdContestWorkJudgeAssignments ContestWorkJudgeAssignment[] @relation("ContestWorkJudgeAssignmentCreator") + createdContests Contest[] @relation("ContestCreator") + modifiedContests Contest[] @relation("ContestModifier") + createdContestAttachments ContestAttachment[] @relation("ContestAttachmentCreator") + modifiedContestAttachments ContestAttachment[] @relation("ContestAttachmentModifier") + createdContestReviewRules ContestReviewRule[] @relation("ContestReviewRuleCreator") + modifiedContestReviewRules ContestReviewRule[] @relation("ContestReviewRuleModifier") + createdContestTeams ContestTeam[] @relation("ContestTeamCreator") + modifiedContestTeams ContestTeam[] @relation("ContestTeamModifier") + ledContestTeams ContestTeam[] @relation("ContestTeamLeader") + createdContestTeamMembers ContestTeamMember[] @relation("ContestTeamMemberCreator") + modifiedContestTeamMembers ContestTeamMember[] @relation("ContestTeamMemberModifier") + contestTeamMembers ContestTeamMember[] @relation("ContestTeamMemberUser") + createdContestRegistrations ContestRegistration[] @relation("ContestRegistrationCreator") + modifiedContestRegistrations ContestRegistration[] @relation("ContestRegistrationModifier") + contestRegistrations ContestRegistration[] @relation("ContestRegistrationUser") + createdContestWorks ContestWork[] @relation("ContestWorkCreator") + modifiedContestWorks ContestWork[] @relation("ContestWorkModifier") + createdContestWorkAttachments ContestWorkAttachment[] @relation("ContestWorkAttachmentCreator") + modifiedContestWorkAttachments ContestWorkAttachment[] @relation("ContestWorkAttachmentModifier") + createdContestWorkJudgeAssignments ContestWorkJudgeAssignment[] @relation("ContestWorkJudgeAssignmentCreator") modifiedContestWorkJudgeAssignments ContestWorkJudgeAssignment[] @relation("ContestWorkJudgeAssignmentModifier") - assignedContestWorks ContestWorkJudgeAssignment[] @relation("ContestWorkJudgeAssignmentJudge") - createdContestWorkScores ContestWorkScore[] @relation("ContestWorkScoreCreator") - modifiedContestWorkScores ContestWorkScore[] @relation("ContestWorkScoreModifier") - scoredContestWorks ContestWorkScore[] @relation("ContestWorkScoreJudge") - createdContestNotices ContestNotice[] @relation("ContestNoticeCreator") - modifiedContestNotices ContestNotice[] @relation("ContestNoticeModifier") - contestJudges ContestJudge[] @relation("ContestJudgeUser") - createdContestJudges ContestJudge[] @relation("ContestJudgeCreator") - modifiedContestJudges ContestJudge[] @relation("ContestJudgeModifier") - contestRegistrationTeachers ContestRegistrationTeacher[] @relation("ContestRegistrationTeacherUser") - createdContestRegistrationTeachers ContestRegistrationTeacher[] @relation("ContestRegistrationTeacherCreator") + assignedContestWorks ContestWorkJudgeAssignment[] @relation("ContestWorkJudgeAssignmentJudge") + createdContestWorkScores ContestWorkScore[] @relation("ContestWorkScoreCreator") + modifiedContestWorkScores ContestWorkScore[] @relation("ContestWorkScoreModifier") + scoredContestWorks ContestWorkScore[] @relation("ContestWorkScoreJudge") + createdContestNotices ContestNotice[] @relation("ContestNoticeCreator") + modifiedContestNotices ContestNotice[] @relation("ContestNoticeModifier") + contestJudges ContestJudge[] @relation("ContestJudgeUser") + createdContestJudges ContestJudge[] @relation("ContestJudgeCreator") + modifiedContestJudges ContestJudge[] @relation("ContestJudgeModifier") + contestRegistrationTeachers ContestRegistrationTeacher[] @relation("ContestRegistrationTeacherUser") + createdContestRegistrationTeachers ContestRegistrationTeacher[] @relation("ContestRegistrationTeacherCreator") modifiedContestRegistrationTeachers ContestRegistrationTeacher[] @relation("ContestRegistrationTeacherModifier") // 作业管理关联 - createdHomeworks Homework[] @relation("HomeworkCreator") - modifiedHomeworks Homework[] @relation("HomeworkModifier") - homeworkSubmissions HomeworkSubmission[] @relation("HomeworkSubmissionStudent") - createdHomeworkSubmissions HomeworkSubmission[] @relation("HomeworkSubmissionCreator") - modifiedHomeworkSubmissions HomeworkSubmission[] @relation("HomeworkSubmissionModifier") - createdHomeworkReviewRules HomeworkReviewRule[] @relation("HomeworkReviewRuleCreator") - modifiedHomeworkReviewRules HomeworkReviewRule[] @relation("HomeworkReviewRuleModifier") - homeworkScoresAsReviewer HomeworkScore[] @relation("HomeworkScoreReviewer") - createdHomeworkScores HomeworkScore[] @relation("HomeworkScoreCreator") - modifiedHomeworkScores HomeworkScore[] @relation("HomeworkScoreModifier") + createdHomeworks Homework[] @relation("HomeworkCreator") + modifiedHomeworks Homework[] @relation("HomeworkModifier") + homeworkSubmissions HomeworkSubmission[] @relation("HomeworkSubmissionStudent") + createdHomeworkSubmissions HomeworkSubmission[] @relation("HomeworkSubmissionCreator") + modifiedHomeworkSubmissions HomeworkSubmission[] @relation("HomeworkSubmissionModifier") + createdHomeworkReviewRules HomeworkReviewRule[] @relation("HomeworkReviewRuleCreator") + modifiedHomeworkReviewRules HomeworkReviewRule[] @relation("HomeworkReviewRuleModifier") + homeworkScoresAsReviewer HomeworkScore[] @relation("HomeworkScoreReviewer") + createdHomeworkScores HomeworkScore[] @relation("HomeworkScoreCreator") + modifiedHomeworkScores HomeworkScore[] @relation("HomeworkScoreModifier") + // AI 3D 生成关联 + ai3dTasks AI3DTask[] @relation("AI3DTaskUser") /// 用户的 AI 3D 生成任务 @@unique([tenantId, username]) @@unique([tenantId, email]) @@ -565,7 +569,7 @@ model Contest { submitStartTime DateTime @map("submit_start_time") /// 作品提交开始时间 submitEndTime DateTime @map("submit_end_time") /// 作品提交结束时间 workType String? @map("work_type") /// 作品类型(如:image/video/document/code) - workRequirement String? @db.Text @map("work_requirement") /// 作品要求说明 + workRequirement String? @map("work_requirement") @db.Text /// 作品要求说明 // 评审配置 reviewRuleId Int? @map("review_rule_id") /// 评审规则id reviewStartTime DateTime @map("review_start_time") /// 评审开始时间 @@ -579,17 +583,17 @@ model Contest { modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - attachments ContestAttachment[] /// 赛事附件 - reviewRule ContestReviewRule? @relation("ContestReviewRuleContest", fields: [reviewRuleId], references: [id], onDelete: SetNull) - teams ContestTeam[] /// 赛事团队 - registrations ContestRegistration[] /// 报名记录 - works ContestWork[] /// 参赛作品 - judges ContestJudge[] /// 比赛评委 - workAssignments ContestWorkJudgeAssignment[] @relation("ContestWorkJudgeAssignmentContest") /// 作品分配 - workScores ContestWorkScore[] @relation("ContestWorkScoreContest") /// 作品评分 - notices ContestNotice[] /// 赛事公告 - creatorUser User? @relation("ContestCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("ContestModifier", fields: [modifier], references: [id], onDelete: SetNull) + attachments ContestAttachment[] /// 赛事附件 + reviewRule ContestReviewRule? @relation("ContestReviewRuleContest", fields: [reviewRuleId], references: [id], onDelete: SetNull) + teams ContestTeam[] /// 赛事团队 + registrations ContestRegistration[] /// 报名记录 + works ContestWork[] /// 参赛作品 + judges ContestJudge[] /// 比赛评委 + workAssignments ContestWorkJudgeAssignment[] @relation("ContestWorkJudgeAssignmentContest") /// 作品分配 + workScores ContestWorkScore[] @relation("ContestWorkScoreContest") /// 作品评分 + notices ContestNotice[] /// 赛事公告 + creatorUser User? @relation("ContestCreator", fields: [creator], references: [id], onDelete: SetNull) + modifierUser User? @relation("ContestModifier", fields: [modifier], references: [id], onDelete: SetNull) @@unique([contestName]) @@index([contestState]) @@ -600,22 +604,22 @@ model Contest { /// 赛事附件表 model ContestAttachment { - id Int @id @default(autoincrement()) - contestId Int @map("contest_id") /// 赛事id - fileName String @map("file_name") /// 文件名 - fileUrl String @map("file_url") /// 文件路径 + id Int @id @default(autoincrement()) + contestId Int @map("contest_id") /// 赛事id + fileName String @map("file_name") /// 文件名 + fileUrl String @map("file_url") /// 文件路径 format String? /// 文件类型(png,mp4) - fileType String? @map("file_type") /// 素材类型(image,video) - size String @default("0") /// 文件大小 + fileType String? @map("file_type") /// 素材类型(image,video) + size String @default("0") /// 文件大小 creator Int? /// 创建人ID modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) - creatorUser User? @relation("ContestAttachmentCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("ContestAttachmentModifier", fields: [modifier], references: [id], onDelete: SetNull) + contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) + creatorUser User? @relation("ContestAttachmentCreator", fields: [creator], references: [id], onDelete: SetNull) + modifierUser User? @relation("ContestAttachmentModifier", fields: [modifier], references: [id], onDelete: SetNull) @@index([contestId]) @@map("t_contest_attachment") @@ -623,18 +627,18 @@ model ContestAttachment { /// 评审规则表(独立存在,可被多个赛事使用) model ContestReviewRule { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 租户ID - ruleName String @map("rule_name") /// 规则名称 - ruleDescription String? @db.Text @map("rule_description") /// 规则说明 - judgeCount Int? @map("judge_count") /// 评委数量 + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") /// 租户ID + ruleName String @map("rule_name") /// 规则名称 + ruleDescription String? @map("rule_description") @db.Text /// 规则说明 + judgeCount Int? @map("judge_count") /// 评委数量 dimensions Json /// 评分维度配置JSON - calculationRule String @default("average") @map("calculation_rule") /// 计算规则:average/remove_max_min/remove_min + calculationRule String @default("average") @map("calculation_rule") /// 计算规则:average/remove_max_min/remove_min creator Int? /// 创建人ID modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) contests Contest[] @relation("ContestReviewRuleContest") /// 使用此规则的赛事列表 @@ -647,25 +651,25 @@ model ContestReviewRule { /// 赛事团队表 model ContestTeam { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 团队所属租户ID - contestId Int @map("contest_id") /// 赛事id - teamName String @map("team_name") /// 团队名称(租户内唯一) + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") /// 团队所属租户ID + contestId Int @map("contest_id") /// 赛事id + teamName String @map("team_name") /// 团队名称(租户内唯一) leaderUserId Int @map("leader_user_id") /// 团队负责人用户id - maxMembers Int? @map("max_members") /// 团队最大成员数 - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 + maxMembers Int? @map("max_members") /// 团队最大成员数 + creator Int? /// 创建人ID + modifier Int? /// 修改人ID + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) - leader User @relation("ContestTeamLeader", fields: [leaderUserId], references: [id], onDelete: Restrict) - members ContestTeamMember[] /// 团队成员 + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) + leader User @relation("ContestTeamLeader", fields: [leaderUserId], references: [id], onDelete: Restrict) + members ContestTeamMember[] /// 团队成员 registrations ContestRegistration[] /// 报名记录 - creatorUser User? @relation("ContestTeamCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("ContestTeamModifier", fields: [modifier], references: [id], onDelete: SetNull) + creatorUser User? @relation("ContestTeamCreator", fields: [creator], references: [id], onDelete: SetNull) + modifierUser User? @relation("ContestTeamModifier", fields: [modifier], references: [id], onDelete: SetNull) @@unique([tenantId, contestId, teamName]) @@index([contestId]) @@ -675,21 +679,21 @@ model ContestTeam { /// 团队成员表 model ContestTeamMember { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 成员所属租户ID - teamId Int @map("team_id") /// 团队id - userId Int @map("user_id") /// 成员用户id - role String @default("member") /// 成员角色:member/leader/mentor - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") /// 成员所属租户ID + teamId Int @map("team_id") /// 团队id + userId Int @map("user_id") /// 成员用户id + role String @default("member") /// 成员角色:member/leader/mentor + 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) - team ContestTeam @relation(fields: [teamId], references: [id], onDelete: Cascade) - user User @relation("ContestTeamMemberUser", fields: [userId], references: [id], onDelete: Cascade) - creatorUser User? @relation("ContestTeamMemberCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("ContestTeamMemberModifier", fields: [modifier], references: [id], onDelete: SetNull) + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + team ContestTeam @relation(fields: [teamId], references: [id], onDelete: Cascade) + user User @relation("ContestTeamMemberUser", fields: [userId], references: [id], onDelete: Cascade) + creatorUser User? @relation("ContestTeamMemberCreator", fields: [creator], references: [id], onDelete: SetNull) + modifierUser User? @relation("ContestTeamMemberModifier", fields: [modifier], references: [id], onDelete: SetNull) @@unique([tenantId, teamId, userId]) @@index([teamId]) @@ -699,34 +703,34 @@ model ContestTeamMember { /// 赛事报名表 model ContestRegistration { - id Int @id @default(autoincrement()) - contestId Int @map("contest_id") /// 赛事id - tenantId Int @map("tenant_id") /// 所属租户ID - registrationType String? @map("registration_type") /// 报名类型:individual/team - teamId Int? @map("team_id") /// 团队id - teamName String? @map("team_name") /// 团队名称快照(团队赛) - userId Int @map("user_id") /// 账号id - accountNo String @map("account_no") /// 报名账号(记录报名快照) - accountName String @map("account_name") /// 报名账号名称(记录报名快照) - role String? /// 报名角色快照:leader/member/mentor - registrationState String @default("pending") @map("registration_state") /// 报名状态:pending/passed/rejected/withdrawn - registrant Int? /// 实际报名人用户ID - registrationTime DateTime @map("registration_time") /// 报名时间 - reason String? @db.VarChar(1023) /// 审核理由 - operator Int? /// 审核人用户ID - operationDate DateTime? @map("operation_date") /// 审核时间 - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + id Int @id @default(autoincrement()) + contestId Int @map("contest_id") /// 赛事id + tenantId Int @map("tenant_id") /// 所属租户ID + registrationType String? @map("registration_type") /// 报名类型:individual/team + teamId Int? @map("team_id") /// 团队id + teamName String? @map("team_name") /// 团队名称快照(团队赛) + userId Int @map("user_id") /// 账号id + accountNo String @map("account_no") /// 报名账号(记录报名快照) + accountName String @map("account_name") /// 报名账号名称(记录报名快照) + role String? /// 报名角色快照:leader/member/mentor + registrationState String @default("pending") @map("registration_state") /// 报名状态:pending/passed/rejected/withdrawn + registrant Int? /// 实际报名人用户ID + registrationTime DateTime @map("registration_time") /// 报名时间 + reason String? @db.VarChar(1023) /// 审核理由 + operator Int? /// 审核人用户ID + operationDate DateTime? @map("operation_date") /// 审核时间 + creator Int? /// 创建人ID + modifier Int? /// 修改人ID + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 - contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - team ContestTeam? @relation(fields: [teamId], references: [id], onDelete: SetNull) - user User @relation("ContestRegistrationUser", fields: [userId], references: [id], onDelete: Restrict) + contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + team ContestTeam? @relation(fields: [teamId], references: [id], onDelete: SetNull) + user User @relation("ContestRegistrationUser", fields: [userId], references: [id], onDelete: Restrict) works ContestWork[] /// 参赛作品 - creatorUser User? @relation("ContestRegistrationCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("ContestRegistrationModifier", fields: [modifier], references: [id], onDelete: SetNull) + creatorUser User? @relation("ContestRegistrationCreator", fields: [creator], references: [id], onDelete: SetNull) + modifierUser User? @relation("ContestRegistrationModifier", fields: [modifier], references: [id], onDelete: SetNull) teachers ContestRegistrationTeacher[] /// 指导老师关联 @@ -739,15 +743,15 @@ model ContestRegistration { /// 报名指导老师关联表 model ContestRegistrationTeacher { - id Int @id @default(autoincrement()) - registrationId Int @map("registration_id") /// 报名记录ID - tenantId Int @map("tenant_id") /// 租户ID - userId Int @map("user_id") /// 指导老师用户ID - isDefault Boolean @default(false) @map("is_default") /// 是否为默认指导老师(报名时自动添加的,不能移除) - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + id Int @id @default(autoincrement()) + registrationId Int @map("registration_id") /// 报名记录ID + tenantId Int @map("tenant_id") /// 租户ID + userId Int @map("user_id") /// 指导老师用户ID + isDefault Boolean @default(false) @map("is_default") /// 是否为默认指导老师(报名时自动添加的,不能移除) + creator Int? /// 创建人ID + modifier Int? /// 修改人ID + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 registration ContestRegistration @relation(fields: [registrationId], references: [id], onDelete: Cascade) tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) @@ -763,43 +767,43 @@ model ContestRegistrationTeacher { /// 参赛作品表 model ContestWork { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 作品所属租户ID - contestId Int @map("contest_id") /// 赛事id - registrationId Int @map("registration_id") /// 报名记录id - workNo String? @unique @map("work_no") /// 作品编号(展示用唯一编号) - title String /// 作品标题 - description String? @db.Text /// 作品说明 - files Json? /// 作品文件列表(简易场景) - version Int @default(1) /// 作品版本号(递增) - isLatest Boolean @default(true) @map("is_latest") /// 是否最新版本 - status String @default("submitted") /// 作品状态:submitted/locked/reviewing/rejected/accepted - submitTime DateTime @default(now()) @map("submit_time") /// 提交时间 - submitterUserId Int? @map("submitter_user_id") /// 提交人用户id - submitterAccountNo String? @map("submitter_account_no") /// 提交人账号 - submitSource String @default("teacher") @map("submit_source") /// 提交来源:teacher/student/team_leader - previewUrl String? @map("preview_url") /// 作品预览URL - aiModelMeta Json? @map("ai_model_meta") /// AI建模元数据 + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") /// 作品所属租户ID + contestId Int @map("contest_id") /// 赛事id + registrationId Int @map("registration_id") /// 报名记录id + workNo String? @unique @map("work_no") /// 作品编号(展示用唯一编号) + title String /// 作品标题 + description String? @db.Text /// 作品说明 + files Json? /// 作品文件列表(简易场景) + version Int @default(1) /// 作品版本号(递增) + isLatest Boolean @default(true) @map("is_latest") /// 是否最新版本 + status String @default("submitted") /// 作品状态:submitted/locked/reviewing/rejected/accepted + submitTime DateTime @default(now()) @map("submit_time") /// 提交时间 + submitterUserId Int? @map("submitter_user_id") /// 提交人用户id + submitterAccountNo String? @map("submitter_account_no") /// 提交人账号 + submitSource String @default("teacher") @map("submit_source") /// 提交来源:teacher/student/team_leader + previewUrl String? @map("preview_url") /// 作品预览URL + aiModelMeta Json? @map("ai_model_meta") /// AI建模元数据 // 赛果相关字段 - finalScore Decimal? @map("final_score") @db.Decimal(10, 2) /// 最终得分(根据规则计算) - rank Int? /// 排名 - awardLevel String? @map("award_level") /// 奖项等级:first/second/third/excellent/none - awardName String? @map("award_name") /// 奖项名称(如:一等奖、金奖) - certificateUrl String? @map("certificate_url") /// 证书URL - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 + finalScore Decimal? @map("final_score") @db.Decimal(10, 2) /// 最终得分(根据规则计算) + rank Int? /// 排名 + awardLevel String? @map("award_level") /// 奖项等级:first/second/third/excellent/none + awardName String? @map("award_name") /// 奖项名称(如:一等奖、金奖) + certificateUrl String? @map("certificate_url") /// 证书URL + creator Int? /// 创建人ID + modifier Int? /// 修改人ID + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) - registration ContestRegistration @relation(fields: [registrationId], references: [id], onDelete: Restrict) - attachments ContestWorkAttachment[] /// 作品附件 - assignments ContestWorkJudgeAssignment[] /// 作品分配 - scores ContestWorkScore[] /// 作品评分 - creatorUser User? @relation("ContestWorkCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("ContestWorkModifier", fields: [modifier], references: [id], onDelete: SetNull) + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) + registration ContestRegistration @relation(fields: [registrationId], references: [id], onDelete: Restrict) + attachments ContestWorkAttachment[] /// 作品附件 + assignments ContestWorkJudgeAssignment[] /// 作品分配 + scores ContestWorkScore[] /// 作品评分 + creatorUser User? @relation("ContestWorkCreator", fields: [creator], references: [id], onDelete: SetNull) + modifierUser User? @relation("ContestWorkModifier", fields: [modifier], references: [id], onDelete: SetNull) @@index([tenantId, contestId, isLatest]) @@index([registrationId]) @@ -810,24 +814,24 @@ model ContestWork { /// 作品附件文件表 model ContestWorkAttachment { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 所属租户ID - contestId Int @map("contest_id") /// 赛事id - workId Int @map("work_id") /// 作品id - fileName String @map("file_name") /// 文件名 - fileUrl String @map("file_url") /// 文件路径 + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") /// 所属租户ID + contestId Int @map("contest_id") /// 赛事id + workId Int @map("work_id") /// 作品id + fileName String @map("file_name") /// 文件名 + fileUrl String @map("file_url") /// 文件路径 format String? /// 文件类型(png,mp4) - fileType String? @map("file_type") /// 素材类型(image,video) - size String @default("0") /// 文件大小 + fileType String? @map("file_type") /// 素材类型(image,video) + size String @default("0") /// 文件大小 creator Int? /// 创建人ID modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 - work ContestWork @relation(fields: [workId], references: [id], onDelete: Cascade) - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - creatorUser User? @relation("ContestWorkAttachmentCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("ContestWorkAttachmentModifier", fields: [modifier], references: [id], onDelete: SetNull) + work ContestWork @relation(fields: [workId], references: [id], onDelete: Cascade) + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + creatorUser User? @relation("ContestWorkAttachmentCreator", fields: [creator], references: [id], onDelete: SetNull) + modifierUser User? @relation("ContestWorkAttachmentModifier", fields: [modifier], references: [id], onDelete: SetNull) @@index([tenantId, contestId, workId]) @@map("t_contest_work_attachment") @@ -835,22 +839,22 @@ model ContestWorkAttachment { /// 比赛评委关联表(比赛与评委的多对多关系) model ContestJudge { - id Int @id @default(autoincrement()) - contestId Int @map("contest_id") /// 比赛id - judgeId Int @map("judge_id") /// 评委用户id + id Int @id @default(autoincrement()) + contestId Int @map("contest_id") /// 比赛id + judgeId Int @map("judge_id") /// 评委用户id specialty String? /// 评审专业领域(可选) - weight Decimal? @db.Decimal(3, 2) /// 评审权重(可选,用于加权平均计算) - description String? @db.Text /// 评委在该比赛中的说明 + weight Decimal? @db.Decimal(3, 2) /// 评审权重(可选,用于加权平均计算) + description String? @db.Text /// 评委在该比赛中的说明 creator Int? /// 创建人ID modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) - judge User @relation("ContestJudgeUser", fields: [judgeId], references: [id], onDelete: Cascade) - creatorUser User? @relation("ContestJudgeCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("ContestJudgeModifier", fields: [modifier], references: [id], onDelete: SetNull) + contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) + judge User @relation("ContestJudgeUser", fields: [judgeId], references: [id], onDelete: Cascade) + creatorUser User? @relation("ContestJudgeCreator", fields: [creator], references: [id], onDelete: SetNull) + modifierUser User? @relation("ContestJudgeModifier", fields: [modifier], references: [id], onDelete: SetNull) @@unique([contestId, judgeId]) @@index([contestId]) @@ -860,23 +864,23 @@ model ContestJudge { /// 作品分配表(评委分配作品) model ContestWorkJudgeAssignment { - id Int @id @default(autoincrement()) - contestId Int @map("contest_id") /// 赛事id - workId Int @map("work_id") /// 作品id - judgeId Int @map("judge_id") /// 评委用户id - assignmentTime DateTime @default(now()) @map("assignment_time") /// 分配时间 - status String @default("assigned") /// 分配状态:assigned/reviewing/completed + id Int @id @default(autoincrement()) + contestId Int @map("contest_id") /// 赛事id + workId Int @map("work_id") /// 作品id + judgeId Int @map("judge_id") /// 评委用户id + assignmentTime DateTime @default(now()) @map("assignment_time") /// 分配时间 + status String @default("assigned") /// 分配状态:assigned/reviewing/completed creator Int? /// 创建人ID modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 - contest Contest @relation("ContestWorkJudgeAssignmentContest", fields: [contestId], references: [id], onDelete: Cascade) - work ContestWork @relation(fields: [workId], references: [id], onDelete: Cascade) - judge User @relation("ContestWorkJudgeAssignmentJudge", fields: [judgeId], references: [id], onDelete: Restrict) - scores ContestWorkScore[] /// 评分记录 - creatorUser User? @relation("ContestWorkJudgeAssignmentCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("ContestWorkJudgeAssignmentModifier", fields: [modifier], references: [id], onDelete: SetNull) + contest Contest @relation("ContestWorkJudgeAssignmentContest", fields: [contestId], references: [id], onDelete: Cascade) + work ContestWork @relation(fields: [workId], references: [id], onDelete: Cascade) + judge User @relation("ContestWorkJudgeAssignmentJudge", fields: [judgeId], references: [id], onDelete: Restrict) + scores ContestWorkScore[] /// 评分记录 + creatorUser User? @relation("ContestWorkJudgeAssignmentCreator", fields: [creator], references: [id], onDelete: SetNull) + modifierUser User? @relation("ContestWorkJudgeAssignmentModifier", fields: [modifier], references: [id], onDelete: SetNull) @@unique([workId, judgeId]) @@index([contestId, judgeId]) @@ -887,30 +891,30 @@ model ContestWorkJudgeAssignment { /// 作品评分表 model ContestWorkScore { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 所属租户ID - contestId Int @map("contest_id") /// 赛事id - workId Int @map("work_id") /// 作品id - assignmentId Int @map("assignment_id") /// 分配记录id - judgeId Int @map("judge_id") /// 评委用户id - judgeName String @map("judge_name") /// 评委姓名 - dimensionScores Json @map("dimension_scores") /// 各维度评分JSON - totalScore Decimal @map("total_score") @db.Decimal(10, 2) /// 总分 - comments String? @db.Text /// 评语 - scoreTime DateTime @map("score_time") /// 评分时间 + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") /// 所属租户ID + contestId Int @map("contest_id") /// 赛事id + workId Int @map("work_id") /// 作品id + assignmentId Int @map("assignment_id") /// 分配记录id + judgeId Int @map("judge_id") /// 评委用户id + judgeName String @map("judge_name") /// 评委姓名 + dimensionScores Json @map("dimension_scores") /// 各维度评分JSON + totalScore Decimal @map("total_score") @db.Decimal(10, 2) /// 总分 + comments String? @db.Text /// 评语 + scoreTime DateTime @map("score_time") /// 评分时间 creator Int? /// 创建人ID modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - contest Contest @relation("ContestWorkScoreContest", fields: [contestId], references: [id], onDelete: Cascade) - work ContestWork @relation(fields: [workId], references: [id], onDelete: Cascade) - assignment ContestWorkJudgeAssignment @relation(fields: [assignmentId], references: [id], onDelete: Restrict) - judge User @relation("ContestWorkScoreJudge", fields: [judgeId], references: [id], onDelete: Restrict) - creatorUser User? @relation("ContestWorkScoreCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("ContestWorkScoreModifier", fields: [modifier], references: [id], onDelete: SetNull) + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + contest Contest @relation("ContestWorkScoreContest", fields: [contestId], references: [id], onDelete: Cascade) + work ContestWork @relation(fields: [workId], references: [id], onDelete: Cascade) + assignment ContestWorkJudgeAssignment @relation(fields: [assignmentId], references: [id], onDelete: Restrict) + judge User @relation("ContestWorkScoreJudge", fields: [judgeId], references: [id], onDelete: Restrict) + creatorUser User? @relation("ContestWorkScoreCreator", fields: [creator], references: [id], onDelete: SetNull) + modifierUser User? @relation("ContestWorkScoreModifier", fields: [modifier], references: [id], onDelete: SetNull) @@index([contestId, workId, judgeId]) @@index([workId]) @@ -933,8 +937,8 @@ model ContestNotice { modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) - creatorUser User? @relation("ContestNoticeCreator", fields: [creator], references: [id], onDelete: SetNull) + contest Contest @relation(fields: [contestId], references: [id], onDelete: Cascade) + creatorUser User? @relation("ContestNoticeCreator", fields: [creator], references: [id], onDelete: SetNull) modifierUser User? @relation("ContestNoticeModifier", fields: [modifier], references: [id], onDelete: SetNull) @@index([contestId]) @@ -951,26 +955,26 @@ model ContestNotice { model Homework { id Int @id @default(autoincrement()) tenantId Int @map("tenant_id") /// 租户ID - name String /// 作业名称 + name String /// 作业名称 content String? @db.Text /// 作业内容(富文本) status String @default("unpublished") /// 作业状态:unpublished/published publishTime DateTime? @map("publish_time") /// 发布时间 submitStartTime DateTime @map("submit_start_time") /// 提交开始时间 submitEndTime DateTime @map("submit_end_time") /// 提交结束时间 - attachments Json? /// 附件列表 [{fileName, fileUrl, size}] + attachments Json? /// 附件列表 [{fileName, fileUrl, size}] publishScope Json? @map("publish_scope") /// 公开范围(班级ID数组) reviewRuleId Int? @map("review_rule_id") /// 评审规则ID - creator Int? /// 创建人ID - modifier Int? /// 修改人ID + creator Int? /// 创建人ID + modifier Int? /// 修改人ID createTime DateTime @default(now()) @map("create_time") /// 创建时间 modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 - tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) - reviewRule HomeworkReviewRule? @relation(fields: [reviewRuleId], references: [id], onDelete: SetNull) + tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) + reviewRule HomeworkReviewRule? @relation(fields: [reviewRuleId], references: [id], onDelete: SetNull) submissions HomeworkSubmission[] /// 作业提交记录 - creatorUser User? @relation("HomeworkCreator", fields: [creator], references: [id], onDelete: SetNull) - modifierUser User? @relation("HomeworkModifier", fields: [modifier], references: [id], onDelete: SetNull) + creatorUser User? @relation("HomeworkCreator", fields: [creator], references: [id], onDelete: SetNull) + modifierUser User? @relation("HomeworkModifier", fields: [modifier], references: [id], onDelete: SetNull) @@index([tenantId, status]) @@index([tenantId, submitStartTime, submitEndTime]) @@ -979,23 +983,23 @@ model Homework { /// 作业提交表 model HomeworkSubmission { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 租户ID - homeworkId Int @map("homework_id") /// 作业ID - studentId Int @map("student_id") /// 学生用户ID - workNo String? @map("work_no") /// 作品编号 - workName String @map("work_name") /// 作品名称 - workDescription String? @db.Text @map("work_description") /// 作品介绍 - files Json? /// 作品文件列表 - attachments Json? /// 附件列表 - submitTime DateTime @default(now()) @map("submit_time") /// 提交时间 - status String @default("pending") /// 状态:pending/reviewed/rejected - totalScore Decimal? @map("total_score") @db.Decimal(10, 2) /// 总分 - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") /// 租户ID + homeworkId Int @map("homework_id") /// 作业ID + studentId Int @map("student_id") /// 学生用户ID + workNo String? @map("work_no") /// 作品编号 + workName String @map("work_name") /// 作品名称 + workDescription String? @map("work_description") @db.Text /// 作品介绍 + files Json? /// 作品文件列表 + attachments Json? /// 附件列表 + submitTime DateTime @default(now()) @map("submit_time") /// 提交时间 + status String @default("pending") /// 状态:pending/reviewed/rejected + totalScore Decimal? @map("total_score") @db.Decimal(10, 2) /// 总分 + creator Int? /// 创建人ID + modifier Int? /// 修改人ID + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) homework Homework @relation(fields: [homeworkId], references: [id], onDelete: Cascade) @@ -1013,16 +1017,16 @@ model HomeworkSubmission { /// 作业评审规则表 model HomeworkReviewRule { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 租户ID - name String /// 规则名称 - description String? @db.Text /// 规则描述 - criteria Json /// 评分标准 [{name, maxScore, description}] - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") /// 租户ID + name String /// 规则名称 + description String? @db.Text /// 规则描述 + criteria Json /// 评分标准 [{name, maxScore, description}] + creator Int? /// 创建人ID + modifier Int? /// 修改人ID + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) homeworks Homework[] /// 关联的作业 @@ -1035,19 +1039,19 @@ model HomeworkReviewRule { /// 作业评分表 model HomeworkScore { - id Int @id @default(autoincrement()) - tenantId Int @map("tenant_id") /// 租户ID - submissionId Int @map("submission_id") /// 提交记录ID - reviewerId Int @map("reviewer_id") /// 评审人ID - dimensionScores Json @map("dimension_scores") /// 各维度评分 - totalScore Decimal @map("total_score") @db.Decimal(10, 2) /// 总分 - comments String? @db.Text /// 评语 - scoreTime DateTime @default(now()) @map("score_time") /// 评分时间 - creator Int? /// 创建人ID - modifier Int? /// 修改人ID - createTime DateTime @default(now()) @map("create_time") /// 创建时间 - modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 - validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 + id Int @id @default(autoincrement()) + tenantId Int @map("tenant_id") /// 租户ID + submissionId Int @map("submission_id") /// 提交记录ID + reviewerId Int @map("reviewer_id") /// 评审人ID + dimensionScores Json @map("dimension_scores") /// 各维度评分 + totalScore Decimal @map("total_score") @db.Decimal(10, 2) /// 总分 + comments String? @db.Text /// 评语 + scoreTime DateTime @default(now()) @map("score_time") /// 评分时间 + creator Int? /// 创建人ID + modifier Int? /// 修改人ID + createTime DateTime @default(now()) @map("create_time") /// 创建时间 + modifyTime DateTime @updatedAt @map("modify_time") /// 更新时间 + validState Int @default(1) @map("valid_state") /// 有效状态:1-有效,2-失效 tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) submission HomeworkSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade) @@ -1060,3 +1064,33 @@ model HomeworkScore { @@index([reviewerId]) @@map("t_homework_score") } + +// ============================================ +// 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 + status String @default("pending") /// 任务状态:pending | processing | completed | failed | timeout + resultUrl String? @map("result_url") /// 生成的3D模型URL + previewUrl String? @map("preview_url") /// 预览图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") +} diff --git a/backend/scripts/init-menus.ts b/backend/scripts/init-menus.ts index 5589645..d321dc8 100644 --- a/backend/scripts/init-menus.ts +++ b/backend/scripts/init-menus.ts @@ -44,8 +44,8 @@ if (!fs.existsSync(menusFilePath)) { const menus = JSON.parse(fs.readFileSync(menusFilePath, 'utf-8')); -// 超级租户可见的菜单名称 -const SUPER_TENANT_MENUS = ['工作台', '赛事活动', '赛事管理', '系统管理']; +// 超级租户可见的菜单名称(工作台只对普通租户可见) +const SUPER_TENANT_MENUS = ['赛事活动', '赛事管理', '系统管理']; // 普通租户可见的菜单名称 const NORMAL_TENANT_MENUS = ['工作台', '学校管理', '赛事活动', '作业管理', '系统管理']; diff --git a/backend/scripts/init-roles-permissions.ts b/backend/scripts/init-roles-permissions.ts index 6e2cd1f..bbb8343 100644 --- a/backend/scripts/init-roles-permissions.ts +++ b/backend/scripts/init-roles-permissions.ts @@ -29,8 +29,9 @@ const prisma = new PrismaClient(); // 基础权限(所有角色共享的权限池) const allPermissions = [ - // 工作台 - { code: 'workbench:read', resource: 'workbench', action: 'read', name: '查看工作台', description: '允许查看工作台' }, + // AI 3D建模 + { code: 'ai-3d:read', resource: 'ai-3d', action: 'read', name: '使用3D建模实验室', description: '允许使用AI 3D建模实验室' }, + { code: 'ai-3d:create', resource: 'ai-3d', action: 'create', name: '创建3D模型任务', description: '允许创建AI 3D模型生成任务' }, // 用户管理 { code: 'user:create', resource: 'user', action: 'create', name: '创建用户', description: '允许创建新用户' }, @@ -192,8 +193,6 @@ const superTenantRoles = [ name: '超级管理员', description: '系统超级管理员,管理赛事和系统配置', permissions: [ - // 工作台 - 'workbench:read', // 系统管理 'user:create', 'user:read', 'user:update', 'user:delete', 'role:create', 'role:read', 'role:update', 'role:delete', 'role:assign', @@ -217,7 +216,6 @@ const superTenantRoles = [ name: '评委', description: '赛事评委,可以评审作品', permissions: [ - 'workbench:read', 'activity:read', // 查看赛事活动 'work:read', // 查看待评审作品 'review:read', // 查看评审任务 @@ -234,7 +232,6 @@ const normalTenantRoles = [ name: '学校管理员', description: '学校管理员,管理学校信息、教师、学生等', permissions: [ - 'workbench:read', 'user:create', 'user:read', 'user:update', 'user:delete', 'role:create', 'role:read', 'role:update', 'role:delete', 'role:assign', 'permission:read', @@ -259,7 +256,8 @@ const normalTenantRoles = [ name: '教师', description: '教师角色,可以报名赛事、指导学生、管理作业', permissions: [ - 'workbench:read', + // AI 3D建模(工作台入口) + 'ai-3d:read', 'ai-3d:create', // 使用AI 3D建模实验室 // 查看基础信息 'grade:read', 'class:read', @@ -282,7 +280,8 @@ const normalTenantRoles = [ name: '学生', description: '学生角色,可以查看赛事、上传作品、提交作业', permissions: [ - 'workbench:read', + // AI 3D建模(工作台入口) + 'ai-3d:read', 'ai-3d:create', // 使用AI 3D建模实验室 // 赛事活动 'activity:read', // 查看赛事活动列表 'notice:read', // 查看赛事公告 diff --git a/backend/scripts/update-tenant-menu-permission.ts b/backend/scripts/update-tenant-menu-permission.ts deleted file mode 100644 index 06c787f..0000000 --- a/backend/scripts/update-tenant-menu-permission.ts +++ /dev/null @@ -1,78 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck -import * as dotenv from 'dotenv'; -import * as path from 'path'; - -const nodeEnv = process.env.NODE_ENV || 'development'; -const envFile = `.env.${nodeEnv}`; -const backendDir = path.resolve(__dirname, '..'); -const envPath = path.resolve(backendDir, envFile); - -dotenv.config({ path: envPath }); - -if (!process.env.DATABASE_URL) { - dotenv.config({ path: path.resolve(backendDir, '.env') }); -} - -if (!process.env.DATABASE_URL) { - console.error('DATABASE_URL not found'); - process.exit(1); -} - -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function updateTenantMenuPermission() { - try { - console.log('🚀 开始更新租户管理菜单权限...\n'); - - // 查找租户管理菜单 - const tenantMenu = await prisma.menu.findFirst({ - where: { - name: '租户管理', - path: '/system/tenants', - }, - }); - - if (!tenantMenu) { - console.log('❌ 租户管理菜单不存在'); - return; - } - - console.log(`找到租户管理菜单: ID=${tenantMenu.id}, 当前权限=${tenantMenu.permission}`); - - if (tenantMenu.permission === 'tenant:update') { - console.log('✅ 菜单权限已经是 tenant:update,无需更新'); - return; - } - - // 更新菜单权限 - await prisma.menu.update({ - where: { id: tenantMenu.id }, - data: { - permission: 'tenant:update', - }, - }); - - console.log('✅ 菜单权限已更新为 tenant:update'); - console.log('\n说明:'); - console.log(' - 普通租户只有 tenant:read 权限,可以读取租户列表(用于发布赛事选择公开范围)'); - console.log(' - 只有超级租户才有 tenant:update 权限,才能看到租户管理菜单'); - } catch (error) { - console.error('❌ 更新失败:', error); - throw error; - } finally { - await prisma.$disconnect(); - } -} - -updateTenantMenuPermission() - .then(() => { - console.log('\n🎉 脚本执行完成!'); - process.exit(0); - }) - .catch((error) => { - console.error('\n💥 脚本执行失败:', error); - process.exit(1); - }); diff --git a/backend/src/ai-3d/ai-3d.controller.ts b/backend/src/ai-3d/ai-3d.controller.ts new file mode 100644 index 0000000..46211eb --- /dev/null +++ b/backend/src/ai-3d/ai-3d.controller.ts @@ -0,0 +1,77 @@ +import { + Controller, + Get, + Post, + Delete, + Body, + Param, + Query, + UseGuards, + Request, + ParseIntPipe, +} from '@nestjs/common'; +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'; + +@Controller('ai-3d') +@UseGuards(JwtAuthGuard) +export class AI3DController { + 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); + } +} diff --git a/backend/src/ai-3d/ai-3d.module.ts b/backend/src/ai-3d/ai-3d.module.ts new file mode 100644 index 0000000..918ca3c --- /dev/null +++ b/backend/src/ai-3d/ai-3d.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { AI3DController } from './ai-3d.controller'; +import { AI3DService } from './ai-3d.service'; +import { MockAI3DProvider } from './providers/mock.provider'; +import { AI3D_PROVIDER } from './providers/ai-3d-provider.interface'; +import { PrismaModule } from '../prisma/prisma.module'; + +@Module({ + imports: [PrismaModule], + controllers: [AI3DController], + providers: [ + AI3DService, + { + provide: AI3D_PROVIDER, + useClass: MockAI3DProvider, + }, + ], + 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 new file mode 100644 index 0000000..4f3caeb --- /dev/null +++ b/backend/src/ai-3d/ai-3d.service.ts @@ -0,0 +1,297 @@ +import { + Injectable, + Inject, + NotFoundException, + BadRequestException, + Logger, +} from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.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_CONCURRENT_TASKS = 3; // 每用户最大并行任务数 +const TASK_TIMEOUT_MS = 10 * 60 * 1000; // 10分钟超时 +const MAX_RETRY_COUNT = 3; // 最大重试次数 + +@Injectable() +export class AI3DService { + private readonly logger = new Logger(AI3DService.name); + + constructor( + private prisma: PrismaService, + @Inject(AI3D_PROVIDER) private ai3dProvider: AI3DProvider, + ) {} + + /** + * 创建生成任务 + */ + async createTask( + userId: number, + tenantId: number, + dto: CreateTaskDto, + ) { + // 1. 检查用户当前进行中的任务数量 + const activeTaskCount = await this.prisma.aI3DTask.count({ + where: { + userId, + status: { in: ['pending', 'processing'] }, + }, + }); + + if (activeTaskCount >= MAX_CONCURRENT_TASKS) { + throw new BadRequestException( + `您当前有 ${activeTaskCount} 个任务正在处理中,最多同时处理 ${MAX_CONCURRENT_TASKS} 个任务,请等待完成后再提交`, + ); + } + + // 2. 创建数据库记录 + const task = await this.prisma.aI3DTask.create({ + data: { + userId, + tenantId, + inputType: dto.inputType, + inputContent: dto.inputContent, + status: 'pending', + }, + }); + + // 3. 提交到 AI 服务 + try { + const externalTaskId = await this.ai3dProvider.submitTask( + dto.inputType, + dto.inputContent, + ); + + // 4. 更新状态为处理中 + await this.prisma.aI3DTask.update({ + where: { id: task.id }, + data: { + status: 'processing', + externalTaskId, + }, + }); + + // 5. 启动轮询检查任务状态 + this.pollTaskStatus(task.id, externalTaskId, Date.now()); + + this.logger.log(`任务 ${task.id} 创建成功,外部ID: ${externalTaskId}`); + + return this.getTask(userId, task.id); + } 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; + } + } + + /** + * 获取任务列表 + */ + 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('任务不存在'); + } + + return task; + } + + /** + * 删除任务 + */ + 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 activeTaskCount = await this.prisma.aI3DTask.count({ + where: { + userId, + status: { in: ['pending', 'processing'] }, + }, + }); + + if (activeTaskCount >= MAX_CONCURRENT_TASKS) { + throw new BadRequestException( + `您当前有 ${activeTaskCount} 个任务正在处理中,请等待完成后再重试`, + ); + } + + // 重置任务状态 + await this.prisma.aI3DTask.update({ + where: { id }, + data: { + status: 'pending', + errorMessage: null, + completeTime: null, + retryCount: { increment: 1 }, + }, + }); + + // 重新提交任务 + try { + const externalTaskId = await this.ai3dProvider.submitTask( + task.inputType as 'text' | 'image', + task.inputContent, + ); + + await this.prisma.aI3DTask.update({ + where: { id }, + data: { + status: 'processing', + externalTaskId, + }, + }); + + this.pollTaskStatus(id, externalTaskId, Date.now()); + + this.logger.log(`任务 ${id} 重试成功,外部ID: ${externalTaskId}`); + + return this.getTask(userId, id); + } catch (error) { + await this.prisma.aI3DTask.update({ + where: { id }, + data: { + status: 'failed', + errorMessage: error.message || 'AI服务提交失败', + completeTime: new Date(), + }, + }); + + this.logger.error(`任务 ${id} 重试失败: ${error.message}`); + throw error; + } + } + + /** + * 轮询检查任务状态 + */ + 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') { + await this.prisma.aI3DTask.update({ + where: { id: taskId }, + data: { + status: result.status, + resultUrl: result.resultUrl, + previewUrl: result.previewUrl, + 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 new file mode 100644 index 0000000..cb0ce08 --- /dev/null +++ b/backend/src/ai-3d/dto/create-task.dto.ts @@ -0,0 +1,12 @@ +import { IsString, IsIn, IsNotEmpty, MaxLength } from 'class-validator'; + +export class CreateTaskDto { + @IsString() + @IsIn(['text', 'image'], { message: '输入类型必须是 text 或 image' }) + inputType: 'text' | 'image'; + + @IsString() + @IsNotEmpty({ message: '输入内容不能为空' }) + @MaxLength(2000, { message: '输入内容最多2000个字符' }) + inputContent: string; +} diff --git a/backend/src/ai-3d/dto/query-task.dto.ts b/backend/src/ai-3d/dto/query-task.dto.ts new file mode 100644 index 0000000..7544f33 --- /dev/null +++ b/backend/src/ai-3d/dto/query-task.dto.ts @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..2de8e7a --- /dev/null +++ b/backend/src/ai-3d/providers/ai-3d-provider.interface.ts @@ -0,0 +1,39 @@ +/** + * AI 3D 生成结果 + */ +export interface AI3DGenerateResult { + taskId: string; // 外部任务ID + status: 'pending' | 'processing' | 'completed' | 'failed'; + resultUrl?: string; // 3D模型URL + previewUrl?: string; // 预览图URL + errorMessage?: string; // 错误信息 +} + +/** + * AI 3D 服务提供者接口 + * 支持 Mock、腾讯混元、Meshy 等实现 + */ +export interface AI3DProvider { + /** + * 提交生成任务 + * @param inputType 输入类型:text | image + * @param inputContent 输入内容:文字描述或图片URL + * @returns 外部任务ID + */ + submitTask( + inputType: 'text' | 'image', + inputContent: string, + ): 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/mock.provider.ts b/backend/src/ai-3d/providers/mock.provider.ts new file mode 100644 index 0000000..e945fa7 --- /dev/null +++ b/backend/src/ai-3d/providers/mock.provider.ts @@ -0,0 +1,135 @@ +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; + 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 = [ + '/mock/models/sample-cube.glb', + '/mock/models/sample-sphere.glb', + '/mock/models/sample-model.glb', + ]; + + 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, + 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) { + // 随机选择一个示例模型 + const modelIndex = Math.floor(Math.random() * this.SAMPLE_MODELS.length); + const modelUrl = this.SAMPLE_MODELS[modelIndex]; + + task.status = 'completed'; + task.resultUrl = modelUrl; + task.previewUrl = modelUrl.replace('.glb', '-preview.png'); + + this.logger.log(`Mock: 任务 ${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/app.module.ts b/backend/src/app.module.ts index fb1d364..0a4bf9a 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -17,6 +17,7 @@ import { JudgesManagementModule } from './judges-management/judges-management.mo 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 { JwtAuthGuard } from './auth/guards/jwt-auth.guard'; import { RolesGuard } from './auth/guards/roles.guard'; import { TransformInterceptor } from './common/interceptors/transform.interceptor'; @@ -50,6 +51,7 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter'; UploadModule, HomeworkModule, OssModule, + AI3DModule, ], providers: [ { diff --git a/frontend/src/api/ai-3d.ts b/frontend/src/api/ai-3d.ts new file mode 100644 index 0000000..64f80aa --- /dev/null +++ b/frontend/src/api/ai-3d.ts @@ -0,0 +1,105 @@ +import request from "@/utils/request"; +import type { PaginationParams, PaginationResponse } 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; + errorMessage?: string; + externalTaskId?: string; + retryCount: number; + createTime: string; + completeTime?: string; +} + +/** + * 创建任务参数 + */ +export interface CreateAI3DTaskParams { + inputType: AI3DInputType; + inputContent: string; +} + +/** + * 查询任务参数 + */ +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}`); +} diff --git a/frontend/src/api/upload.ts b/frontend/src/api/upload.ts index 91482a7..85113f6 100644 --- a/frontend/src/api/upload.ts +++ b/frontend/src/api/upload.ts @@ -22,3 +22,12 @@ export const uploadApi = { return response; }, }; + +/** + * 上传单个文件 + */ +export async function uploadFile(file: File): Promise { + const formData = new FormData(); + formData.append("file", file); + return uploadApi.upload(formData); +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 573cce0..d32c91b 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -29,10 +29,7 @@ const baseRoutes: RouteRecordRaw[] = [ path: "/:tenantCode", name: "Main", component: () => import("@/layouts/BasicLayout.vue"), - redirect: (to) => { - // 默认跳转到 workbench,路由守卫会处理重定向到第一个菜单 - return { path: `/${to.params.tenantCode}/workbench` } - }, + // 不设置固定redirect,由路由守卫根据用户菜单动态跳转到第一个可见菜单 meta: {}, children: [ // 创建比赛路由(不需要在菜单中显示) @@ -165,6 +162,16 @@ const baseRoutes: RouteRecordRaw[] = [ permissions: ["activity:read"], }, }, + // 3D建模实验室路由(工作台模块下) + { + path: "workbench/3d-lab", + name: "3DModelingLab", + component: () => import("@/views/workbench/ai-3d/Index.vue"), + meta: { + title: "3D建模实验室", + requiresAuth: true, + }, + }, // 动态路由将在这里添加 ], }, @@ -281,9 +288,9 @@ function buildPathWithTenantCode(tenantCode: string, path: string): string { } // 移除开头的斜杠(如果有) const cleanPath = path.startsWith("/") ? path.slice(1) : path - // 如果路径是根路径,返回租户编码路径 + // 如果路径是根路径,返回租户编码根路径(路由守卫会处理跳转到第一个菜单) if (cleanPath === "" || cleanPath === tenantCode) { - return `/${tenantCode}/workbench` + return `/${tenantCode}` } return `/${tenantCode}/${cleanPath}` } @@ -469,8 +476,8 @@ router.beforeEach(async (to, _from, next) => { // 路由已生效,重新解析目标路由 const resolved = router.resolve(targetPath) - // 如果访问的是主路由或 workbench,且路由不存在,重定向到第一个菜单 - const isMainRoute = to.name === "Main" || to.path.endsWith("/workbench") + // 如果访问的是主路由,重定向到第一个菜单 + const isMainRoute = to.name === "Main" // 如果解析后的路由不是404,说明路由存在,重新导航 if (resolved.name !== "NotFound" && !isMainRoute) { @@ -535,9 +542,10 @@ router.beforeEach(async (to, _from, next) => { dynamicRoutesAdded ) { const resolved = router.resolve(to.fullPath) - // 如果路由不存在,且不是登录页,尝试重定向到用户第一个菜单 + // 如果访问的是 Main 路由(无具体子路径)或路由不存在,重定向到用户第一个菜单 + const isMainRouteWithoutChild = to.name === "Main" || to.matched.length === 1 && to.matched[0].name === "Main" if ( - resolved.name === "NotFound" && + (resolved.name === "NotFound" || isMainRouteWithoutChild) && to.name !== "Login" && to.name !== "LoginFallback" ) { @@ -622,9 +630,9 @@ router.beforeEach(async (to, _from, next) => { if (!dynamicRoutesAdded && authStore.menus.length > 0) { await addDynamicRoutes() } - // 重定向到带租户编码的工作台 + // 重定向到带租户编码的根路径(路由守卫会处理跳转到第一个菜单) const userTenantCode = authStore.user?.tenantCode || "default" - next({ path: `/${userTenantCode}/workbench` }) + next({ path: `/${userTenantCode}` }) return } diff --git a/frontend/src/views/workbench/ai-3d/Index.vue b/frontend/src/views/workbench/ai-3d/Index.vue new file mode 100644 index 0000000..aa12343 --- /dev/null +++ b/frontend/src/views/workbench/ai-3d/Index.vue @@ -0,0 +1,972 @@ + + + + +