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

修复剧本解析状态更新时序

日期: 2026-02-06
类型: Bug 修复
关联: RFC 140 - 剧本文件存储重构


🐛 问题描述

在真实上传剧本文件时,前端轮询 parse-status 接口时从未看到 parsing 状态,直接从 pending 跳转到 completedfailed

根本原因

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.py
    • parse_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