baodan/api/insurance/admin/ppt_admin_service.py
wsb1224 6d571f56a8 fix: auto-fetch Dify models using existing API keys as fallback
- Backend: try DIFY_CHAT/WORKFLOW_API_KEY when workspace key is missing
- Frontend: always show '从 Dify 获取' button, not just after first fetch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:48:37 +08:00

566 lines
25 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 json
import logging
import os
import uuid
from flask import current_app, request
from insurance.db.compat import db
from insurance.models.ppt_config import PptCompany, PptProduct, PptTemplate
from insurance.models.ppt_history import PptHistory
from insurance.models.poster_template_model import PosterTemplate
from insurance.models.poster_copy_template import PosterCopyTemplate
from insurance.models.poster_record import PosterRecord
from insurance.models.system_setting import SystemSetting
from sqlalchemy import or_
logger = logging.getLogger(__name__)
def _safe_page_params(params: dict) -> tuple[int, int]:
"""安全获取分页参数,防止负数和超大值。"""
page = max(1, params.get("page", 1))
page_size = min(100, max(1, params.get("page_size", 20)))
return page, page_size
class PptAdminService:
"""PPT/海报管理后台业务逻辑。"""
# ---- 保司管理 ----
def list_companies(self, params: dict) -> dict:
query = db.session.query(PptCompany)
if params.get("status") is not None:
query = query.filter(PptCompany.status == params["status"])
if params.get("keyword"):
kw = f"%{params['keyword']}%"
query = query.filter(or_(
PptCompany.display_name.ilike(kw),
PptCompany.name_zh.ilike(kw),
PptCompany.name_en.ilike(kw),
))
query = query.order_by(PptCompany.sort_order.asc(), PptCompany.id.asc())
page, page_size = _safe_page_params(params)
total = query.count()
items = query.offset((page - 1) * page_size).limit(page_size).all()
return {
"code": 0,
"data": {
"total": total,
"items": [c.to_dict() for c in items],
},
}
def create_company(self, data: dict) -> dict:
if not data.get("id") or not data.get("displayName"):
return {"code": 1001, "message": "id 和 displayName 必填", "data": None}
if PptCompany.query.get(data["id"]):
return {"code": 1001, "message": "公司 ID 已存在", "data": None}
company = PptCompany(
id=data["id"],
display_name=data["displayName"],
aliases_json=json.dumps(data.get("aliases", []), ensure_ascii=False),
name_zh=data.get("nameZh"),
name_en=data.get("nameEn"),
short_en=data.get("shortEn"),
logo_url=data.get("logoUrl"),
company_intro=data.get("companyIntro"),
status=data.get("status", 1),
sort_order=data.get("sortOrder", 0),
)
db.session.add(company)
db.session.commit()
return {"code": 0, "data": company.to_dict()}
def update_company(self, company_id: str, data: dict) -> dict:
company = PptCompany.query.get(company_id)
if not company:
return {"code": 1002, "message": "公司不存在", "data": None}
if "displayName" in data:
company.display_name = data["displayName"]
if "aliases" in data:
company.aliases_json = json.dumps(data["aliases"], ensure_ascii=False)
if "nameZh" in data:
company.name_zh = data["nameZh"]
if "nameEn" in data:
company.name_en = data["nameEn"]
if "shortEn" in data:
company.short_en = data["shortEn"]
if "logoUrl" in data:
company.logo_url = data["logoUrl"]
if "companyIntro" in data:
company.company_intro = data["companyIntro"]
if "status" in data:
company.status = data["status"]
if "sortOrder" in data:
company.sort_order = data["sortOrder"]
db.session.commit()
return {"code": 0, "data": company.to_dict()}
def update_company_status(self, company_id: str, data: dict) -> dict:
company = PptCompany.query.get(company_id)
if not company:
return {"code": 1002, "message": "公司不存在", "data": None}
company.status = data.get("status", 1)
db.session.commit()
return {"code": 0, "data": company.to_dict()}
# ---- 产品管理 ----
def list_products(self, params: dict) -> dict:
query = db.session.query(PptProduct)
if params.get("company_id"):
query = query.filter(PptProduct.company_id == params["company_id"])
if params.get("plan_type"):
query = query.filter(PptProduct.plan_type == params["plan_type"])
if params.get("status") is not None:
query = query.filter(PptProduct.status == params["status"])
query = query.order_by(PptProduct.sort_order.asc(), PptProduct.id.asc())
page, page_size = _safe_page_params(params)
total = query.count()
items = query.offset((page - 1) * page_size).limit(page_size).all()
return {
"code": 0,
"data": {
"total": total,
"items": [p.to_dict() for p in items],
},
}
def create_product(self, data: dict) -> dict:
if not data.get("id") or not data.get("displayName"):
return {"code": 1001, "message": "id 和 displayName 必填", "data": None}
if PptProduct.query.get(data["id"]):
return {"code": 1001, "message": "产品 ID 已存在", "data": None}
product = PptProduct(
id=data["id"],
company_id=data.get("companyId", ""),
plan_type=data.get("planType", "savings"),
display_name=data["displayName"],
aliases_json=json.dumps(data.get("aliases", []), ensure_ascii=False),
required_modules_json=json.dumps(data.get("requiredModules", []), ensure_ascii=False),
product_code=data.get("productCode"),
product_type=data.get("productType"),
coverage_period=data.get("coveragePeriod"),
payment_period=data.get("paymentPeriod"),
insured_age_range=data.get("insuredAgeRange"),
waiting_period=data.get("waitingPeriod"),
highlights=json.dumps(data.get("highlights", []), ensure_ascii=False) if data.get("highlights") else None,
extra_fields=json.dumps(data.get("extraFields", {}), ensure_ascii=False) if data.get("extraFields") else None,
status=data.get("status", 1),
sort_order=data.get("sortOrder", 0),
)
db.session.add(product)
db.session.commit()
return {"code": 0, "data": product.to_dict()}
def update_product(self, product_id: str, data: dict) -> dict:
product = PptProduct.query.get(product_id)
if not product:
return {"code": 1002, "message": "产品不存在", "data": None}
simple_fields = {
"displayName": "display_name", "companyId": "company_id",
"planType": "plan_type", "productCode": "product_code",
"productType": "product_type", "coveragePeriod": "coverage_period",
"paymentPeriod": "payment_period", "insuredAgeRange": "insured_age_range",
"waitingPeriod": "waiting_period", "status": "status", "sortOrder": "sort_order",
}
for key, attr in simple_fields.items():
if key in data:
setattr(product, attr, data[key])
json_fields = {
"aliases": "aliases_json", "requiredModules": "required_modules_json",
"highlights": "highlights", "extraFields": "extra_fields",
}
for key, attr in json_fields.items():
if key in data:
setattr(product, attr, json.dumps(data[key], ensure_ascii=False) if data[key] else None)
db.session.commit()
return {"code": 0, "data": product.to_dict()}
def update_product_status(self, product_id: str, data: dict) -> dict:
product = PptProduct.query.get(product_id)
if not product:
return {"code": 1002, "message": "产品不存在", "data": None}
product.status = data.get("status", 1)
db.session.commit()
return {"code": 0, "data": product.to_dict()}
# ---- 产品小册子 ----
def upload_manual(self, product_id: str, file) -> dict:
product = PptProduct.query.get(product_id)
if not product:
return {"code": 1002, "message": "产品不存在", "data": None}
if not file.filename or not file.filename.lower().endswith(".pdf"):
return {"code": 1003, "message": "仅支持 PDF 文件", "data": None}
upload_dir = os.path.join(current_app.config.get("UPLOAD_FOLDER", "uploads"), "manuals")
os.makedirs(upload_dir, exist_ok=True)
filename = f"{product_id}_{uuid.uuid4().hex[:8]}.pdf"
filepath = os.path.join(upload_dir, filename)
file.save(filepath)
product.manual_file_url = filepath
product.manual_parse_status = "pending"
db.session.commit()
return {"code": 0, "data": {"fileUrl": filepath}}
def parse_manual(self, product_id: str) -> dict:
product = PptProduct.query.get(product_id)
if not product:
return {"code": 1002, "message": "产品不存在", "data": None}
if not product.manual_file_url:
return {"code": 1003, "message": "请先上传小册子", "data": None}
if not os.path.exists(product.manual_file_url):
return {"code": 1004, "message": "小册子文件不存在", "data": None}
product.manual_parse_status = "parsing"
db.session.commit()
# 调用 LLM 解析 PDF
try:
import asyncio
from insurance.poster.manual_parser import parse_manual_pdf
from insurance.poster.service import _run_async
result = _run_async(parse_manual_pdf(product.manual_file_url))
product.manual_parsed_rules = json.dumps(result, ensure_ascii=False)
product.manual_parse_status = "parsed"
db.session.commit()
return {"code": 0, "data": {"status": "parsed", "rules": result}}
except Exception as e:
logger.warning(f"小册子解析失败: {e}")
product.manual_parse_status = "none"
db.session.commit()
return {"code": 1005, "message": f"解析失败: {e}", "data": None}
def review_manual(self, product_id: str, data: dict) -> dict:
product = PptProduct.query.get(product_id)
if not product:
return {"code": 1002, "message": "产品不存在", "data": None}
if product.manual_parse_status not in ("parsed", "reviewed"):
return {"code": 1003, "message": "产品尚未解析,无法核对", "data": None}
if data.get("rules"):
product.manual_parsed_rules = json.dumps(data["rules"], ensure_ascii=False)
product.manual_parse_status = "reviewed"
product.manual_reviewed_by = data.get("reviewedBy", "")
from datetime import datetime
product.manual_reviewed_at = datetime.now()
db.session.commit()
return {"code": 0, "data": product.to_dict()}
# ---- PPT 模板管理 ----
def list_templates(self, params: dict) -> dict:
query = db.session.query(PptTemplate)
if params.get("plan_type"):
query = query.filter(PptTemplate.plan_type == params["plan_type"])
if params.get("status") is not None:
query = query.filter(PptTemplate.status == params["status"])
page, page_size = _safe_page_params(params)
total = query.count()
items = query.offset((page - 1) * page_size).limit(page_size).all()
return {
"code": 0,
"data": {
"total": total,
"items": [t.to_dict() for t in items],
},
}
def create_template(self, data: dict) -> dict:
if not data.get("id"):
return {"code": 1001, "message": "id 必填", "data": None}
if PptTemplate.query.get(data["id"]):
return {"code": 1001, "message": "模板 ID 已存在", "data": None}
template = PptTemplate(
id=data["id"],
plan_type=data.get("planType", "savings"),
style_preset=data.get("stylePreset", "broker"),
name=data.get("name"),
scenario_tag=data.get("scenarioTag"),
preview_image=data.get("previewImage"),
applicable_company_ids=json.dumps(data.get("applicableCompanyIds", []), ensure_ascii=False) if data.get("applicableCompanyIds") else None,
applicable_product_ids=json.dumps(data.get("applicableProductIds", []), ensure_ascii=False) if data.get("applicableProductIds") else None,
status=data.get("status", 1),
)
db.session.add(template)
db.session.commit()
return {"code": 0, "data": template.to_dict()}
def update_template(self, template_id: str, data: dict) -> dict:
template = PptTemplate.query.get(template_id)
if not template:
return {"code": 1002, "message": "模板不存在", "data": None}
for key, attr in [("name", "name"), ("scenarioTag", "scenario_tag"), ("previewImage", "preview_image")]:
if key in data:
setattr(template, attr, data[key])
if "applicableCompanyIds" in data:
template.applicable_company_ids = json.dumps(data["applicableCompanyIds"], ensure_ascii=False) if data["applicableCompanyIds"] else None
if "applicableProductIds" in data:
template.applicable_product_ids = json.dumps(data["applicableProductIds"], ensure_ascii=False) if data["applicableProductIds"] else None
if "status" in data:
template.status = data["status"]
db.session.commit()
return {"code": 0, "data": template.to_dict()}
def update_template_status(self, template_id: str, data: dict) -> dict:
template = PptTemplate.query.get(template_id)
if not template:
return {"code": 1002, "message": "模板不存在", "data": None}
template.status = data.get("status", 1)
db.session.commit()
return {"code": 0, "data": template.to_dict()}
# ---- 海报模板管理 ----
def list_poster_templates(self, params: dict) -> dict:
query = db.session.query(PosterTemplate)
if params.get("status") is not None:
query = query.filter(PosterTemplate.status == params["status"])
page, page_size = _safe_page_params(params)
total = query.count()
items = query.offset((page - 1) * page_size).limit(page_size).all()
return {
"code": 0,
"data": {
"total": total,
"items": [t.to_dict() for t in items],
},
}
def create_poster_template(self, data: dict) -> dict:
if not data.get("name") or not data.get("styleDescription"):
return {"code": 1001, "message": "name 和 styleDescription 必填", "data": None}
template = PosterTemplate(
name=data["name"],
scenario_tag=data.get("scenarioTag"),
style_description=data["styleDescription"],
color_scheme=json.dumps(data["colorScheme"], ensure_ascii=False) if data.get("colorScheme") else None,
reference_image=data.get("referenceImage"),
preview_image=data.get("previewImage"),
status=data.get("status", 1),
)
db.session.add(template)
db.session.commit()
return {"code": 0, "data": template.to_dict()}
def update_poster_template(self, template_id: int, data: dict) -> dict:
template = PosterTemplate.query.get(template_id)
if not template:
return {"code": 1002, "message": "模板不存在", "data": None}
for key, attr in [("name", "name"), ("scenarioTag", "scenario_tag"),
("styleDescription", "style_description"),
("referenceImage", "reference_image"), ("previewImage", "preview_image")]:
if key in data:
setattr(template, attr, data[key])
if "colorScheme" in data:
template.color_scheme = json.dumps(data["colorScheme"], ensure_ascii=False) if data["colorScheme"] else None
if "status" in data:
template.status = data["status"]
db.session.commit()
return {"code": 0, "data": template.to_dict()}
def update_poster_template_status(self, template_id: int, data: dict) -> dict:
template = PosterTemplate.query.get(template_id)
if not template:
return {"code": 1002, "message": "模板不存在", "data": None}
template.status = data.get("status", 1)
db.session.commit()
return {"code": 0, "data": template.to_dict()}
# ---- 文案模板管理 ----
def list_copy_templates(self, params: dict) -> dict:
query = db.session.query(PosterCopyTemplate)
if params.get("status") is not None:
query = query.filter(PosterCopyTemplate.status == params["status"])
page, page_size = _safe_page_params(params)
total = query.count()
items = query.offset((page - 1) * page_size).limit(page_size).all()
return {
"code": 0,
"data": {
"total": total,
"items": [t.to_dict() for t in items],
},
}
def create_copy_template(self, data: dict) -> dict:
if not data.get("name") or not data.get("content"):
return {"code": 1001, "message": "name 和 content 必填", "data": None}
template = PosterCopyTemplate(
name=data["name"],
scenario_tag=data.get("scenarioTag"),
content=data["content"],
variables=json.dumps(data.get("variables", []), ensure_ascii=False) if data.get("variables") else None,
status=data.get("status", 1),
)
db.session.add(template)
db.session.commit()
return {"code": 0, "data": template.to_dict()}
def update_copy_template(self, template_id: int, data: dict) -> dict:
template = PosterCopyTemplate.query.get(template_id)
if not template:
return {"code": 1002, "message": "模板不存在", "data": None}
for key, attr in [("name", "name"), ("scenarioTag", "scenario_tag"), ("content", "content")]:
if key in data:
setattr(template, attr, data[key])
if "variables" in data:
template.variables = json.dumps(data["variables"], ensure_ascii=False) if data["variables"] else None
if "status" in data:
template.status = data["status"]
db.session.commit()
return {"code": 0, "data": template.to_dict()}
def delete_copy_template(self, template_id: int) -> dict:
template = PosterCopyTemplate.query.get(template_id)
if not template:
return {"code": 1002, "message": "模板不存在", "data": None}
db.session.delete(template)
db.session.commit()
return {"code": 0, "data": None}
# ---- 历史记录管理 ----
def list_history(self, params: dict) -> dict:
query = db.session.query(PptHistory)
if params.get("user_id"):
query = query.filter(PptHistory.user_id == params["user_id"])
if params.get("company_id"):
query = query.filter(PptHistory.company_id == params["company_id"])
if params.get("action_type"):
query = query.filter(PptHistory.action_type == params["action_type"])
query = query.order_by(PptHistory.created_at.desc())
page, page_size = _safe_page_params(params)
total = query.count()
items = query.offset((page - 1) * page_size).limit(page_size).all()
return {
"code": 0,
"data": {
"total": total,
"items": [h.to_dict() for h in items],
},
}
def delete_history(self, history_id: int) -> dict:
record = PptHistory.query.get(history_id)
if not record:
return {"code": 1002, "message": "记录不存在", "data": None}
db.session.delete(record)
db.session.commit()
return {"code": 0, "data": None}
def export_history_csv(self, params: dict):
"""导出历史记录为 CSV。"""
import csv
import io
query = db.session.query(PptHistory)
if params.get("user_id"):
query = query.filter(PptHistory.user_id == params["user_id"])
if params.get("company_id"):
query = query.filter(PptHistory.company_id == params["company_id"])
if params.get("action_type"):
query = query.filter(PptHistory.action_type == params["action_type"])
query = query.order_by(PptHistory.created_at.desc())
items = query.limit(10000).all() # 限制最大导出行数
output = io.StringIO()
output.write("") # UTF-8 BOM确保 Excel 正确显示中文
writer = csv.writer(output)
writer.writerow(["ID", "用户ID", "会话ID", "操作类型", "保司ID", "产品ID", "文件地址", "时间"])
for h in items:
writer.writerow([
h.id, h.user_id, h.session_id or "", h.action_type,
h.company_id or "", h.product_id or "",
h.file_url or "", str(h.created_at) if h.created_at else "",
])
return output.getvalue()
# ---- 系统配置 ----
# 内置常用模型列表Dify 不可用时的降级方案)
_BUILTIN_MODELS = [
{"provider": "deepseek", "model": "deepseek-chat", "label": "DeepSeek Chat"},
{"provider": "deepseek", "model": "deepseek-reasoner", "label": "DeepSeek Reasoner"},
{"provider": "minimax", "model": "MiniMax-2.7-Flash", "label": "MiniMax 2.7 Flash"},
{"provider": "gemini", "model": "gemini-2.5-flash", "label": "Gemini 2.5 Flash"},
{"provider": "gemini", "model": "gemini-2.5-pro", "label": "Gemini 2.5 Pro"},
{"provider": "openai", "model": "gpt-4o", "label": "GPT-4o"},
{"provider": "openai", "model": "gpt-4o-mini", "label": "GPT-4o Mini"},
{"provider": "openai", "model": "gpt-image-1", "label": "GPT Image 1"},
]
def get_available_models(self) -> dict:
"""获取可用 LLM 模型列表,优先从 Dify 获取,失败时返回内置列表。"""
# 尝试从 Dify 获取
dify_models = self._fetch_dify_models()
if dify_models is not None:
return {"code": 0, "data": {"models": dify_models, "source": "dify"}}
return {"code": 0, "data": {"models": self._BUILTIN_MODELS, "source": "builtin"}}
def _fetch_dify_models(self) -> list | None:
"""调用 Dify API 获取可用 LLM 模型列表,失败返回 None。"""
import requests
try:
base_url = os.getenv("DIFY_BASE_URL", "http://localhost:5001")
# 收集可用的 API Key按优先级尝试
candidate_keys: list[str] = []
key_setting = SystemSetting.query.filter_by(key="dify_workspace_api_key").first()
if key_setting and key_setting.value:
candidate_keys.append(key_setting.value)
for env_key in ("DIFY_CHAT_APP_API_KEY", "DIFY_WORKFLOW_APP_API_KEY",
"BAODAN_CHAT_API_KEY", "BAODAN_WORKFLOW_API_KEY"):
val = os.getenv(env_key, "").strip()
if val:
candidate_keys.append(val)
# 去重保持顺序
seen = set()
api_keys = []
for k in candidate_keys:
if k not in seen:
seen.add(k)
api_keys.append(k)
for api_key in api_keys:
try:
resp = requests.get(
f"{base_url}/v1/workspaces/current/models/model-types/llm",
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)
if resp.status_code != 200:
continue
data = resp.json()
models = []
for item in data.get("data", []):
provider = item.get("provider", "")
for model in item.get("models", []):
model_name = model.get("model", "")
label = model.get("label", {}).get("zh_Hans", "") or model.get("label", {}).get("en_US", "") or model_name
models.append({"provider": provider, "model": model_name, "label": label})
if models:
return models
except Exception:
continue
return None
except Exception as e:
logger.warning(f"从 Dify 获取模型列表失败: {e}")
return None
def get_settings(self) -> dict:
settings = SystemSetting.query.all()
return {
"code": 0,
"data": {s.key: s.value for s in settings},
}
def update_settings(self, data: dict, updated_by: str = "") -> dict:
for key, value in data.items():
setting = SystemSetting.query.filter_by(key=key).first()
if setting:
setting.value = str(value)
setting.updated_by = updated_by
else:
db.session.add(SystemSetting(key=key, value=str(value), updated_by=updated_by))
db.session.commit()
return {"code": 0, "data": None}