You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
7.6 KiB
7.6 KiB
修复剧本解析状态更新时序
日期: 2026-02-06
类型: Bug 修复
关联: RFC 140 - 剧本文件存储重构
🐛 问题描述
在真实上传剧本文件时,前端轮询 parse-status 接口时从未看到 parsing 状态,直接从 pending 跳转到 completed 或 failed。
根本原因
ScreenplayFileParserService.parse_file() 和 parse_file_sync() 方法在开始解析时,没有更新状态为 PARSING,导致状态流转缺失:
# ❌ 原代码(缺少状态更新)
async def parse_file(self, screenplay_id: str, file_path: str, mime_type: str):
logger.info("开始解析剧本文件...")
try:
# 直接开始下载和解析
local_file_path = await self._download_file(file_path)
content = await self._parse_docx(local_file_path)
# ...
# 直接跳到 COMPLETED
await self.repository.update(screenplay_id, {
'parsing_status': ParsingStatus.COMPLETED
})
正确的状态流转
创建剧本 → PENDING
↓
Celery 任务启动
↓
parse_file() 开始 → PARSING ← ⚠️ 缺失此步骤
↓
解析成功/失败
↓
COMPLETED / FAILED
✅ 解决方案
1. 修改 ScreenplayFileParserService
文件: server/app/services/screenplay_file_parser_service.py
在两个解析方法(parse_file() 和 parse_file_sync())的开始处,先更新状态为 PARSING,并立即 commit:
async def parse_file(self, screenplay_id: str, file_path: str, mime_type: str):
logger.info("开始解析剧本文件 | 剧本ID: %s | 类型: %s", screenplay_id, mime_type)
try:
# ✅ 0. 更新状态为"解析中" (RFC 140)
await self.repository.update(screenplay_id, {
'parsing_status': ParsingStatus.PARSING
})
await self.db.commit() # ✅ 立即提交,使状态对外部可见
logger.debug("状态已更新为 PARSING 并已提交 | 剧本ID: %s", screenplay_id)
# 1. 下载文件到临时目录(如果是 URL)
local_file_path = await self._download_file(file_path)
# ... 后续解析逻辑
🔑 关键点:立即 commit
为什么需要 commit()?
如果只更新状态而不 commit,外部查询(如前端轮询 parse-status 接口)会因为事务隔离而看不到 PARSING 状态:
# ❌ 没有 commit - 外部看不到状态
await self.repository.update({'parsing_status': ParsingStatus.PARSING})
# ... 长时间解析 ...
await self.repository.update({'parsing_status': ParsingStatus.COMPLETED})
await db.commit() # 外层才 commit,外部直接从 PENDING 跳到 COMPLETED
# ✅ 立即 commit - 外部能看到状态
await self.repository.update({'parsing_status': ParsingStatus.PARSING})
await self.db.commit() # ← 立即提交,状态对外可见
# ... 长时间解析 ...
await self.repository.update({'parsing_status': ParsingStatus.COMPLETED})
await db.commit() # 最终状态提交
2. 修复硬编码状态值
在 parse_file_sync() 中还发现一个硬编码的 parsing_status: 3,已改为使用枚举:
# ❌ 硬编码
'parsing_status': 3, # 3=completed
# ✅ 使用枚举
'parsing_status': ParsingStatus.COMPLETED,
🔍 影响范围
涉及文件
server/app/services/screenplay_file_parser_service.pyparse_file()方法 - 添加状态更新parse_file_sync()方法 - 添加状态更新 + 修复硬编码
数据库变更
无需数据迁移。
API 变更
无 API 变更,但现在 GET /api/v1/screenplays/{screenplay_id}/parse-status 接口能够正常返回 parsing 状态了。
🧪 验证方法
1. 上传剧本文件
curl -X POST "http://localhost:8000/api/v1/screenplays/file" \
-H "Authorization: Bearer <token>" \
-F "file=@test.docx" \
-F "name=测试剧本" \
-F "projectId=<project_id>"
预期响应:
{
"data": {
"screenplayId": "019c3248-ce62-7f72-a19f-7246551a7497",
"parsingStatus": "pending", // ✅ 初始状态
"taskId": "d030a2a4-2993-41c5-86de-f95eb0da4fc0"
}
}
2. 立即轮询状态
curl "http://localhost:8000/api/v1/screenplays/019c3248-ce62-7f72-a19f-7246551a7497/parse-status" \
-H "Authorization: Bearer <token>"
预期响应(Celery 任务开始后 1-2 秒内):
{
"data": {
"screenplayId": "019c3248-ce62-7f72-a19f-7246551a7497",
"parsingStatus": "parsing", // ✅ 现在能看到此状态
"message": "正在解析中...",
"progress": null
}
}
3. 等待解析完成(约 5-20 秒)
再次轮询:
{
"data": {
"screenplayId": "019c3248-ce62-7f72-a19f-7246551a7497",
"parsingStatus": "completed", // ✅ 最终状态
"wordCount": 8524,
"fileUrl": "http://localhost:9000/jointo/screenplays/parsed/019c3248-ce62-7f72-a19f-7246551a7497.md",
"message": "解析完成"
}
}
字段命名统一:
fileUrl字段名与列表接口保持一致,统一返回解析后 Markdown 文件的完整访问 URL。
📊 状态流转时序图
修复后的完整流程
时间轴 | 用户操作 | 后端状态 | 前端显示
--------|---------------------|----------------|------------------
T+0s | 上传文件 | PENDING | "等待解析"
T+1s | Celery 任务启动 | PARSING | "正在解析中..." ✅
T+2s | 下载文件 | PARSING | "正在解析中..." ✅
T+5s | 解析 DOCX | PARSING | "正在解析中..." ✅
T+8s | 上传 Markdown | PARSING | "正在解析中..." ✅
T+10s | 更新数据库 | COMPLETED | "解析完成" ✅
🎯 最佳实践
1. 状态更新原则
在长时间操作开始前,立即更新状态:
# ✅ 正确做法
await update_status(PARSING) # 先更新状态
result = await long_running_task() # 再执行任务
# ❌ 错误做法
result = await long_running_task() # 任务完成才更新状态
await update_status(COMPLETED)
2. 状态粒度设计
对于文件处理,推荐的状态流转:
| 状态 | 时机 | 用户提示 |
|---|---|---|
IDLE |
文本剧本,无需解析 | - |
PENDING |
文件已上传,等待 Celery 调度 | "等待解析" |
PARSING |
Celery 任务开始执行 | "正在解析中..." |
COMPLETED |
解析成功,数据已保存 | "解析完成" |
FAILED |
解析失败(含错误消息) | "解析失败:xxx" |
3. 日志记录
在状态转换时记录调试日志,便于排查:
logger.debug("状态已更新为 PARSING | 剧本ID: %s", screenplay_id)
🔗 相关文档
- RFC 140: 剧本文件存储重构
ParsingStatus枚举定义:server/app/models/screenplay.py- 解析服务:
server/app/services/screenplay_file_parser_service.py - Celery 任务:
server/app/tasks/screenplay_tasks.py - 状态查询 API:
GET /api/v1/screenplays/{screenplay_id}/parse-status
✨ 总结
- 问题: 解析状态从
pending直接跳到completed,缺少parsing中间状态 - 原因: 解析服务开始时未更新状态
- 解决: 在
parse_file()和parse_file_sync()开始处添加状态更新 - 收益: 前端能正确显示"正在解析中",用户体验更好
- 附加修复: 消除硬编码状态值
3,改用枚举ParsingStatus.COMPLETED