baodan/api/insurance/poster/service.py

451 lines
20 KiB
Python
Raw Normal View History

2026-07-23 15:04:16 +08:00
"""海报业务逻辑服务。"""
import asyncio
2026-07-31 09:50:46 +08:00
import copy
2026-07-23 15:04:16 +08:00
import json
import os
import re
import uuid
import logging
from flask import current_app
from insurance.db.compat import db
from insurance.models.ppt_config import PptProduct, PptCompany
from insurance.models.poster_case_upload import PosterCaseUpload
from insurance.models.poster_template_model import PosterTemplate
from insurance.models.poster_copy_template import PosterCopyTemplate
from insurance.models.poster_record import PosterRecord
2026-07-31 09:50:46 +08:00
from insurance.poster.product_source_resolver import (
ProductSourceError,
case_matches_product,
product_snapshot,
resolve_product_source,
)
2026-07-23 15:04:16 +08:00
logger = logging.getLogger(__name__)
def _safe_user_id(user_id: str) -> str:
"""清理 user_id 中的路径分隔符,防止路径穿越。"""
return re.sub(r'[/\\.]', '_', user_id)
def _run_async(coro):
"""安全执行异步函数,处理事件循环已存在的情况。"""
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop and loop.is_running():
# 已有运行中的事件循环,用线程池执行
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as pool:
return pool.submit(asyncio.run, coro).result()
return asyncio.run(coro)
2026-07-31 09:50:46 +08:00
def _replace_names(value, replacements: dict[str, str]):
"""递归替换规则、卖点和文案中的产品/保司名称。"""
if isinstance(value, str):
for real_name, masked_name in replacements.items():
value = value.replace(real_name, masked_name)
return value
if isinstance(value, list):
return [_replace_names(item, replacements) for item in value]
if isinstance(value, dict):
return {key: _replace_names(item, replacements) for key, item in value.items()}
return value
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
def _configured_context(context: dict) -> tuple[dict, dict, dict, dict]:
"""按后台品牌策略返回规则、产品和保司副本。"""
from insurance.ppt.masking import apply_brand_policy, build_brand_policy
2026-07-31 09:50:46 +08:00
product_data = copy.deepcopy(context.get("productData") or {})
company_data = copy.deepcopy(context.get("companyData") or {})
rules = copy.deepcopy(context.get("rules") or {})
real_product_name = context.get("productName") or ""
real_company_name = context.get("companyName") or ""
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
brand_policy = build_brand_policy(company_data, [product_data])
company_data, product_data = apply_brand_policy(
company_data, product_data, brand_policy
)
2026-07-31 09:50:46 +08:00
replacements = {}
masked_product_name = product_data.get("displayName", "")
masked_company_name = company_data.get("displayName", "")
if real_product_name and masked_product_name and real_product_name != masked_product_name:
replacements[real_product_name] = masked_product_name
if real_company_name and masked_company_name and real_company_name != masked_company_name:
replacements[real_company_name] = masked_company_name
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
return _replace_names(rules, replacements), product_data, company_data, brand_policy
2026-07-31 09:50:46 +08:00
2026-07-23 15:04:16 +08:00
class PosterService:
"""海报业务逻辑。"""
def get_reviewed_products(self) -> dict:
"""获取已 reviewed 的产品列表(供选择)。"""
products = PptProduct.query.filter(
PptProduct.manual_parse_status == "reviewed",
PptProduct.status == 1,
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
PptProduct.deleted_at.is_(None),
2026-07-23 15:04:16 +08:00
).all()
文件 问题 严重度 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
if not products:
return {"code": 0, "data": []}
# 批量查询公司(避免 N+1
company_ids = list({p.company_id for p in products})
companies = {
c.id: c for c in PptCompany.query.filter(
PptCompany.id.in_(company_ids),
PptCompany.status == 1,
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
PptCompany.deleted_at.is_(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
).all()
}
2026-07-23 15:04:16 +08:00
result = []
for p in products:
文件 问题 严重度 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
company = companies.get(p.company_id)
2026-07-29 12:19:26 +08:00
if not company:
continue
2026-07-23 15:04:16 +08:00
result.append({
**p.to_dict(),
2026-07-29 12:19:26 +08:00
"companyName": company.display_name,
2026-07-23 15:04:16 +08:00
})
return {"code": 0, "data": result}
2026-07-31 09:50:46 +08:00
def upload_case(self, user_id: str, product_id: str, file, password: str = "",
product_source: dict | None = None) -> dict:
"""上传计划书 PDF + 排队解析(异步)。"""
2026-07-31 09:50:46 +08:00
source_data = {
"productId": product_id,
"productSource": product_source or {},
}
try:
context = resolve_product_source(user_id, source_data)
except ProductSourceError as exc:
return {"code": exc.code, "message": exc.message, "data": None}
2026-07-23 15:04:16 +08:00
# 文件安全校验SEC-P1-01
from insurance.utils.security import prepare_pdf_upload
2026-07-29 12:19:26 +08:00
is_valid, err_msg, pdf_bytes = prepare_pdf_upload(file, password)
if not is_valid:
code = 4003 if "密码" in err_msg else 4002
return {"code": code, "message": err_msg, "data": None}
# 保存文件(使用持久化存储)
2026-07-23 15:04:16 +08:00
safe_uid = _safe_user_id(user_id)
from insurance.config import get_storage_root
upload_dir = os.path.join(get_storage_root(), "uploads", "poster-cases")
2026-07-23 15:04:16 +08:00
os.makedirs(upload_dir, exist_ok=True)
filename = f"{safe_uid}_{uuid.uuid4().hex[:8]}.pdf"
filepath = os.path.join(upload_dir, filename)
with open(filepath, "wb") as output:
output.write(pdf_bytes)
2026-07-23 15:04:16 +08:00
# 创建记录
record = PosterCaseUpload(
user_id=user_id,
2026-07-31 09:50:46 +08:00
product_id=context.get("productId") or "",
product_source_type=context["sourceType"],
product_source_id=context["sourceId"],
product_snapshot_json=json.dumps(product_snapshot(context), ensure_ascii=False),
2026-07-23 15:04:16 +08:00
source_file_url=filepath,
parse_status="pending",
)
db.session.add(record)
db.session.commit()
# 启动后台解析任务
from insurance.poster.tasks import start_case_parse_task
if start_case_parse_task(current_app._get_current_object(), record.id):
record.parse_status = "queued"
2026-07-23 15:04:16 +08:00
db.session.commit()
else:
2026-07-23 15:04:16 +08:00
record.parse_status = "failed"
db.session.commit()
return {"code": 5001, "message": "任务排队失败,请重试", "data": None}
2026-07-23 15:04:16 +08:00
return {"code": 0, "data": record.to_dict()}
def get_case_upload(self, record_id: int, user_id: str) -> dict:
"""获取解析结果。"""
record = PosterCaseUpload.query.get(record_id)
if not record or record.user_id != user_id:
return {"code": 1002, "message": "记录不存在", "data": None}
return {"code": 0, "data": record.to_dict()}
def confirm_case_upload(self, record_id: int, user_id: str, data: dict) -> dict:
"""人工核对/修正解析数据。"""
record = PosterCaseUpload.query.get(record_id)
if not record or record.user_id != user_id:
return {"code": 1002, "message": "记录不存在", "data": None}
record.confirmed_data = json.dumps(data.get("confirmedData", {}), ensure_ascii=False)
record.confirmed_by = user_id
from datetime import datetime
record.confirmed_at = datetime.now()
db.session.commit()
return {"code": 0, "data": record.to_dict()}
def get_templates(self) -> dict:
"""获取可用海报模板列表。"""
2026-07-29 12:19:26 +08:00
templates = PosterTemplate.query.filter_by(status=1).order_by(
PosterTemplate.id.asc()
).all()
2026-07-23 15:04:16 +08:00
return {"code": 0, "data": [t.to_dict() for t in templates]}
def get_copy_templates(self) -> dict:
"""获取可用文案模板列表。"""
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
templates = PosterCopyTemplate.query.filter_by(
status=1, deleted_at=None
).order_by(
2026-07-29 12:19:26 +08:00
PosterCopyTemplate.id.asc()
).all()
2026-07-23 15:04:16 +08:00
return {"code": 0, "data": [t.to_dict() for t in templates]}
def generate_copy(self, user_id: str, data: dict) -> dict:
"""生成文案template/ai 模式)。"""
mode = data.get("mode", "template")
case_upload_id = data.get("caseUploadId")
2026-07-31 09:50:46 +08:00
try:
context = resolve_product_source(user_id, data)
except ProductSourceError as exc:
return {"code": exc.code, "message": exc.message, "data": None}
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
product_rules, _product_data, _company_data, _brand_policy = _configured_context(context)
2026-07-28 16:45:14 +08:00
# 获取客户数据(校验 case 所有权 — 防止越权读取他人客户数据)
2026-07-23 15:04:16 +08:00
customer_data = {}
if case_upload_id:
case = PosterCaseUpload.query.get(case_upload_id)
if not case or case.user_id != user_id:
return {"code": 404, "message": "记录不存在", "data": None}
2026-07-31 09:50:46 +08:00
if not case_matches_product(case, context):
2026-07-29 12:19:26 +08:00
return {"code": 1002, "message": "计划书与所选产品不一致", "data": None}
if case.confirmed_data:
文件 问题 严重度 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
try:
customer_data = json.loads(case.confirmed_data)
except (json.JSONDecodeError, TypeError):
logger.warning("confirmed_data JSON 解析失败: case_id=%s", case_upload_id)
customer_data = {}
2026-07-23 15:04:16 +08:00
if mode == "template":
template_id = data.get("templateId")
2026-07-29 12:19:26 +08:00
template = PosterCopyTemplate.query.filter_by(
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
id=template_id, status=1, deleted_at=None
2026-07-29 12:19:26 +08:00
).first()
2026-07-23 15:04:16 +08:00
if not template:
return {"code": 1002, "message": "文案模板不存在", "data": None}
from insurance.poster.copy_generator import CopyGenerator
generator = CopyGenerator()
result = generator.generate_template_copy(template.content, product_rules, customer_data)
return {"code": 0, "data": {"copy": result, "mode": "template"}}
else:
style = data.get("style", "专业")
import asyncio
from insurance.poster.copy_generator import CopyGenerator
generator = CopyGenerator()
result = _run_async(generator.generate_ai_copy(product_rules, customer_data, style))
return {"code": 0, "data": {"copy": result, "mode": "ai"}}
已按计划书完成本轮核心 P0/P1 修复,覆盖用户反馈的三个主要问题。 主要结果: PPT: 已完成步骤可自由返回,刷新后保留当前步骤。 结果页改为固定三栏视口,缩略图和质量面板独立滚动。 修复画布宽度计算和宽高自适应缩放。 返回配置页时恢复上次模板。 Worker 禁止静默替换模板,不兼容时明确失败。 结果页显示实际应用的模板。 海报解析与合规: 修复 insured/policy 嵌套字段映射。 增加 partial/failed 解析质量判断和缺失字段提示。 传入险种、产品、保司和别名上下文。 修复性别、币种归一化和错误字段计数。 新增服务端字符级合规接口与生成前复检。 不合规文字可高亮、点击定位并选中对应文字。 海报编辑: AI 图片现在作为背景资产,不再替换整个海报。 单图、长图生成后仍可编辑标题、正文、CTA、数据和卖点。 背景生成失败不会清空当前编辑内容。 下载时合成背景、文字、数据、图表和免责声明。 增加可编辑文档快照、最终 PNG 保存和历史继续编辑。 新增数据库迁移:[migrate_028.py](/D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_028.py) 验证结果: 专项及相关后端测试:26 passed, 1 skipped Python 全模块编译检查:通过 前端生产构建:通过 git diff --check:通过 全量测试收集受本机缺少 python-pptx 依赖影响,报错为 ModuleNotFoundError: pptx,不是本次修改产生的测试失败。
2026-07-31 15:20:37 +08:00
def check_compliance(self, data: dict) -> dict:
"""检查文案并返回可定位的问题。"""
from insurance.poster.compliance import check_copy_compliance
copy_content = data.get("copyContent") if "copyContent" in data else data
return {"code": 0, "data": check_copy_compliance(copy_content or {})}
2026-07-23 15:04:16 +08:00
def generate_poster(self, user_id: str, data: dict) -> dict:
"""生成海报图片(异步 — 排队后立即返回)。"""
2026-07-23 15:04:16 +08:00
case_upload_id = data.get("caseUploadId")
template_id = data.get("templateId")
copy_content = data.get("copyContent", {})
size = data.get("size", "1024x1792")
reference_image = data.get("referenceImage")
2026-07-30 16:07:57 +08:00
output_mode = data.get("outputMode", "single")
force_regenerate = data.get("regenerate", False)
2026-07-23 15:04:16 +08:00
已按计划书完成本轮核心 P0/P1 修复,覆盖用户反馈的三个主要问题。 主要结果: PPT: 已完成步骤可自由返回,刷新后保留当前步骤。 结果页改为固定三栏视口,缩略图和质量面板独立滚动。 修复画布宽度计算和宽高自适应缩放。 返回配置页时恢复上次模板。 Worker 禁止静默替换模板,不兼容时明确失败。 结果页显示实际应用的模板。 海报解析与合规: 修复 insured/policy 嵌套字段映射。 增加 partial/failed 解析质量判断和缺失字段提示。 传入险种、产品、保司和别名上下文。 修复性别、币种归一化和错误字段计数。 新增服务端字符级合规接口与生成前复检。 不合规文字可高亮、点击定位并选中对应文字。 海报编辑: AI 图片现在作为背景资产,不再替换整个海报。 单图、长图生成后仍可编辑标题、正文、CTA、数据和卖点。 背景生成失败不会清空当前编辑内容。 下载时合成背景、文字、数据、图表和免责声明。 增加可编辑文档快照、最终 PNG 保存和历史继续编辑。 新增数据库迁移:[migrate_028.py](/D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_028.py) 验证结果: 专项及相关后端测试:26 passed, 1 skipped Python 全模块编译检查:通过 前端生产构建:通过 git diff --check:通过 全量测试收集受本机缺少 python-pptx 依赖影响,报错为 ModuleNotFoundError: pptx,不是本次修改产生的测试失败。
2026-07-31 15:20:37 +08:00
from insurance.poster.compliance import check_copy_compliance
compliance_result = check_copy_compliance(copy_content)
if compliance_result["status"] == "block":
return {
"code": 1002,
"message": "文案包含不合规表达,请修改后再生成",
"data": compliance_result,
}
2026-07-30 16:07:57 +08:00
# 提取参考图元数据(不含 base64 数据,仅保存文件名供展示)
ref_images_meta = []
if reference_image:
ref_images_meta = [{"name": "参考图", "hasData": True}]
2026-07-29 12:19:26 +08:00
template = PosterTemplate.query.filter_by(id=template_id, status=1).first()
if not template:
return {"code": 1002, "message": "海报模板不存在或已停用", "data": None}
2026-07-31 09:50:46 +08:00
try:
context = resolve_product_source(user_id, data)
except ProductSourceError as exc:
return {"code": exc.code, "message": exc.message, "data": None}
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
_rules, _product_data, _company_data, brand_policy = _configured_context(context)
source_snapshot = product_snapshot(context)
2026-07-29 12:19:26 +08:00
# 校验 case 所有权SEC-P0-03
if case_upload_id:
case = PosterCaseUpload.query.get(case_upload_id)
if not case or case.user_id != user_id:
return {"code": 404, "message": "记录不存在", "data": None}
2026-07-31 09:50:46 +08:00
if not case_matches_product(case, context):
2026-07-29 12:19:26 +08:00
return {"code": 1002, "message": "计划书与所选产品不一致", "data": None}
if case.confirmed_data is None:
return {"code": 1002, "message": "请先确认解析数据", "data": None}
# 幂等检查TASK-P1-02相同参数的未完成任务直接返回
if not force_regenerate and case_upload_id:
existing = PosterRecord.query.filter(
PosterRecord.user_id == user_id,
PosterRecord.case_upload_id == case_upload_id,
PosterRecord.template_id == template_id,
PosterRecord.task_status.in_(["pending", "queued", "generating"]),
).order_by(PosterRecord.created_at.desc()).first()
if existing:
return {"code": 0, "data": existing.to_dict()}
# 创建记录(任务状态为 pending
2026-07-23 15:04:16 +08:00
ai_raw_content = data.get("aiRawContent")
record = PosterRecord(
user_id=user_id,
2026-07-31 09:50:46 +08:00
product_id=context.get("productId"),
product_source_type=context["sourceType"],
product_source_id=context["sourceId"],
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
product_snapshot_json=json.dumps(source_snapshot, ensure_ascii=False),
2026-07-23 15:04:16 +08:00
case_upload_id=case_upload_id,
template_id=template_id,
copy_mode=data.get("copyMode", "ai"),
copy_content=json.dumps(copy_content, ensure_ascii=False) if copy_content else None,
ai_raw_content=json.dumps(ai_raw_content, ensure_ascii=False) if ai_raw_content else None,
export_size=size,
reference_image_used=reference_image,
task_status="pending",
task_progress=0,
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
extra_data=json.dumps({
"brandPolicy": brand_policy,
"referenceImages": ref_images_meta,
"outputMode": output_mode,
已按计划书完成本轮核心 P0/P1 修复,覆盖用户反馈的三个主要问题。 主要结果: PPT: 已完成步骤可自由返回,刷新后保留当前步骤。 结果页改为固定三栏视口,缩略图和质量面板独立滚动。 修复画布宽度计算和宽高自适应缩放。 返回配置页时恢复上次模板。 Worker 禁止静默替换模板,不兼容时明确失败。 结果页显示实际应用的模板。 海报解析与合规: 修复 insured/policy 嵌套字段映射。 增加 partial/failed 解析质量判断和缺失字段提示。 传入险种、产品、保司和别名上下文。 修复性别、币种归一化和错误字段计数。 新增服务端字符级合规接口与生成前复检。 不合规文字可高亮、点击定位并选中对应文字。 海报编辑: AI 图片现在作为背景资产,不再替换整个海报。 单图、长图生成后仍可编辑标题、正文、CTA、数据和卖点。 背景生成失败不会清空当前编辑内容。 下载时合成背景、文字、数据、图表和免责声明。 增加可编辑文档快照、最终 PNG 保存和历史继续编辑。 新增数据库迁移:[migrate_028.py](/D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_028.py) 验证结果: 专项及相关后端测试:26 passed, 1 skipped Python 全模块编译检查:通过 前端生产构建:通过 git diff --check:通过 全量测试收集受本机缺少 python-pptx 依赖影响,报错为 ModuleNotFoundError: pptx,不是本次修改产生的测试失败。
2026-07-31 15:20:37 +08:00
"compliance": compliance_result,
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
}, ensure_ascii=False),
已按计划书完成本轮核心 P0/P1 修复,覆盖用户反馈的三个主要问题。 主要结果: PPT: 已完成步骤可自由返回,刷新后保留当前步骤。 结果页改为固定三栏视口,缩略图和质量面板独立滚动。 修复画布宽度计算和宽高自适应缩放。 返回配置页时恢复上次模板。 Worker 禁止静默替换模板,不兼容时明确失败。 结果页显示实际应用的模板。 海报解析与合规: 修复 insured/policy 嵌套字段映射。 增加 partial/failed 解析质量判断和缺失字段提示。 传入险种、产品、保司和别名上下文。 修复性别、币种归一化和错误字段计数。 新增服务端字符级合规接口与生成前复检。 不合规文字可高亮、点击定位并选中对应文字。 海报编辑: AI 图片现在作为背景资产,不再替换整个海报。 单图、长图生成后仍可编辑标题、正文、CTA、数据和卖点。 背景生成失败不会清空当前编辑内容。 下载时合成背景、文字、数据、图表和免责声明。 增加可编辑文档快照、最终 PNG 保存和历史继续编辑。 新增数据库迁移:[migrate_028.py](/D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_028.py) 验证结果: 专项及相关后端测试:26 passed, 1 skipped Python 全模块编译检查:通过 前端生产构建:通过 git diff --check:通过 全量测试收集受本机缺少 python-pptx 依赖影响,报错为 ModuleNotFoundError: pptx,不是本次修改产生的测试失败。
2026-07-31 15:20:37 +08:00
document_json=json.dumps({
"revision": 1,
"templateId": template_id,
"outputMode": output_mode,
"copy": copy_content,
"facts": (
json.loads(case.confirmed_data)
if case_upload_id and case and case.confirmed_data else {}
),
"style": {},
}, ensure_ascii=False),
document_revision=1,
compliance_json=json.dumps(compliance_result, ensure_ascii=False),
compliance_revision=compliance_result["revision"],
2026-07-23 15:04:16 +08:00
)
db.session.add(record)
db.session.commit()
# 启动后台生成任务Celery
from insurance.generation import task_service
2026-07-31 09:50:46 +08:00
task_input = dict(data)
主要完成内容: 修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。 脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。 保司支持独立控制 PPT、海报 Logo 显示。 PPT 核验新增吸烟状态、币种及三个条件字段。 利益演示、退保提取调整为警告,不再阻止生成。 PPT 生成完成后可以直接返回数据核验页修改。 建立不同险种、单图/长图共六套海报字段画像。 PPT“生成场景”支持后台新增、启停和删除。 保司、产品、PPT 模板、文案模板均支持安全删除。 内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。 补充策略变更及删除审计日志。 更新 API 文档、部署文档及修复计划实施记录。 关键交付文件: [数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py) [海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py) [动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py) [新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py) [优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md) 验证结果: 核心链路测试:37 passed,1 skipped 扩展回归测试:140 passed PPT 渲染器测试:6 passed 前端生产构建:通过 Python 编译检查:通过 完整测试集:190 passed,1 failed 唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00
task_input.pop("useMaskedData", None)
task_input["brandPolicy"] = brand_policy
2026-07-31 09:50:46 +08:00
task_input["productSource"] = {
"type": context["sourceType"],
"id": context["sourceId"],
}
task_result = task_service.create_task(
user_id=user_id,
artifact_type="poster",
operation="generate",
workspace_id=str(record.id),
title=f"海报 #{record.id}",
2026-07-31 09:50:46 +08:00
input_snapshot=task_input,
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
input_revision=record.draft_revision or 1,
idempotency_key=f"poster_gen_{record.id}",
)
if task_result.get("code") == 0:
record.task_status = "queued"
record.latest_task_id = task_result["data"]["id"]
db.session.commit()
else:
record.task_status = "failed"
record.task_error = task_result.get("message", "任务排队失败")
db.session.commit()
return {"code": 5001, "message": "任务排队失败,请重试", "data": None}
2026-07-23 15:04:16 +08:00
return {"code": 0, "data": record.to_dict()}
def list_records(self, user_id: str, params: dict) -> dict:
"""海报生成记录列表。"""
query = db.session.query(PosterRecord).filter(PosterRecord.user_id == user_id)
query = query.order_by(PosterRecord.created_at.desc())
page = max(1, params.get("page", 1))
page_size = min(100, max(1, params.get("page_size", 20)))
total = query.count()
items = query.offset((page - 1) * page_size).limit(page_size).all()
return {
"code": 0,
"data": {
"total": total,
"items": [r.to_dict() for r in items],
},
}
def get_record(self, record_id: int, user_id: str) -> dict:
"""记录详情(含任务状态,用于轮询)。"""
2026-07-23 15:04:16 +08:00
record = PosterRecord.query.get(record_id)
if not record or record.user_id != user_id:
return {"code": 1002, "message": "记录不存在", "data": None}
return {"code": 0, "data": record.to_dict()}
已按计划书完成本轮核心 P0/P1 修复,覆盖用户反馈的三个主要问题。 主要结果: PPT: 已完成步骤可自由返回,刷新后保留当前步骤。 结果页改为固定三栏视口,缩略图和质量面板独立滚动。 修复画布宽度计算和宽高自适应缩放。 返回配置页时恢复上次模板。 Worker 禁止静默替换模板,不兼容时明确失败。 结果页显示实际应用的模板。 海报解析与合规: 修复 insured/policy 嵌套字段映射。 增加 partial/failed 解析质量判断和缺失字段提示。 传入险种、产品、保司和别名上下文。 修复性别、币种归一化和错误字段计数。 新增服务端字符级合规接口与生成前复检。 不合规文字可高亮、点击定位并选中对应文字。 海报编辑: AI 图片现在作为背景资产,不再替换整个海报。 单图、长图生成后仍可编辑标题、正文、CTA、数据和卖点。 背景生成失败不会清空当前编辑内容。 下载时合成背景、文字、数据、图表和免责声明。 增加可编辑文档快照、最终 PNG 保存和历史继续编辑。 新增数据库迁移:[migrate_028.py](/D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_028.py) 验证结果: 专项及相关后端测试:26 passed, 1 skipped Python 全模块编译检查:通过 前端生产构建:通过 git diff --check:通过 全量测试收集受本机缺少 python-pptx 依赖影响,报错为 ModuleNotFoundError: pptx,不是本次修改产生的测试失败。
2026-07-31 15:20:37 +08:00
def save_document(self, record_id: int, user_id: str, document: dict) -> dict:
"""保存可编辑海报文档,不覆盖历史导出文件。"""
record = PosterRecord.query.get(record_id)
if not record or record.user_id != user_id:
return {"code": 1002, "message": "记录不存在", "data": None}
record.document_revision = (record.document_revision or 1) + 1
document = dict(document or {})
document["revision"] = record.document_revision
record.document_json = json.dumps(document, ensure_ascii=False)
record.draft_revision = (record.draft_revision or 1) + 1
db.session.commit()
return {"code": 0, "data": record.to_dict()}
def save_rendered(self, record_id: int, user_id: str, file, document: dict) -> dict:
"""保存浏览器合成后的最终 PNG并绑定同一文档版本。"""
record = PosterRecord.query.get(record_id)
if not record or record.user_id != user_id:
return {"code": 1002, "message": "记录不存在", "data": None}
if not file:
return {"code": 1002, "message": "请上传最终海报文件", "data": None}
image_bytes = file.read()
if len(image_bytes) > 30 * 1024 * 1024:
return {"code": 1002, "message": "海报文件不能超过 30MB", "data": None}
if not image_bytes.startswith(b"\x89PNG\r\n\x1a\n"):
return {"code": 1002, "message": "仅支持 PNG 海报文件", "data": None}
from insurance.config import get_storage_root
output_dir = os.path.join(
get_storage_root(), "outputs", "posters", str(record.latest_task_id or record.id)
)
os.makedirs(output_dir, exist_ok=True)
revision = (record.document_revision or 1) + 1
filepath = os.path.join(output_dir, f"final_v{revision}.png")
with open(filepath, "wb") as output:
output.write(image_bytes)
document = dict(document or {})
document["revision"] = revision
record.document_json = json.dumps(document, ensure_ascii=False)
record.document_revision = revision
record.export_url = filepath
record.export_format = "png"
record.draft_revision = (record.draft_revision or 1) + 1
record.generated_revision = record.draft_revision
db.session.commit()
return {"code": 0, "data": record.to_dict()}