baodan/api/insurance/recommend/service.py

307 lines
12 KiB
Python
Raw Normal View History

"""推荐服务:调用 BaoDan Workflow 生成推荐方案。"""
import os
import json
import re
import uuid
import requests
from flask import current_app
from insurance.db.compat import db
from insurance.models.recommendation import RecommendationRecord
class RecommendService:
"""产品推荐业务逻辑。"""
def generate(self, user_id: str, data: dict) -> dict:
"""提交推荐方案生成任务。"""
customer = data.get("customer", {})
# 保存推荐记录
record = RecommendationRecord(
user_id=user_id,
customer_name=customer.get("name", ""),
customer_age=customer.get("age"),
customer_gender=customer.get("gender", ""),
health_status=customer.get("health_status", ""),
occupation=customer.get("occupation", ""),
annual_income=customer.get("annual_income", 0),
monthly_budget=customer.get("monthly_budget", 0),
insurance_types=json.dumps(data.get("insurance_types", []), ensure_ascii=False),
coverage_amount=data.get("coverage_amount", 0),
coverage_period=data.get("coverage_period", ""),
existing_policies=json.dumps(data.get("existing_policies", []), ensure_ascii=False),
status="processing",
)
db.session.add(record)
2026-07-12 14:17:18 +08:00
try:
db.session.commit()
except Exception:
db.session.rollback()
raise
# 调用 BaoDan Workflow API
api_key = os.environ.get("BAODAN_WORKFLOW_API_KEY", "")
2026-07-12 14:17:18 +08:00
# 统一使用 DIFY_BASE_URL 环境变量(与 app.py 保持一致)
base_url = os.environ.get("DIFY_BASE_URL", os.environ.get("BAODAN_API_URL", "http://localhost:5001"))
# Workflow 期望全部 string 类型输入insurance_types 为逗号分隔字符串
insurance_types = data.get("insurance_types", [])
if isinstance(insurance_types, list):
insurance_types_str = ",".join(insurance_types)
else:
insurance_types_str = str(insurance_types)
try:
resp = requests.post(
f"{base_url}/v1/workflows/run",
json={
"inputs": {
"age": str(customer.get("age", "")),
"gender": customer.get("gender", ""),
"occupation": customer.get("occupation", ""),
"annual_income": str(customer.get("annual_income", 0)),
"monthly_budget": str(customer.get("monthly_budget", 0)),
"insurance_types": insurance_types_str,
"coverage_amount": str(data.get("coverage_amount", 0)),
"coverage_period": data.get("coverage_period", ""),
},
"response_mode": "blocking",
"user": f"user_{user_id}",
},
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
timeout=120,
)
result = resp.json()
if result.get("data", {}).get("status") == "succeeded":
output = result["data"].get("outputs", {})
# Workflow 输出 resultMarkdown 字符串)
recommendation_md = output.get("result", "") or output.get("recommendation", "")
if recommendation_md:
record.generated_plan = recommendation_md
record.plan_variants = json.dumps(
self._parse_markdown_to_plans(recommendation_md),
ensure_ascii=False,
)
record.status = "done"
else:
record.status = "failed"
record.error_message = "方案生成失败:无输出"
else:
record.status = "failed"
error_msg = result.get("message", "Workflow 执行失败")
record.error_message = error_msg
except Exception as e:
record.status = "failed"
record.error_message = str(e)
2026-07-12 14:17:18 +08:00
try:
db.session.commit()
except Exception:
db.session.rollback()
raise
from insurance.utils.audit import log_operation
log_operation(user_id, "generate", "proposal", str(record.id), {
"customer_name": customer.get("name", ""),
"insurance_types": data.get("insurance_types", []),
})
return {
"code": 0,
"data": {
"task_id": str(record.id),
"status": record.status,
},
}
def get_status(self, task_id: str) -> dict:
"""查询推荐任务状态。"""
record = db.session.query(RecommendationRecord).filter_by(id=int(task_id)).first()
if not record:
return {"code": 1005, "message": "任务不存在", "data": None}
data = {
"task_id": str(record.id),
"status": record.status,
# 客户信息
"customer_name": record.customer_name or "",
"customer_age": record.customer_age,
"customer_gender": record.customer_gender or "",
"health_status": record.health_status or "",
"occupation": record.occupation or "",
"annual_income": record.annual_income,
"monthly_budget": record.monthly_budget,
"insurance_types": record.insurance_types or "[]",
"coverage_amount": record.coverage_amount,
"coverage_period": record.coverage_period or "",
"created_at": str(record.created_at) if record.created_at else None,
}
if record.status == "done":
# 返回 Markdown 原文 + 解析后的结构化方案
plans = json.loads(record.plan_variants) if record.plan_variants else []
data["proposal"] = {
"id": str(record.id),
"recommendation": record.generated_plan or "",
"plans": plans,
}
elif record.status == "failed":
data["error_message"] = record.error_message
return {"code": 0, "data": data}
@staticmethod
def _parse_markdown_to_plans(md_text: str) -> list:
"""解析 LLM 输出的 Markdown 为结构化方案列表。
析格式
### 基础方案(年保费约 XXXX 元)
|产品名称|所属保险公司|险种|保额|年保费|推荐理由|
...
方案总结...
### 均衡方案(年保费约 XXXX 元)
...
"""
plans = []
# 按 ### 分割方案
sections = re.split(r'(?=###\s)', md_text)
for section in sections:
section = section.strip()
if not section.startswith('###'):
continue
# 提取方案名称和年保费
title_match = re.match(r'###\s*(.+?)(?:.*?)?$', section, re.MULTILINE)
plan_name = title_match.group(1).strip() if title_match else "未知方案"
# 提取年保费
premium_match = re.search(r'年保费约\s*([\d,]+)\s*元', section)
total_premium = premium_match.group(1).replace(',', '') if premium_match else ""
# 提取表格行
items = []
table_lines = re.findall(r'\|(.+)\|', section)
for line in table_lines:
cells = [c.strip() for c in line.split('|')]
# 跳过表头和分隔行
if len(cells) < 6 or cells[0] in ('产品名称', '---------', '---', ''):
continue
items.append({
"product_name": cells[0],
"company": cells[1],
"insurance_type": cells[2],
"coverage": cells[3],
"premium": cells[4],
"reason": cells[5],
})
# 提取方案总结
summary_match = re.search(r'方案总结[:]\s*(.+?)(?=###|\Z)', section, re.DOTALL)
summary = summary_match.group(1).strip() if summary_match else ""
plans.append({
"name": plan_name,
"total_premium": total_premium,
"items": items,
"summary": summary,
})
return plans
def delete(self, proposal_id: str) -> dict:
"""删除推荐方案。"""
record = db.session.query(RecommendationRecord).filter_by(id=int(proposal_id)).first()
if not record:
return {"code": 1005, "message": "方案不存在", "data": None}
db.session.delete(record)
db.session.commit()
return {"code": 0, "message": "删除成功", "data": None}
def share(self, proposal_id: str, expire_hours: int) -> dict:
"""生成分享链接。"""
if expire_hours < 1 or expire_hours > 720:
return {"code": 1001, "message": "有效期应在 1-720 小时之间", "data": None}
record = db.session.query(RecommendationRecord).filter_by(id=int(proposal_id)).first()
if not record:
return {"code": 1005, "message": "方案不存在", "data": None}
# 生成分享 Token
share_token = str(uuid.uuid4())
from insurance.db.compat import redis_client
redis_client.setex(f"share:{share_token}", expire_hours * 3600, proposal_id)
domain = current_app.config.get("DOMAIN", "localhost")
return {
"code": 0,
"data": {
"share_url": f"https://{domain}/shared/{proposal_id}?token={share_token}",
"expire_at": "",
},
}
def list_proposals(self, user_id: str, params: dict) -> dict:
"""推荐历史列表(带数据权限)。"""
from insurance.middleware.auth_middleware import get_data_scope
scope = get_data_scope()
query = db.session.query(RecommendationRecord)
# 根据数据权限过滤
if scope["scope"] == "self":
# 销售:只看自己的
query = query.filter(RecommendationRecord.user_id == user_id)
elif scope["scope"] == "team":
# 主管:看本部门的
from insurance.models.wecom_user import WeComUserMapping
query = query.filter(
RecommendationRecord.user_id.in_(
db.session.query(WeComUserMapping.id).filter(
WeComUserMapping.department == scope["department"]
)
)
)
# admin/super_admin看全部
if params.get("customer_name"):
query = query.filter(RecommendationRecord.customer_name.ilike(f"%{params['customer_name']}%"))
if params.get("insurance_type"):
query = query.filter(RecommendationRecord.insurance_types.ilike(f"%{params['insurance_type']}%"))
total = query.count()
items = (
query.order_by(RecommendationRecord.created_at.desc())
.offset((params["page"] - 1) * params["page_size"])
.limit(params["page_size"])
.all()
)
return {
"code": 0,
"data": {
"total": total,
"items": [
{
"id": str(r.id),
"customer_name": r.customer_name or "",
"customer_age": r.customer_age,
"customer_gender": r.customer_gender or "",
"occupation": r.occupation or "",
"insurance_types": r.insurance_types or "[]",
"coverage_amount": r.coverage_amount,
"coverage_period": r.coverage_period or "",
"monthly_budget": r.monthly_budget,
"status": r.status,
"created_at": str(r.created_at) if r.created_at else None,
}
for r in items
],
},
}