2026-03-27 22:20:25 +08:00
|
|
|
|
# 比赛评委存储设计说明
|
|
|
|
|
|
|
|
|
|
|
|
## 📋 设计决策
|
|
|
|
|
|
|
|
|
|
|
|
### 问题:是否需要专门的评委表?
|
|
|
|
|
|
|
|
|
|
|
|
**结论:需要创建 `ContestJudge` 关联表,但不需要单独的评委信息表。**
|
|
|
|
|
|
|
|
|
|
|
|
## 🎯 设计分析
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 为什么需要 `ContestJudge` 关联表?
|
|
|
|
|
|
|
|
|
|
|
|
#### 1.1 业务需求
|
|
|
|
|
|
|
|
|
|
|
|
1. **比赛与评委的多对多关系**
|
|
|
|
|
|
- 一个比赛可以有多个评委
|
|
|
|
|
|
- 一个评委可以评审多个比赛
|
|
|
|
|
|
- 需要管理这种多对多关系
|
|
|
|
|
|
|
|
|
|
|
|
2. **评委管理功能**
|
|
|
|
|
|
- 查询某个比赛的所有评委列表
|
|
|
|
|
|
- 批量添加/删除比赛的评委
|
|
|
|
|
|
- 管理评委在特定比赛中的特殊信息
|
|
|
|
|
|
|
|
|
|
|
|
3. **评委特殊属性**
|
|
|
|
|
|
- 评审专业领域(specialty):如"创意设计"、"技术实现"等
|
|
|
|
|
|
- 评审权重(weight):用于加权平均计算最终得分
|
|
|
|
|
|
- 评委说明(description):在该比赛中的特殊说明
|
|
|
|
|
|
|
|
|
|
|
|
#### 1.2 当前设计的不足
|
|
|
|
|
|
|
|
|
|
|
|
**之前的设计**:
|
|
|
|
|
|
- 评委信息只存储在 `ContestWorkJudgeAssignment` 表中
|
|
|
|
|
|
- 无法直接查询某个比赛有哪些评委
|
|
|
|
|
|
- 无法批量管理比赛的评委列表
|
|
|
|
|
|
- 无法存储评委在特定比赛中的特殊信息
|
|
|
|
|
|
|
|
|
|
|
|
**问题场景**:
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// ❌ 无法直接查询比赛的评委列表
|
|
|
|
|
|
// 需要从作品分配表中去重查询,效率低且不准确
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 有了 ContestJudge 表后
|
|
|
|
|
|
const judges = await prisma.contestJudge.findMany({
|
|
|
|
|
|
where: { contestId: contestId }
|
|
|
|
|
|
});
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 为什么不需要单独的评委信息表?
|
|
|
|
|
|
|
|
|
|
|
|
#### 2.1 评委就是用户
|
|
|
|
|
|
|
|
|
|
|
|
- 评委本身就是系统中的 `User`
|
|
|
|
|
|
- 基本信息(姓名、账号等)存储在 `User` 表
|
|
|
|
|
|
- 如果是教师,详细信息存储在 `Teacher` 表
|
|
|
|
|
|
- 如果是外部专家,可以创建普通 `User` 账号
|
|
|
|
|
|
|
|
|
|
|
|
#### 2.2 统一用户体系
|
|
|
|
|
|
|
|
|
|
|
|
- 系统采用统一的用户体系
|
|
|
|
|
|
- 通过角色和权限区分用户身份
|
|
|
|
|
|
- 评委通过角色(如 `judge`)和权限(如 `review:score`)控制
|
|
|
|
|
|
|
|
|
|
|
|
## 📊 数据模型设计
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 表结构
|
|
|
|
|
|
|
|
|
|
|
|
```prisma
|
|
|
|
|
|
model ContestJudge {
|
|
|
|
|
|
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 /// 评委在该比赛中的说明
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
|
contest Contest @relation(fields: [contestId], references: [id])
|
|
|
|
|
|
judge User @relation(fields: [judgeId], references: [id])
|
|
|
|
|
|
|
|
|
|
|
|
@@unique([contestId, judgeId])
|
|
|
|
|
|
@@index([contestId])
|
|
|
|
|
|
@@index([judgeId])
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 数据关系
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
Contest (比赛)
|
|
|
|
|
|
↓ (1:N)
|
|
|
|
|
|
ContestJudge (比赛评委关联)
|
|
|
|
|
|
↓ (N:1)
|
|
|
|
|
|
User (用户/评委)
|
|
|
|
|
|
↓ (1:1, 可选)
|
|
|
|
|
|
Teacher (教师信息,如果是教师评委)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 与其他表的关系
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
ContestJudge (比赛评委)
|
|
|
|
|
|
↓
|
|
|
|
|
|
ContestWorkJudgeAssignment (作品分配)
|
|
|
|
|
|
↓
|
|
|
|
|
|
ContestWorkScore (作品评分)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**关系说明**:
|
|
|
|
|
|
- `ContestJudge`:定义哪些评委可以评审某个比赛
|
|
|
|
|
|
- `ContestWorkJudgeAssignment`:将具体作品分配给评委
|
|
|
|
|
|
- `ContestWorkScore`:记录评委的评分结果
|
|
|
|
|
|
|
|
|
|
|
|
## 🔄 业务流程
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 添加评委到比赛
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 1. 创建比赛评委关联
|
|
|
|
|
|
const contestJudge = await prisma.contestJudge.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
contestId: contestId,
|
|
|
|
|
|
judgeId: userId,
|
|
|
|
|
|
specialty: "创意设计",
|
|
|
|
|
|
weight: 1.2, // 权重1.2倍
|
|
|
|
|
|
description: "专业设计领域评委"
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 查询比赛的评委列表
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 查询某个比赛的所有评委
|
|
|
|
|
|
const judges = await prisma.contestJudge.findMany({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId: contestId,
|
|
|
|
|
|
validState: 1
|
|
|
|
|
|
},
|
|
|
|
|
|
include: {
|
|
|
|
|
|
judge: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
teacher: true // 如果是教师,获取教师信息
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 分配作品给评委
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 分配作品时,验证评委是否属于该比赛
|
|
|
|
|
|
const contestJudge = await prisma.contestJudge.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
contestId: contestId,
|
|
|
|
|
|
judgeId: judgeId,
|
|
|
|
|
|
validState: 1
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!contestJudge) {
|
|
|
|
|
|
throw new Error('该评委不属于此比赛');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建作品分配记录
|
|
|
|
|
|
const assignment = await prisma.contestWorkJudgeAssignment.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
contestId: contestId,
|
|
|
|
|
|
workId: workId,
|
|
|
|
|
|
judgeId: judgeId
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 计算加权平均分
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 获取所有评委的评分和权重
|
|
|
|
|
|
const scores = await prisma.contestWorkScore.findMany({
|
|
|
|
|
|
where: { workId: workId },
|
|
|
|
|
|
include: {
|
|
|
|
|
|
judge: {
|
|
|
|
|
|
include: {
|
|
|
|
|
|
contestJudges: {
|
|
|
|
|
|
where: { contestId: contestId }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 计算加权平均分
|
|
|
|
|
|
let totalWeightedScore = 0;
|
|
|
|
|
|
let totalWeight = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (const score of scores) {
|
|
|
|
|
|
const contestJudge = score.judge.contestJudges[0];
|
|
|
|
|
|
const weight = contestJudge?.weight || 1.0;
|
|
|
|
|
|
totalWeightedScore += score.totalScore * weight;
|
|
|
|
|
|
totalWeight += weight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const finalScore = totalWeightedScore / totalWeight;
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## ✅ 设计优势
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 清晰的业务逻辑
|
|
|
|
|
|
|
|
|
|
|
|
- **比赛评委管理**:通过 `ContestJudge` 表统一管理
|
|
|
|
|
|
- **作品分配**:通过 `ContestWorkJudgeAssignment` 表管理
|
|
|
|
|
|
- **评分记录**:通过 `ContestWorkScore` 表记录
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 灵活的扩展性
|
|
|
|
|
|
|
|
|
|
|
|
- 支持评委专业领域分类
|
|
|
|
|
|
- 支持评审权重设置
|
|
|
|
|
|
- 支持评委说明信息
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 高效的查询
|
|
|
|
|
|
|
|
|
|
|
|
- 直接查询比赛的评委列表
|
|
|
|
|
|
- 支持评委维度的统计分析
|
|
|
|
|
|
- 支持权重计算
|
|
|
|
|
|
|
|
|
|
|
|
### 4. 数据一致性
|
|
|
|
|
|
|
|
|
|
|
|
- 通过外键约束保证数据完整性
|
|
|
|
|
|
- 删除比赛时,级联删除评委关联
|
|
|
|
|
|
- 删除用户时,级联删除评委关联
|
|
|
|
|
|
|
|
|
|
|
|
## 📝 使用建议
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 评委添加流程
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
1. 确保用户已创建(User 表)
|
|
|
|
|
|
2. 为用户分配评委角色和权限
|
|
|
|
|
|
3. 创建 ContestJudge 记录,关联比赛和用户
|
|
|
|
|
|
4. 可选:设置专业领域、权重等信息
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 评委验证
|
|
|
|
|
|
|
|
|
|
|
|
在分配作品给评委时,应该验证:
|
|
|
|
|
|
- 该评委是否属于该比赛(查询 `ContestJudge` 表)
|
|
|
|
|
|
- 该评委是否有效(`validState = 1`)
|
|
|
|
|
|
|
|
|
|
|
|
### 3. 权限控制
|
|
|
|
|
|
|
|
|
|
|
|
- 只有比赛管理员可以添加/删除评委
|
|
|
|
|
|
- 评委只能查看和评分分配给自己的作品
|
|
|
|
|
|
- 通过 RBAC 权限系统控制访问
|
|
|
|
|
|
|
|
|
|
|
|
## 🔍 总结
|
|
|
|
|
|
|
|
|
|
|
|
**评委存储方案**:
|
|
|
|
|
|
- ✅ **需要** `ContestJudge` 关联表:管理比赛与评委的多对多关系
|
|
|
|
|
|
- ❌ **不需要** 单独的评委信息表:评委信息通过 `User` 和 `Teacher` 表存储
|
|
|
|
|
|
|
|
|
|
|
|
**核心设计原则**:
|
|
|
|
|
|
1. 评委就是用户,统一用户体系
|
|
|
|
|
|
2. 通过关联表管理比赛与评委的关系
|
|
|
|
|
|
3. 支持评委在特定比赛中的特殊属性
|
|
|
|
|
|
4. 保证数据一致性和查询效率
|
|
|
|
|
|
|