baodan/api/insurance/models/generation_task.py
wsb1224 5f78598b3a input_revision 正确记录
task_service.py — create_task() 新增 input_revision 参数,存入 GenerationTask.input_revision
generation_task.py — 模型的静态 create_task 方法同步新增参数
ppt/routes.py — 三处调用(parse/generate/regenerate)均传入 session.draft_revision
poster/service.py — 海报生成传入 record.draft_revision
现在每个任务快照都记录了创建时的草稿版本号,不再一直是默认值 1
2. 海报任务进度实时同步 
celery_tasks.py — 在海报生成的每个阶段(preparing_data→20%、building_prompt→40%、requesting_image→60%、saving→85%)调用 sync_poster_progress() 同步到 PosterRecord.task_status/task_progress
任务领取时立即将 PosterRecord.task_status 设为 running
解决了"页面轮询海报记录时一直显示排队中"的问题
3. 后端测试覆盖 
test_task_state_sync.py — 新增 13 个测试用例:
sync_workspace_status 同步 cancelled/failed 到 PPT session(4 个)
sync_workspace_status 同步 cancelled/failed 到 PosterRecord(2 个)
旧任务跳过同步的安全检查(1 个)
cancel_task 状态更新和非 queued 拒绝(2 个)
mark_task_viewed 正确标记和权限校验(2 个)
input_revision 正确记录和默认值(2 个)
list_active_tasks 排除终态任务(1 个)
所有 13 个新测试 + 原有 10 个测试全部通过
4. useAutoSave TypeScript 类型修复 
useAutoSave.ts — getEndpoint 变量显式标注 () => string 类型,消除 TS2345 编译错误
2026-07-30 13:54:44 +08:00

100 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""统一生成任务模型。"""
import uuid
from sqlalchemy import Column, String, Text, Integer, TIMESTAMP, func
from insurance.db.compat import db
class GenerationTask(db.Model):
"""统一生成任务表PPT + 海报)。
职责分离:
- 工作区PptSession/PosterRecord保存可编辑数据
- 任务表保存一次执行的不可变快照、状态和结果
"""
__tablename__ = "insurance_generation_tasks"
id = Column(String(36), primary_key=True, default=lambda: uuid.uuid4().hex)
user_id = Column(String(64), nullable=False, comment="用户 ID")
artifact_type = Column(String(10), nullable=False, comment="ppt/poster")
operation = Column(String(10), nullable=False, comment="parse/generate")
workspace_id = Column(String(36), nullable=False, comment="PptSession.id 或 PosterRecord.id")
title_snapshot = Column(String(200), default="", comment="提交时任务名称")
status = Column(String(20), nullable=False, default="queued",
comment="queued/running/done/failed/cancelled")
stage = Column(String(30), default="", comment="当前执行阶段")
progress = Column(Integer, default=0, comment="进度 0-100")
message = Column(String(500), default="", comment="用户可读进度")
error_code = Column(String(50), default="", comment="结构化错误代码")
error_message = Column(Text, nullable=True, comment="用户可读错误")
input_revision = Column(Integer, default=1, comment="本次生成使用的草稿版本")
input_snapshot_json = Column(Text, nullable=True, comment="不可变输入快照")
output_json = Column(Text, nullable=True, comment="输出路径、页数、预览等")
idempotency_key = Column(String(100), nullable=True, comment="防止重复提交")
celery_task_id = Column(String(200), nullable=True, comment="Celery ID")
attempt_count = Column(Integer, default=0, comment="重试次数")
heartbeat_at = Column(TIMESTAMP, nullable=True, comment="Worker 心跳")
started_at = Column(TIMESTAMP, nullable=True, comment="开始时间")
finished_at = Column(TIMESTAMP, nullable=True, comment="完成时间")
viewed_at = Column(TIMESTAMP, nullable=True, comment="用户查看时间")
dock_hidden_at = Column(TIMESTAMP, nullable=True, comment="从任务坞隐藏时间")
created_at = Column(TIMESTAMP, server_default=func.now())
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
def to_dict(self):
import json
return {
"id": self.id,
"userId": self.user_id,
"artifactType": self.artifact_type,
"operation": self.operation,
"workspaceId": self.workspace_id,
"titleSnapshot": self.title_snapshot,
"status": self.status,
"stage": self.stage,
"progress": self.progress,
"message": self.message,
"errorCode": self.error_code,
"errorMessage": self.error_message,
"inputRevision": self.input_revision,
"inputSnapshot": json.loads(self.input_snapshot_json) if self.input_snapshot_json else None,
"output": json.loads(self.output_json) if self.output_json else None,
"idempotencyKey": self.idempotency_key,
"celeryTaskId": self.celery_task_id,
"attemptCount": self.attempt_count,
"heartbeatAt": self.heartbeat_at.isoformat() if self.heartbeat_at else None,
"startedAt": self.started_at.isoformat() if self.started_at else None,
"finishedAt": self.finished_at.isoformat() if self.finished_at else None,
"viewedAt": self.viewed_at.isoformat() if self.viewed_at else None,
"dockHiddenAt": self.dock_hidden_at.isoformat() if self.dock_hidden_at else None,
"createdAt": self.created_at.isoformat() if self.created_at else None,
"updatedAt": self.updated_at.isoformat() if self.updated_at else None,
}
@staticmethod
def create_task(user_id: str, artifact_type: str, operation: str,
workspace_id: str, title: str = "",
input_snapshot: dict = None, idempotency_key: str = None,
input_revision: int = None):
"""创建新任务(幂等检查)。"""
import json
if idempotency_key:
existing = GenerationTask.query.filter_by(
idempotency_key=idempotency_key,
).filter(GenerationTask.status.in_(["queued", "running", "done"])).first()
if existing:
return existing
task = GenerationTask(
user_id=user_id,
artifact_type=artifact_type,
operation=operation,
workspace_id=workspace_id,
title_snapshot=title,
input_snapshot_json=json.dumps(input_snapshot, ensure_ascii=False) if input_snapshot else None,
input_revision=input_revision or 1,
idempotency_key=idempotency_key,
)
db.session.add(task)
db.session.commit()
return task