feat: restore risk assessment workspace and improve page scrolling

This commit is contained in:
NathanAlex7
2026-09-07 02:28:37 +08:00
parent db5d6763f1
commit f2439ee5a3
65 changed files with 13612 additions and 2649 deletions
+20
View File
@@ -0,0 +1,20 @@
"""兴邦金租数据层模块
提供统一的数据访问接口:
- ERP 数据库连接(加密)
- 历史评审案例向量库
- 主审员画像库
- 团队集体经验库
"""
from .erp_connector import ERPConnector
from .history_store import HistoryStore
from .reviewer_profile import ReviewerProfileStore
from .team_experience import TeamExperienceStore
__all__ = [
"ERPConnector",
"HistoryStore",
"ReviewerProfileStore",
"TeamExperienceStore",
]
+301
View File
@@ -0,0 +1,301 @@
"""ERP 数据库连接器(加密+只读)
安全措施:
1. SSL/TLS 加密连接
2. 只读账号(SELECT 权限)
3. 敏感字段脱敏
4. 连接池管理
5. 审计日志
"""
import os
import logging
from typing import Dict, List, Optional, Any
from datetime import datetime
import asyncio
from contextlib import asynccontextmanager
# 使用 asyncpg 作为 PostgreSQL 连接器(如果 ERP 是 PostgreSQL)
# 如果是 MySQL,则使用 aiomysql
# 如果是 Oracle,则使用 cx_Oracle
logger = logging.getLogger(__name__)
class ERPConnector:
"""ERP 数据库连接器(加密+只读)"""
def __init__(self):
self.host = os.getenv("ERP_DB_HOST")
self.port = int(os.getenv("ERP_DB_PORT", "5432"))
self.database = os.getenv("ERP_DB_NAME")
self.user = os.getenv("ERP_DB_USER") # 只读账号
self.password = os.getenv("ERP_DB_PASSWORD")
self.ssl_mode = os.getenv("ERP_DB_SSL_MODE", "require")
self._pool = None
self._audit_log_path = os.getenv("ERP_AUDIT_LOG", "./logs/erp_access.log")
async def initialize(self):
"""初始化连接池"""
if self._pool is not None:
return
try:
# 这里以 asyncpg (PostgreSQL) 为例
import asyncpg
self._pool = await asyncpg.create_pool(
host=self.host,
port=self.port,
database=self.database,
user=self.user,
password=self.password,
ssl=self.ssl_mode,
min_size=2,
max_size=10,
command_timeout=30,
)
logger.info(f"ERP 连接池初始化成功: {self.host}:{self.port}/{self.database}")
except ImportError:
logger.warning("asyncpg 未安装,使用模拟数据模式")
self._pool = "mock" # 标记为模拟模式
except Exception as e:
logger.error(f"ERP 连接池初始化失败: {e}")
self._pool = "mock"
async def close(self):
"""关闭连接池"""
if self._pool and self._pool != "mock":
await self._pool.close()
self._pool = None
def _audit_log(self, operation: str, table: str, filters: Dict):
"""记录数据访问审计日志"""
timestamp = datetime.now().isoformat()
log_entry = f"[{timestamp}] {operation} | table={table} | filters={filters}\n"
os.makedirs(os.path.dirname(self._audit_log_path), exist_ok=True)
with open(self._audit_log_path, "a", encoding="utf-8") as f:
f.write(log_entry)
def _mask_sensitive_fields(self, data: Dict, sensitive_fields: List[str]) -> Dict:
"""脱敏敏感字段"""
masked = data.copy()
for field in sensitive_fields:
if field in masked and masked[field]:
value = str(masked[field])
if len(value) > 4:
masked[field] = value[:2] + "***" + value[-2:]
else:
masked[field] = "***"
return masked
async def get_project_by_id(self, project_id: str) -> Optional[Dict]:
"""根据项目 ID 获取项目基础数据"""
self._audit_log("SELECT", "projects", {"project_id": project_id})
if self._pool == "mock":
return self._mock_project_data(project_id)
async with self._pool.acquire() as conn:
query = """
SELECT
project_id, project_name, client_name, industry,
credit_amount, lease_object, lease_value, status,
created_at, updated_at
FROM projects
WHERE project_id = $1
"""
row = await conn.fetchrow(query, project_id)
return dict(row) if row else None
async def get_client_financial_data(self, client_name: str) -> Dict:
"""获取客户财务数据"""
self._audit_log("SELECT", "client_financials", {"client_name": client_name})
if self._pool == "mock":
return self._mock_financial_data(client_name)
async with self._pool.acquire() as conn:
query = """
SELECT
registered_capital, established_years,
asset_liability_ratio, current_ratio, quick_ratio,
net_profit_2024, net_profit_2025, revenue_growth_rate,
roe, debt_to_equity_ratio
FROM client_financials
WHERE client_name = $1
ORDER BY fiscal_year DESC
LIMIT 1
"""
row = await conn.fetchrow(query, client_name)
return dict(row) if row else {}
async def get_credit_report(self, client_name: str) -> Dict:
"""获取征信报告(敏感信息脱敏)"""
self._audit_log("SELECT", "credit_reports", {"client_name": client_name})
if self._pool == "mock":
return self._mock_credit_report(client_name)
async with self._pool.acquire() as conn:
query = """
SELECT
overdue_count, query_count_recent_6m,
credit_limit, enforcement_records,
last_update_date
FROM credit_reports
WHERE client_name = $1
ORDER BY last_update_date DESC
LIMIT 1
"""
row = await conn.fetchrow(query, client_name)
data = dict(row) if row else {}
# 脱敏:隐藏具体授信额度的前几位
return self._mask_sensitive_fields(data, ["credit_limit"])
async def get_litigation_records(self, client_name: str) -> List[Dict]:
"""获取涉诉记录"""
self._audit_log("SELECT", "litigation_records", {"client_name": client_name})
if self._pool == "mock":
return self._mock_litigation_records(client_name)
async with self._pool.acquire() as conn:
query = """
SELECT
case_number, court, case_type, status,
amount, filing_date, verdict_date
FROM litigation_records
WHERE client_name = $1
ORDER BY filing_date DESC
LIMIT 10
"""
rows = await conn.fetch(query, client_name)
return [dict(row) for row in rows]
async def get_insurance_policies(self, project_id: str) -> List[Dict]:
"""获取保单信息"""
self._audit_log("SELECT", "insurance_policies", {"project_id": project_id})
if self._pool == "mock":
return self._mock_insurance_policies(project_id)
async with self._pool.acquire() as conn:
query = """
SELECT
policy_number, insurance_company, policy_type,
coverage_amount, premium, start_date, end_date,
policy_pdf_url
FROM insurance_policies
WHERE project_id = $1
ORDER BY start_date DESC
"""
rows = await conn.fetch(query, project_id)
return [dict(row) for row in rows]
async def get_reviewer_workload(self) -> List[Dict]:
"""获取主审员当前工作负荷"""
self._audit_log("SELECT", "reviewer_workload", {})
if self._pool == "mock":
return self._mock_reviewer_workload()
async with self._pool.acquire() as conn:
query = """
SELECT
reviewer_name,
COUNT(*) as current_projects,
STRING_AGG(industry, ',') as industries
FROM project_reviews
WHERE status IN ('待评审', '评审中')
GROUP BY reviewer_name
ORDER BY current_projects DESC
"""
rows = await conn.fetch(query)
return [dict(row) for row in rows]
# ============ Mock 数据(用于测试/演示)============
def _mock_project_data(self, project_id: str) -> Dict:
"""模拟项目数据"""
return {
"project_id": project_id,
"project_name": "阳光光伏设备融资租赁项目",
"client_name": "阳光光伏设备有限公司",
"industry": "光伏设备制造",
"credit_amount": 5000.0,
"lease_object": "光伏组件生产线设备",
"lease_value": 6500.0,
"status": "待评审",
"created_at": datetime.now(),
"updated_at": datetime.now(),
}
def _mock_financial_data(self, client_name: str) -> Dict:
"""模拟财务数据"""
return {
"registered_capital": 8000,
"established_years": 6,
"asset_liability_ratio": 0.58,
"current_ratio": 1.45,
"quick_ratio": 1.2,
"net_profit_2024": 1200,
"net_profit_2025": 1450,
"revenue_growth_rate": 0.18,
"roe": 0.15,
"debt_to_equity_ratio": 1.38,
}
def _mock_credit_report(self, client_name: str) -> Dict:
"""模拟征信报告"""
return {
"overdue_count": 0,
"query_count_recent_6m": 4,
"credit_limit": "20***00", # 已脱敏
"enforcement_records": 0,
"last_update_date": datetime.now().date(),
}
def _mock_litigation_records(self, client_name: str) -> List[Dict]:
"""模拟涉诉记录"""
return []
def _mock_insurance_policies(self, project_id: str) -> List[Dict]:
"""模拟保单数据"""
return [
{
"policy_number": "PICC-2026-001234",
"insurance_company": "中国人保财险",
"policy_type": "财产一切险",
"coverage_amount": 7000.0,
"premium": 35.0,
"start_date": datetime(2026, 1, 1).date(),
"end_date": datetime(2029, 12, 31).date(),
"policy_pdf_url": "/policies/PICC-2026-001234.pdf",
}
]
def _mock_reviewer_workload(self) -> List[Dict]:
"""模拟主审员负荷"""
return [
{"reviewer_name": "张强", "current_projects": 7, "industries": "交通运输,制造业"},
{"reviewer_name": "李明", "current_projects": 5, "industries": "制造业,新能源"},
{"reviewer_name": "刘洋", "current_projects": 4, "industries": "医疗健康,服务业"},
{"reviewer_name": "王芳", "current_projects": 3, "industries": "新能源,光伏"},
]
# 全局单例
_erp_connector: Optional[ERPConnector] = None
async def get_erp_connector() -> ERPConnector:
"""获取 ERP 连接器单例"""
global _erp_connector
if _erp_connector is None:
_erp_connector = ERPConnector()
await _erp_connector.initialize()
return _erp_connector
+209
View File
@@ -0,0 +1,209 @@
"""历史评审案例向量库
功能:
1. 存储历史评审案例(项目特征+评审结果+评审员行为)
2. 相似案例检索(基于向量相似度)
3. 评审员行为学习(团队集体经验,非个人预测)
"""
import json
import os
from typing import List, Dict, Optional, Tuple
from datetime import datetime
from pathlib import Path
import logging
logger = logging.getLogger(__name__)
class HistoryStore:
"""历史评审案例存储(简化版,生产环境建议使用 Chroma/Qdrant)"""
def __init__(self, data_dir: str = "./xingbang_data/history"):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
self.cases_file = self.data_dir / "review_cases.jsonl"
# 确保文件存在
if not self.cases_file.exists():
self.cases_file.touch()
def add_case(self, case_data: Dict) -> str:
"""添加一条历史评审案例"""
case_id = f"case_{datetime.now().strftime('%Y%m%d%H%M%S')}"
case_record = {
"case_id": case_id,
"timestamp": datetime.now().isoformat(),
**case_data
}
# 追加写入 JSONL
with open(self.cases_file, "a", encoding="utf-8") as f:
f.write(json.dumps(case_record, ensure_ascii=False) + "\n")
logger.info(f"案例已保存: {case_id}")
return case_id
def search_similar_cases(
self,
industry: str,
credit_amount: float,
risk_level: str = None,
limit: int = 5
) -> List[Dict]:
"""检索相似案例(简化版:基于行业+金额范围)"""
if not self.cases_file.exists():
return []
similar_cases = []
with open(self.cases_file, "r", encoding="utf-8") as f:
for line in f:
if not line.strip():
continue
try:
case = json.loads(line)
# 行业匹配
industry_match = industry in case.get("industry", "") or case.get("industry", "") in industry
# 金额范围匹配(±30%)
case_amount = case.get("credit_amount", 0)
amount_match = 0.7 * credit_amount <= case_amount <= 1.3 * credit_amount
# 风险等级匹配(可选)
risk_match = risk_level is None or case.get("risk_level") == risk_level
if industry_match and amount_match and risk_match:
similar_cases.append(case)
except json.JSONDecodeError:
continue
# 按时间倒序返回最近的案例
similar_cases.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
return similar_cases[:limit]
def get_case_by_id(self, case_id: str) -> Optional[Dict]:
"""根据 ID 获取案例详情"""
if not self.cases_file.exists():
return None
with open(self.cases_file, "r", encoding="utf-8") as f:
for line in f:
if not line.strip():
continue
try:
case = json.loads(line)
if case.get("case_id") == case_id:
return case
except json.JSONDecodeError:
continue
return None
def get_team_patterns(self, industry: str = None) -> Dict:
"""提取团队集体经验模式(高频风险点、常见处理方式)"""
if not self.cases_file.exists():
return {"high_frequency_risks": [], "common_solutions": []}
risk_counter = {}
solution_counter = {}
with open(self.cases_file, "r", encoding="utf-8") as f:
for line in f:
if not line.strip():
continue
try:
case = json.loads(line)
# 行业过滤
if industry and industry not in case.get("industry", ""):
continue
# 统计高频风险点
for risk in case.get("identified_risks", []):
risk_key = risk.get("type", "unknown")
risk_counter[risk_key] = risk_counter.get(risk_key, 0) + 1
# 统计常见解决方案
solution = case.get("solution", "")
if solution:
solution_counter[solution] = solution_counter.get(solution, 0) + 1
except json.JSONDecodeError:
continue
# 排序
high_frequency_risks = sorted(
[{"risk": k, "count": v} for k, v in risk_counter.items()],
key=lambda x: x["count"],
reverse=True
)[:10]
common_solutions = sorted(
[{"solution": k, "count": v} for k, v in solution_counter.items()],
key=lambda x: x["count"],
reverse=True
)[:10]
return {
"high_frequency_risks": high_frequency_risks,
"common_solutions": common_solutions,
}
def get_statistics(self) -> Dict:
"""获取统计数据"""
if not self.cases_file.exists():
return {"total_cases": 0}
total_cases = 0
industry_dist = {}
approval_rate = {"approved": 0, "rejected": 0, "pending": 0}
with open(self.cases_file, "r", encoding="utf-8") as f:
for line in f:
if not line.strip():
continue
try:
case = json.loads(line)
total_cases += 1
# 行业分布
industry = case.get("industry", "未知")
industry_dist[industry] = industry_dist.get(industry, 0) + 1
# 通过率
result = case.get("review_result", "pending")
if result in approval_rate:
approval_rate[result] += 1
except json.JSONDecodeError:
continue
return {
"total_cases": total_cases,
"industry_distribution": industry_dist,
"approval_rate": approval_rate,
}
# 全局单例
_history_store: Optional[HistoryStore] = None
def get_history_store() -> HistoryStore:
"""获取历史案例存储单例"""
global _history_store
if _history_store is None:
_history_store = HistoryStore()
return _history_store
+262
View File
@@ -0,0 +1,262 @@
"""主审员画像库
功能:
1. 存储主审员基础信息(专长、效率、质量评分)
2. 动态更新主审员负荷
3. 多维度评分(用于智能分配)
"""
import json
from typing import Dict, List, Optional
from pathlib import Path
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class ReviewerProfileStore:
"""主审员画像存储"""
def __init__(self, data_dir: str = "./xingbang_data/reviewers"):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
self.profiles_file = self.data_dir / "reviewer_profiles.json"
# 初始化默认数据
if not self.profiles_file.exists():
self._init_default_profiles()
def _init_default_profiles(self):
"""初始化默认主审员画像"""
default_profiles = {
"李明": {
"name": "李明",
"specialty": ["制造业", "新能源"],
"current_projects": 5,
"avg_completion_days": 8,
"quality_score": 0.85, # 0-1,基于历史评审质量
"efficiency_score": 0.78, # 0-1,基于完成速度
"workload_capacity": 7, # 最大承载项目数
"industry_experience": {
"制造业": 45, # 历史评审项目数
"新能源": 32,
"光伏": 18,
},
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
},
"王芳": {
"name": "王芳",
"specialty": ["新能源", "光伏", "风电"],
"current_projects": 3,
"avg_completion_days": 7,
"quality_score": 0.92,
"efficiency_score": 0.88,
"workload_capacity": 6,
"industry_experience": {
"新能源": 56,
"光伏": 38,
"风电": 22,
},
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
},
"张强": {
"name": "张强",
"specialty": ["交通运输", "物流", "制造业"],
"current_projects": 7,
"avg_completion_days": 9,
"quality_score": 0.80,
"efficiency_score": 0.72,
"workload_capacity": 8,
"industry_experience": {
"交通运输": 67,
"物流": 43,
"制造业": 28,
},
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
},
"刘洋": {
"name": "刘洋",
"specialty": ["医疗健康", "服务业", "消费"],
"current_projects": 4,
"avg_completion_days": 8,
"quality_score": 0.88,
"efficiency_score": 0.82,
"workload_capacity": 7,
"industry_experience": {
"医疗健康": 51,
"服务业": 34,
"消费": 29,
},
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
},
}
with open(self.profiles_file, "w", encoding="utf-8") as f:
json.dump(default_profiles, f, ensure_ascii=False, indent=2)
logger.info("主审员画像初始化完成")
def get_all_profiles(self) -> Dict[str, Dict]:
"""获取所有主审员画像"""
with open(self.profiles_file, "r", encoding="utf-8") as f:
return json.load(f)
def get_profile(self, reviewer_name: str) -> Optional[Dict]:
"""获取指定主审员画像"""
profiles = self.get_all_profiles()
return profiles.get(reviewer_name)
def update_workload(self, reviewer_name: str, current_projects: int):
"""更新主审员工作负荷"""
profiles = self.get_all_profiles()
if reviewer_name in profiles:
profiles[reviewer_name]["current_projects"] = current_projects
profiles[reviewer_name]["updated_at"] = datetime.now().isoformat()
with open(self.profiles_file, "w", encoding="utf-8") as f:
json.dump(profiles, f, ensure_ascii=False, indent=2)
logger.info(f"已更新 {reviewer_name} 的负荷: {current_projects} 个项目")
def update_quality_score(self, reviewer_name: str, quality_score: float):
"""更新主审员质量评分"""
profiles = self.get_all_profiles()
if reviewer_name in profiles:
profiles[reviewer_name]["quality_score"] = quality_score
profiles[reviewer_name]["updated_at"] = datetime.now().isoformat()
with open(self.profiles_file, "w", encoding="utf-8") as f:
json.dump(profiles, f, ensure_ascii=False, indent=2)
logger.info(f"已更新 {reviewer_name} 的质量评分: {quality_score:.2f}")
def calculate_match_score(
self,
reviewer_name: str,
project_industry: str,
project_complexity: int = 3
) -> Dict:
"""计算主审员与项目的匹配度(多维度评分)
评分维度:
- 35% 行业匹配度(专长+历史经验)
- 35% 可用性(当前负荷)
- 20% 质量评分(历史评审质量)
- 10% 团队平衡(避免某人长期超载)
"""
profile = self.get_profile(reviewer_name)
if not profile:
return {"total_score": 0, "breakdown": {}}
# 1. 行业匹配度(35%)
industry_match = 0
if any(spec in project_industry or project_industry in spec for spec in profile["specialty"]):
industry_match = 0.8 # 专长匹配
# 检查历史经验
exp_count = 0
for industry_key in profile["industry_experience"]:
if industry_key in project_industry or project_industry in industry_key:
exp_count = profile["industry_experience"][industry_key]
break
if exp_count >= 30:
industry_match = 1.0
elif exp_count >= 10:
industry_match = max(industry_match, 0.6)
elif exp_count > 0:
industry_match = max(industry_match, 0.3)
industry_score = industry_match * 35
# 2. 可用性(35%)
current_load = profile["current_projects"]
capacity = profile["workload_capacity"]
if current_load >= capacity:
availability = 0.1 # 超载
elif current_load >= capacity * 0.8:
availability = 0.5 # 偏高
else:
availability = 1.0 - (current_load / capacity) # 线性递减
availability_score = availability * 35
# 3. 质量评分(20%)
quality_score = profile["quality_score"] * 20
# 4. 团队平衡(10%)- 优先分配给负荷较低的
all_profiles = self.get_all_profiles()
avg_load = sum(p["current_projects"] for p in all_profiles.values()) / len(all_profiles)
if current_load < avg_load:
balance_score = 10
elif current_load == avg_load:
balance_score = 7
else:
balance_score = max(0, 10 - (current_load - avg_load) * 2)
# 总分
total_score = industry_score + availability_score + quality_score + balance_score
return {
"total_score": round(total_score, 2),
"breakdown": {
"industry_match": round(industry_score, 2),
"availability": round(availability_score, 2),
"quality": round(quality_score, 2),
"balance": round(balance_score, 2),
},
"estimated_days": profile["avg_completion_days"] + current_load // 2, # 考虑负荷影响
}
def get_recommended_reviewers(
self,
project_industry: str,
project_complexity: int = 3,
top_n: int = 3
) -> List[Dict]:
"""获取推荐主审员列表(Top N)"""
all_profiles = self.get_all_profiles()
recommendations = []
for reviewer_name in all_profiles:
match_result = self.calculate_match_score(
reviewer_name,
project_industry,
project_complexity
)
profile = all_profiles[reviewer_name]
recommendations.append({
"reviewer": reviewer_name,
"score": match_result["total_score"],
"breakdown": match_result["breakdown"],
"estimated_days": match_result["estimated_days"],
"current_projects": profile["current_projects"],
"specialty": profile["specialty"],
})
# 按总分降序排序
recommendations.sort(key=lambda x: x["score"], reverse=True)
return recommendations[:top_n]
# 全局单例
_reviewer_store: Optional[ReviewerProfileStore] = None
def get_reviewer_store() -> ReviewerProfileStore:
"""获取主审员画像存储单例"""
global _reviewer_store
if _reviewer_store is None:
_reviewer_store = ReviewerProfileStore()
return _reviewer_store
+195
View File
@@ -0,0 +1,195 @@
"""团队集体经验库
功能:
1. 学习团队历史评审行为(聚合,非个人)
2. 提取高频风险模式
3. 为预审助手提供经验支持
"""
import json
from typing import Dict, List, Optional
from pathlib import Path
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class TeamExperienceStore:
"""团队集体经验存储"""
def __init__(self, data_dir: str = "./xingbang_data/team_experience"):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
self.experience_file = self.data_dir / "team_patterns.json"
# 初始化默认经验模式
if not self.experience_file.exists():
self._init_default_patterns()
def _init_default_patterns(self):
"""初始化默认团队经验模式"""
default_patterns = {
"risk_patterns": {
"制造业": [
{
"risk_type": "应收账款集中度过高",
"frequency": 23,
"typical_threshold": "单一客户占比 > 30%",
"team_action": "要求补充第二担保人或增加保证金",
},
{
"risk_type": "原材料价格波动风险",
"frequency": 18,
"typical_threshold": "大宗商品占成本 > 50%",
"team_action": "要求提供价格锁定合同或套期保值证明",
},
],
"新能源": [
{
"risk_type": "政策依赖度高",
"frequency": 34,
"typical_threshold": "补贴收入 > 营收 20%",
"team_action": "降低授信额度或缩短租赁期限",
},
{
"risk_type": "技术迭代淘汰风险",
"frequency": 21,
"typical_threshold": "设备技术落后 2 代以上",
"team_action": "要求租赁物残值担保或回购承诺",
},
],
"交通运输": [
{
"risk_type": "油价波动影响",
"frequency": 15,
"typical_threshold": "燃油成本 > 营收 40%",
"team_action": "要求提供运价调整机制证明",
},
],
},
"approval_thresholds": {
"资产负债率": {
"safe": 0.6,
"warning": 0.7,
"critical": 0.8,
"team_consensus": "≤60% 低风险,60-70% 中风险需补充说明,>70% 高风险需额外担保",
},
"流动比率": {
"safe": 1.5,
"warning": 1.2,
"critical": 1.0,
"team_consensus": "≥1.5 优秀,1.2-1.5 合格,<1.2 需关注短期偿债能力",
},
"净利润增长率": {
"safe": 0.15,
"warning": 0.05,
"critical": 0.0,
"team_consensus": "持续两年负增长通常不予通过",
},
},
"industry_red_flags": {
"光伏": ["产能严重过剩", "欧美双反调查", "地方政府欠补"],
"房地产": ["三道红线踩线", "土地储备不足", "回款周期超18个月"],
"餐饮": ["单店模型未跑通", "加盟商纠纷", "食品安全事故"],
},
"common_rejection_reasons": [
{"reason": "担保方资产不足", "count": 45, "solution": "要求补充第二担保人"},
{"reason": "征信查询次数过多", "count": 32, "solution": "要求客户说明查询原因"},
{"reason": "涉诉金额过大", "count": 28, "solution": "等待诉讼结果或要求提供和解协议"},
{"reason": "租赁物适格性存疑", "count": 25, "solution": "要求第三方评估报告"},
],
"metadata": {
"last_updated": datetime.now().isoformat(),
"total_cases_analyzed": 156,
"data_source": "2024-2025 年历史评审案例",
},
}
with open(self.experience_file, "w", encoding="utf-8") as f:
json.dump(default_patterns, f, ensure_ascii=False, indent=2)
logger.info("团队经验模式初始化完成")
def get_risk_patterns(self, industry: str = None) -> List[Dict]:
"""获取风险模式(可按行业过滤)"""
with open(self.experience_file, "r", encoding="utf-8") as f:
data = json.load(f)
risk_patterns = data.get("risk_patterns", {})
if industry:
# 精确匹配或模糊匹配
for key in risk_patterns:
if industry in key or key in industry:
return risk_patterns[key]
return []
else:
# 返回所有模式
all_patterns = []
for patterns in risk_patterns.values():
all_patterns.extend(patterns)
return all_patterns
def get_approval_thresholds(self) -> Dict:
"""获取团队共识的审批阈值"""
with open(self.experience_file, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("approval_thresholds", {})
def get_industry_red_flags(self, industry: str) -> List[str]:
"""获取行业红线(团队总结的高危信号)"""
with open(self.experience_file, "r", encoding="utf-8") as f:
data = json.load(f)
industry_flags = data.get("industry_red_flags", {})
# 模糊匹配
for key in industry_flags:
if industry in key or key in industry:
return industry_flags[key]
return []
def get_common_rejection_reasons(self, top_n: int = 10) -> List[Dict]:
"""获取高频退回原因及解决方案"""
with open(self.experience_file, "r", encoding="utf-8") as f:
data = json.load(f)
reasons = data.get("common_rejection_reasons", [])
return reasons[:top_n]
def update_pattern(self, pattern_type: str, pattern_data: Dict):
"""更新经验模式(用于持续学习)"""
with open(self.experience_file, "r", encoding="utf-8") as f:
data = json.load(f)
# 更新指定类型的模式
if pattern_type in data:
data[pattern_type].update(pattern_data)
data["metadata"]["last_updated"] = datetime.now().isoformat()
with open(self.experience_file, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"团队经验模式已更新: {pattern_type}")
def get_metadata(self) -> Dict:
"""获取元数据"""
with open(self.experience_file, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("metadata", {})
# 全局单例
_team_experience_store: Optional[TeamExperienceStore] = None
def get_team_experience_store() -> TeamExperienceStore:
"""获取团队经验存储单例"""
global _team_experience_store
if _team_experience_store is None:
_team_experience_store = TeamExperienceStore()
return _team_experience_store