chore(v2): 从兴邦预审v2白名单抽取独立可运行基座
按 docs/v2/05-implementation-plan.md §1 候选迁移表与 claude_use/T00 白名单, 从 55e2cdb 抽取 224 个文件(2.48 MB)到独立目录 F:/mini-OpenClaw-v2。 - 排除:凭据(.env/server-deploy.conf/旧DELIVERY.md)、运行数据(xingbang_data 等 4 个 SQLite)、依赖(venv/node_modules 符号链接)、归档(_backup*/test-evidence/ ori_file)、Office 临时文件 - 目标目录独立 git 仓库,初始恢复点;Gitea 远程与分支待 OQ-06 Q2 定案 - 未部署、未启动服务、未调用模型 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user