deepdata: core product code (backend, frontend, runtime, deepflow module)

This commit is contained in:
figmar
2026-08-30 19:23:38 +08:00
commit bd3db30be9
1405 changed files with 271727 additions and 0 deletions
@@ -0,0 +1,11 @@
from fastapi import APIRouter
from .about import router as about_router
from app.module.system.interface.models import router as models_router
from .sys_param import router as sys_param_router
router = APIRouter()
router.include_router(about_router)
router.include_router(models_router)
router.include_router(sys_param_router)
@@ -0,0 +1,22 @@
from fastapi import APIRouter
from typing import Dict, Any
from app.core.config import settings
from app.module.shared.schema import StandardResponse
from ..schema import HealthResponse
router = APIRouter()
@router.get("/health", response_model=StandardResponse[HealthResponse])
async def health_check():
"""健康检查端点"""
return StandardResponse(
code="0",
message="success",
data=HealthResponse(
status="healthy",
service="Label Studio Adapter",
version=settings.app_version
)
)
@@ -0,0 +1,76 @@
from fastapi import APIRouter, Depends, Query
from app.core.exception import SuccessResponse
from app.module.shared.schema import StandardResponse, PaginatedData
from app.module.system.schema.models import (
CreateModelRequest,
QueryModelRequest,
ModelsResponse,
ProviderItem,
ModelType,
)
from app.module.system.service.models_service import ModelsService
router = APIRouter(prefix="/models", tags=["models"])
@router.get("/providers", response_model=StandardResponse[list[ProviderItem]])
async def get_providers(svc: ModelsService = Depends()):
"""获取厂商列表,与 Java GET /models/providers 一致。"""
data = await svc.get_providers()
return SuccessResponse(data=data)
@router.get("/list", response_model=StandardResponse[PaginatedData[ModelsResponse]])
async def get_models(
page: int = Query(0, ge=0, description="页码,从 0 开始"),
size: int = Query(20, gt=0, le=2000, description="每页大小"),
provider: str | None = Query(None, description="模型提供商"),
type: ModelType | None = Query(None, description="模型类型"),
isEnabled: bool | None = Query(None, description="是否启用"),
isDefault: bool | None = Query(None, description="是否默认"),
svc: ModelsService = Depends(),
):
"""分页查询模型列表,与 Java GET /models/list 一致。"""
q = QueryModelRequest(
page=page,
size=size,
provider=provider,
type=type,
isEnabled=isEnabled,
isDefault=isDefault,
)
data = await svc.get_models(q)
return SuccessResponse(data=data)
@router.post("/create", response_model=StandardResponse[ModelsResponse])
async def create_model(req: CreateModelRequest, svc: ModelsService = Depends()):
"""创建模型配置,与 Java POST /models/create 一致。"""
data = await svc.create_model(req)
return SuccessResponse(data=data)
@router.get("/{model_id}", response_model=StandardResponse[ModelsResponse])
async def get_model_detail(model_id: str, svc: ModelsService = Depends()):
"""获取模型详情,与 Java GET /models/{modelId} 一致。"""
data = await svc.get_model_detail(model_id)
return SuccessResponse(data=data)
@router.put("/{model_id}", response_model=StandardResponse[ModelsResponse])
async def update_model(
model_id: str,
req: CreateModelRequest,
svc: ModelsService = Depends(),
):
"""更新模型配置,与 Java PUT /models/{modelId} 一致。"""
data = await svc.update_model(model_id, req)
return SuccessResponse(data=data)
@router.delete("/{model_id}", response_model=StandardResponse[None])
async def delete_model(model_id: str, svc: ModelsService = Depends()):
"""删除模型配置,与 Java DELETE /models/{modelId} 一致。"""
await svc.delete_model(model_id)
return SuccessResponse(data=None)
@@ -0,0 +1,106 @@
"""
System Parameter API Routes
系统参数 REST API 路由
"""
from typing import List
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import get_db
from app.module.shared.schema.common import StandardResponse
from app.module.system.schema.sys_param import (
SysParamDto,
UpdateParamValueRequest,
CreateSysParamRequest,
)
from app.module.system.service.sys_param_service import SysParamService
router = APIRouter(prefix="/sys-param", tags=["System Parameters"])
@router.get(
"/list",
response_model=StandardResponse[List[SysParamDto]],
summary="获取系统参数列表",
description="获取所有系统参数配置",
)
async def list_params(db: AsyncSession = Depends(get_db)):
"""获取系统参数列表"""
service = SysParamService(db)
params = await service.list_params()
return StandardResponse(code="0", message="success", data=params)
@router.get(
"/{param_id}",
response_model=StandardResponse[SysParamDto],
summary="获取系统参数详情",
description="根据ID获取系统参数详情",
)
async def get_param(
param_id: str,
db: AsyncSession = Depends(get_db),
):
"""获取系统参数详情"""
service = SysParamService(db)
param = await service.get_param_by_id(param_id)
if not param:
from app.core.exception import BusinessError, ErrorCodes
raise BusinessError(
ErrorCodes.NOT_FOUND, f"System parameter {param_id} not found"
)
return StandardResponse(code="0", message="success", data=param)
@router.post(
"",
response_model=StandardResponse[SysParamDto],
summary="创建系统参数",
description="创建新的系统参数",
)
async def create_param(
request: CreateSysParamRequest,
db: AsyncSession = Depends(get_db),
):
"""创建系统参数"""
service = SysParamService(db)
param = await service.create_param(request)
await db.commit()
return StandardResponse(code="0", message="success", data=param)
@router.put(
"/{param_id}",
response_model=StandardResponse[SysParamDto],
summary="更新系统参数值",
description="更新指定系统参数的值",
)
async def update_param_value(
param_id: str,
request: UpdateParamValueRequest,
db: AsyncSession = Depends(get_db),
):
"""更新系统参数值"""
service = SysParamService(db)
param = await service.update_param_value(param_id, request.param_value)
await db.commit()
return StandardResponse(code="0", message="success", data=param)
@router.delete(
"/{param_id}",
response_model=StandardResponse[str],
summary="删除系统参数",
description="删除指定的系统参数",
)
async def delete_param(
param_id: str,
db: AsyncSession = Depends(get_db),
):
"""删除系统参数"""
service = SysParamService(db)
await service.delete_param(param_id)
await db.commit()
return StandardResponse(code="0", message="success", data=param_id)
@@ -0,0 +1,17 @@
from .health import HealthResponse
from .models import (
ModelType,
CreateModelRequest,
QueryModelRequest,
ModelsResponse,
ProviderItem,
)
__all__ = [
"HealthResponse",
"ModelType",
"CreateModelRequest",
"QueryModelRequest",
"ModelsResponse",
"ProviderItem",
]
@@ -0,0 +1,10 @@
from pydantic import Field
from app.module.shared.schema import BaseResponseModel
from app.module.shared.schema import StandardResponse
class HealthResponse(BaseResponseModel):
"""健康检查响应模型"""
status: str = Field(..., description="服务状态")
service: str = Field(..., description="服务名称")
version: str = Field(..., description="应用版本")
@@ -0,0 +1,71 @@
"""
模型配置 DTO:与 Java 版 t_models 接口保持一致。
"""
from datetime import datetime
from enum import Enum
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class ModelType(str, Enum):
"""模型类型枚举,与 Java ModelType 一致。"""
CHAT = "CHAT"
EMBEDDING = "EMBEDDING"
# --- 请求 DTO ---
class CreateModelRequest(BaseModel):
"""创建/更新模型配置请求,与 Java CreateModelRequest 一致。"""
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
modelName: str = Field(..., min_length=1, description="模型名称(如 qwen2)")
provider: str = Field(..., min_length=1, description="模型提供商(如 Ollama、OpenAI、DeepSeek)")
baseUrl: str = Field(..., min_length=1, description="API 基础地址")
apiKey: Optional[str] = Field(None, description="API 密钥(无密钥则为空)")
type: ModelType = Field(..., description="模型类型(如 chat、embedding)")
isEnabled: Optional[bool] = Field(None, description="是否启用:1-启用,0-禁用")
isDefault: Optional[bool] = Field(None, description="是否默认:1-默认,0-非默认")
class QueryModelRequest(BaseModel):
"""模型查询请求,与 Java QueryModelRequest + PagingQuery 一致。page 从 0 开始,size 默认 20。"""
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
page: int = Field(0, ge=0, description="页码,从 0 开始")
size: int = Field(20, gt=0, le=2000, description="每页大小")
provider: Optional[str] = Field(None, description="模型提供商")
type: Optional[ModelType] = Field(None, description="模型类型")
isEnabled: Optional[bool] = Field(None, description="是否启用")
isDefault: Optional[bool] = Field(None, description="是否默认")
# --- 响应 DTO ---
class ModelsResponse(BaseModel):
"""模型配置响应,与 Java Models 实体字段一致。"""
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
id: str
modelName: str
provider: str
baseUrl: str
apiKey: str = ""
type: str # "CHAT" | "EMBEDDING"
isEnabled: bool = True
isDefault: bool = False
createdAt: Optional[datetime] = None
updatedAt: Optional[datetime] = None
createdBy: Optional[str] = None
updatedBy: Optional[str] = None
class ProviderItem(BaseModel):
"""厂商项,仅含 provider、baseUrl,与 Java getProviders() 返回结构一致。"""
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
provider: str
baseUrl: str
@@ -0,0 +1,51 @@
"""
System Parameter DTOs
系统参数数据传输对象
"""
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, Field
from app.module.shared.schema import BaseResponseModel
class SysParamDto(BaseResponseModel):
"""系统参数 DTO"""
id: str = Field(..., description="参数ID")
param_value: str = Field(..., description="参数值")
param_type: str = Field(
default="string", description="参数类型(string、integer、boolean)"
)
option_list: Optional[str] = Field(None, description="选项列表(JSON格式)")
description: Optional[str] = Field(None, description="参数描述")
is_built_in: bool = Field(default=False, description="是否内置")
can_modify: bool = Field(default=True, description="是否可修改")
is_enabled: bool = Field(default=True, description="是否启用")
created_at: Optional[datetime] = Field(None, description="创建时间")
updated_at: Optional[datetime] = Field(None, description="更新时间")
created_by: Optional[str] = Field(None, description="创建者")
updated_by: Optional[str] = Field(None, description="更新者")
class Config:
from_attributes = True
class UpdateParamValueRequest(BaseResponseModel):
"""更新参数值请求"""
param_value: str = Field(..., description="参数值")
class CreateSysParamRequest(BaseResponseModel):
"""创建系统参数请求"""
id: str = Field(..., description="参数ID")
param_value: str = Field(..., description="参数值")
param_type: str = Field(default="string", description="参数类型")
option_list: Optional[str] = Field(None, description="选项列表(JSON格式)")
description: Optional[str] = Field(None, description="参数描述")
is_built_in: bool = Field(default=False, description="是否内置")
can_modify: bool = Field(default=True, description="是否可修改")
is_enabled: bool = Field(default=True, description="是否启用")
@@ -0,0 +1,16 @@
"""通用系统服务:仅保留与 DB 直接相关的查询。LLM 创建与调用统一使用 app.core.llm.LLMFactory。"""
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models.models import Models
async def get_model_by_id(db: AsyncSession, model_id: str) -> Optional[Models]:
"""根据模型ID获取未删除的 Models 记录。"""
q = select(Models).where(Models.id == model_id).where(
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
)
result = await db.execute(q)
return result.scalar_one_or_none()
@@ -0,0 +1,173 @@
from __future__ import annotations
import shutil
from collections.abc import Callable, Iterable
from pathlib import Path
from typing import NamedTuple
from app.core.config import settings
from app.core.logging import get_logger
from app.module.shared.schedule import Scheduler
logger = get_logger(__name__)
class DiskUsage(NamedTuple):
total: int
used: int
free: int
class LogPvcMonitor:
"""Monitor log PVC usage and delete oldest log-like files when it is full."""
def __init__(
self,
root_path: str | Path,
threshold: float,
delete_batch_size: int,
file_suffixes: str | Iterable[str],
disk_usage: Callable[[Path], DiskUsage] = shutil.disk_usage,
unlink_file: Callable[[Path], None] | None = None,
) -> None:
self.root_path = Path(root_path)
self.threshold = threshold
self.delete_batch_size = max(1, delete_batch_size)
self.file_suffixes = self._normalize_suffixes(file_suffixes)
self._disk_usage = disk_usage
self._unlink_file = unlink_file or (lambda path: path.unlink())
def cleanup_if_needed(self) -> int:
usage_ratio = self._get_usage_ratio()
if usage_ratio is None:
return 0
if usage_ratio < self.threshold:
logger.debug(
"Log PVC usage %.2f%% is below threshold %.2f%%",
usage_ratio * 100,
self.threshold * 100,
)
return 0
total_deleted = 0
logger.warning(
"Log PVC usage %.2f%% reached threshold %.2f%%, starting cleanup under %s",
usage_ratio * 100,
self.threshold * 100,
self.root_path,
)
while usage_ratio >= self.threshold:
candidates = self._find_candidates()
if not candidates:
logger.warning(
"Log PVC usage remains %.2f%%, but no matching log files are available under %s",
usage_ratio * 100,
self.root_path,
)
break
deleted_in_batch = self._delete_batch(candidates[: self.delete_batch_size])
total_deleted += deleted_in_batch
if deleted_in_batch == 0:
logger.warning("Log PVC cleanup made no progress; stopping this cleanup run")
break
next_usage_ratio = self._get_usage_ratio()
if next_usage_ratio is None:
break
usage_ratio = next_usage_ratio
if total_deleted:
logger.info("Deleted %s files during log PVC cleanup", total_deleted)
return total_deleted
def _find_candidates(self) -> list[Path]:
candidates: list[tuple[float, str, Path]] = []
if not self.file_suffixes:
return []
try:
paths = self.root_path.rglob("*")
for path in paths:
try:
if path.is_symlink() or not path.is_file():
continue
if path.suffix.lower().lstrip(".") not in self.file_suffixes:
continue
stat_info = path.stat()
except OSError as exc:
logger.warning("Skipping inaccessible log file candidate %s: %s", path, exc)
continue
candidates.append((stat_info.st_mtime, str(path), path))
except OSError as exc:
logger.warning("Failed to scan log PVC path %s: %s", self.root_path, exc)
return []
candidates.sort(key=lambda item: (item[0], item[1]))
return [path for _, _, path in candidates]
def _delete_batch(self, candidates: list[Path]) -> int:
deleted = 0
for path in candidates:
try:
self._unlink_file(path)
deleted += 1
logger.info("Deleted old log file %s", path)
except FileNotFoundError:
logger.info("Log file already deleted before cleanup: %s", path)
except OSError as exc:
logger.warning("Failed to delete old log file %s: %s", path, exc)
return deleted
def _get_usage_ratio(self) -> float | None:
try:
usage = self._disk_usage(self.root_path)
except OSError as exc:
logger.warning("Failed to inspect log PVC path %s: %s", self.root_path, exc)
return None
if usage.total <= 0:
logger.warning("Log PVC path %s has invalid total size: %s", self.root_path, usage.total)
return None
return usage.used / usage.total
@staticmethod
def _normalize_suffixes(file_suffixes: str | Iterable[str]) -> set[str]:
if isinstance(file_suffixes, str):
raw_suffixes = file_suffixes.split(",")
else:
raw_suffixes = file_suffixes
return {
suffix.strip().lower().lstrip(".")
for suffix in raw_suffixes
if suffix and suffix.strip().lstrip(".")
}
def schedule_log_pvc_monitor(scheduler: Scheduler) -> None:
if not settings.log_pvc_monitor_enabled:
logger.info("Log PVC monitor is disabled")
return
monitor = LogPvcMonitor(
root_path=settings.log_pvc_monitor_path,
threshold=settings.log_pvc_monitor_threshold,
delete_batch_size=settings.log_pvc_monitor_delete_batch_size,
file_suffixes=settings.log_pvc_monitor_file_suffixes,
)
interval_seconds = max(1, settings.log_pvc_monitor_interval_seconds)
scheduler.add_interval_job(
job_id="system:log-pvc-monitor",
seconds=interval_seconds,
func=monitor.cleanup_if_needed,
replace_existing=True,
max_instances=1,
coalesce=True,
misfire_grace_time=60,
)
logger.info(
"Scheduled log PVC monitor for %s every %s seconds",
settings.log_pvc_monitor_path,
interval_seconds,
)
@@ -0,0 +1,249 @@
import math
import uuid
from datetime import datetime, timezone
from fastapi import Depends, HTTPException
from sqlalchemy import select, func, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.module.shared.llm import LLMFactory
from app.core.logging import get_logger
from app.db.datascope import DataScopeHandle, SYSTEM_USER
from app.db.models.models import Models
from app.db.session import get_db
from app.module.shared.schema import PaginatedData
from app.module.system.schema.models import (
CreateModelRequest,
QueryModelRequest,
ModelsResponse,
ProviderItem,
)
logger = get_logger(__name__)
# 固定厂商列表
PROVIDERS = [
ProviderItem(provider="ModelEngine", baseUrl="http://nginxservice/open/router/v1"),
ProviderItem(provider="Ollama", baseUrl="http://localhost:11434"),
ProviderItem(provider="OpenAI", baseUrl="https://api.openai.com/v1"),
ProviderItem(provider="DeepSeek", baseUrl="https://api.deepseek.com/v1"),
ProviderItem(provider="火山方舟", baseUrl="https://ark.cn-beijing.volces.com/api/v3"),
ProviderItem(provider="阿里云百炼", baseUrl="https://dashscope.aliyuncs.com/compatible-mode/v1"),
ProviderItem(provider="硅基流动", baseUrl="https://api.siliconflow.cn/v1"),
ProviderItem(provider="智谱AI", baseUrl="https://open.bigmodel.cn/api/paas/v4"),
ProviderItem(provider="自定义模型", baseUrl=""),
]
def _orm_to_response(row: Models) -> ModelsResponse:
return ModelsResponse(
id=row.id,
modelName=row.model_name,
provider=row.provider,
baseUrl=row.base_url,
apiKey=row.api_key or "",
type=row.type or "CHAT",
isEnabled=bool(row.is_enabled) if row.is_enabled is not None else True,
isDefault=bool(row.is_default) if row.is_default is not None else False,
createdAt=row.created_at,
updatedAt=row.updated_at,
createdBy=row.created_by,
updatedBy=row.updated_by,
)
class ModelsService:
"""模型配置服务:db 通过 FastAPI Depends(get_db) 注入,不在路由中传递。"""
def __init__(self, db: AsyncSession = Depends(get_db)):
self.db = db
async def get_providers(self) -> list[ProviderItem]:
"""返回固定厂商列表,与 Java getProviders() 一致。"""
return list(PROVIDERS)
async def get_models(self, q: QueryModelRequest) -> PaginatedData[ModelsResponse]:
"""分页查询,支持 provider/type/isEnabled/isDefault;排除已删除;page 从 0 开始。"""
query = select(Models).where(
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
)
if q.provider is not None and q.provider != "":
query = query.where(Models.provider == q.provider)
if q.type is not None:
query = query.where(Models.type == q.type.value)
if q.isEnabled is not None:
query = query.where(Models.is_enabled == q.isEnabled)
if q.isDefault is not None:
query = query.where(Models.is_default == q.isDefault)
total = (await self.db.execute(select(func.count()).select_from(query.subquery()))).scalar_one()
size = max(1, min(500, q.size))
offset = max(0, q.page) * size
rows = (
await self.db.execute(
query.order_by(Models.created_at.desc()).offset(offset).limit(size)
)
).scalars().all()
total_pages = math.ceil(total / size) if total else 0
current = max(0, q.page) + 1
return PaginatedData(
page=current,
size=size,
total_elements=total,
total_pages=total_pages,
content=[_orm_to_response(r) for r in rows],
)
async def get_model_detail(self, model_id: str) -> ModelsResponse:
"""获取模型详情,已删除或不存在则 404。"""
query = select(Models).where(Models.id == model_id).where(
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
)
r = (await self.db.execute(query)).scalar_one_or_none()
if not r:
raise HTTPException(status_code=404, detail="模型配置不存在")
return _orm_to_response(r)
async def create_model(self, req: CreateModelRequest) -> ModelsResponse:
"""创建模型:健康检查后 saveAndSetDefault;isEnabled 恒为 True。
若 uk_model_provider 已存在且 is_deleted=True,则恢复并更新相关字段。"""
try:
LLMFactory.check_health(req.modelName, req.baseUrl, req.apiKey, req.type.value)
except Exception as e:
logger.error("Model health check failed: model=%s type=%s err=%s", req.modelName, req.type, e)
raise HTTPException(status_code=400, detail="模型健康检查失败") from e
# 检查 uk_model_provider (model_name, provider, created_by) 是否存在且已删除
effective_user = (DataScopeHandle.get_user_info() or "").strip() or SYSTEM_USER
created_by_match = (
(Models.created_by == effective_user)
| ((Models.created_by.is_(None)) & (effective_user == SYSTEM_USER))
)
deleted_row = (
await self.db.execute(
select(Models).where(
Models.model_name == req.modelName,
Models.provider == req.provider,
created_by_match,
)
)
).scalar_one_or_none()
if deleted_row and deleted_row.is_deleted:
# 恢复并更新
entity = deleted_row
entity.base_url = req.baseUrl
entity.api_key = req.apiKey or ""
entity.type = req.type.value
entity.is_enabled = True
entity.is_deleted = False
entity.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
want_default = req.isDefault if req.isDefault is not None else False
if want_default:
await self.db.execute(
update(Models)
.where(Models.type == req.type.value, Models.is_default == True)
.values(is_default=False)
)
entity.is_default = want_default
self.db.add(entity)
await self.db.commit()
await self.db.refresh(entity)
return _orm_to_response(entity)
# 正常创建
existing = (
await self.db.execute(
select(Models).where(
(Models.is_deleted == False) | (Models.is_deleted.is_(None)),
Models.type == req.type.value,
Models.is_default == True,
)
)
).scalar_one_or_none()
is_default: bool
if existing is None:
is_default = True
else:
await self.db.execute(
update(Models)
.where(Models.type == req.type.value, Models.is_default == True)
.values(is_default=False)
)
is_default = req.isDefault if req.isDefault is not None else False
now = datetime.now(timezone.utc).replace(tzinfo=None)
entity = Models(
id=str(uuid.uuid4()),
model_name=req.modelName,
provider=req.provider,
base_url=req.baseUrl,
api_key=req.apiKey or "",
type=req.type.value,
is_enabled=True,
is_default=is_default,
is_deleted=False,
created_at=now,
updated_at=now,
)
self.db.add(entity)
await self.db.commit()
await self.db.refresh(entity)
return _orm_to_response(entity)
async def update_model(self, model_id: str, req: CreateModelRequest) -> ModelsResponse:
"""更新模型:存在性校验、健康检查后 updateAndSetDefault;isEnabled 恒为 True。"""
res = await self.db.execute(
select(Models).where(Models.id == model_id).where(
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
)
)
entity = res.scalar_one_or_none()
if not entity:
raise HTTPException(status_code=404, detail="模型配置不存在")
try:
LLMFactory.check_health(req.modelName, req.baseUrl, req.apiKey, req.type.value)
except Exception as e:
logger.error("Model health check failed: model=%s type=%s err=%s", req.modelName, req.type, e)
raise HTTPException(status_code=400, detail="模型健康检查失败") from e
entity.model_name = req.modelName
entity.provider = req.provider
entity.base_url = req.baseUrl
entity.api_key = req.apiKey or ""
entity.type = req.type.value
entity.is_enabled = True
want_default = req.isDefault if req.isDefault is not None else False
if (entity.is_default is not True) and want_default:
await self.db.execute(
update(Models)
.where(Models.type == req.type.value, Models.is_default == True)
.values(is_default=False)
)
entity.is_default = want_default
entity.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
await self.db.commit()
await self.db.refresh(entity)
return _orm_to_response(entity)
async def delete_model(self, model_id: str) -> None:
"""软删除模型配置。"""
entity = (
await self.db.execute(
select(Models).where(Models.id == model_id).where(
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
)
)
).scalar_one_or_none()
if not entity:
raise HTTPException(status_code=404, detail="模型配置不存在")
entity.is_deleted = True
entity.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
await self.db.commit()
await self.db.refresh(entity)
@@ -0,0 +1,141 @@
"""
System Parameter Service
系统参数服务
"""
from typing import List, Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.logging import get_logger
from app.core.exception import BusinessError, ErrorCodes
from app.db.models.sys_param import SysParam
from app.module.system.schema.sys_param import (
SysParamDto,
UpdateParamValueRequest,
CreateSysParamRequest,
)
logger = get_logger(__name__)
class SysParamService:
"""系统参数服务"""
def __init__(self, db: AsyncSession):
self.db = db
async def list_params(self) -> List[SysParamDto]:
"""获取系统参数列表"""
query = select(SysParam).order_by(SysParam.param_type, SysParam.id)
result = await self.db.execute(query)
params = result.scalars().all()
return [self._orm_to_response(p) for p in params]
async def get_param_by_id(self, param_id: str) -> Optional[SysParamDto]:
"""根据ID获取系统参数"""
query = select(SysParam).where(SysParam.id == param_id)
result = await self.db.execute(query)
param = result.scalar_one_or_none()
if not param:
return None
return self._orm_to_response(param)
async def update_param_value(self, param_id: str, param_value: str) -> SysParamDto:
"""更新系统参数值"""
# 查询参数
query = select(SysParam).where(SysParam.id == param_id)
result = await self.db.execute(query)
param = result.scalar_one_or_none()
if not param:
raise BusinessError(
ErrorCodes.NOT_FOUND, f"System parameter {param_id} not found"
)
# 检查是否可修改
if not param.can_modify:
raise BusinessError(
ErrorCodes.OPERATION_FAILED,
f"System parameter {param_id} cannot be modified",
)
# 更新值
param.param_value = param_value
await self.db.commit()
await self.db.refresh(param)
logger.info(f"Updated system parameter {param_id} = {param_value}")
return self._orm_to_response(param)
async def create_param(self, request: CreateSysParamRequest) -> SysParamDto:
"""创建系统参数"""
# 检查是否已存在
existing = await self.get_param_by_id(request.id)
if existing:
raise BusinessError(
ErrorCodes.OPERATION_FAILED,
f"System parameter {request.id} already exists",
)
# 创建新参数
param = SysParam(
id=request.id,
param_value=request.param_value,
param_type=request.param_type,
option_list=request.option_list,
description=request.description,
is_built_in=request.is_built_in,
can_modify=request.can_modify,
is_enabled=request.is_enabled,
)
self.db.add(param)
await self.db.commit()
await self.db.refresh(param)
logger.info(f"Created system parameter {request.id}")
return self._orm_to_response(param)
async def delete_param(self, param_id: str) -> None:
"""删除系统参数"""
# 查询参数
query = select(SysParam).where(SysParam.id == param_id)
result = await self.db.execute(query)
param = result.scalar_one_or_none()
if not param:
raise BusinessError(
ErrorCodes.NOT_FOUND, f"System parameter {param_id} not found"
)
# 检查是否为内置参数
if param.is_built_in:
raise BusinessError(
ErrorCodes.OPERATION_FAILED,
f"Cannot delete built-in system parameter {param_id}",
)
# 删除参数
await self.db.delete(param)
await self.db.commit()
logger.info(f"Deleted system parameter {param_id}")
@staticmethod
def _orm_to_response(row: SysParam) -> SysParamDto:
"""Convert ORM model to response DTO"""
return SysParamDto(
id=row.id,
param_value=row.param_value,
param_type=row.param_type or "string",
option_list=row.option_list,
description=row.description,
is_built_in=bool(row.is_built_in) if row.is_built_in is not None else False,
can_modify=bool(row.can_modify) if row.can_modify is not None else True,
is_enabled=bool(row.is_enabled) if row.is_enabled is not None else True,
created_at=row.created_at,
updated_at=row.updated_at,
created_by=row.created_by,
updated_by=row.updated_by,
)