refactor: 引入 ExportMixin 统一配置导出,补全导入缺失字段

- 新增 ExportMixin 基于排除列表自动收集可导出列,新增字段无需修改导出代码
- GlobalModel/Model/Provider/ProviderEndpoint/ProviderAPIKey 混入 ExportMixin
- 导出逻辑改用 to_export_dict(),消除 GlobalModel N+1 查询
- 导入逻辑补全 provider_type、auth_config、body_rules、format_acceptance_config 等字段
This commit is contained in:
fawney19
2026-02-15 17:14:14 +08:00
parent 1c16b77a92
commit a7bc7f25b4
4 changed files with 208 additions and 101 deletions

View File

@@ -5,6 +5,47 @@ SQLAlchemy Base 声明基类
直接复用 database.py 的 Base避免出现两套 MetaData 实例。
"""
from __future__ import annotations
from enum import Enum
from typing import Any, ClassVar
from src.models.database import Base
__all__ = ["Base"]
class ExportMixin:
"""配置导出 Mixin -- 基于排除列表自动收集字段。
子类定义 ``_export_exclude`` 列出不需要导出的列名(如 id、外键、
运行时状态、时间戳等),``to_export_dict()`` 会自动收集其余所有列。
新增数据库列时无需修改导出代码,只有确认不需要导出的列才需要
加入 ``_export_exclude``。
"""
# 子类覆盖:不导出的列名集合
_export_exclude: ClassVar[frozenset[str]] = frozenset()
def to_export_dict(self) -> dict[str, Any]:
"""将模型实例转为可导出的字典(排除 _export_exclude 中的字段)。"""
result: dict[str, Any] = {}
for col in self.__table__.columns: # type: ignore[attr-defined]
if col.name in self._export_exclude:
continue
value = getattr(self, col.name)
if isinstance(value, Enum):
value = value.value
result[col.name] = value
return result
@classmethod
def get_export_fields(cls) -> frozenset[str]:
"""返回可导出字段名集合(用于测试验证导入代码覆盖率)。"""
return frozenset(
col.name
for col in cls.__table__.columns # type: ignore[attr-defined]
if col.name not in cls._export_exclude
)
__all__ = ["Base", "ExportMixin"]

View File

@@ -27,10 +27,10 @@ from sqlalchemy import (
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import relationship
from ._base import Base
from ._base import Base, ExportMixin
class GlobalModel(Base):
class GlobalModel(ExportMixin, Base):
"""全局统一模型定义 - 包含价格和能力配置
设计原则:
@@ -41,6 +41,15 @@ class GlobalModel(Base):
__tablename__ = "global_models"
_export_exclude = frozenset(
{
"id",
"usage_count",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
name = Column(String(100), unique=True, nullable=False, index=True) # 统一模型名(唯一)
display_name = Column(String(100), nullable=False)
@@ -118,7 +127,7 @@ class GlobalModel(Base):
models = relationship("Model", back_populates="global_model")
class Model(Base):
class Model(ExportMixin, Base):
"""Provider 模型配置表 - Provider 如何使用某个 GlobalModel
设计原则:
@@ -132,6 +141,17 @@ class Model(Base):
__tablename__ = "models"
_export_exclude = frozenset(
{
"id",
"provider_id",
"global_model_id",
"is_available",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
provider_id = Column(String(36), ForeignKey("providers.id"), nullable=False)
# 可为空NULL 表示未关联,不参与路由;非 NULL 表示已关联,参与路由

View File

@@ -30,14 +30,25 @@ from sqlalchemy.orm import relationship
from src.core.enums import ProviderBillingType
from ._base import Base
from ._base import Base, ExportMixin
class Provider(Base):
class Provider(ExportMixin, Base):
"""提供商配置表"""
__tablename__ = "providers"
_export_exclude = frozenset(
{
"id",
"monthly_used_usd",
"quota_last_reset_at",
"quota_expires_at",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
name = Column(String(100), unique=True, nullable=False, index=True) # 提供商名称(唯一)
description = Column(Text, nullable=True) # 提供商描述
@@ -133,11 +144,22 @@ class Provider(Base):
)
class ProviderEndpoint(Base):
class ProviderEndpoint(ExportMixin, Base):
"""提供商端点 - 一个提供商可以有多个 API 格式端点"""
__tablename__ = "provider_endpoints"
_export_exclude = frozenset(
{
"id",
"provider_id",
"api_family",
"endpoint_kind",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
provider_id = Column(String(36), ForeignKey("providers.id", ondelete="CASCADE"), nullable=False)
@@ -287,11 +309,50 @@ class ProxyNode(Base):
__table_args__ = (UniqueConstraint("ip", "port", name="uq_proxy_node_ip_port"),)
class ProviderAPIKey(Base):
class ProviderAPIKey(ExportMixin, Base):
"""Provider API密钥表 - 直接归属于 Provider支持多种 API 格式"""
__tablename__ = "provider_api_keys"
_export_exclude = frozenset(
{
"id",
"provider_id",
# 加密存储,导出时需手动解密
"api_key",
"auth_config",
# 运行时状态 / 自适应学习
"learned_rpm_limit",
"concurrent_429_count",
"rpm_429_count",
"last_429_at",
"last_429_type",
"last_rpm_peak",
"adjustment_history",
"utilization_samples",
"last_probe_increase_at",
"health_by_format",
"circuit_breaker_by_format",
# 使用统计
"request_count",
"success_count",
"error_count",
"total_response_time_ms",
"last_used_at",
"last_error_at",
"last_error_msg",
# 运行时状态
"expires_at",
"last_models_fetch_at",
"last_models_fetch_error",
"upstream_metadata",
"oauth_invalid_at",
"oauth_invalid_reason",
"created_at",
"updated_at",
}
)
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), index=True)
# 外键关系 - 直接关联 Provider