"""海报功能路由。""" import os from flask import Blueprint, request, jsonify, send_file from insurance.middleware.auth_middleware import jwt_required from insurance.utils.response import success, error, ErrorCode poster_bp = Blueprint("poster", __name__) poster_service = None def _get_service(): global poster_service if poster_service is None: from insurance.poster.service import PosterService poster_service = PosterService() return poster_service def _user_manual_upload_enabled() -> bool: from insurance.config import get_config return get_config("POSTER_USER_MANUAL_UPLOAD_ENABLED", "true").strip().lower() in ( "1", "true", "yes", "on", ) # ─── 健康检查 ───────────────────────────────────────────── @poster_bp.route("/health", methods=["GET"]) def health(): """海报模块健康检查:reviewed 产品、模板、文案模板、模型配置。""" from flask import jsonify checks = {} critical_missing = [] try: from insurance.models.ppt_config import PptProduct count = PptProduct.query.filter( PptProduct.manual_parse_status == "reviewed", PptProduct.status == 1, ).count() checks["reviewed_products"] = count if count == 0: critical_missing.append("reviewed_products") except Exception as e: checks["reviewed_products"] = f"error: {e}" critical_missing.append("reviewed_products") try: from insurance.models.poster_template_model import PosterTemplate count = PosterTemplate.query.filter_by(status=1).count() checks["poster_templates"] = count if count == 0: critical_missing.append("poster_templates") except Exception as e: checks["poster_templates"] = f"error: {e}" critical_missing.append("poster_templates") try: from insurance.models.poster_copy_template import PosterCopyTemplate count = PosterCopyTemplate.query.filter_by(status=1).count() checks["copy_templates"] = count if count == 0: critical_missing.append("copy_templates") except Exception as e: checks["copy_templates"] = f"error: {e}" critical_missing.append("copy_templates") try: from insurance.models.system_setting import SystemSetting keys = ["poster_image_provider", "poster_image_api_key"] settings = {s.key: s.value for s in SystemSetting.query.filter(SystemSetting.key.in_(keys)).all()} checks["image_model_configured"] = bool(settings.get("poster_image_api_key")) except Exception as e: checks["image_model_configured"] = f"error: {e}" if critical_missing: checks["status"] = "degraded" checks["missing"] = critical_missing return jsonify(checks), 503 checks["status"] = "ok" return success(checks) # ─── 产品列表 ───────────────────────────────────────────── @poster_bp.route("/products", methods=["GET"]) @jwt_required def list_products(): """获取已 reviewed 的产品列表。""" return jsonify(_get_service().get_reviewed_products()) @poster_bp.route("/product-sources", methods=["GET"]) @jwt_required def list_product_sources(): """获取公共产品和当前用户已确认的私有产品资料。""" user_id = str(getattr(request, "user_id", "guest")) from insurance.poster.product_material_service import ProductMaterialService manual_enabled = _user_manual_upload_enabled() library_result = _get_service().get_reviewed_products() return jsonify({ "code": 0, "message": "success", "data": { "libraryProducts": library_result.get("data", []), "myMaterials": ( ProductMaterialService().list_available(user_id) if manual_enabled else [] ), "userManualUploadEnabled": manual_enabled, }, }) # ─── 用户产品小册子 ─────────────────────────────────────── @poster_bp.route("/product-materials", methods=["GET"]) @jwt_required def list_product_materials(): user_id = str(getattr(request, "user_id", "guest")) from insurance.poster.product_material_service import ProductMaterialService if not _user_manual_upload_enabled(): return jsonify({"code": 0, "data": {"total": 0, "items": []}}) return jsonify(ProductMaterialService().list_materials(user_id, { "page": request.args.get("page", 1, type=int), "page_size": request.args.get("page_size", 50, type=int), "search": request.args.get("search", ""), })) @poster_bp.route("/product-materials", methods=["POST"]) @jwt_required def upload_product_material(): user_id = str(getattr(request, "user_id", "guest")) file = request.files.get("file") from insurance.poster.product_material_service import ProductMaterialService if not _user_manual_upload_enabled(): return error(4108, "用户上传产品小册子功能暂未开放", 403) return jsonify(ProductMaterialService().upload( user_id=user_id, file=file, password=request.form.get("password", ""), company_id=request.form.get("companyId", ""), plan_type=request.form.get("planType", ""), )) @poster_bp.route("/product-materials/", methods=["GET"]) @jwt_required def get_product_material(material_id): user_id = str(getattr(request, "user_id", "guest")) from insurance.poster.product_material_service import ProductMaterialService return jsonify(ProductMaterialService().get(material_id, user_id)) @poster_bp.route("/product-materials//confirm", methods=["PUT"]) @jwt_required def confirm_product_material(material_id): user_id = str(getattr(request, "user_id", "guest")) from insurance.poster.product_material_service import ProductMaterialService return jsonify(ProductMaterialService().confirm( material_id, user_id, request.get_json(silent=True) or {}, )) @poster_bp.route("/product-materials//retry", methods=["POST"]) @jwt_required def retry_product_material(material_id): user_id = str(getattr(request, "user_id", "guest")) from insurance.poster.product_material_service import ProductMaterialService return jsonify(ProductMaterialService().retry(material_id, user_id)) @poster_bp.route("/product-materials//submit-review", methods=["POST"]) @jwt_required def submit_product_material_review(material_id): user_id = str(getattr(request, "user_id", "guest")) from insurance.poster.product_material_service import ProductMaterialService return jsonify(ProductMaterialService().submit_review(material_id, user_id)) @poster_bp.route("/product-materials/", methods=["DELETE"]) @jwt_required def delete_product_material(material_id): user_id = str(getattr(request, "user_id", "guest")) from insurance.poster.product_material_service import ProductMaterialService return jsonify(ProductMaterialService().delete(material_id, user_id)) @poster_bp.route("/product-materials//file", methods=["GET"]) @jwt_required def get_product_material_file(material_id): user_id = str(getattr(request, "user_id", "guest")) from insurance.poster.product_material_service import ProductMaterialService material, filepath = ProductMaterialService().get_file(material_id, user_id) if not material: return error(4106, "产品资料不存在或无权访问", 404) if not filepath: return error(4107, "小册子原文件不存在", 404) return send_file( filepath, mimetype="application/pdf", as_attachment=False, download_name=material.original_name, ) # ─── 计划书上传 ─────────────────────────────────────────── @poster_bp.route("/case-upload", methods=["POST"]) @jwt_required def upload_case(): """上传计划书 PDF + 触发解析。""" user_id = str(getattr(request, "user_id", "guest")) product_id = request.form.get("productId", "") product_source = { "type": request.form.get("productSourceType", "") or ( "library_product" if product_id else "" ), "id": request.form.get("productSourceId", "") or product_id, } password = request.form.get("password", "") file = request.files.get("file") if not file: return error(ErrorCode.PARAM_ERROR, "请上传文件") return jsonify(_get_service().upload_case( user_id, product_id, file, password, product_source=product_source, )) @poster_bp.route("/case-upload/", methods=["GET"]) @jwt_required def get_case_upload(record_id): """获取解析结果。""" user_id = str(getattr(request, "user_id", "guest")) return jsonify(_get_service().get_case_upload(record_id, user_id)) @poster_bp.route("/case-upload//confirm", methods=["PUT"]) @jwt_required def confirm_case_upload(record_id): """核对/修正解析数据。""" user_id = str(getattr(request, "user_id", "guest")) return jsonify(_get_service().confirm_case_upload(record_id, user_id, request.get_json(silent=True) or {})) # ─── 模板列表 ───────────────────────────────────────────── @poster_bp.route("/templates", methods=["GET"]) @jwt_required def list_templates(): """获取可用海报模板列表。""" return jsonify(_get_service().get_templates()) @poster_bp.route("/copy-templates", methods=["GET"]) @jwt_required def list_copy_templates(): """获取可用文案模板列表。""" return jsonify(_get_service().get_copy_templates()) # ─── 文案生成 ───────────────────────────────────────────── @poster_bp.route("/generate-copy", methods=["POST"]) @jwt_required def generate_copy(): """生成文案(template/ai 模式)。""" user_id = str(getattr(request, "user_id", "guest")) return jsonify(_get_service().generate_copy(user_id, request.get_json(silent=True) or {})) @poster_bp.route("/compliance-check", methods=["POST"]) @jwt_required def check_compliance(): """检查海报文案并返回字符级问题位置。""" return jsonify(_get_service().check_compliance(request.get_json(silent=True) or {})) # ─── 海报生成 ───────────────────────────────────────────── @poster_bp.route("/generate", methods=["POST"]) @jwt_required def generate_poster(): """生成海报图片。""" user_id = str(getattr(request, "user_id", "guest")) return jsonify(_get_service().generate_poster(user_id, request.get_json(silent=True) or {})) # ─── 下载海报 ───────────────────────────────────────────── @poster_bp.route("/download/", methods=["GET"]) @jwt_required def download_poster(record_id): """下载海报文件。""" from insurance.models.poster_record import PosterRecord from insurance.config import get_storage_root user_id = str(getattr(request, "user_id", "guest")) record = PosterRecord.query.get(record_id) if not record or record.user_id != user_id: return error(ErrorCode.NOT_FOUND, "记录不存在") if not record.export_url: return error(ErrorCode.NOT_FOUND, "文件不存在") # 路径安全校验:仅允许 outputs/posters 目录 file_abs = os.path.abspath(record.export_url) posters_dir = os.path.abspath(os.path.join(get_storage_root(), "outputs", "posters")) if not file_abs.startswith(posters_dir) or not os.path.exists(file_abs): return error(ErrorCode.NOT_FOUND, "文件不存在") return send_file(file_abs, as_attachment=True, download_name=f"poster_{record_id}.png", mimetype="image/png") # ─── 记录列表 ───────────────────────────────────────────── @poster_bp.route("/records", methods=["GET"]) @jwt_required def list_records(): """当前用户的海报生成记录。""" user_id = str(getattr(request, "user_id", "guest")) params = { "page": request.args.get("page", 1, type=int), "page_size": request.args.get("page_size", 20, type=int), } return jsonify(_get_service().list_records(user_id, params)) @poster_bp.route("/records/", methods=["GET"]) @jwt_required def get_record(record_id): """记录详情。""" user_id = str(getattr(request, "user_id", "guest")) return jsonify(_get_service().get_record(record_id, user_id)) @poster_bp.route("/records//document", methods=["GET", "PUT"]) @jwt_required def poster_document(record_id): """读取或保存可编辑海报文档。""" user_id = str(getattr(request, "user_id", "guest")) if request.method == "PUT": payload = request.get_json(silent=True) or {} return jsonify(_get_service().save_document( record_id, user_id, payload.get("document") or payload )) result = _get_service().get_record(record_id, user_id) if result.get("code") != 0: return jsonify(result) record = result["data"] return jsonify({ "code": 0, "message": "success", "data": { "document": record.get("document"), "revision": record.get("documentRevision"), }, }) @poster_bp.route("/records//rendered", methods=["POST"]) @jwt_required def save_rendered_poster(record_id): """保存前端 HTML 画布合成后的最终 PNG。""" import json user_id = str(getattr(request, "user_id", "guest")) try: document = json.loads(request.form.get("document") or "{}") except (json.JSONDecodeError, TypeError): return error(ErrorCode.PARAM_ERROR, "海报文档格式错误") return jsonify(_get_service().save_rendered( record_id, user_id, request.files.get("file"), document )) @poster_bp.route("/records//background", methods=["GET"]) @jwt_required def get_poster_background(record_id): """读取 AI 背景资产;兼容迁移前将背景写入 export_url 的记录。""" from insurance.models.poster_record import PosterRecord from insurance.config import get_storage_root user_id = str(getattr(request, "user_id", "guest")) record = PosterRecord.query.get(record_id) if not record or record.user_id != user_id: return error(ErrorCode.NOT_FOUND, "记录不存在") filepath = record.background_file_url or record.export_url file_abs = os.path.abspath(filepath or "") posters_dir = os.path.abspath(os.path.join(get_storage_root(), "outputs", "posters")) if not file_abs.startswith(posters_dir) or not os.path.exists(file_abs): return error(ErrorCode.NOT_FOUND, "背景文件不存在") return send_file(file_abs, as_attachment=False, mimetype="image/png")