|
|
@@ -95,9 +95,10 @@ Response: { aiMessage: string }
|
|
|
2. 查该 session `round=1 && role='ai'` 消息,若存在 → 直接返回其 content
|
|
|
3. 否则按原 `create_session` 步骤 2-3 调 LLM + 落 `DialogueMessage` + commit
|
|
|
|
|
|
-**幂等性实现重点:**
|
|
|
-- 后端加 DB 层兜底:在 `dialogue_messages` 表上建条件唯一索引 `UNIQUE(session_id, round, role) WHERE role='ai' AND round=1`(PostgreSQL 条件索引语法 `CREATE UNIQUE INDEX ... WHERE ...`)。只约束 round=1 的 AI 消息以限定作用域,避免干扰后续轮次的常规 INSERT。并发 INSERT 时第二个会被 DB 拒掉,捕获 `IntegrityError` 后重查已有消息返回即可。
|
|
|
-- 前端配合做按钮 disabled 锁(§4.6),防止用户双击派生两个并发请求。前端锁 + 后端约束双层保险。
|
|
|
+**幂等性实现重点(MySQL 环境):**
|
|
|
+- 后端在同一事务内对 `dialogue_session` 行加行锁:`SELECT * FROM dialogue_session WHERE uuid = :uuid FOR UPDATE`(SQLAlchemy `.with_for_update()`)。并发请求 serialize 到同一 session,后到者会阻塞等待,拿到锁后查 round=1 AI 消息已存在 → 直接返回。
|
|
|
+- 不新增 schema / 索引;MySQL 不支持 PostgreSQL 的条件唯一索引,而全局 `UNIQUE(session_id, round, role)` 对历史数据风险太大,放弃 DB 约束路线。
|
|
|
+- 前端配合做按钮 disabled 锁(§4.6),防止用户双击派生两个并发请求。前端锁 + 后端行锁双层保险。
|
|
|
|
|
|
### 3.3 其它接口
|
|
|
|
|
|
@@ -362,17 +363,16 @@ onUnmounted(() => {
|
|
|
|---|---|
|
|
|
| UI | "开始对话"按钮 `sessionCreating` 期间 disabled;error-card "重新生成"按钮在 `greetingInflight` 期间 disabled |
|
|
|
| 前端逻辑 | `generateGreeting` / `retryGreeting` 入口处检 `greetingInflight` ref,in-flight 直接 return |
|
|
|
-| 后端 | `dialogue_messages` 加条件唯一索引 `UNIQUE(session_id, round, role) WHERE role='ai' AND round=1`(PostgreSQL `CREATE UNIQUE INDEX ... WHERE ...`);并发 INSERT 由 DB 保证唯一,第二个捕 `IntegrityError` → 重查已有消息返回 |
|
|
|
+| 后端 | 在 `dialogue_session` 行上加行锁 `SELECT ... FOR UPDATE`(SQLAlchemy `.with_for_update()`),同一 session 的并发 greeting 请求 serialize;后到者拿锁后查已有 round=1 AI 消息 → 直接返回。MySQL 原生支持,无需 schema 变更 |
|
|
|
|
|
|
---
|
|
|
|
|
|
## 5. 实施顺序
|
|
|
|
|
|
-1. **后端**
|
|
|
- 1. `migrations/`:新增 migration,在 `dialogue_messages` 上建条件唯一索引 `UNIQUE(session_id, round, role) WHERE role='ai' AND round=1`
|
|
|
- 2. `dialogue_service.py`:`create_session` 拆成 `create_session_only` + `generate_greeting`(含幂等查现有消息 + 捕获 `IntegrityError` 重查兜底)
|
|
|
- 3. `api/dialogue.py`:`/session` 返回去掉 `aiMessage`;新增 `POST /session/{session_id}/greeting` 路由
|
|
|
- 4. 单测:session 只建不生成、greeting 幂等(重复调返回同一内容)、greeting 对非 active session 返 409、greeting 对不存在 session 返 404、并发 greeting 只产一条 AI 消息
|
|
|
+1. **后端** (MySQL,表名 `dialogue_session` / `dialogue_message`)
|
|
|
+ 1. `dialogue_service.py`:`create_session` 拆成 `create_session_only`(仅建 session,立即 commit) + `generate_greeting`(`with_for_update()` 行锁 → 查 round=1 AI → 有则返回 / 无则调 LLM 落消息 commit)
|
|
|
+ 2. `api/dialogue.py`:`/session` 响应去掉 `aiMessage`;新增 `POST /session/{session_id}/greeting` 路由
|
|
|
+ 3. 单测:session 只建不生成、greeting 幂等(重复调返回同一内容且 DB 只一条)、greeting 对非 active session 返 409、greeting 对不存在 session 返 404
|
|
|
2. **前端**
|
|
|
1. `llmService.ts`:新增 `DialogueApiError` + `createDialogueApi` 工厂;`DialogueAPI` 接口扩展;Real/Mock 都实现 `generateGreeting(signal)`;`createSession` 返回类型更新
|
|
|
2. `useDialogueEngine.ts`:拆 `initSession` → `attachSession` + `generateGreeting`(含 `greetingInflight` 锁 + `greetingAbortController`) + `retryGreeting`;`onUnmounted` 追加 abort
|
|
|
@@ -394,7 +394,7 @@ onUnmounted(() => {
|
|
|
- [ ] `POST /session` 响应体不含 `aiMessage`
|
|
|
- [ ] `POST /session/{id}/greeting` 对同一 session 重复调用返回完全相同的 `aiMessage` 且 DB 中仅一条 round=1 AI 消息
|
|
|
- [ ] `POST /session/{id}/greeting` 对非 active session 返 409,对不存在 session 返 404
|
|
|
-- [ ] 并发两次 greeting 请求(模拟双击)DB 仅产生一条 round=1 AI 消息,其中一个请求由 `IntegrityError` 路径重查后返回相同内容
|
|
|
+- [ ] 并发两次 greeting 请求(模拟双击)DB 仅产生一条 round=1 AI 消息;两个请求返回相同 `aiMessage`,第二个通过行锁等待后走"已存在直接返回"路径
|
|
|
- [ ] Ready 页点击"开始对话"期间按钮 disabled + loading,session 创建失败后按钮恢复可点且下方显示错误文案,再点击可成功重试
|
|
|
- [ ] 进入聊天页后立刻出现 typing-bubble(greeting 期间),greeting 到达后变为正常 AI 消息气泡
|
|
|
- [ ] greeting 500/网络失败时,error-card 按钮文案 "重新生成",点击后复用同一 sessionId 再请求;in-flight 期间按钮 disabled
|