feat: 添加公司AI管理平台基础架构 添加了完整的FastAPI后端项目结构,包括: - 环境配置文件(.env.example)和项目说明文档(README.md、AGENTS.md) - Dockerfile用于容器化部署 - 核心基础设施:配置管理、数据库连接、调度器、安全认证 - 模块化设计:AI代理、审批流程、审计日志、业务台账等功能模块 - 支持多数据库连接(主库和遗留系统只读库) - AI适配器支持OpenClaw、Hermes、OpenAI兼容接口 - 飞书集成、报表生成、风险监控等企业级功能 - 完整的依赖管理和测试指南 ```
139 lines
4.9 KiB
Python
139 lines
4.9 KiB
Python
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
|
|
from fastapi import HTTPException, status
|
|
from sqlalchemy import Select, func, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.modules.audit.schemas import AuditLogCreate
|
|
from app.modules.audit.service import AuditService
|
|
from app.modules.approvals.service import ApprovalService
|
|
from app.modules.business.registry import HIGH_RISK_DOMAINS, get_domain_model
|
|
|
|
|
|
def serialize_model(record: Any) -> dict[str, Any]:
|
|
"""Convert a SQLAlchemy model instance into a JSON-friendly dictionary."""
|
|
|
|
data: dict[str, Any] = {}
|
|
for column in record.__table__.columns:
|
|
value = getattr(record, column.name)
|
|
if isinstance(value, (datetime, date)):
|
|
data[column.name] = value.isoformat()
|
|
elif isinstance(value, Decimal):
|
|
data[column.name] = float(value)
|
|
else:
|
|
data[column.name] = value
|
|
return data
|
|
|
|
|
|
class BusinessService:
|
|
"""Manage generic CRUD operations across registered business domains."""
|
|
|
|
def __init__(self, db: Session):
|
|
self.db = db
|
|
self.audit = AuditService(db)
|
|
|
|
def list_records(
|
|
self,
|
|
domain: str,
|
|
limit: int = 50,
|
|
offset: int = 0,
|
|
status_filter: str | None = None,
|
|
) -> tuple[int, list[dict[str, Any]]]:
|
|
model = get_domain_model(domain)
|
|
stmt: Select = select(model)
|
|
count_stmt = select(func.count()).select_from(model)
|
|
if status_filter and hasattr(model, "status"):
|
|
stmt = stmt.where(model.status == status_filter)
|
|
count_stmt = count_stmt.where(model.status == status_filter)
|
|
stmt = stmt.order_by(model.id.desc()).limit(min(limit, 500)).offset(max(offset, 0))
|
|
total = int(self.db.execute(count_stmt).scalar() or 0)
|
|
return total, [serialize_model(item) for item in self.db.execute(stmt).scalars()]
|
|
|
|
def get_record(self, domain: str, record_id: int) -> dict[str, Any]:
|
|
model = get_domain_model(domain)
|
|
record = self.db.get(model, record_id)
|
|
if record is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Record not found")
|
|
return serialize_model(record)
|
|
|
|
def create_record(
|
|
self,
|
|
domain: str,
|
|
data: dict[str, Any],
|
|
actor: str = "api",
|
|
) -> dict[str, Any]:
|
|
model = get_domain_model(domain)
|
|
allowed = {column.name for column in model.__table__.columns if column.name != "id"}
|
|
payload = {key: value for key, value in data.items() if key in allowed}
|
|
record = model(**payload)
|
|
self.db.add(record)
|
|
self.db.commit()
|
|
self.db.refresh(record)
|
|
result = serialize_model(record)
|
|
self.audit.log(
|
|
AuditLogCreate(
|
|
actor=actor,
|
|
source="api",
|
|
action=f"create:{domain}",
|
|
target_type=domain,
|
|
target_id=str(record.id),
|
|
request_payload=data,
|
|
response_payload=result,
|
|
)
|
|
)
|
|
return result
|
|
|
|
def update_record(
|
|
self,
|
|
domain: str,
|
|
record_id: int,
|
|
data: dict[str, Any],
|
|
actor: str = "api",
|
|
approval_ticket_id: str | None = None,
|
|
) -> dict[str, Any]:
|
|
if domain in HIGH_RISK_DOMAINS:
|
|
if not approval_ticket_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="High-risk domain update requires approval_ticket_id",
|
|
)
|
|
if not ApprovalService(self.db).is_approved_for(
|
|
approval_ticket_id,
|
|
domain,
|
|
record_id,
|
|
f"update:{domain}",
|
|
):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Approval ticket is not approved for this update",
|
|
)
|
|
model = get_domain_model(domain)
|
|
record = self.db.get(model, record_id)
|
|
if record is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Record not found",
|
|
)
|
|
allowed = {column.name for column in model.__table__.columns if column.name != "id"}
|
|
for key, value in data.items():
|
|
if key in allowed:
|
|
setattr(record, key, value)
|
|
self.db.commit()
|
|
self.db.refresh(record)
|
|
result = serialize_model(record)
|
|
self.audit.log(
|
|
AuditLogCreate(
|
|
actor=actor,
|
|
source="api",
|
|
action=f"update:{domain}",
|
|
target_type=domain,
|
|
target_id=str(record.id),
|
|
risk_level="high" if domain in HIGH_RISK_DOMAINS else "low",
|
|
request_payload={"data": data, "approval_ticket_id": approval_ticket_id},
|
|
response_payload=result,
|
|
)
|
|
)
|
|
return result
|