数据库设计 - 道馆系统 (Release 2)#
本文档描述 Release 2 引入的道馆挑战系统的数据库设计。
ER 图#
erDiagram
Chapter ||--o{ Gym : contains
Gym ||--o{ GymPrerequisite : has
Gym ||--o{ Challenge : contains
Mission ||--o{ GymPrerequisite : required_by
Challenge ||--o{ ChallengeField : defines
Challenge ||--o{ Submission : has
User ||--o{ Submission : submits
User ||--o{ GradeResult : graded
User ||--o{ UserSkill : has
Submission ||--o{ SubmissionFile : contains
Submission ||--|| GradeResult : graded_with
GradeResult ||--o{ SkillRating : has
Gym {
bigint id PK
bigint chapter_id FK
string name
text description
string category
string difficulty
int order_index
}
GymPrerequisite {
bigint id PK
bigint gym_id FK
bigint required_mission_id FK
boolean require_reward_claimed
}
Challenge {
bigint id PK
bigint gym_id FK
string title
text description
string type
int order_index
int exp_reward
}
ChallengeField {
bigint id PK
bigint challenge_id FK
string field_key
string field_label
string field_type
boolean required
int order_index
}
Submission {
bigint id PK
bigint challenge_id FK
bigint user_id FK
string status
timestamp submitted_at
timestamp last_modified_at
}
SubmissionFile {
bigint id PK
bigint submission_id FK
string field_key
string file_name
string file_url
bigint file_size
timestamp uploaded_at
}
GradeResult {
bigint id PK
bigint submission_id FK
bigint graded_by_user_id FK
int score
text feedback
boolean passed
timestamp graded_at
timestamp last_modified_at
}
SkillRating {
bigint id PK
bigint grade_result_id FK
string category
string level
text comment
}
UserSkill {
bigint id PK
bigint user_id FK
string category
string level
timestamp updated_at
}表结构#
gyms 表#
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT | 道馆 ID |
| chapter_id | BIGINT | FK, NOT NULL | 所属章节 ID |
| name | VARCHAR(200) | NOT NULL | 道馆名称 |
| description | TEXT | 道馆描述 | |
| category | VARCHAR(20) | NOT NULL | 类别 (WHITE/BLACK) |
| difficulty | VARCHAR(20) | NOT NULL | 难度 (BEGINNER/INTERMEDIATE/ADVANCED/EXPERT) |
| order_index | INT | NOT NULL, DEFAULT 0 | 排序索引 |
外键:
- FOREIGN KEY (chapter_id) REFERENCES chapters(id) ON DELETE CASCADE
说明:
category: 白段道馆 (WHITE) 适合初学者,黑段道馆 (BLACK) 适合进阶学员difficulty: 难度等级,前端显示为星级 (★ 到 ★★★★)
gym_prerequisites 表#
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT | 前置条件 ID |
| gym_id | BIGINT | FK, NOT NULL | 道馆 ID |
| required_mission_id | BIGINT | FK, NOT NULL | 必须完成的任务 ID |
| require_reward_claimed | BOOLEAN | NOT NULL, DEFAULT true | 是否要求领取奖励 |
外键:
- FOREIGN KEY (gym_id) REFERENCES gyms(id) ON DELETE CASCADE
- FOREIGN KEY (required_mission_id) REFERENCES missions(id) ON DELETE CASCADE
说明:
- 道馆解锁逻辑:必须完成所有前置任务
require_reward_claimed = true: 不仅要完成任务,还要领取经验值require_reward_claimed = false: 只需观看完成即可
challenges 表#
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT | 挑战 ID |
| gym_id | BIGINT | FK, NOT NULL | 所属道馆 ID |
| title | VARCHAR(200) | NOT NULL | 挑战标题 |
| description | TEXT | 挑战描述/要求 | |
| type | VARCHAR(30) | NOT NULL | 类型 (INSTANT_CHALLENGE/PRACTICAL_CHALLENGE) |
| order_index | INT | NOT NULL, DEFAULT 0 | 排序索引 |
| exp_reward | INT | NOT NULL, DEFAULT 0 | 经验值奖励 |
外键:
- FOREIGN KEY (gym_id) REFERENCES gyms(id) ON DELETE CASCADE
说明:
INSTANT_CHALLENGE: 速战速决,预估 3 天,可重复提交PRACTICAL_CHALLENGE: 实战演练,预估 14 天,只能提交一次
challenge_fields 表#
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT | 字段 ID |
| challenge_id | BIGINT | FK, NOT NULL | 挑战 ID |
| field_key | VARCHAR(50) | NOT NULL | 字段键 (如 “ooa_uml”, “code_files”) |
| field_label | VARCHAR(100) | NOT NULL | 字段显示名 |
| field_type | VARCHAR(20) | NOT NULL | 字段类型 (IMAGE/ZIP) |
| required | BOOLEAN | NOT NULL, DEFAULT true | 是否必填 |
| order_index | INT | NOT NULL, DEFAULT 0 | 排序索引 |
外键:
- FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE
说明:
- 定义每个挑战需要提交的文件类型
IMAGE: 图片文件 (.png, .jpg, .jpeg),最大 5MBZIP: 压缩文件 (.zip),最大 10MB
submissions 表#
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT | 提交 ID |
| challenge_id | BIGINT | FK, NOT NULL | 挑战 ID |
| user_id | BIGINT | FK, NOT NULL | 用户 ID |
| status | VARCHAR(20) | NOT NULL, DEFAULT ‘DRAFT’ | 状态 |
| submitted_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 提交时间 |
| last_modified_at | TIMESTAMP | 最后修改时间 |
索引:
- UNIQUE INDEX (user_id, challenge_id)
外键:
- FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
状态值:
DRAFT: 草稿SUBMITTED: 已提交,等待批改GRADING: 批改中PASSED: 通过FAILED: 未通过GRADED: 已批改(包含通过和未通过)
submission_files 表#
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT | 文件 ID |
| submission_id | BIGINT | FK, NOT NULL | 提交 ID |
| field_key | VARCHAR(50) | NOT NULL | 字段键 (对应 challenge_fields) |
| file_name | VARCHAR(255) | NOT NULL | 文件名 |
| file_url | VARCHAR(500) | NOT NULL | 文件路径/URL |
| file_size | BIGINT | NOT NULL | 文件大小 (字节) |
| uploaded_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 上传时间 |
外键:
- FOREIGN KEY (submission_id) REFERENCES submissions(id) ON DELETE CASCADE
说明:
- 文件存储在本地文件系统:
uploads/submissions/{userId}/{challengeId}/{timestamp}_{uuid}_{filename} - 未来可迁移到 MinIO/S3
grade_results 表#
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT | 批改结果 ID |
| submission_id | BIGINT | FK, NOT NULL, UNIQUE | 提交 ID (一对一) |
| graded_by_user_id | BIGINT | FK, NOT NULL | 批改人 ID |
| score | INT | NOT NULL | 分数 (0-100) |
| feedback | TEXT | 批改反馈 | |
| passed | BOOLEAN | NOT NULL, DEFAULT false | 是否通过 |
| graded_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 批改时间 |
| last_modified_at | TIMESTAMP | 最后修改时间 |
外键:
- FOREIGN KEY (submission_id) REFERENCES submissions(id) ON DELETE CASCADE
- FOREIGN KEY (graded_by_user_id) REFERENCES users(id)
说明:
- 分数 ≥ 60 视为通过,自动授予经验值奖励
- 通过后自动升级用户等级
skill_ratings 表#
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT | 评级 ID |
| grade_result_id | BIGINT | FK, NOT NULL | 批改结果 ID |
| category | VARCHAR(50) | NOT NULL | 技能类别 |
| level | VARCHAR(10) | NOT NULL | 技能等级 |
| comment | TEXT | 评语 |
外键:
- FOREIGN KEY (grade_result_id) REFERENCES grade_results(id) ON DELETE CASCADE
技能类别 (SkillCategory):
DESIGN_PATTERN_FORM- 熟悉设计模式的 FormSTRUCTURE_BEHAVIOR_SEPARATION- 区分结构与行为DEVELOPMENT_PROFICIENCY- 游刃有余的开发能力REQUIREMENT_STRUCTURING- 需求结构化分析ABSTRACTION_ABILITY- 抽象/萃取能力WELL_DEFINED_CONTEXT- 建立 Well-Defined Context
技能等级 (SkillLevel) - 34 个等级: F-, F, F+, E-, E, E+, D-, D, D+, C-, C, C+, B-, B, B+, A-, A, A+, AA-, AA, AA+, AAA-, AAA, AAA+, S-, S, S+, SS-, SS, SS+, SSS-, SSS, SSS+, ACE
user_skills 表#
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT | 用户技能 ID |
| user_id | BIGINT | FK, NOT NULL | 用户 ID |
| category | VARCHAR(50) | NOT NULL | 技能类别 |
| level | VARCHAR(10) | NOT NULL | 当前等级 |
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 更新时间 |
索引:
- UNIQUE INDEX (user_id, category)
外键:
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
说明:
- 记录用户在每个技能类别的当前等级
- 每次批改后自动更新对应技能等级
- 每个用户每个类别只有一条记录
枚举类型#
GymCategory (道馆类别)#
public enum GymCategory {
WHITE, // 白段道馆 (入门级)
BLACK // 黑段道馆 (进阶级)
}GymDifficulty (道馆难度)#
public enum GymDifficulty {
BEGINNER, // ★
INTERMEDIATE, // ★★
ADVANCED, // ★★★
EXPERT // ★★★★
}ChallengeType (挑战类型)#
public enum ChallengeType {
INSTANT_CHALLENGE, // 速战速决 (3 天)
PRACTICAL_CHALLENGE // 实战演练 (14 天)
}FieldType (字段类型)#
public enum FieldType {
IMAGE, // 图片 (.png, .jpg, .jpeg, 最大 5MB)
ZIP // 压缩文件 (.zip, 最大 10MB)
}SubmissionStatus (提交状态)#
public enum SubmissionStatus {
DRAFT, // 草稿
SUBMITTED, // 已提交
GRADING, // 批改中
PASSED, // 通过
FAILED, // 未通过
GRADED // 已批改
}业务逻辑说明#
道馆解锁逻辑#
- 系统检查
gym_prerequisites表中该道馆的所有前置条件 - 对于每个前置任务:
- 检查
mission_progress表中用户是否完成该任务 (progress >= 100) - 如果
require_reward_claimed = true,检查reward_claimed是否为 true
- 检查
- 只有所有前置条件都满足时,道馆才解锁
挑战提交流程#
- 学生上传所需文件(根据
challenge_fields定义) - 系统验证文件类型和大小
- 创建
Submission记录,状态为SUBMITTED - 保存文件到
submission_files表 - 等待教师批改
批改与评级流程#
- 教师下载提交文件进行审阅
- 提交批改结果:
- 分数 (0-100)
- 文字反馈
- 6 个技能类别的评级
- 创建
GradeResult记录 - 创建 6 条
SkillRating记录 - 如果分数 ≥ 60:
- 用户获得
challenge.exp_reward经验值 - 自动调用
User.updateLevel()升级 Submission.status更新为GRADED
- 用户获得
- 更新
user_skills表:- 每个技能类别的等级更新为最新评级
文件存储策略#
当前实现 (Release 2):
- 本地文件系统存储
- 路径格式:
uploads/submissions/{userId}/{challengeId}/{timestamp}_{uuid}_{filename} - Docker 卷挂载确保数据持久化
未来扩展:
- 可迁移到 MinIO/S3 对象存储
- 支持 CDN 加速访问
数据库优化#
索引策略#
唯一索引:
- (user_id, challenge_id) - 防止重复提交
- (user_id, category) - 用户技能唯一性
- submission_id (grade_results) - 一对一关系
外键索引: 自动创建
查询优化索引:
- gyms(chapter_id, order_index) - 按章节查询道馆
- challenges(gym_id, order_index) - 按道馆查询挑战
常见查询示例#
-- 检查道馆是否解锁
SELECT gp.id, gp.require_reward_claimed, mp.progress, mp.reward_claimed
FROM gym_prerequisites gp
LEFT JOIN mission_progress mp ON mp.mission_id = gp.required_mission_id AND mp.user_id = ?
WHERE gp.gym_id = ?;
-- 查询用户的提交历史
SELECT s.*, c.title AS challenge_title, gr.score, gr.passed
FROM submissions s
JOIN challenges c ON c.id = s.challenge_id
LEFT JOIN grade_results gr ON gr.submission_id = s.id
WHERE s.user_id = ?
ORDER BY s.submitted_at DESC;
-- 查询用户当前技能等级
SELECT category, level, updated_at
FROM user_skills
WHERE user_id = ?
ORDER BY category;
-- 查询待批改的提交
SELECT s.*, u.name AS student_name, c.title AS challenge_title
FROM submissions s
JOIN users u ON u.id = s.user_id
JOIN challenges c ON c.id = s.challenge_id
WHERE s.status IN ('SUBMITTED', 'GRADING')
ORDER BY s.submitted_at ASC;数据完整性#
级联删除规则#
- 删除 Chapter → 级联删除 Gym → 级联删除 Challenge → 级联删除 Submission
- 删除 Submission → 级联删除 SubmissionFile 和 GradeResult
- 删除 GradeResult → 级联删除 SkillRating
- 删除 User → 级联删除 Submission 和 UserSkill
唯一性约束#
- (user_id, challenge_id): 每个用户每个挑战只能有一次提交
- (user_id, category): 每个用户每个技能类别只有一条记录
- submission_id (grade_results): 每个提交只能有一个批改结果