Files
mini-openclaw/backend/data_layer/erp_connector.py
T

302 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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