433 lines
18 KiB
Python
433 lines
18 KiB
Python
"""兴邦金租智能评审助手 - 统一API(7 个助手完整版)
|
||
|
||
基于 2026.8.18 会议痛点的完整解决方案:
|
||
1. 立项前置风控助手 🔍 - 业务经理风险自查
|
||
2. 中诚信智能填报助手 📊 - 激活十几万投资的评级模板
|
||
3. 空窗期动态监控助手 🚨 - 17天空窗期风险重扫
|
||
4. 保单条款合规核验助手 🛡️ - OCR+条款解析
|
||
5. 评审报告辅助生成助手 📄 - 看资料+写报告提效
|
||
6. 主审员智能分配助手 🎯 - 多维度负荷优化
|
||
7. AI预审过滤+加速助手 🤖 - 团队经验预判+加速
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
from datetime import datetime
|
||
from typing import AsyncGenerator, Dict, List, Optional
|
||
|
||
from fastapi import APIRouter, HTTPException
|
||
from fastapi.responses import StreamingResponse
|
||
from pydantic import BaseModel, Field
|
||
|
||
# 导入数据层
|
||
from data_layer.erp_connector import get_erp_connector
|
||
from data_layer.history_store import get_history_store
|
||
from data_layer.reviewer_profile import get_reviewer_store
|
||
from data_layer.team_experience import get_team_experience_store
|
||
|
||
router = APIRouter(prefix="/api/xingbang/unified", tags=["兴邦智能评审统一接口"])
|
||
|
||
|
||
# ============ Pydantic 模型 ============
|
||
|
||
class ProjectBasicInfo(BaseModel):
|
||
"""项目基础信息"""
|
||
project_id: str = ""
|
||
project_name: str
|
||
client_name: str
|
||
industry: str
|
||
credit_amount: float # 万元
|
||
lease_object: str = ""
|
||
lease_value: float = 0 # 万元
|
||
|
||
|
||
class FinancialData(BaseModel):
|
||
"""财务数据"""
|
||
registered_capital: float = 0
|
||
established_years: int = 0
|
||
asset_liability_ratio: float = 0
|
||
current_ratio: float = 0
|
||
quick_ratio: float = 0
|
||
net_profit_2024: float = 0
|
||
net_profit_2025: float = 0
|
||
revenue_growth_rate: float = 0
|
||
|
||
|
||
class CreditReport(BaseModel):
|
||
"""征信报告"""
|
||
overdue_count: int = 0
|
||
query_count_recent_6m: int = 0
|
||
credit_limit: float = 0
|
||
enforcement_records: int = 0
|
||
|
||
|
||
class InsurancePolicyData(BaseModel):
|
||
"""保单数据"""
|
||
policy_number: str
|
||
insurance_company: str
|
||
policy_type: str
|
||
coverage_amount: float
|
||
exclusions: List[str] = []
|
||
policy_pdf_path: str = ""
|
||
|
||
|
||
# ============ 助手 1:立项前置风控 ============
|
||
|
||
class Assistant1Request(BaseModel):
|
||
"""立项前置风控请求"""
|
||
project: ProjectBasicInfo
|
||
financial: FinancialData
|
||
credit: CreditReport
|
||
|
||
|
||
@router.post("/assistant1/risk-check")
|
||
async def assistant1_risk_check(request: Assistant1Request):
|
||
"""助手1:立项前置风控自查(只给风险点,不给结论)"""
|
||
|
||
async def generate():
|
||
try:
|
||
# 获取团队经验
|
||
team_exp = get_team_experience_store()
|
||
thresholds = team_exp.get_approval_thresholds()
|
||
risk_patterns = team_exp.get_risk_patterns(request.project.industry)
|
||
red_flags = team_exp.get_industry_red_flags(request.project.industry)
|
||
|
||
yield f"data: {json.dumps({'type': 'start'}, ensure_ascii=False)}\n\n"
|
||
|
||
# 生成 Markdown 报告
|
||
content = f"""# 立项前置风险自查清单
|
||
|
||
## 📋 项目基本信息
|
||
- **项目名称**:{request.project.project_name}
|
||
- **客户名称**:{request.project.client_name}
|
||
- **行业**:{request.project.industry}
|
||
- **授信金额**:{request.project.credit_amount} 万元
|
||
- **租赁物**:{request.project.lease_object or '未填写'}
|
||
- **租赁物价值**:{request.project.lease_value} 万元
|
||
|
||
---
|
||
|
||
## 🔍 风险自查结果
|
||
|
||
"""
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content}, ensure_ascii=False)}\n\n"
|
||
|
||
# 检查高风险项
|
||
high_risks = []
|
||
medium_risks = []
|
||
low_risks = []
|
||
|
||
# 1. 客户资质检查
|
||
if request.financial.registered_capital < 1000:
|
||
high_risks.append("**注册资本不足**:注册资本 {:.0f} 万元 < 1000 万元(标准要求)\n - **建议**:考虑提高授信门槛或要求集团担保".format(request.financial.registered_capital))
|
||
|
||
if request.financial.established_years < 3:
|
||
medium_risks.append("**成立时间较短**:成立 {} 年 < 3 年(标准要求)\n - **建议**:补充实控人其他企业经营情况".format(request.financial.established_years))
|
||
|
||
# 2. 财务指标检查
|
||
asset_liability = request.financial.asset_liability_ratio
|
||
threshold_al = thresholds.get("资产负债率", {})
|
||
|
||
if asset_liability > threshold_al.get("critical", 0.8):
|
||
high_risks.append(f"**资产负债率过高**:{asset_liability:.1%} > {threshold_al.get('critical', 0.8):.0%}(警戒线)\n - **团队共识**:{threshold_al.get('team_consensus', '需要额外担保')}")
|
||
elif asset_liability > threshold_al.get("warning", 0.7):
|
||
medium_risks.append(f"**资产负债率偏高**:{asset_liability:.1%}(位于 {threshold_al.get('warning', 0.7):.0%}-{threshold_al.get('critical', 0.8):.0%} 区间)\n - **建议**:补充负债结构说明,关注短期偿债压力")
|
||
else:
|
||
low_risks.append(f"✅ **资产负债率**:{asset_liability:.1%}(良好)")
|
||
|
||
current_ratio = request.financial.current_ratio
|
||
threshold_cr = thresholds.get("流动比率", {})
|
||
|
||
if current_ratio < threshold_cr.get("critical", 1.0):
|
||
high_risks.append(f"**流动比率不足**:{current_ratio:.2f} < {threshold_cr.get('critical', 1.0):.2f}(红线)\n - **团队共识**:{threshold_cr.get('team_consensus', '短期偿债能力堪忧')}")
|
||
elif current_ratio < threshold_cr.get("warning", 1.2):
|
||
medium_risks.append(f"**流动比率偏低**:{current_ratio:.2f}\n - **建议**:关注应收账款周转情况")
|
||
else:
|
||
low_risks.append(f"✅ **流动比率**:{current_ratio:.2f}(符合标准)")
|
||
|
||
# 3. 征信检查
|
||
if request.credit.overdue_count > 0:
|
||
high_risks.append(f"**征信逾期记录**:近 2 年有 {request.credit.overdue_count} 次逾期\n - **建议**:要求客户提供逾期说明及还款证明")
|
||
else:
|
||
low_risks.append("✅ **征信记录**:近 2 年无逾期(良好)")
|
||
|
||
if request.credit.enforcement_records > 0:
|
||
high_risks.append(f"**被执行记录**:存在 {request.credit.enforcement_records} 条被执行记录\n - **重要**:评审大概率退回,需先解决执行案件")
|
||
else:
|
||
low_risks.append("✅ **被执行记录**:无(良好)")
|
||
|
||
if request.credit.query_count_recent_6m > 10:
|
||
medium_risks.append(f"**征信查询频繁**:近 6 个月查询 {request.credit.query_count_recent_6m} 次\n - **建议**:了解客户融资需求是否过于迫切")
|
||
|
||
# 4. 租赁物检查
|
||
if request.project.lease_value > 0:
|
||
coverage_ratio = request.project.lease_value / request.project.credit_amount
|
||
if coverage_ratio < 1.2:
|
||
high_risks.append(f"**租赁物价值不足**:租赁物价值 {request.project.lease_value} 万元,覆盖率 {coverage_ratio:.1%} < 120%(标准要求)\n - **建议**:补充其他担保措施")
|
||
else:
|
||
low_risks.append(f"✅ **租赁物覆盖率**:{coverage_ratio:.1%}(符合要求)")
|
||
|
||
# 5. 行业红线检查
|
||
if red_flags:
|
||
content_flags = f"\n### ⚠️ {request.project.industry} 行业常见风险点\n\n"
|
||
for flag in red_flags:
|
||
content_flags += f"- {flag}\n"
|
||
content_flags += "\n**建议**:核实客户是否触及以上风险点\n\n"
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content_flags}, ensure_ascii=False)}\n\n"
|
||
|
||
# 输出风险项
|
||
if high_risks:
|
||
content_high = "### 🔴 高风险项(必须处理,否则评审大概率退回)\n\n"
|
||
for i, risk in enumerate(high_risks, 1):
|
||
content_high += f"{i}. {risk}\n\n"
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content_high}, ensure_ascii=False)}\n\n"
|
||
|
||
if medium_risks:
|
||
content_medium = "### 🟡 中风险项(建议补充材料或说明)\n\n"
|
||
for i, risk in enumerate(medium_risks, 1):
|
||
content_medium += f"{i}. {risk}\n\n"
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content_medium}, ensure_ascii=False)}\n\n"
|
||
|
||
if low_risks:
|
||
content_low = "### 🟢 低风险项(符合要求)\n\n"
|
||
for risk in low_risks:
|
||
content_low += f"- {risk}\n"
|
||
content_low += "\n"
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content_low}, ensure_ascii=False)}\n\n"
|
||
|
||
# 整体评估
|
||
if len(high_risks) == 0 and len(medium_risks) == 0:
|
||
suggestion = "✅ **直接提交**:项目风险可控,可直接提交评审"
|
||
elif len(high_risks) == 0:
|
||
suggestion = "🟡 **补充材料后提交**:建议先处理中风险项,提高通过率"
|
||
else:
|
||
suggestion = "🔴 **重大调整后提交**:存在高风险项,建议先与客户沟通调整方案"
|
||
|
||
content_final = f"""---
|
||
|
||
## 💡 整体评估
|
||
|
||
{suggestion}
|
||
|
||
**说明**:本清单基于团队历史评审经验生成,仅供参考,不代表评审结论。最终是否通过由主审员独立判断。
|
||
"""
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content_final}, ensure_ascii=False)}\n\n"
|
||
yield f"data: {json.dumps({'type': 'done'}, ensure_ascii=False)}\n\n"
|
||
|
||
except Exception as e:
|
||
yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n"
|
||
|
||
return StreamingResponse(generate(), media_type="text/event-stream")
|
||
|
||
|
||
# ============ 助手 6:主审员智能分配 ============
|
||
|
||
class Assistant6Request(BaseModel):
|
||
"""主审员智能分配请求"""
|
||
project_industry: str
|
||
project_amount: float # 万元
|
||
project_complexity: int = Field(3, ge=1, le=5, description="项目复杂度 1-5")
|
||
|
||
|
||
@router.post("/assistant6/allocate")
|
||
async def assistant6_allocate(request: Assistant6Request):
|
||
"""助手6:主审员智能分配(多维度评分)"""
|
||
|
||
try:
|
||
reviewer_store = get_reviewer_store()
|
||
|
||
# 获取推荐列表
|
||
recommendations = reviewer_store.get_recommended_reviewers(
|
||
project_industry=request.project_industry,
|
||
project_complexity=request.project_complexity,
|
||
top_n=3
|
||
)
|
||
|
||
# 获取所有主审员当前状态
|
||
all_profiles = reviewer_store.get_all_profiles()
|
||
reviewer_status = [
|
||
{
|
||
"name": name,
|
||
"current_projects": profile["current_projects"],
|
||
"specialty": profile["specialty"],
|
||
"workload_capacity": profile["workload_capacity"],
|
||
"status": "🔴 超载" if profile["current_projects"] >= profile["workload_capacity"]
|
||
else ("🟡 偏高" if profile["current_projects"] >= profile["workload_capacity"] * 0.8 else "🟢 正常")
|
||
}
|
||
for name, profile in all_profiles.items()
|
||
]
|
||
|
||
return {
|
||
"reviewers": reviewer_status,
|
||
"recommendations": recommendations,
|
||
"project_info": {
|
||
"industry": request.project_industry,
|
||
"amount": request.project_amount,
|
||
"complexity": request.project_complexity,
|
||
},
|
||
"algorithm_version": "v1.0_multidim_scoring",
|
||
}
|
||
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ============ 助手 7:AI预审过滤+加速 ============
|
||
|
||
class Assistant7Request(BaseModel):
|
||
"""AI预审请求"""
|
||
project: ProjectBasicInfo
|
||
financial: FinancialData
|
||
credit: CreditReport
|
||
|
||
|
||
@router.post("/assistant7/pre-screen")
|
||
async def assistant7_pre_screen(request: Assistant7Request):
|
||
"""助手7:AI预审过滤(基于团队经验,快速判断是否值得进入正式评审)"""
|
||
|
||
async def generate():
|
||
try:
|
||
yield f"data: {json.dumps({'type': 'start'}, ensure_ascii=False)}\n\n"
|
||
|
||
# 获取团队经验和历史案例
|
||
team_exp = get_team_experience_store()
|
||
history_store = get_history_store()
|
||
|
||
thresholds = team_exp.get_approval_thresholds()
|
||
rejection_reasons = team_exp.get_common_rejection_reasons(top_n=5)
|
||
|
||
# 相似案例检索
|
||
similar_cases = history_store.search_similar_cases(
|
||
industry=request.project.industry,
|
||
credit_amount=request.project.credit_amount,
|
||
limit=3
|
||
)
|
||
|
||
content = f"""# AI预审报告(团队经验模式)
|
||
|
||
## 📊 项目信息
|
||
- **项目名称**:{request.project.project_name}
|
||
- **客户名称**:{request.project.client_name}
|
||
- **行业**:{request.project.industry}
|
||
- **授信金额**:{request.project.credit_amount} 万元
|
||
|
||
---
|
||
|
||
## 🤖 AI预审判断(基于团队 {team_exp.get_metadata().get('total_cases_analyzed', 0)} 个历史案例)
|
||
|
||
"""
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content}, ensure_ascii=False)}\n\n"
|
||
|
||
# 快速红线检查
|
||
red_line_hit = []
|
||
warning_items = []
|
||
|
||
# 1. 财务红线
|
||
if request.financial.asset_liability_ratio > thresholds.get("资产负债率", {}).get("critical", 0.8):
|
||
red_line_hit.append("资产负债率超过红线")
|
||
|
||
if request.financial.current_ratio < thresholds.get("流动比率", {}).get("critical", 1.0):
|
||
red_line_hit.append("流动比率低于红线")
|
||
|
||
# 2. 征信红线
|
||
if request.credit.enforcement_records > 0:
|
||
red_line_hit.append(f"存在 {request.credit.enforcement_records} 条被执行记录")
|
||
|
||
if request.credit.overdue_count > 2:
|
||
red_line_hit.append(f"征信逾期次数过多({request.credit.overdue_count} 次)")
|
||
|
||
# 3. 警告项
|
||
if request.financial.revenue_growth_rate < 0:
|
||
warning_items.append("营收负增长")
|
||
|
||
if request.credit.query_count_recent_6m > 10:
|
||
warning_items.append("征信查询频繁")
|
||
|
||
# AI预审结论
|
||
if red_line_hit:
|
||
conclusion = "🔴 **不建议进入正式评审**"
|
||
reason_text = "\n".join([f"- {item}" for item in red_line_hit])
|
||
content_conclusion = f"""### {conclusion}
|
||
|
||
**触及红线项**:
|
||
{reason_text}
|
||
|
||
**建议**:与客户沟通调整方案,或直接退回立项申请,避免浪费评审资源。
|
||
|
||
"""
|
||
elif len(warning_items) >= 2:
|
||
conclusion = "🟡 **建议谨慎评审**"
|
||
reason_text = "\n".join([f"- {item}" for item in warning_items])
|
||
content_conclusion = f"""### {conclusion}
|
||
|
||
**警告项**:
|
||
{reason_text}
|
||
|
||
**建议**:可以进入正式评审,但需重点关注以上风险点,建议增加尽调深度。
|
||
|
||
"""
|
||
else:
|
||
conclusion = "🟢 **建议进入正式评审**"
|
||
content_conclusion = f"""### {conclusion}
|
||
|
||
**初步判断**:项目风险可控,符合基本准入标准,建议正常推进评审流程。
|
||
|
||
"""
|
||
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content_conclusion}, ensure_ascii=False)}\n\n"
|
||
|
||
# 相似案例参考
|
||
if similar_cases:
|
||
content_cases = "\n---\n\n## 📚 相似案例参考\n\n"
|
||
for i, case in enumerate(similar_cases, 1):
|
||
result_emoji = "✅" if case.get("review_result") == "approved" else "❌"
|
||
content_cases += f"""### 案例 {i}:{case.get('project_name', '未知项目')}
|
||
- **行业**:{case.get('industry', '未知')}
|
||
- **金额**:{case.get('credit_amount', 0)} 万元
|
||
- **评审结果**:{result_emoji} {case.get('review_result', '未知')}
|
||
- **关键风险点**:{', '.join([r.get('type', '未知') for r in case.get('identified_risks', [])])}
|
||
|
||
"""
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content_cases}, ensure_ascii=False)}\n\n"
|
||
|
||
# 高频退回原因提示
|
||
content_tips = "\n---\n\n## 💡 团队经验提示(高频退回原因)\n\n"
|
||
for reason in rejection_reasons:
|
||
content_tips += f"- **{reason['reason']}**({reason['count']} 次) → {reason['solution']}\n"
|
||
|
||
yield f"data: {json.dumps({'type': 'content', 'data': content_tips}, ensure_ascii=False)}\n\n"
|
||
|
||
yield f"data: {json.dumps({'type': 'done'}, ensure_ascii=False)}\n\n"
|
||
|
||
except Exception as e:
|
||
yield f"data: {json.dumps({'type': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n"
|
||
|
||
return StreamingResponse(generate(), media_type="text/event-stream")
|
||
|
||
|
||
# ============ 数据管理接口 ============
|
||
|
||
@router.get("/data/reviewers")
|
||
async def get_all_reviewers():
|
||
"""获取所有主审员画像"""
|
||
reviewer_store = get_reviewer_store()
|
||
return reviewer_store.get_all_profiles()
|
||
|
||
|
||
@router.get("/data/team-experience")
|
||
async def get_team_experience():
|
||
"""获取团队集体经验"""
|
||
team_exp = get_team_experience_store()
|
||
return {
|
||
"approval_thresholds": team_exp.get_approval_thresholds(),
|
||
"common_rejections": team_exp.get_common_rejection_reasons(),
|
||
"metadata": team_exp.get_metadata(),
|
||
}
|
||
|
||
|
||
@router.get("/data/history-stats")
|
||
async def get_history_statistics():
|
||
"""获取历史案例统计"""
|
||
history_store = get_history_store()
|
||
return history_store.get_statistics()
|