baodan/api/insurance/admin/ppt_admin_routes.py
wsb1224 b8c4e8b672 主要完成内容:
修复 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

479 lines
17 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.

"""PPT/海报管理后台路由"""
import os
from flask import Blueprint, request, jsonify, send_file
from insurance.admin.ppt_admin_service import PptAdminService
from insurance.middleware.auth_middleware import permission_required, jwt_required
ppt_admin_bp = Blueprint("ppt_admin", __name__)
ppt_admin_service = PptAdminService()
def _is_within(root: str, path: str) -> bool:
"""检查文件是否位于指定目录内,兼容 Windows 不同盘符。"""
try:
return os.path.commonpath([root, path]) == root
except ValueError:
return False
# ---- 保司管理 ----
@ppt_admin_bp.route("/companies", methods=["GET"])
@jwt_required
def list_companies():
params = {
"page": request.args.get("page", 1, type=int),
"page_size": request.args.get("page_size", 20, type=int),
"status": request.args.get("status", type=int),
"keyword": request.args.get("keyword", ""),
}
return jsonify(ppt_admin_service.list_companies(params))
@ppt_admin_bp.route("/companies", methods=["POST"])
@permission_required("config_manage")
def create_company():
return jsonify(ppt_admin_service.create_company(request.get_json(force=True)))
@ppt_admin_bp.route("/companies/<company_id>", methods=["PUT"])
@permission_required("config_manage")
def update_company(company_id):
return jsonify(ppt_admin_service.update_company(
company_id, request.get_json(force=True), str(getattr(request, "user_id", "system"))
))
@ppt_admin_bp.route("/companies/<company_id>/status", methods=["PUT"])
@permission_required("config_manage")
def update_company_status(company_id):
return jsonify(ppt_admin_service.update_company_status(company_id, request.get_json(force=True)))
@ppt_admin_bp.route("/companies/<company_id>", methods=["DELETE"])
@permission_required("config_manage")
def delete_company(company_id):
return jsonify(ppt_admin_service.delete_company(
company_id, str(getattr(request, "user_id", "system"))
))
# ---- 保司Logo管理 ----
@ppt_admin_bp.route("/companies/<company_id>/logos", methods=["GET"])
@jwt_required
def list_company_logos(company_id):
return jsonify(ppt_admin_service.list_company_logos(company_id))
@ppt_admin_bp.route("/companies/<company_id>/logos", methods=["POST"])
@permission_required("config_manage")
def upload_company_logos(company_id):
files = request.files.getlist("files")
if not files:
return jsonify({"code": 1001, "message": "请选择图片文件", "data": None})
return jsonify(ppt_admin_service.upload_company_logos(company_id, files))
@ppt_admin_bp.route("/companies/<company_id>/logos/<logo_id>/primary", methods=["PUT"])
@permission_required("config_manage")
def set_primary_logo(company_id, logo_id):
return jsonify(ppt_admin_service.set_primary_logo(company_id, logo_id))
@ppt_admin_bp.route("/companies/<company_id>/logos/order", methods=["PUT"])
@permission_required("config_manage")
def reorder_logos(company_id):
data = request.get_json(force=True)
return jsonify(ppt_admin_service.reorder_logos(company_id, data.get("order", [])))
@ppt_admin_bp.route("/companies/<company_id>/logos/<logo_id>", methods=["DELETE"])
@permission_required("config_manage")
def delete_company_logo(company_id, logo_id):
return jsonify(ppt_admin_service.delete_company_logo(company_id, logo_id))
@ppt_admin_bp.route("/assets/company-logos/<logo_id>", methods=["GET"])
def serve_company_logo(logo_id):
"""提供Logo图片文件访问。"""
from insurance.models.ppt_config import CompanyLogo
logo = CompanyLogo.query.get(logo_id)
if not logo or not logo.file_path or not os.path.exists(logo.file_path):
return jsonify({"code": 404, "message": "图片不存在", "data": None}), 404
from insurance.config import get_storage_root
logo_root = os.path.abspath(os.path.join(get_storage_root(), "uploads", "company-logos"))
logo_path = os.path.abspath(logo.file_path)
if not _is_within(logo_root, logo_path):
return jsonify({"code": 404, "message": "图片不存在", "data": None}), 404
return send_file(
logo_path,
mimetype=logo.mime_type or "image/png",
as_attachment=False,
)
# ---- 产品管理 ----
@ppt_admin_bp.route("/products", methods=["GET"])
@jwt_required
def list_products():
params = {
"page": request.args.get("page", 1, type=int),
"page_size": request.args.get("page_size", 20, type=int),
"company_id": request.args.get("company_id", ""),
"plan_type": request.args.get("plan_type", ""),
"status": request.args.get("status", type=int),
}
return jsonify(ppt_admin_service.list_products(params))
@ppt_admin_bp.route("/products", methods=["POST"])
@permission_required("config_manage")
def create_product():
return jsonify(ppt_admin_service.create_product(request.get_json(force=True)))
@ppt_admin_bp.route("/products/<product_id>", methods=["PUT"])
@permission_required("config_manage")
def update_product(product_id):
return jsonify(ppt_admin_service.update_product(
product_id, request.get_json(force=True), str(getattr(request, "user_id", "system"))
))
@ppt_admin_bp.route("/products/<product_id>/status", methods=["PUT"])
@permission_required("config_manage")
def update_product_status(product_id):
return jsonify(ppt_admin_service.update_product_status(product_id, request.get_json(force=True)))
@ppt_admin_bp.route("/products/<product_id>", methods=["DELETE"])
@permission_required("config_manage")
def delete_product(product_id):
return jsonify(ppt_admin_service.delete_product(
product_id, str(getattr(request, "user_id", "system"))
))
@ppt_admin_bp.route("/products/<product_id>/manual/upload", methods=["POST"])
@permission_required("config_manage")
def upload_manual(product_id):
file = request.files.get("file")
password = request.form.get("password", "")
if not file:
return jsonify({"code": 1001, "message": "请上传文件", "data": None})
return jsonify(ppt_admin_service.upload_manual(product_id, file, password))
@ppt_admin_bp.route("/products/<product_id>/manual/parse", methods=["POST"])
@permission_required("config_manage")
def parse_manual(product_id):
return jsonify(ppt_admin_service.parse_manual(product_id))
@ppt_admin_bp.route("/products/<product_id>/manual/review", methods=["PUT"])
@permission_required("config_manage")
def review_manual(product_id):
data = request.get_json(force=True)
if data.get("confirm") and not data.get("reviewedBy"):
data["reviewedBy"] = str(getattr(request, "user_id", ""))
return jsonify(ppt_admin_service.review_manual(product_id, data))
@ppt_admin_bp.route("/products/<product_id>/manual", methods=["GET"])
@jwt_required
def get_manual(product_id):
return jsonify(ppt_admin_service.get_manual(product_id))
@ppt_admin_bp.route("/products/<product_id>/manual/file", methods=["GET"])
def get_manual_file(product_id):
"""流式返回小册子 PDF 文件(支持 ?token= 以兼容 iframe/window.open"""
# 手动认证:支持 Authorization header 或 ?token= query param
from insurance.middleware.auth_middleware import jwt_required as _jwt_decorator
# 将 query param token 注入到 environ 中,供 jwt_required 读取
if not request.headers.get("Authorization") and request.args.get("token"):
request.environ["HTTP_AUTHORIZATION"] = f"Bearer {request.args.get('token')}"
# 用装饰器包装内联函数执行认证
@_jwt_decorator
def _check_and_serve():
from insurance.models.ppt_config import PptProduct
product = PptProduct.query.get(product_id)
if not product or not product.manual_file_url:
return jsonify({"code": 1002, "message": "小册子文件不存在", "data": None}), 404
if not os.path.exists(product.manual_file_url):
return jsonify({"code": 1002, "message": "小册子文件已被删除", "data": None}), 404
from insurance.config import get_storage_root
manual_root = os.path.abspath(os.path.join(get_storage_root(), "uploads", "product-manuals"))
manual_path = os.path.abspath(product.manual_file_url)
if not _is_within(manual_root, manual_path):
return jsonify({"code": 1002, "message": "小册子文件不存在", "data": None}), 404
return send_file(
manual_path,
mimetype="application/pdf",
as_attachment=False,
download_name=f"{product_id}_manual.pdf",
)
return _check_and_serve()
# ---- PPT 模板管理 ----
@ppt_admin_bp.route("/scenarios", methods=["GET"])
@jwt_required
def list_scenarios():
return jsonify(ppt_admin_service.list_scenarios())
@ppt_admin_bp.route("/scenarios", methods=["POST"])
@permission_required("config_manage")
def create_scenario():
return jsonify(ppt_admin_service.create_scenario(request.get_json(force=True)))
@ppt_admin_bp.route("/scenarios/<code>", methods=["PUT"])
@permission_required("config_manage")
def update_scenario(code):
return jsonify(ppt_admin_service.update_scenario(code, request.get_json(force=True)))
@ppt_admin_bp.route("/scenarios/<code>", methods=["DELETE"])
@permission_required("config_manage")
def delete_scenario(code):
return jsonify(ppt_admin_service.delete_scenario(
code, str(getattr(request, "user_id", "system"))
))
@ppt_admin_bp.route("/templates", methods=["GET"])
@jwt_required
def list_templates():
params = {
"page": request.args.get("page", 1, type=int),
"page_size": request.args.get("page_size", 20, type=int),
"plan_type": request.args.get("plan_type", ""),
"status": request.args.get("status", type=int),
}
return jsonify(ppt_admin_service.list_templates(params))
@ppt_admin_bp.route("/templates", methods=["POST"])
@permission_required("config_manage")
def create_template():
return jsonify(ppt_admin_service.create_template(request.get_json(force=True)))
@ppt_admin_bp.route("/templates/upload", methods=["POST"])
@permission_required("config_manage")
def upload_template():
"""上传并自动解析 PPTX 模板。"""
return jsonify(ppt_admin_service.upload_template(
request.files.get("file"),
request.form.to_dict(),
))
@ppt_admin_bp.route("/templates/<template_id>/asset", methods=["POST"])
@permission_required("config_manage")
def replace_template_asset(template_id):
"""替换模板源 PPTX 并重新解析页面结构。"""
return jsonify(ppt_admin_service.upload_template(
request.files.get("file"),
request.form.to_dict(),
template_id=template_id,
))
@ppt_admin_bp.route("/templates/<template_id>/file", methods=["GET"])
@jwt_required
def get_template_file(template_id):
"""查看或下载模板源 PPTX。"""
from insurance.models.ppt_config import PptTemplate
from insurance.ppt.template_asset_service import resolve_template_asset
template = PptTemplate.query.get(template_id)
if not template or not template.source_template_asset_id:
return jsonify({"code": 1002, "message": "模板文件不存在", "data": None}), 404
try:
file_path = resolve_template_asset(template.source_template_asset_id)
except ValueError:
file_path = None
if not file_path or not os.path.isfile(file_path):
return jsonify({"code": 1002, "message": "模板文件不存在", "data": None}), 404
return send_file(
file_path,
mimetype="application/vnd.openxmlformats-officedocument.presentationml.presentation",
as_attachment=False,
download_name=f"{template_id}.pptx",
)
@ppt_admin_bp.route("/templates/<template_id>", methods=["PUT"])
@permission_required("config_manage")
def update_template(template_id):
return jsonify(ppt_admin_service.update_template(template_id, request.get_json(force=True)))
@ppt_admin_bp.route("/templates/<template_id>/status", methods=["PUT"])
@permission_required("config_manage")
def update_template_status(template_id):
return jsonify(ppt_admin_service.update_template_status(template_id, request.get_json(force=True)))
@ppt_admin_bp.route("/templates/<template_id>", methods=["DELETE"])
@permission_required("config_manage")
def delete_template(template_id):
return jsonify(ppt_admin_service.delete_template(
template_id, str(getattr(request, "user_id", "system"))
))
# ---- 海报模板管理 ----
@ppt_admin_bp.route("/poster-templates", methods=["GET"])
@jwt_required
def list_poster_templates():
params = {
"page": request.args.get("page", 1, type=int),
"page_size": request.args.get("page_size", 20, type=int),
"status": request.args.get("status", type=int),
}
return jsonify(ppt_admin_service.list_poster_templates(params))
@ppt_admin_bp.route("/poster-templates", methods=["POST"])
@permission_required("config_manage")
def create_poster_template():
return jsonify(ppt_admin_service.create_poster_template(request.get_json(force=True)))
@ppt_admin_bp.route("/poster-templates/<int:template_id>", methods=["PUT"])
@permission_required("config_manage")
def update_poster_template(template_id):
return jsonify(ppt_admin_service.update_poster_template(template_id, request.get_json(force=True)))
@ppt_admin_bp.route("/poster-templates/<int:template_id>/status", methods=["PUT"])
@permission_required("config_manage")
def update_poster_template_status(template_id):
return jsonify(ppt_admin_service.update_poster_template_status(template_id, request.get_json(force=True)))
# ---- 文案模板管理 ----
@ppt_admin_bp.route("/copy-templates", methods=["GET"])
@jwt_required
def list_copy_templates():
params = {
"page": request.args.get("page", 1, type=int),
"page_size": request.args.get("page_size", 20, type=int),
"status": request.args.get("status", type=int),
}
return jsonify(ppt_admin_service.list_copy_templates(params))
@ppt_admin_bp.route("/copy-templates", methods=["POST"])
@permission_required("config_manage")
def create_copy_template():
return jsonify(ppt_admin_service.create_copy_template(request.get_json(force=True)))
@ppt_admin_bp.route("/copy-templates/<int:template_id>", methods=["PUT"])
@permission_required("config_manage")
def update_copy_template(template_id):
return jsonify(ppt_admin_service.update_copy_template(template_id, request.get_json(force=True)))
@ppt_admin_bp.route("/copy-templates/<int:template_id>", methods=["DELETE"])
@permission_required("config_manage")
def delete_copy_template(template_id):
return jsonify(ppt_admin_service.delete_copy_template(
template_id, str(getattr(request, "user_id", "system"))
))
# ---- 历史记录管理(需要审计权限) ----
@ppt_admin_bp.route("/history", methods=["GET"])
@permission_required("audit_view")
def list_history():
params = {
"page": request.args.get("page", 1, type=int),
"page_size": request.args.get("page_size", 20, type=int),
"user_id": request.args.get("user_id", ""),
"company_id": request.args.get("company_id", ""),
"action_type": request.args.get("action_type", ""),
}
return jsonify(ppt_admin_service.list_history(params))
@ppt_admin_bp.route("/history/<int:history_id>", methods=["DELETE"])
@permission_required("config_manage")
def delete_history(history_id):
return jsonify(ppt_admin_service.delete_history(history_id))
@ppt_admin_bp.route("/history/export", methods=["GET"])
@permission_required("audit_view")
def export_history():
from flask import Response
params = {
"user_id": request.args.get("user_id", ""),
"company_id": request.args.get("company_id", ""),
"action_type": request.args.get("action_type", ""),
}
csv_content = ppt_admin_service.export_history_csv(params)
return Response(
csv_content,
mimetype="text/csv",
headers={"Content-Disposition": "attachment; filename=ppt_history.csv"},
)
# ---- 系统配置 ----
@ppt_admin_bp.route("/available-models", methods=["GET"])
@permission_required("config_manage")
def get_available_models():
return jsonify(ppt_admin_service.get_available_models())
@ppt_admin_bp.route("/settings", methods=["GET"])
@permission_required("config_manage")
def get_settings():
return jsonify(ppt_admin_service.get_settings())
@ppt_admin_bp.route("/settings", methods=["PUT"])
@permission_required("config_manage")
def update_settings():
updated_by = getattr(request, "user_id", "")
return jsonify(ppt_admin_service.update_settings(request.get_json(force=True), updated_by))
@ppt_admin_bp.route("/sync-models", methods=["POST"])
@permission_required("config_manage")
def sync_models():
data = request.get_json(force=True)
return jsonify(ppt_admin_service.sync_models(
provider=data.get("provider", ""),
api_key=data.get("api_key", ""),
base_url=data.get("base_url", ""),
))
# ---- 数据清理 ----
@ppt_admin_bp.route("/cleanup", methods=["POST"])
@permission_required("config_manage")
def run_cleanup():
"""手动触发过期数据清理。"""
from insurance.utils.cleanup import cleanup_expired_data
dry_run = request.args.get("dry_run", "false").lower() == "true"
result = cleanup_expired_data(dry_run=dry_run)
return jsonify({"code": 0, "data": result})