重大改进: - 练习进度模型优化:从"每题一条记录"改为"每用户每类型一条记录",提升性能和数据管理 - 完全基于后端数据库恢复答题进度,移除 localStorage 依赖,提高可靠性 - AI评分结果持久化:在答题记录中保存AI评分、评语和建议,支持历史查看 后端改进: - 新增 /api/practice/progress 接口获取练习进度(支持按类型筛选) - 新增 /api/practice/progress 接口清除练习进度(支持按类型清除) - PracticeProgress 模型重构:添加 current_question_id 和 user_answer_records 字段 - UserAnswerRecord 模型增强:添加 ai_score、ai_feedback、ai_suggestion 字段 - 提交答案时自动保存AI评分到数据库 前端优化: - 答题进度完全从后端加载,移除 localStorage 备份逻辑 - 修复判断题答案格式转换问题(boolean -> string) - 优化随机模式:首次答题时随机选择起始题目 - 改进答题历史显示:显示答题序号和历史答案标识 - 已答题目切换时保持答案和结果显示状态 - 清除进度时支持按类型清除(而非清空所有) 技术优化: - 统一索引策略:从 idx_user_question 改为 idx_user_type - JSON 字段类型从 jsonp 改为 jsonb(PostgreSQL 性能优化) - 增加详细的日志记录,便于调试和追踪 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
192 lines
5.3 KiB
Markdown
192 lines
5.3 KiB
Markdown
# 修复 practice_progress 表索引问题
|
||
|
||
## 问题描述
|
||
|
||
在测试中发现,除了单选题(multiple-choice)能正常插入进度数据外,其他类型的题目无法插入数据到 `practice_progress` 表。
|
||
|
||
## 问题原因
|
||
|
||
### 错误的索引定义
|
||
|
||
**之前的模型定义**(错误):
|
||
```go
|
||
type PracticeProgress struct {
|
||
ID int64 `gorm:"primarykey" json:"id"`
|
||
CurrentQuestionID int64 `gorm:"not null" json:"current_question_id"`
|
||
UserID int64 `gorm:"not null;uniqueIndex:idx_user_question" json:"user_id"` // ❌ 只在 user_id 上建索引
|
||
Type string `gorm:"type:varchar(255);not null" json:"type"`
|
||
UserAnswerRecords datatypes.JSON `gorm:"type:jsonp" json:"answers"`
|
||
}
|
||
```
|
||
|
||
**问题**:
|
||
- 唯一索引 `idx_user_question` 只在 `user_id` 字段上
|
||
- 同一用户只能有一条进度记录
|
||
- 当用户答第二种题型时,因为 `user_id` 重复,插入失败
|
||
- 日志中应该会看到类似错误:`duplicate key value violates unique constraint "idx_user_question"`
|
||
|
||
### 正确的索引定义
|
||
|
||
**修复后的模型定义**(正确):
|
||
```go
|
||
type PracticeProgress struct {
|
||
ID int64 `gorm:"primarykey" json:"id"`
|
||
CurrentQuestionID int64 `gorm:"not null" json:"current_question_id"`
|
||
UserID int64 `gorm:"not null;uniqueIndex:idx_user_type" json:"user_id"` // ✅ 联合索引
|
||
Type string `gorm:"type:varchar(255);not null;uniqueIndex:idx_user_type" json:"type"` // ✅ 联合索引
|
||
UserAnswerRecords datatypes.JSON `gorm:"type:jsonb" json:"answers"`
|
||
}
|
||
```
|
||
|
||
**改进**:
|
||
- 唯一索引 `idx_user_type` 是 `(user_id, type)` 的联合索引
|
||
- 同一用户可以有多条进度记录(每种题型一条)
|
||
- 例如:用户1 可以有 `(1, "multiple-choice")` 和 `(1, "true-false")` 两条记录
|
||
|
||
## 解决方案
|
||
|
||
### 方案1:使用 GORM 自动迁移(推荐)
|
||
|
||
1. **停止当前服务**
|
||
|
||
2. **删除旧表并重建**(谨慎:会丢失所有进度数据)
|
||
|
||
连接到 PostgreSQL 数据库:
|
||
```bash
|
||
psql -U your_username -d your_database
|
||
```
|
||
|
||
执行:
|
||
```sql
|
||
DROP TABLE IF EXISTS practice_progress;
|
||
```
|
||
|
||
3. **重启服务,GORM 会自动创建正确的表结构**
|
||
```bash
|
||
.\bin\server.exe
|
||
```
|
||
|
||
### 方案2:手动修复索引(保留数据)
|
||
|
||
1. **连接到 PostgreSQL 数据库**:
|
||
```bash
|
||
psql -U your_username -d your_database
|
||
```
|
||
|
||
2. **执行 SQL 脚本**:
|
||
```bash
|
||
\i scripts/fix_practice_progress_index.sql
|
||
```
|
||
|
||
或手动执行:
|
||
```sql
|
||
-- 删除旧索引
|
||
DROP INDEX IF EXISTS idx_user_question;
|
||
|
||
-- 创建新索引
|
||
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_type ON practice_progress(user_id, type);
|
||
```
|
||
|
||
3. **验证索引**:
|
||
```sql
|
||
SELECT indexname, indexdef
|
||
FROM pg_indexes
|
||
WHERE tablename = 'practice_progress';
|
||
```
|
||
|
||
应该看到:
|
||
```
|
||
indexname | indexdef
|
||
--------------|--------------------------------------------------
|
||
idx_user_type | CREATE UNIQUE INDEX idx_user_type ON practice_progress USING btree (user_id, type)
|
||
```
|
||
|
||
4. **检查现有数据是否有冲突**:
|
||
```sql
|
||
SELECT user_id, type, COUNT(*)
|
||
FROM practice_progress
|
||
GROUP BY user_id, type
|
||
HAVING COUNT(*) > 1;
|
||
```
|
||
|
||
如果有重复数据,需要手动清理:
|
||
```sql
|
||
-- 保留每组的最新记录,删除旧记录
|
||
DELETE FROM practice_progress a
|
||
WHERE id NOT IN (
|
||
SELECT MAX(id)
|
||
FROM practice_progress b
|
||
WHERE a.user_id = b.user_id AND a.type = b.type
|
||
);
|
||
```
|
||
|
||
## 验证修复
|
||
|
||
### 1. 检查表结构
|
||
```sql
|
||
\d practice_progress
|
||
```
|
||
|
||
应该看到:
|
||
```
|
||
Indexes:
|
||
"practice_progress_pkey" PRIMARY KEY, btree (id)
|
||
"idx_user_type" UNIQUE, btree (user_id, type)
|
||
```
|
||
|
||
### 2. 测试插入不同题型
|
||
|
||
**测试步骤**:
|
||
1. 登录系统
|
||
2. 选择"单选题",答几道题
|
||
3. 切换到"多选题",答几道题
|
||
4. 切换到"判断题",答几道题
|
||
|
||
**检查数据库**:
|
||
```sql
|
||
SELECT id, user_id, type, current_question_id
|
||
FROM practice_progress
|
||
WHERE user_id = 1; -- 替换为你的用户ID
|
||
```
|
||
|
||
应该看到多条记录:
|
||
```
|
||
id | user_id | type | current_question_id
|
||
---|---------|---------------------|--------------------
|
||
1 | 1 | multiple-choice | 157
|
||
2 | 1 | multiple-selection | 45
|
||
3 | 1 | true-false | 10
|
||
```
|
||
|
||
### 3. 检查后端日志
|
||
|
||
如果之前有错误,应该不再看到类似日志:
|
||
```
|
||
保存练习进度失败: duplicate key value violates unique constraint "idx_user_question"
|
||
```
|
||
|
||
## 其他注意事项
|
||
|
||
1. **JSONB vs JSONP**:
|
||
- 修改了 `UserAnswerRecords` 的类型从 `jsonp` 改为 `jsonb`
|
||
- `jsonb` 是正确的 PostgreSQL JSON 类型
|
||
- 性能更好,支持索引
|
||
|
||
2. **数据备份**:
|
||
- 在修改表结构前,建议备份数据:
|
||
```bash
|
||
pg_dump -U your_username -d your_database -t practice_progress > backup_practice_progress.sql
|
||
```
|
||
|
||
3. **回滚方案**:
|
||
- 如果需要回滚,可以恢复备份:
|
||
```bash
|
||
psql -U your_username -d your_database < backup_practice_progress.sql
|
||
```
|
||
|
||
## 相关文件
|
||
|
||
- 模型定义:`internal/models/practice_progress.go`
|
||
- 写入逻辑:`internal/handlers/practice_handler.go:356-387`
|
||
- SQL 修复脚本:`scripts/fix_practice_progress_index.sql`
|