952 lines
27 KiB
Markdown
952 lines
27 KiB
Markdown
|
|
# NestJS 后端项目分析报告
|
|||
|
|
|
|||
|
|
> 项目路径:`backend/`
|
|||
|
|
> 分析日期:2026-03-28
|
|||
|
|
> 项目类型:多租户竞赛/活动管理系统
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、技术栈概览
|
|||
|
|
|
|||
|
|
| 组件 | 技术选型 | 版本 |
|
|||
|
|
|------|----------|------|
|
|||
|
|
| **框架** | NestJS | 10.3.3 |
|
|||
|
|
| **语言** | TypeScript | 5.3.3 |
|
|||
|
|
| **数据库** | MySQL | 8.0+ |
|
|||
|
|
| **ORM** | Prisma | 6.19.0 |
|
|||
|
|
| **认证** | JWT + Passport | - |
|
|||
|
|
| **加密** | bcrypt | 6.0.0 |
|
|||
|
|
| **文件存储** | 腾讯云 COS | - |
|
|||
|
|
| **部署** | PM2 | - |
|
|||
|
|
| **AI 集成** | 腾讯混元 | - |
|
|||
|
|
|
|||
|
|
### 核心依赖
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"@nestjs/common": "^10.3.3",
|
|||
|
|
"@nestjs/core": "^10.3.3",
|
|||
|
|
"@nestjs/jwt": "^10.2.0",
|
|||
|
|
"@nestjs/passport": "^10.0.3",
|
|||
|
|
"@prisma/client": "^6.19.0",
|
|||
|
|
"passport-jwt": "^4.0.1",
|
|||
|
|
"bcrypt": "^6.0.0",
|
|||
|
|
"class-validator": "^0.14.1",
|
|||
|
|
"class-transformer": "^0.5.1",
|
|||
|
|
"cos-nodejs-sdk-v5": "^2.15.4"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、项目架构
|
|||
|
|
|
|||
|
|
### 目录结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
backend/
|
|||
|
|
├── src/
|
|||
|
|
│ ├── ai-3d/ # AI 3D 生成模块
|
|||
|
|
│ ├── auth/ # 认证授权模块
|
|||
|
|
│ ├── common/ # 公共模块(过滤器、拦截器)
|
|||
|
|
│ ├── config/ # 系统配置模块
|
|||
|
|
│ ├── contests/ # 竞赛管理模块(核心)
|
|||
|
|
│ │ ├── contests/ # 活动管理
|
|||
|
|
│ │ ├── attachments/ # 附件管理
|
|||
|
|
│ │ ├── judges/ # 评委管理
|
|||
|
|
│ │ ├── notices/ # 公告管理
|
|||
|
|
│ │ ├── preset-comments/ # 预设评语库
|
|||
|
|
│ │ ├── registrations/ # 报名管理
|
|||
|
|
│ │ ├── results/ # 结果管理
|
|||
|
|
│ │ ├── review-rules/ # 评审规则
|
|||
|
|
│ │ ├── reviews/ # 评审管理
|
|||
|
|
│ │ ├── teams/ # 团队管理
|
|||
|
|
│ │ └── works/ # 作品管理
|
|||
|
|
│ ├── dict/ # 数据字典模块
|
|||
|
|
│ ├── homework/ # 作业模块
|
|||
|
|
│ │ ├── homeworks/ # 作业管理
|
|||
|
|
│ │ ├── review-rules/ # 评审规则
|
|||
|
|
│ │ ├── scores/ # 评分管理
|
|||
|
|
│ │ └── submissions/ # 提交管理
|
|||
|
|
│ ├── judges-management/ # 评委管理模块
|
|||
|
|
│ ├── logs/ # 日志模块
|
|||
|
|
│ ├── menus/ # 菜单管理模块
|
|||
|
|
│ ├── oss/ # 对象存储模块
|
|||
|
|
│ ├── permissions/ # 权限管理模块
|
|||
|
|
│ ├── prisma/ # Prisma 服务
|
|||
|
|
│ ├── public/ # 公共接口模块
|
|||
|
|
│ ├── roles/ # 角色管理模块
|
|||
|
|
│ ├── school/ # 学校管理模块
|
|||
|
|
│ │ ├── schools/ # 学校管理
|
|||
|
|
│ │ ├── grades/ # 年级管理
|
|||
|
|
│ │ ├── classes/ # 班级管理
|
|||
|
|
│ │ ├── departments/ # 部门管理
|
|||
|
|
│ │ ├── teachers/ # 教师管理
|
|||
|
|
│ │ └── students/ # 学生管理
|
|||
|
|
│ ├── tenants/ # 租户管理模块
|
|||
|
|
│ ├── upload/ # 文件上传模块
|
|||
|
|
│ ├── users/ # 用户管理模块
|
|||
|
|
│ ├── app.module.ts # 根模块
|
|||
|
|
│ └── main.ts # 入口文件
|
|||
|
|
├── prisma/
|
|||
|
|
│ ├── schema.prisma # 数据库模型定义
|
|||
|
|
│ └── migrations/ # 数据库迁移文件
|
|||
|
|
├── scripts/ # 初始化脚本
|
|||
|
|
├── test/ # 测试文件
|
|||
|
|
└── package.json
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、核心功能模块详解
|
|||
|
|
|
|||
|
|
### 1. 认证授权模块 (`auth/`)
|
|||
|
|
|
|||
|
|
**文件结构:**
|
|||
|
|
```
|
|||
|
|
auth/
|
|||
|
|
├── auth.controller.ts # 认证控制器
|
|||
|
|
├── auth.service.ts # 认证服务
|
|||
|
|
├── auth.module.ts # 认证模块
|
|||
|
|
├── decorators/ # 装饰器
|
|||
|
|
│ ├── current-tenant-id.decorator.ts # 当前租户 ID 装饰器
|
|||
|
|
│ ├── public.decorator.ts # 公开接口装饰器
|
|||
|
|
│ ├── require-permission.decorator.ts # 权限装饰器
|
|||
|
|
│ └── roles.decorator.ts # 角色装饰器
|
|||
|
|
├── guards/ # 守卫
|
|||
|
|
│ ├── jwt-auth.guard.ts # JWT 认证守卫
|
|||
|
|
│ ├── roles.guard.ts # 角色守卫
|
|||
|
|
│ └── permissions.guard.ts # 权限守卫
|
|||
|
|
└── strategies/ # Passport 策略
|
|||
|
|
├── jwt.strategy.ts # JWT 策略
|
|||
|
|
└── local.strategy.ts # 本地策略
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**核心功能:**
|
|||
|
|
- 用户名密码登录
|
|||
|
|
- JWT Token 签发与验证
|
|||
|
|
- 权限验证(基于装饰器)
|
|||
|
|
- 角色验证
|
|||
|
|
- 多租户识别
|
|||
|
|
|
|||
|
|
**核心代码示例:**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// auth.service.ts - 登录逻辑
|
|||
|
|
async login(user: any, tenantId?: number) {
|
|||
|
|
// 验证租户有效
|
|||
|
|
const tenant = await this.prisma.tenant.findUnique({
|
|||
|
|
where: { id: finalTenantId },
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 签发 JWT Token
|
|||
|
|
const payload = {
|
|||
|
|
username: user.username,
|
|||
|
|
sub: user.id,
|
|||
|
|
tenantId: finalTenantId,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
token: this.jwtService.sign(payload),
|
|||
|
|
user: {
|
|||
|
|
id: user.id,
|
|||
|
|
username: user.username,
|
|||
|
|
roles: user.roles?.map((ur: any) => ur.role.code) || [],
|
|||
|
|
permissions: await this.getUserPermissions(user.id),
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. 用户管理模块 (`users/`)
|
|||
|
|
|
|||
|
|
**核心功能:**
|
|||
|
|
- 用户 CRUD 操作
|
|||
|
|
- 用户状态管理(启用/禁用)
|
|||
|
|
- 关键字搜索(用户名、昵称、邮箱、手机号)
|
|||
|
|
- 超级租户跨租户查询
|
|||
|
|
- 用户统计分析
|
|||
|
|
|
|||
|
|
**接口列表:**
|
|||
|
|
|
|||
|
|
| 接口 | 方法 | 权限 | 说明 |
|
|||
|
|
|------|------|------|------|
|
|||
|
|
| `POST /users` | POST | `user:create` | 创建用户 |
|
|||
|
|
| `GET /users` | GET | `user:view` | 用户列表(分页) |
|
|||
|
|
| `GET /users/stats` | GET | `super_admin` | 用户统计 |
|
|||
|
|
| `GET /users/:id` | GET | `user:view` | 用户详情 |
|
|||
|
|
| `PUT /users/:id` | PUT | `user:update` | 更新用户 |
|
|||
|
|
| `PUT /users/:id/status` | PUT | `user:manage` | 切换状态 |
|
|||
|
|
| `DELETE /users/:id` | DELETE | `user:delete` | 删除用户 |
|
|||
|
|
|
|||
|
|
**特色功能:**
|
|||
|
|
```typescript
|
|||
|
|
// 用户类型统计(仅超管)
|
|||
|
|
async getStats() {
|
|||
|
|
const superTenantIds = ...; // 平台租户 ID
|
|||
|
|
const orgTenantIds = ...; // 机构租户 ID
|
|||
|
|
const judgeTenantIds = ...; // 评委租户 ID
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
total, // 总用户数
|
|||
|
|
platform, // 平台用户数
|
|||
|
|
org, // 机构用户数
|
|||
|
|
judge, // 评委用户数
|
|||
|
|
public: publicCount, // 公共用户数
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. 竞赛/活动管理模块 (`contests/`) ⭐核心业务
|
|||
|
|
|
|||
|
|
**模块结构:**
|
|||
|
|
```
|
|||
|
|
contests/
|
|||
|
|
├── contests/ # 活动主体管理
|
|||
|
|
├── registrations/ # 报名管理
|
|||
|
|
├── teams/ # 团队管理
|
|||
|
|
├── works/ # 作品管理
|
|||
|
|
├── reviews/ # 评审管理
|
|||
|
|
├── judges/ # 评委管理
|
|||
|
|
├── results/ # 结果管理
|
|||
|
|
├── review-rules/ # 评审规则
|
|||
|
|
├── preset-comments/ # 预设评语库
|
|||
|
|
├── notices/ # 公告管理
|
|||
|
|
└── attachments/ # 附件管理
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.1 活动管理 (`contests/contests/`)
|
|||
|
|
|
|||
|
|
**活动生命周期:**
|
|||
|
|
```
|
|||
|
|
┌─────────────┐
|
|||
|
|
│ 未发布 │
|
|||
|
|
│ unpublished │
|
|||
|
|
└──────┬──────┘
|
|||
|
|
│ 发布
|
|||
|
|
▼
|
|||
|
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|||
|
|
│ 已发布 │ ──► │ 报名中 │ ──► │ 作品提交中 │ ──► │ 评审中 │
|
|||
|
|
│ published │ │ registering │ │ submitting │ │ reviewing │
|
|||
|
|
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
|
|||
|
|
│
|
|||
|
|
┌─────────────┐ │
|
|||
|
|
│ 已完结 │ ◄────────────────┘
|
|||
|
|
│ finished │
|
|||
|
|
└─────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**核心服务方法:**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 判断活动当前所处阶段
|
|||
|
|
getContestStage(contest: any): string {
|
|||
|
|
const now = new Date();
|
|||
|
|
const regStart = new Date(contest.registerStartTime);
|
|||
|
|
const regEnd = new Date(contest.registerEndTime);
|
|||
|
|
const subStart = new Date(contest.submitStartTime);
|
|||
|
|
const subEnd = new Date(contest.submitEndTime);
|
|||
|
|
const revStart = new Date(contest.reviewStartTime);
|
|||
|
|
const revEnd = new Date(contest.reviewEndTime);
|
|||
|
|
|
|||
|
|
if (now >= regStart && now <= regEnd) return 'registering';
|
|||
|
|
if (now >= subStart && now <= subEnd) return 'submitting';
|
|||
|
|
if (now >= revStart && now <= revEnd) return 'reviewing';
|
|||
|
|
if (revEnd && now > revEnd) return 'finished';
|
|||
|
|
return 'published';
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.2 报名管理 (`registrations/`)
|
|||
|
|
|
|||
|
|
**功能:**
|
|||
|
|
- 个人/团队报名
|
|||
|
|
- 报名审核(需要审核的活动)
|
|||
|
|
- 报名状态管理
|
|||
|
|
|
|||
|
|
#### 3.3 作品管理 (`works/`)
|
|||
|
|
|
|||
|
|
**功能:**
|
|||
|
|
- 作品提交(支持多次提交配置)
|
|||
|
|
- 作品版本管理(`isLatest` 标识最新版本)
|
|||
|
|
- 作品状态流转
|
|||
|
|
|
|||
|
|
#### 3.4 评审管理 (`reviews/`)
|
|||
|
|
|
|||
|
|
**功能:**
|
|||
|
|
- 评委分配(手动/批量/自动)
|
|||
|
|
- 作品评分
|
|||
|
|
- 分数统计
|
|||
|
|
|
|||
|
|
**评分 DTO:**
|
|||
|
|
```typescript
|
|||
|
|
// create-score.dto.ts
|
|||
|
|
export class CreateScoreDto {
|
|||
|
|
@IsInt()
|
|||
|
|
workId: number;
|
|||
|
|
|
|||
|
|
@IsInt()
|
|||
|
|
judgeId: number;
|
|||
|
|
|
|||
|
|
@IsNumber()
|
|||
|
|
score: number;
|
|||
|
|
|
|||
|
|
@IsString()
|
|||
|
|
@IsOptional()
|
|||
|
|
comment: string; // 评语
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.5 结果管理 (`results/`)
|
|||
|
|
|
|||
|
|
**功能:**
|
|||
|
|
- 手动设置奖项
|
|||
|
|
- 批量设置奖项
|
|||
|
|
- 自动设置奖项(按分数排名)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 自动设置奖项 DTO
|
|||
|
|
export class AutoSetAwardsDto {
|
|||
|
|
@IsInt()
|
|||
|
|
contestId: number;
|
|||
|
|
|
|||
|
|
@IsInt()
|
|||
|
|
firstPrizeCount: number; // 一等奖数量
|
|||
|
|
@IsInt()
|
|||
|
|
secondPrizeCount: number; // 二等奖数量
|
|||
|
|
@IsInt()
|
|||
|
|
thirdPrizeCount: number; // 三等奖数量
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. 学校管理模块 (`school/`)
|
|||
|
|
|
|||
|
|
**模块结构:**
|
|||
|
|
```
|
|||
|
|
school/
|
|||
|
|
├── schools/ # 学校信息
|
|||
|
|
├── grades/ # 年级管理
|
|||
|
|
├── classes/ # 班级管理
|
|||
|
|
├── departments/ # 部门管理
|
|||
|
|
├── teachers/ # 教师管理
|
|||
|
|
└── students/ # 学生管理
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**数据库关系:**
|
|||
|
|
```
|
|||
|
|
Tenant (1) ── (1) School
|
|||
|
|
Tenant (1) ── (N) Grade
|
|||
|
|
Tenant (1) ── (N) Class
|
|||
|
|
Tenant (1) ── (N) Teacher
|
|||
|
|
Tenant (1) ── (N) Student
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5. AI 3D 生成模块 (`ai-3d/`) ⭐特色功能
|
|||
|
|
|
|||
|
|
**文件结构:**
|
|||
|
|
```
|
|||
|
|
ai-3d/
|
|||
|
|
├── ai-3d.controller.ts
|
|||
|
|
├── ai-3d.service.ts
|
|||
|
|
├── ai-3d.module.ts
|
|||
|
|
├── providers/
|
|||
|
|
│ ├── ai-3d-provider.interface.ts # AI 提供商接口
|
|||
|
|
│ ├── hunyuan.provider.ts # 腾讯混元实现
|
|||
|
|
│ └── mock.provider.ts # Mock 实现
|
|||
|
|
└── utils/
|
|||
|
|
├── tencent-cloud-sign.ts # 腾讯云签名
|
|||
|
|
└── zip-handler.ts # ZIP 处理
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**功能:**
|
|||
|
|
- AI 生成 3D 模型(腾讯混元 API 集成)
|
|||
|
|
- 任务创建与状态查询
|
|||
|
|
- 模型文件压缩与下载
|
|||
|
|
|
|||
|
|
**DTO:**
|
|||
|
|
```typescript
|
|||
|
|
// create-task.dto.ts
|
|||
|
|
export class CreateTaskDto {
|
|||
|
|
@IsString()
|
|||
|
|
prompt: string; // 生成提示词
|
|||
|
|
|
|||
|
|
@IsString()
|
|||
|
|
@IsOptional()
|
|||
|
|
style?: string; // 风格(写实/卡通/低多边形)
|
|||
|
|
|
|||
|
|
@IsString()
|
|||
|
|
@IsOptional()
|
|||
|
|
negativePrompt?: string; // 负面提示词
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 6. 作业模块 (`homework/`)
|
|||
|
|
|
|||
|
|
**功能:**
|
|||
|
|
- 作业发布与提交
|
|||
|
|
- 作业评审
|
|||
|
|
- 评分管理
|
|||
|
|
|
|||
|
|
**与竞赛模块的区别:**
|
|||
|
|
- 作业更轻量化,适合日常教学场景
|
|||
|
|
- 评审流程简化
|
|||
|
|
- 支持班级维度布置
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 7. 系统管理模块
|
|||
|
|
|
|||
|
|
#### 7.1 角色权限 (`roles/`, `permissions/`)
|
|||
|
|
|
|||
|
|
**RBAC 模型:**
|
|||
|
|
```
|
|||
|
|
User ──(N)── UserRole ──(1)── Role
|
|||
|
|
Role ──(N)── RolePermission ──(1)── Permission
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**权限装饰器:**
|
|||
|
|
```typescript
|
|||
|
|
@RequirePermission('user:create')
|
|||
|
|
async createUser(...) {
|
|||
|
|
// 只有拥有 user:create 权限的用户才能访问
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 7.2 菜单管理 (`menus/`)
|
|||
|
|
|
|||
|
|
**功能:**
|
|||
|
|
- 动态菜单配置
|
|||
|
|
- 菜单权限绑定
|
|||
|
|
- 租户菜单定制(`TenantMenu`)
|
|||
|
|
|
|||
|
|
#### 7.3 数据字典 (`dict/`)
|
|||
|
|
|
|||
|
|
**功能:**
|
|||
|
|
- 常用数据字典配置
|
|||
|
|
- 支持租户自定义字典
|
|||
|
|
|
|||
|
|
#### 7.4 系统配置 (`config/`)
|
|||
|
|
|
|||
|
|
**功能:**
|
|||
|
|
- 系统参数配置
|
|||
|
|
- 租户级配置隔离
|
|||
|
|
- 配置验证接口
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 8. 文件存储模块 (`upload/`, `oss/`)
|
|||
|
|
|
|||
|
|
**腾讯云 COS 集成:**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// oss.service.ts
|
|||
|
|
async uploadFile(file: Express.Multer.File, dir: string) {
|
|||
|
|
const client = new COS({
|
|||
|
|
SecretId: this.secretId,
|
|||
|
|
SecretKey: this.secretKey,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const key = `${dir}/${Date.now()}-${file.originalname}`;
|
|||
|
|
|
|||
|
|
await client.putObject({
|
|||
|
|
Bucket: this.bucket,
|
|||
|
|
Region: this.region,
|
|||
|
|
Key: key,
|
|||
|
|
Body: file.buffer,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return `https://${this.bucket}.cos.${this.region}.myqcloud.com/${key}`;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、数据库设计
|
|||
|
|
|
|||
|
|
### 核心数据表
|
|||
|
|
|
|||
|
|
#### 1. 租户表 (`Tenant`)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
model Tenant {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
name String // 租户名称
|
|||
|
|
code String @unique // 租户编码
|
|||
|
|
domain String? @unique // 租户域名
|
|||
|
|
description String?
|
|||
|
|
isSuper Int @default(0) // 是否超级租户
|
|||
|
|
tenantType String @default("other")
|
|||
|
|
validState Int @default(1)
|
|||
|
|
createTime DateTime @default(now())
|
|||
|
|
modifyTime DateTime @updatedAt
|
|||
|
|
|
|||
|
|
users User[]
|
|||
|
|
roles Role[]
|
|||
|
|
contests Contest[]
|
|||
|
|
school School?
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2. 用户表 (`User`)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
model User {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
tenantId Int
|
|||
|
|
username String
|
|||
|
|
password String
|
|||
|
|
nickname String?
|
|||
|
|
email String?
|
|||
|
|
phone String?
|
|||
|
|
avatar String?
|
|||
|
|
status String @default("enabled")
|
|||
|
|
validState Int @default(1)
|
|||
|
|
createTime DateTime @default(now())
|
|||
|
|
|
|||
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|||
|
|
roles UserRole[]
|
|||
|
|
children User[] // 家长 - 孩子关联
|
|||
|
|
parent User? @relation("ParentChild", fields: [parentId], references: [id])
|
|||
|
|
contestRegistrations ContestRegistration[]
|
|||
|
|
contestWorks ContestWork[]
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3. 活动表 (`Contest`)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
model Contest {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
contestName String
|
|||
|
|
contestType String // 竞赛类型
|
|||
|
|
contestState String @default("unpublished")
|
|||
|
|
status String @default("ongoing")
|
|||
|
|
visibility String @default("designated")
|
|||
|
|
contestTenants String? // JSON: 可见租户 ID 列表
|
|||
|
|
|
|||
|
|
registerStartTime DateTime
|
|||
|
|
registerEndTime DateTime
|
|||
|
|
submitStartTime DateTime
|
|||
|
|
submitEndTime DateTime
|
|||
|
|
reviewStartTime DateTime
|
|||
|
|
reviewEndTime DateTime
|
|||
|
|
resultPublishTime DateTime?
|
|||
|
|
|
|||
|
|
teamMinMembers Int @default(1)
|
|||
|
|
teamMaxMembers Int @default(1)
|
|||
|
|
requireAudit Boolean @default(true)
|
|||
|
|
|
|||
|
|
attachments ContestAttachment[]
|
|||
|
|
registrations ContestRegistration[]
|
|||
|
|
teams ContestTeam[]
|
|||
|
|
works ContestWork[]
|
|||
|
|
judges ContestJudge[]
|
|||
|
|
reviewRule ContestReviewRule?
|
|||
|
|
notices ContestNotice[]
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4. 作品表 (`ContestWork`)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
model ContestWork {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
contestId Int
|
|||
|
|
userId Int
|
|||
|
|
teamId Int?
|
|||
|
|
title String
|
|||
|
|
description String?
|
|||
|
|
coverUrl String?
|
|||
|
|
workUrl String?
|
|||
|
|
workType String // 作品类型
|
|||
|
|
version Int @default(1)
|
|||
|
|
isLatest Boolean @default(true)
|
|||
|
|
status String @default("submitted")
|
|||
|
|
|
|||
|
|
contest Contest @relation(fields: [contestId], references: [id])
|
|||
|
|
author User @relation(fields: [userId], references: [id])
|
|||
|
|
team ContestTeam?
|
|||
|
|
scores ContestWorkScore[]
|
|||
|
|
attachments ContestWorkAttachment[]
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5. 评分表 (`ContestWorkScore`)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
model ContestWorkScore {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
workId Int
|
|||
|
|
judgeId Int
|
|||
|
|
contestId Int
|
|||
|
|
score Decimal
|
|||
|
|
comment String?
|
|||
|
|
|
|||
|
|
work ContestWork @relation(fields: [workId], references: [id])
|
|||
|
|
judge User @relation(fields: [judgeId], references: [id])
|
|||
|
|
contest Contest @relation(fields: [contestId], references: [id])
|
|||
|
|
|
|||
|
|
@@unique([workId, judgeId]) // 同一评委对同一作品只能评分一次
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 完整 ER 图(核心部分)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────┐
|
|||
|
|
│ Tenant │
|
|||
|
|
│ (租户表) │
|
|||
|
|
└──────┬──────┘
|
|||
|
|
│
|
|||
|
|
├───► ┌─────────────┐ ┌─────────────┐
|
|||
|
|
│ │ User │──────►│ Role │
|
|||
|
|
│ │ (用户表) │ │ (角色表) │
|
|||
|
|
│ └──────┬──────┘ └─────────────┘
|
|||
|
|
│ │
|
|||
|
|
│ ├───► ┌─────────────┐ ┌─────────────┐
|
|||
|
|
│ │ │ Contest │──────►│ContestWork │
|
|||
|
|
│ │ │ (活动表) │ │ (作品表) │
|
|||
|
|
│ │ └──────┬──────┘ └──────┬──────┘
|
|||
|
|
│ │ │ │
|
|||
|
|
│ │ ├───► ┌─────────────┐ │
|
|||
|
|
│ │ │ │ContestTeam │◄┘
|
|||
|
|
│ │ │ │ (团队表) │
|
|||
|
|
│ │ │ └─────────────┘
|
|||
|
|
│ │ │
|
|||
|
|
│ │ ├───► ┌─────────────┐
|
|||
|
|
│ │ │ │ContestJudge │
|
|||
|
|
│ │ │ │ (评委表) │
|
|||
|
|
│ │ │ └─────────────┘
|
|||
|
|
│ │ │
|
|||
|
|
│ │ └───► ┌─────────────┐
|
|||
|
|
│ │ │ContestWork │
|
|||
|
|
│ │ │ Score │
|
|||
|
|
│ │ │ (评分表) │
|
|||
|
|
│ │ └─────────────┘
|
|||
|
|
│ │
|
|||
|
|
│ └───► ┌─────────────┐
|
|||
|
|
│ │ School │
|
|||
|
|
│ │ (学校表) │
|
|||
|
|
│ └──────┬──────┘
|
|||
|
|
│ │
|
|||
|
|
│ ├───► ┌─────────────┐
|
|||
|
|
│ │ │ Teacher │
|
|||
|
|
│ │ │ (教师表) │
|
|||
|
|
│ │ └─────────────┘
|
|||
|
|
│ │
|
|||
|
|
│ └───► ┌─────────────┐
|
|||
|
|
│ │ Student │
|
|||
|
|
│ │ (学生表) │
|
|||
|
|
│ └─────────────┘
|
|||
|
|
│
|
|||
|
|
└───► ┌─────────────┐
|
|||
|
|
│ Permission │
|
|||
|
|
│ (权限表) │
|
|||
|
|
└─────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、核心业务流程
|
|||
|
|
|
|||
|
|
### 1. 活动创建流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. 管理员创建活动
|
|||
|
|
↓
|
|||
|
|
2. 配置报名参数(时间、人数限制、需审核等)
|
|||
|
|
↓
|
|||
|
|
3. 配置评审规则(评分维度、权重)
|
|||
|
|
↓
|
|||
|
|
4. 设置可见范围(公开/指定租户)
|
|||
|
|
↓
|
|||
|
|
5. 发布活动
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 用户参赛流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. 用户查看活动列表
|
|||
|
|
↓
|
|||
|
|
2. 个人报名 / 创建团队
|
|||
|
|
↓
|
|||
|
|
3. 等待审核(如需)
|
|||
|
|
↓
|
|||
|
|
4. 提交作品(可多次提交,保留最新版本)
|
|||
|
|
↓
|
|||
|
|
5. 等待评审
|
|||
|
|
↓
|
|||
|
|
6. 查看结果
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 作品评审流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. 系统/管理员分配评委到作品
|
|||
|
|
↓
|
|||
|
|
2. 评委登录系统查看分配的作品
|
|||
|
|
↓
|
|||
|
|
3. 评委评分(填写分数 + 评语)
|
|||
|
|
↓
|
|||
|
|
4. 系统统计总分/平均分
|
|||
|
|
↓
|
|||
|
|
5. 自动生成获奖名单
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. AI 3D 生成流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. 用户提交生成请求(提示词)
|
|||
|
|
↓
|
|||
|
|
2. 系统调用腾讯混元 API
|
|||
|
|
↓
|
|||
|
|
3. 轮询任务状态
|
|||
|
|
↓
|
|||
|
|
4. 下载生成的 3D 模型
|
|||
|
|
↓
|
|||
|
|
5. 可选:压缩为 ZIP 格式
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、多租户架构
|
|||
|
|
|
|||
|
|
### 租户类型
|
|||
|
|
|
|||
|
|
| 类型 | 说明 | 权限 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| **超级租户** | 平台管理员 | 访问所有数据 |
|
|||
|
|
| **机构租户** | 学校/图书馆等 | 访问本机构数据 |
|
|||
|
|
| **评委租户** | 评委专用 | 访问评审相关数据 |
|
|||
|
|
| **公共租户** | 公共资源 | 访问公开数据 |
|
|||
|
|
|
|||
|
|
### 数据隔离实现
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 所有业务查询必须包含租户 ID
|
|||
|
|
const contests = await prisma.contest.findMany({
|
|||
|
|
where: {
|
|||
|
|
tenantId: currentTenantId,
|
|||
|
|
validState: 1,
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 超级租户跨租户查询
|
|||
|
|
const allContests = await prisma.contest.findMany({
|
|||
|
|
where: isSuperTenant
|
|||
|
|
? { validState: 1 } // 超管查询全部
|
|||
|
|
: { tenantId: currentTenantId, validState: 1 }, // 普通租户仅查询本租户
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、中间件与拦截器
|
|||
|
|
|
|||
|
|
### 1. JWT 认证守卫 (`JwtAuthGuard`)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
@Injectable()
|
|||
|
|
export class JwtAuthGuard implements CanActivate {
|
|||
|
|
canActivate(context: ExecutionContext): boolean {
|
|||
|
|
const request = context.switchToHttp().getRequest();
|
|||
|
|
const token = this.extractTokenFromHeader(request);
|
|||
|
|
|
|||
|
|
if (!token) throw new UnauthorizedException();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const payload = this.jwtService.verify(token);
|
|||
|
|
request['user'] = payload;
|
|||
|
|
} catch {
|
|||
|
|
throw new UnauthorizedException();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 权限守卫 (`PermissionsGuard`)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
@Injectable()
|
|||
|
|
export class PermissionsGuard implements CanActivate {
|
|||
|
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|||
|
|
const requiredPermission = this.reflector.get<string[]>(
|
|||
|
|
REQUIRE_PERMISSION_KEY,
|
|||
|
|
context.getHandler()
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (!requiredPermission) return true;
|
|||
|
|
|
|||
|
|
const { user } = context.switchToHttp().getRequest();
|
|||
|
|
const permissions = await this.authService.getUserPermissions(user.sub);
|
|||
|
|
|
|||
|
|
return permissions.some(p => requiredPermission.includes(p));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 响应转换拦截器 (`TransformInterceptor`)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
@Injectable()
|
|||
|
|
export class TransformInterceptor<T>
|
|||
|
|
implements NestInterceptor<T, Response<T>> {
|
|||
|
|
intercept(
|
|||
|
|
context: ExecutionContext,
|
|||
|
|
next: CallHandler,
|
|||
|
|
): Observable<Response<T>> {
|
|||
|
|
return next.handle().pipe(
|
|||
|
|
map(data => ({
|
|||
|
|
code: 200,
|
|||
|
|
message: '操作成功',
|
|||
|
|
data: data.data || data,
|
|||
|
|
total: data.total,
|
|||
|
|
})),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、初始化脚本
|
|||
|
|
|
|||
|
|
项目提供了丰富的初始化脚本:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 初始化管理员账户
|
|||
|
|
pnpm init:admin
|
|||
|
|
|
|||
|
|
# 初始化菜单
|
|||
|
|
pnpm init:menus
|
|||
|
|
|
|||
|
|
# 初始化超级租户
|
|||
|
|
pnpm init:super-tenant
|
|||
|
|
|
|||
|
|
# 初始化租户管理员
|
|||
|
|
pnpm init:tenant-admin
|
|||
|
|
|
|||
|
|
# 初始化角色权限
|
|||
|
|
pnpm init:roles:all
|
|||
|
|
|
|||
|
|
# 数据库迁移
|
|||
|
|
pnpm prisma:migrate
|
|||
|
|
|
|||
|
|
# Prisma Studio(数据库可视化)
|
|||
|
|
pnpm prisma:studio
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 九、API 接口概览
|
|||
|
|
|
|||
|
|
### 认证相关
|
|||
|
|
| 接口 | 方法 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `POST /auth/login` | POST | 用户登录 |
|
|||
|
|
| `POST /auth/logout` | POST | 用户登出 |
|
|||
|
|
| `GET /auth/info` | GET | 获取当前用户信息 |
|
|||
|
|
|
|||
|
|
### 用户管理
|
|||
|
|
| 接口 | 方法 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `GET /users` | GET | 用户列表 |
|
|||
|
|
| `POST /users` | POST | 创建用户 |
|
|||
|
|
| `GET /users/:id` | GET | 用户详情 |
|
|||
|
|
| `PUT /users/:id` | PUT | 更新用户 |
|
|||
|
|
| `PUT /users/:id/status` | PUT | 切换状态 |
|
|||
|
|
| `DELETE /users/:id` | DELETE | 删除用户 |
|
|||
|
|
|
|||
|
|
### 活动管理
|
|||
|
|
| 接口 | 方法 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `GET /contests` | GET | 活动列表 |
|
|||
|
|
| `POST /contests` | POST | 创建活动 |
|
|||
|
|
| `GET /contests/:id` | GET | 活动详情 |
|
|||
|
|
| `PUT /contests/:id` | PUT | 更新活动 |
|
|||
|
|
| `POST /contests/:id/publish` | POST | 发布/取消发布 |
|
|||
|
|
| `DELETE /contests/:id` | DELETE | 删除活动 |
|
|||
|
|
|
|||
|
|
### 报名管理
|
|||
|
|
| 接口 | 方法 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `GET /contests/:contestId/registrations` | GET | 报名列表 |
|
|||
|
|
| `POST /contests/:contestId/registrations` | POST | 创建报名 |
|
|||
|
|
| `PUT /registrations/:id/review` | PUT | 审核报名 |
|
|||
|
|
|
|||
|
|
### 作品管理
|
|||
|
|
| 接口 | 方法 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `GET /contests/:contestId/works` | GET | 作品列表 |
|
|||
|
|
| `POST /contests/:contestId/works` | POST | 提交作品 |
|
|||
|
|
| `PUT /works/:id` | PUT | 更新作品 |
|
|||
|
|
|
|||
|
|
### 评审管理
|
|||
|
|
| 接口 | 方法 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `POST /contests/:contestId/reviews/assign` | POST | 分配评审 |
|
|||
|
|
| `POST /reviews/scores` | POST | 提交评分 |
|
|||
|
|
| `GET /contests/:contestId/reviews/stats` | GET | 评审统计 |
|
|||
|
|
|
|||
|
|
### 结果管理
|
|||
|
|
| 接口 | 方法 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `GET /contests/:contestId/results` | GET | 结果列表 |
|
|||
|
|
| `POST /contests/:contestId/results/set-awards` | POST | 设置奖项 |
|
|||
|
|
| `POST /contests/:contestId/results/auto-set-awards` | POST | 自动设置奖项 |
|
|||
|
|
|
|||
|
|
### AI 3D 生成
|
|||
|
|
| 接口 | 方法 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `POST /ai-3d/tasks` | POST | 创建生成任务 |
|
|||
|
|
| `GET /ai-3d/tasks/:id` | GET | 查询任务状态 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十、总结
|
|||
|
|
|
|||
|
|
### 项目特点
|
|||
|
|
|
|||
|
|
1. **完整的多租户 SaaS 架构** - 支持平台、机构、评委等多角色
|
|||
|
|
2. **丰富的竞赛管理功能** - 从活动创建到评审颁奖全流程覆盖
|
|||
|
|
3. **灵活的 RBAC 权限系统** - 支持细粒度权限控制
|
|||
|
|
4. **AI 能力集成** - 腾讯混元 3D 模型生成
|
|||
|
|
5. **完善的初始化脚本** - 快速部署和配置
|
|||
|
|
|
|||
|
|
### 适用场景
|
|||
|
|
|
|||
|
|
- 📚 图书馆绘本创作比赛
|
|||
|
|
- 🏫 学校各类竞赛活动
|
|||
|
|
- 🎨 艺术创作比赛
|
|||
|
|
- 📖 作文/阅读比赛
|
|||
|
|
- 🤖 科技创新大赛
|
|||
|
|
|
|||
|
|
### 技术亮点
|
|||
|
|
|
|||
|
|
- NestJS 模块化架构
|
|||
|
|
- Prisma ORM 类型安全
|
|||
|
|
- JWT 无状态认证
|
|||
|
|
- 事务处理保证数据一致性
|
|||
|
|
- 软删除保留数据追溯
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
> 文档生成时间:2026-03-28
|
|||
|
|
> 分析人:AI Assistant
|