Files
company-ai-platform/app/modules/business/service.py
JiuContinent aa81fc5321 ```
feat(ai_agent): 完善AI适配器和服务功能

- 添加OpenClaw和Hermes健康检查接口
- 实现OpenClaw工具调用功能
- 重构AI适配器使用常量定义
- 增加AI技能系统支持
- 更新配置文件中的默认模型提供者设置

refactor(scheduler): 使用常量替换硬编码值

- 将硬编码的actor值替换为ActorValue常量
- 将receive_id_type替换为FeishuReceiveIdType枚举

refactor(audit): 统一审计日志常量使用

- 将硬编码的actor、source、risk_level等值替换为对应常量
- 更新审核服务中的状态和操作常量引用

refactor(approvals): 标准化审批模块常量使用

- 将applicant默认值替换为ActorValue.API常量
- 使用ApprovalStatus常量替代硬编码状态值
- 更新审核操作常量引用
```
2026-07-06 00:02:03 +08:00

177 lines
6.4 KiB
Python

from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from typing import Any
from fastapi import HTTPException, status
from sqlalchemy import Date as SQLDate
from sqlalchemy import DateTime as SQLDateTime
from sqlalchemy import Numeric as SQLNumeric
from sqlalchemy import Select, func, select
from sqlalchemy.sql.schema import Column
from sqlalchemy.orm import Session
from app.core.constants import ActorValue
from app.modules.audit.constants import AuditRiskLevel, AuditSource
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
def _coerce_column_value(column: Column, value: Any) -> Any:
"""Coerce API JSON values into the Python type expected by a SQLAlchemy column."""
if value is None:
return None
if isinstance(column.type, SQLDateTime) and isinstance(value, str):
return datetime.fromisoformat(value.replace("Z", "+00:00"))
if isinstance(column.type, SQLDate) and isinstance(value, str):
return date.fromisoformat(value)
if isinstance(column.type, SQLNumeric) and not isinstance(value, Decimal):
return Decimal(str(value))
return value
def _model_payload(model: Any, data: dict[str, Any]) -> dict[str, Any]:
"""Filter unknown keys and coerce values according to model column types."""
columns = {column.name: column for column in model.__table__.columns if column.name != "id"}
payload: dict[str, Any] = {}
for key, value in data.items():
column = columns.get(key)
if column is None:
continue
try:
payload[key] = _coerce_column_value(column, value)
except (ValueError, TypeError, InvalidOperation) as exc:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid value for field '{key}'",
) from exc
return payload
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 = ActorValue.API,
) -> dict[str, Any]:
model = get_domain_model(domain)
payload = _model_payload(model, data)
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=AuditSource.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 = ActorValue.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",
)
for key, value in _model_payload(model, data).items():
setattr(record, key, value)
self.db.commit()
self.db.refresh(record)
result = serialize_model(record)
self.audit.log(
AuditLogCreate(
actor=actor,
source=AuditSource.API,
action=f"update:{domain}",
target_type=domain,
target_id=str(record.id),
risk_level=(
AuditRiskLevel.HIGH if domain in HIGH_RISK_DOMAINS else AuditRiskLevel.LOW
),
request_payload={"data": data, "approval_ticket_id": approval_ticket_id},
response_payload=result,
)
)
return result