按 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>
302 lines
11 KiB
Python
302 lines
11 KiB
Python
"""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
|