一、超管端设计优化 - 文档管理SOP体系建立,docs目录重组 - 统一用户管理:跨租户全局视角,合并用户管理+公众用户 - 活动监管全模块重构:全部活动(统计卡片+阶段筛选+SuperDetail详情页)、报名数据/作品数据/评审进度(两层合一扁平列表)、成果发布(去Tab+统计+隐藏写操作) - 菜单精简:移除评委管理/评审规则/通知管理 - Bug修复:租户编辑丢失隐藏菜单、pageSize限制、主色统一 二、UGC绘本创作社区P0 - 数据库:10张新表(user_works/user_work_pages/work_tags等) - 子女账号独立化:Child升级为独立User,家长切换+独立登录 - 用户作品库:CRUD+发布审核,8个API - AI创作流程:提交→生成→保存到作品库,4个API - 作品广场:首页改造为推荐流,标签+搜索+排序 - 内容审核(超管端):作品审核+作品管理+标签管理 - 活动联动:WorkSelector作品选择器 - 布局改造:底部5Tab(发现/创作/活动/作品库/我的) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
272 lines
6.9 KiB
Markdown
272 lines
6.9 KiB
Markdown
# 比赛评委存储设计说明
|
||
|
||
## 📋 设计决策
|
||
|
||
### 问题:是否需要专门的评委表?
|
||
|
||
**结论:需要创建 `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. 保证数据一致性和查询效率
|
||
|