"""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