baodan/api/insurance/models/poster_case_upload.py
wsb1224 9939188a4b 主要修复:
海报 PDF 解析从 Gunicorn 后台线程迁移到 Celery Worker,消除 gevent/asyncio.run 冲突和任务卡死。
海报改为紧凑解析,只选择客户资料、保费和核心利益页;排除提领方案、悲观/乐观情景页。
LLM 调用由原来的约 27 次降为 1 次。
补充年缴保费、首年实缴、缴费期、总保费及第 1/5/10/15/20/25/30 年退保价值。
增加真实解析进度、错误信息、任务 ID、心跳和完成时间。
相同用户重复上传同一份计划书时复用现有任务或结果。
前端取消 180 秒本地假超时,改为串行轮询后端真实状态;网络波动不再误判解析失败。
增加服务重启后的过期任务恢复机制。
修复解析结果 JSON 序列化遗漏问题。
关键文件:
[extraction.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py)
[tasks.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/tasks.py)
[celery_tasks.py](D:/work/code/python/coding/baodanagent/api/insurance/generation/celery_tasks.py)
[service.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/service.py)
[migrate_032.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_032.py)
[PosterSourcePanel.vue](D:/work/code/python/coding/baodanagent/frontend/src/components/poster/workspace/PosterSourcePanel.vue)
[回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py)
2026-08-01 03:15:43 +08:00

83 lines
4.0 KiB
Python

"""海报计划书上传模型。"""
from sqlalchemy import Column, String, Text, BigInteger, Integer, TIMESTAMP, func
from insurance.db.compat import db
class PosterCaseUpload(db.Model):
"""海报计划书上传记录表。"""
__tablename__ = "poster_case_uploads"
id = Column(BigInteger, primary_key=True, autoincrement=True)
user_id = Column(String(50), nullable=False, comment="用户 ID")
product_id = Column(String(50), nullable=False, comment="产品 ID")
product_source_type = Column(String(20), nullable=True, comment="library_product/user_material")
product_source_id = Column(String(64), nullable=True, comment="产品来源 ID")
product_snapshot_json = Column(Text, nullable=True, comment="产品信息快照 JSON")
source_file_url = Column(String(500), nullable=False, comment="源文件地址")
parse_status = Column(
String(20),
default="pending",
comment="解析状态: pending/queued/parsing/parsed/partial/failed",
)
parse_progress = Column(Integer, nullable=False, default=0, comment="解析进度 0-100")
parse_message = Column(String(500), nullable=False, default="", comment="解析进度说明")
parse_error = Column(Text, nullable=True, comment="解析失败原因")
parse_task_id = Column(String(200), nullable=True, comment="Celery 任务 ID")
parse_started_at = Column(TIMESTAMP, nullable=True)
parse_heartbeat_at = Column(TIMESTAMP, nullable=True)
parse_finished_at = Column(TIMESTAMP, nullable=True)
file_hash = Column(String(64), nullable=True, index=True, comment="源文件 SHA-256")
parsed_data = Column(Text, nullable=True, comment="系统解析结果 JSON")
confirmed_data = Column(Text, nullable=True, comment="人工核对后最终结果 JSON")
confirmed_by = Column(String(50), nullable=True, comment="核对人")
confirmed_at = Column(TIMESTAMP, nullable=True, comment="核对时间")
created_at = Column(TIMESTAMP, server_default=func.now())
def to_dict(self):
import json
def _safe_json(text):
if not text:
return None
try:
return json.loads(text)
except (json.JSONDecodeError, TypeError):
return None
def _safe_error(value):
if not value:
return ""
message = str(value).strip()
unsafe_markers = (
"traceback", "file \"", "\\", "/app/", "/api/",
"http://", "https://", "api_key", "token=",
)
if "\n" in message or any(marker in message.lower() for marker in unsafe_markers):
return "解析失败,请重试;如多次失败请联系管理员"
return message[:300]
return {
"id": self.id,
"userId": self.user_id,
"productId": self.product_id,
"productSource": {
"type": self.product_source_type or "library_product",
"id": self.product_source_id or self.product_id,
},
"productSnapshot": _safe_json(self.product_snapshot_json),
"sourceFileUrl": self.source_file_url,
"parseStatus": self.parse_status,
"parseProgress": self.parse_progress or 0,
"parseMessage": self.parse_message or "",
"parseError": _safe_error(self.parse_error),
"parseTaskId": self.parse_task_id,
"parseStartedAt": self.parse_started_at.isoformat() if self.parse_started_at else None,
"parseHeartbeatAt": self.parse_heartbeat_at.isoformat() if self.parse_heartbeat_at else None,
"parseFinishedAt": self.parse_finished_at.isoformat() if self.parse_finished_at else None,
"parsedData": _safe_json(self.parsed_data),
"confirmedData": _safe_json(self.confirmed_data),
"confirmedBy": self.confirmed_by,
"confirmedAt": self.confirmed_at.isoformat() if self.confirmed_at else None,
"createdAt": self.created_at.isoformat() if self.created_at else None,
}