feat(education): save practice answers idempotently

This commit is contained in:
2026-07-27 21:49:02 +08:00
parent a43a58513a
commit 046bb4efee
21 changed files with 1923 additions and 2 deletions

View File

@@ -14,6 +14,7 @@
- 题库目录端点 (见下方 Catalog API) — 学生端已认证
- 题目浏览与筛选端点 (见下方 Questions API) — 学生端已认证
- 练习配置预览端点 (见下方 Practice API) — 学生端已认证
- 答案保存端点 (见下方 Answer API) — 幂等保存,安全重试
- 题目安全过滤(答案/解析绝不暴露到前端)
- 独立的功能开关配置 + Scalar 数据源配置
- 错误码常量(通用 + 租户 + Catalog/Scalar + 题目/练习)
@@ -406,3 +407,56 @@ Browser -> Controller(/education/questions/*) -> QuestionCatalogService -> Quest
| 1_005_003_002 | 无效的练习配置 |
| 1_005_003_003 | 符合条件的题目数量不足 |
| 1_005_003_004 | 题库数据源返回不安全内容 |
### 用户 APP - 答案保存Answer
需要学生登录态。`userId`/`tenantId` 由安全上下文派生。
答案保存具有幂等性:同一 `idempotencyKey` + 相同载荷返回首次结果,相同 key + 不同载荷返回冲突。
服务端乐观锁防止旧版本/旧序号覆盖更新答案。
```
PUT /app-api/education/practice-session/answer
```
**请求体:**
```json
{"sessionId":1001,"questionSequence":3,"selectedAnswer":"A","idempotencyKey":"uuid","clientSequence":5,"expectedSessionVersion":1}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| sessionId | Long | 是 | 练习会话 ID |
| questionSequence | Integer | 是 | 题目序号1-based |
| selectedAnswer | String | 否 | 学生选择的答案null 表示清除 |
| idempotencyKey | String | 是 | 客户端幂等键UUID |
| clientSequence | Integer | 是 | 客户端命令序号(单调递增) |
| expectedSessionVersion | Integer | 是 | 客户端期望的会话版本号 |
**成功响应:** `{"sessionId":1001,"questionSequence":3,"selectedAnswer":"A","serverVersion":2,"acceptedSequence":5}`
**前端保存状态契约**(客户端根据 API 响应派生,后端不提供状态枚举):
| 状态 | 条件 | 说明 |
|------|------|------|
| SAVING | 请求发送中 | 显示保存中指示器 |
| SAVED | code=0 | 更新本地版本号和序号 |
| RETRYING | 网络超时/5xx | 相同 idempotencyKey 安全重试 |
| FAILED | 1\_005\_003\_014/015/016 | 刷新页面获取最新状态后重试 |
刷新页面通过 `GET /practice-session/current` 恢复服务端最后确认的答案。
### 答案保存错误码
| 错误码 | 说明 |
|--------|------|
| 1\_005\_003\_006 | 练习会话不存在 |
| 1\_005\_003\_007 | 无权访问该练习会话 |
| 1\_005\_003\_008 | 练习会话已过期 |
| 1\_005\_003\_009 | 练习会话已提交 |
| 1\_005\_003\_010 | 练习会话已取消 |
| 1\_005\_003\_014 | 幂等键相同但请求内容不一致 |
| 1\_005\_003\_015 | 会话版本已更新,请刷新后重试 |
| 1\_005\_003\_016 | 客户端命令序号已过期 |
| 1\_005\_003\_017 | 无效的选项 |
| 1\_005\_003\_019 | 题目不属于当前会话 |