"""统一生成任务模型。""" import uuid from sqlalchemy import BigInteger, 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="不可变输入快照") snapshot_id = Column(BigInteger, nullable=True, comment="主 PlanData 快照 ID") snapshot_hash = Column(String(64), nullable=True, comment="单个或组合快照哈希") scenario_version_id = Column(BigInteger, nullable=True) scenario_hash = Column(String(64), nullable=True) policy_version_id = Column(BigInteger, nullable=True) policy_hash = Column(String(64), nullable=True) template_version_id = Column(BigInteger, nullable=True) template_hash = Column(String(64), nullable=True) asset_sha256 = Column(String(64), nullable=True) renderer_version = Column(String(100), nullable=True) submit_revision = Column(Integer, nullable=True) output_reconciliation_json = Column(Text, nullable=True) visual_check_json = Column(Text, nullable=True) result_state = Column(String(20), nullable=False, default="current") 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 from insurance.utils.public_payload import strip_server_paths input_snapshot = 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 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": strip_server_paths(input_snapshot), "snapshotId": self.snapshot_id, "snapshotHash": self.snapshot_hash, "scenarioVersionId": self.scenario_version_id, "scenarioHash": self.scenario_hash, "policyVersionId": self.policy_version_id, "policyHash": self.policy_hash, "templateVersionId": self.template_version_id, "templateHash": self.template_hash, "assetSha256": self.asset_sha256, "rendererVersion": self.renderer_version, "submitRevision": self.submit_revision or self.input_revision, "outputReconciliation": json.loads(self.output_reconciliation_json) if self.output_reconciliation_json else None, "visualCheck": json.loads(self.visual_check_json) if self.visual_check_json else None, "resultState": self.result_state or "current", "output": strip_server_paths(output), "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( user_id=user_id, 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