2026-07-23 15:04:16 +08:00
|
|
|
"""海报计划书上传模型。"""
|
2026-08-01 03:15:43 +08:00
|
|
|
from sqlalchemy import Column, String, Text, BigInteger, Integer, TIMESTAMP, func
|
2026-07-23 15:04:16 +08:00
|
|
|
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")
|
2026-07-31 09:50:46 +08:00
|
|
|
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")
|
2026-07-23 15:04:16 +08:00
|
|
|
source_file_url = Column(String(500), nullable=False, comment="源文件地址")
|
2026-08-01 03:15:43 +08:00
|
|
|
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")
|
2026-07-23 15:04:16 +08:00
|
|
|
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
|
2026-08-01 03:15:43 +08:00
|
|
|
|
文件 问题 严重度
1 poster/routes.py:156 下载路径硬编码,与实际存储路径不一致 → 下载 404 P0
2 poster/service.py:41 get_reviewed_products() N+1 查询 P1
3 poster/service.py:169,226 json.loads 无异常处理 → 数据损坏时崩溃 P1
4 3 个 model 文件 to_dict() 中 json.loads 无防御 → 序列化崩溃 P1
5 poster/tasks.py ~120 行死代码(线程版海报生成) P2
6 generation/celery_tasks.py parse_poster_task + _execute_poster_parse 死代码(~70 行) P2
7 generation/task_service.py 对应移除 ("poster", "parse") 映射 P2
8 utils/security.py SSRF TOCTOU:DNS 检查与请求之间的时间窗口可被 DNS rebinding 利用 → 新增 _SafeHTTPTransport 在连接时重新验证 IP P1 安全
9 poster/image_generator.py anchor="mm" 在旧 Pillow 默认字体上崩溃;改用 hasattr 检测 P1
前端(3 项)
# 文件 问题 严重度
10 poster-api.ts:54 downloadPoster() 返回 AxiosResponse 而非 Blob → 海报永远无法下载 P0
11 PosterStepUpload.vue 解析轮询无超时 → 无限轮询 P1
12 PosterStepPreview.vue 生成轮询无超时 → 无限轮询 P1
修改的文件总计
后端 7 个:security.py, image_generator.py, service.py, routes.py, tasks.py, celery_tasks.py, task_service.py, poster_case_upload.py, poster_record.py, poster_template_model.py
前端 3 个:poster-api.ts, PosterStepUpload.vue, PosterStepPreview.vue
未修复(确认无需修复)
manual_parser.py — 之前误判为死代码,实际被 Celery 产品小册子解析任务使用,保留不动
llm_client.py 中的 httpx 调用 — URL 来自管理员配置的系统设置,不是用户输入,SSRF 风险极低;加检查反而会阻断合法的私网 LLM 端点
2026-07-29 22:41:27 +08:00
|
|
|
def _safe_json(text):
|
|
|
|
|
if not text:
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
return json.loads(text)
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
return None
|
|
|
|
|
|
2026-08-01 03:15:43 +08:00
|
|
|
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]
|
|
|
|
|
|
2026-07-23 15:04:16 +08:00
|
|
|
return {
|
|
|
|
|
"id": self.id,
|
|
|
|
|
"userId": self.user_id,
|
|
|
|
|
"productId": self.product_id,
|
2026-07-31 09:50:46 +08:00
|
|
|
"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),
|
2026-07-23 15:04:16 +08:00
|
|
|
"sourceFileUrl": self.source_file_url,
|
|
|
|
|
"parseStatus": self.parse_status,
|
2026-08-01 03:15:43 +08:00
|
|
|
"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,
|
文件 问题 严重度
1 poster/routes.py:156 下载路径硬编码,与实际存储路径不一致 → 下载 404 P0
2 poster/service.py:41 get_reviewed_products() N+1 查询 P1
3 poster/service.py:169,226 json.loads 无异常处理 → 数据损坏时崩溃 P1
4 3 个 model 文件 to_dict() 中 json.loads 无防御 → 序列化崩溃 P1
5 poster/tasks.py ~120 行死代码(线程版海报生成) P2
6 generation/celery_tasks.py parse_poster_task + _execute_poster_parse 死代码(~70 行) P2
7 generation/task_service.py 对应移除 ("poster", "parse") 映射 P2
8 utils/security.py SSRF TOCTOU:DNS 检查与请求之间的时间窗口可被 DNS rebinding 利用 → 新增 _SafeHTTPTransport 在连接时重新验证 IP P1 安全
9 poster/image_generator.py anchor="mm" 在旧 Pillow 默认字体上崩溃;改用 hasattr 检测 P1
前端(3 项)
# 文件 问题 严重度
10 poster-api.ts:54 downloadPoster() 返回 AxiosResponse 而非 Blob → 海报永远无法下载 P0
11 PosterStepUpload.vue 解析轮询无超时 → 无限轮询 P1
12 PosterStepPreview.vue 生成轮询无超时 → 无限轮询 P1
修改的文件总计
后端 7 个:security.py, image_generator.py, service.py, routes.py, tasks.py, celery_tasks.py, task_service.py, poster_case_upload.py, poster_record.py, poster_template_model.py
前端 3 个:poster-api.ts, PosterStepUpload.vue, PosterStepPreview.vue
未修复(确认无需修复)
manual_parser.py — 之前误判为死代码,实际被 Celery 产品小册子解析任务使用,保留不动
llm_client.py 中的 httpx 调用 — URL 来自管理员配置的系统设置,不是用户输入,SSRF 风险极低;加检查反而会阻断合法的私网 LLM 端点
2026-07-29 22:41:27 +08:00
|
|
|
"parsedData": _safe_json(self.parsed_data),
|
|
|
|
|
"confirmedData": _safe_json(self.confirmed_data),
|
2026-07-23 15:04:16 +08:00
|
|
|
"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,
|
|
|
|
|
}
|