feat: restore risk assessment workspace and improve page scrolling
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user