feat: add A-card operations frontend and backend foundation
This commit is contained in:
@@ -26,6 +26,7 @@ from venv import logger
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from pydantic import Field, field_validator, model_validator
|
||||
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
|
||||
from sqlalchemy.engine import make_url
|
||||
|
||||
|
||||
# ── AES 解密核心函数 ────────────────────────────────────────────────────────
|
||||
@@ -63,6 +64,43 @@ class Settings(BaseSettings):
|
||||
),
|
||||
description="SQLAlchemy async URI for the platform MySQL.",
|
||||
)
|
||||
operations_database_url: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"SQLAlchemy async URI for the isolated model_operations database. "
|
||||
"When omitted it reuses DATABASE_URL credentials and switches only "
|
||||
"the schema name to model_operations."
|
||||
),
|
||||
)
|
||||
platform_read_database_url: str | None = Field(
|
||||
default=None,
|
||||
description="Read-only model_platform connection used by operations APIs.",
|
||||
)
|
||||
deploy_database_url: str | None = Field(
|
||||
default=None,
|
||||
description="Read-only model_deploy connection used by operations APIs.",
|
||||
)
|
||||
|
||||
# ── operations Redis accelerator ─────────────────────────────
|
||||
redis_url: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Optional Redis URL for operations query cache and event Streams. "
|
||||
"MySQL remains the source of truth when Redis is unavailable."
|
||||
),
|
||||
)
|
||||
redis_socket_connect_timeout_seconds: float = Field(default=2.0, gt=0, le=30)
|
||||
redis_socket_timeout_seconds: float = Field(default=2.0, gt=0, le=30)
|
||||
operations_cache_ttl_seconds: int = Field(default=300, ge=10, le=86400)
|
||||
operations_redis_prefix: str = Field(default="model-platform:operations")
|
||||
operations_event_stream: str = Field(
|
||||
default="model-platform:operations:events"
|
||||
)
|
||||
operations_event_stream_maxlen: int = Field(default=10000, ge=100)
|
||||
operations_data_mode: str = Field(
|
||||
default="database",
|
||||
description="Operations data source: database or mock.",
|
||||
)
|
||||
|
||||
# ── JWT ───────────────────────────────────────────────────────
|
||||
jwt_secret: str = Field(
|
||||
@@ -268,6 +306,23 @@ class Settings(BaseSettings):
|
||||
# print(values[key])
|
||||
return values
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _derive_operations_database_url(self) -> "Settings":
|
||||
"""Default the operations database to the platform server credentials."""
|
||||
if not self.operations_database_url:
|
||||
platform_url = make_url(self.database_url)
|
||||
self.operations_database_url = platform_url.set(
|
||||
database="model_operations"
|
||||
).render_as_string(hide_password=False)
|
||||
if not self.platform_read_database_url:
|
||||
self.platform_read_database_url = self.database_url
|
||||
if not self.deploy_database_url:
|
||||
platform_url = make_url(self.database_url)
|
||||
self.deploy_database_url = platform_url.set(
|
||||
database="model_deploy"
|
||||
).render_as_string(hide_password=False)
|
||||
return self
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
|
||||
@@ -5,6 +5,7 @@ from common.db.session import (
|
||||
AsyncSessionFactory,
|
||||
create_database_engine,
|
||||
create_session_factory,
|
||||
readonly_session_scope,
|
||||
session_scope,
|
||||
)
|
||||
|
||||
@@ -13,5 +14,6 @@ __all__ = [
|
||||
"Base",
|
||||
"create_database_engine",
|
||||
"create_session_factory",
|
||||
"readonly_session_scope",
|
||||
"session_scope",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from common.db.models.operations.base import OperationsBase
|
||||
from common.db.models.operations.governance import (
|
||||
OpsMonitorEvaluation,
|
||||
OpsMonitorReview,
|
||||
OpsRuleItem,
|
||||
OpsRuleVersion,
|
||||
)
|
||||
from common.db.models.operations.models import OpsModelInstance, OpsModelVersion
|
||||
from common.db.models.operations.monitoring import (
|
||||
OpsMonitorBatch,
|
||||
OpsMonitorDistribution,
|
||||
OpsMonitorFeatureMetric,
|
||||
OpsMonitorResult,
|
||||
)
|
||||
from common.db.models.operations.reference import OpsBank, OpsModelCategory
|
||||
|
||||
__all__ = [
|
||||
"OperationsBase",
|
||||
"OpsBank",
|
||||
"OpsModelCategory",
|
||||
"OpsModelInstance",
|
||||
"OpsModelVersion",
|
||||
"OpsMonitorBatch",
|
||||
"OpsMonitorDistribution",
|
||||
"OpsMonitorEvaluation",
|
||||
"OpsMonitorFeatureMetric",
|
||||
"OpsMonitorResult",
|
||||
"OpsMonitorReview",
|
||||
"OpsRuleItem",
|
||||
"OpsRuleVersion",
|
||||
]
|
||||
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Integer, text
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
class OperationsBase(DeclarativeBase):
|
||||
"""Declarative base isolated from the model_platform metadata."""
|
||||
|
||||
|
||||
class CreatedAtMixin:
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(), nullable=False, server_default=text("CURRENT_TIMESTAMP")
|
||||
)
|
||||
|
||||
|
||||
class TimestampMixin(CreatedAtMixin):
|
||||
updated_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(),
|
||||
nullable=False,
|
||||
server_default=text("CURRENT_TIMESTAMP"),
|
||||
onupdate=datetime.datetime.utcnow,
|
||||
)
|
||||
|
||||
|
||||
class SoftDeleteMixin:
|
||||
is_deleted: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, server_default=text("0")
|
||||
)
|
||||
deleted_at: Mapped[datetime.datetime | None] = mapped_column(DateTime())
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CreatedAtMixin",
|
||||
"OperationsBase",
|
||||
"SoftDeleteMixin",
|
||||
"TimestampMixin",
|
||||
]
|
||||
@@ -0,0 +1,155 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import DateTime, Index, Integer, JSON, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from common.db.models.operations.base import (
|
||||
CreatedAtMixin,
|
||||
OperationsBase,
|
||||
SoftDeleteMixin,
|
||||
TimestampMixin,
|
||||
)
|
||||
|
||||
|
||||
class OpsRuleVersion(CreatedAtMixin, SoftDeleteMixin, OperationsBase):
|
||||
__tablename__ = "ops_rule_versions"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"uk_ops_rule_versions_label",
|
||||
"workspace_id",
|
||||
"category_code",
|
||||
"version_label",
|
||||
unique=True,
|
||||
),
|
||||
Index(
|
||||
"idx_ops_rule_versions_status",
|
||||
"workspace_id",
|
||||
"category_code",
|
||||
"rule_status",
|
||||
"published_at",
|
||||
),
|
||||
{"comment": "监控判级规则版本"},
|
||||
)
|
||||
|
||||
rule_version_id: Mapped[str] = mapped_column(String(26), primary_key=True)
|
||||
workspace_id: Mapped[str] = mapped_column(String(26), nullable=False)
|
||||
category_code: Mapped[str] = mapped_column(String(32), nullable=False, default="*")
|
||||
version_label: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
rule_status: Mapped[str] = mapped_column(String(24), nullable=False, default="draft")
|
||||
threshold_json: Mapped[dict[str, Any]] = mapped_column(JSON(), nullable=False)
|
||||
supersedes_rule_version_id: Mapped[str | None] = mapped_column(String(26))
|
||||
change_note: Mapped[str | None] = mapped_column(String(1000))
|
||||
created_by: Mapped[str] = mapped_column(String(26), nullable=False)
|
||||
published_by: Mapped[str | None] = mapped_column(String(26))
|
||||
published_at: Mapped[datetime.datetime | None] = mapped_column(DateTime())
|
||||
|
||||
|
||||
class OpsRuleItem(CreatedAtMixin, SoftDeleteMixin, OperationsBase):
|
||||
__tablename__ = "ops_rule_items"
|
||||
__table_args__ = (
|
||||
Index("uk_ops_rule_items_order", "rule_version_id", "sort_order", unique=True),
|
||||
Index("uk_ops_rule_items_reason", "rule_version_id", "reason_code", unique=True),
|
||||
{"comment": "规则版本下的判级矩阵行"},
|
||||
)
|
||||
|
||||
rule_item_id: Mapped[str] = mapped_column(String(26), primary_key=True)
|
||||
rule_version_id: Mapped[str] = mapped_column(String(26), nullable=False, index=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
ranking_result: Mapped[str] = mapped_column(String(24), nullable=False)
|
||||
ks_band_code: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
psi_band_code: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
ks_drop_band_code: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
conditions_json: Mapped[dict[str, Any] | None] = mapped_column(JSON())
|
||||
abnormal_level: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
monitor_grade: Mapped[str] = mapped_column(String(1), nullable=False)
|
||||
secondary_upgrade_threshold: Mapped[int | None] = mapped_column(Integer)
|
||||
reason_code: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
reason_template: Mapped[str] = mapped_column(String(1000), nullable=False)
|
||||
action_text: Mapped[str] = mapped_column(String(1000), nullable=False)
|
||||
|
||||
|
||||
class OpsMonitorEvaluation(CreatedAtMixin, SoftDeleteMixin, OperationsBase):
|
||||
__tablename__ = "ops_monitor_evaluations"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"uk_ops_evaluations_result_rule",
|
||||
"monitor_result_id",
|
||||
"rule_version_id",
|
||||
unique=True,
|
||||
),
|
||||
Index(
|
||||
"idx_ops_evaluations_grade",
|
||||
"workspace_id",
|
||||
"monitor_grade",
|
||||
"abnormal_level",
|
||||
"evaluated_at",
|
||||
),
|
||||
{"comment": "按规则版本生成的不可变监控判级快照"},
|
||||
)
|
||||
|
||||
evaluation_id: Mapped[str] = mapped_column(String(26), primary_key=True)
|
||||
workspace_id: Mapped[str] = mapped_column(String(26), nullable=False)
|
||||
monitor_result_id: Mapped[str] = mapped_column(String(26), nullable=False, index=True)
|
||||
rule_version_id: Mapped[str] = mapped_column(String(26), nullable=False)
|
||||
rule_item_id: Mapped[str] = mapped_column(String(26), nullable=False)
|
||||
ks_mom_drop_rate: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
secondary_level2_hits_6m: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0
|
||||
)
|
||||
abnormal_level: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
monitor_grade: Mapped[str] = mapped_column(String(1), nullable=False)
|
||||
reason_code: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
reason_text_snapshot: Mapped[str] = mapped_column(String(2000), nullable=False)
|
||||
action_snapshot: Mapped[str] = mapped_column(String(2000), nullable=False)
|
||||
is_current: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
evaluated_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(), nullable=False, default=datetime.datetime.utcnow
|
||||
)
|
||||
|
||||
|
||||
class OpsMonitorReview(TimestampMixin, SoftDeleteMixin, OperationsBase):
|
||||
__tablename__ = "ops_monitor_reviews"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"uk_ops_reviews_result_stage",
|
||||
"monitor_result_id",
|
||||
"review_stage",
|
||||
unique=True,
|
||||
),
|
||||
Index(
|
||||
"idx_ops_reviews_pending",
|
||||
"workspace_id",
|
||||
"review_status",
|
||||
"review_stage",
|
||||
"due_at",
|
||||
),
|
||||
{"comment": "模型团队初审与业务团队终审"},
|
||||
)
|
||||
|
||||
review_id: Mapped[str] = mapped_column(String(26), primary_key=True)
|
||||
workspace_id: Mapped[str] = mapped_column(String(26), nullable=False)
|
||||
monitor_result_id: Mapped[str] = mapped_column(String(26), nullable=False, index=True)
|
||||
evaluation_id: Mapped[str] = mapped_column(String(26), nullable=False)
|
||||
review_stage: Mapped[str] = mapped_column(String(24), nullable=False)
|
||||
review_status: Mapped[str] = mapped_column(
|
||||
String(24), nullable=False, default="pending"
|
||||
)
|
||||
decision_code: Mapped[str | None] = mapped_column(String(32))
|
||||
handling_note: Mapped[str | None] = mapped_column(String(2000))
|
||||
due_at: Mapped[datetime.datetime | None] = mapped_column(DateTime())
|
||||
handled_by: Mapped[str | None] = mapped_column(String(26))
|
||||
handled_at: Mapped[datetime.datetime | None] = mapped_column(DateTime())
|
||||
auto_closed_at: Mapped[datetime.datetime | None] = mapped_column(DateTime())
|
||||
state_version: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"OpsMonitorEvaluation",
|
||||
"OpsMonitorReview",
|
||||
"OpsRuleItem",
|
||||
"OpsRuleVersion",
|
||||
]
|
||||
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import Date, DateTime, Index, Integer, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from common.db.models.operations.base import (
|
||||
OperationsBase,
|
||||
SoftDeleteMixin,
|
||||
TimestampMixin,
|
||||
)
|
||||
|
||||
|
||||
class OpsModelInstance(TimestampMixin, SoftDeleteMixin, OperationsBase):
|
||||
__tablename__ = "ops_model_instances"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"uk_ops_models_business_id", "workspace_id", "model_id", unique=True
|
||||
),
|
||||
Index(
|
||||
"uk_ops_models_source",
|
||||
"workspace_id",
|
||||
"source_system",
|
||||
"source_model_ref",
|
||||
unique=True,
|
||||
),
|
||||
Index(
|
||||
"idx_ops_models_filters",
|
||||
"workspace_id",
|
||||
"category_code",
|
||||
"model_status",
|
||||
"is_deleted",
|
||||
),
|
||||
{"comment": "业务模型实例;模型平台可受控直写"},
|
||||
)
|
||||
|
||||
model_instance_id: Mapped[str] = mapped_column(String(26), primary_key=True)
|
||||
workspace_id: Mapped[str] = mapped_column(String(26), nullable=False)
|
||||
bank_id: Mapped[str] = mapped_column(String(26), nullable=False, index=True)
|
||||
category_code: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
model_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
model_name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
model_status: Mapped[str] = mapped_column(
|
||||
String(24), nullable=False, default="normal"
|
||||
)
|
||||
current_version_id: Mapped[str | None] = mapped_column(String(26), index=True)
|
||||
is_common_model: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
common_source_model_id: Mapped[str | None] = mapped_column(String(26), index=True)
|
||||
common_model_name: Mapped[str | None] = mapped_column(String(200))
|
||||
source_system: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, default="model_platform"
|
||||
)
|
||||
source_model_ref: Mapped[str | None] = mapped_column(String(128))
|
||||
source_updated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime())
|
||||
|
||||
|
||||
class OpsModelVersion(TimestampMixin, SoftDeleteMixin, OperationsBase):
|
||||
__tablename__ = "ops_model_versions"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"uk_ops_model_versions_label",
|
||||
"model_instance_id",
|
||||
"version_label",
|
||||
unique=True,
|
||||
),
|
||||
Index(
|
||||
"uk_ops_model_versions_source",
|
||||
"workspace_id",
|
||||
"source_system",
|
||||
"source_version_ref",
|
||||
unique=True,
|
||||
),
|
||||
Index(
|
||||
"idx_ops_model_versions_lifecycle",
|
||||
"workspace_id",
|
||||
"version_status",
|
||||
"online_date",
|
||||
"offline_date",
|
||||
),
|
||||
{"comment": "模型版本与生命周期;模型平台可受控直写"},
|
||||
)
|
||||
|
||||
model_version_id: Mapped[str] = mapped_column(String(26), primary_key=True)
|
||||
workspace_id: Mapped[str] = mapped_column(String(26), nullable=False)
|
||||
model_instance_id: Mapped[str] = mapped_column(String(26), nullable=False, index=True)
|
||||
version_label: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
version_status: Mapped[str] = mapped_column(
|
||||
String(24), nullable=False, default="active"
|
||||
)
|
||||
platform_versions_id: Mapped[str | None] = mapped_column(String(26))
|
||||
developer_user_id: Mapped[str | None] = mapped_column(String(26))
|
||||
developer_display_name: Mapped[str | None] = mapped_column(String(100))
|
||||
development_date: Mapped[datetime.date | None] = mapped_column(Date())
|
||||
iteration_start_date: Mapped[datetime.date | None] = mapped_column(Date())
|
||||
last_iteration_date: Mapped[datetime.date | None] = mapped_column(Date())
|
||||
iteration_reason: Mapped[str | None] = mapped_column(String(1000))
|
||||
escort_start_date: Mapped[datetime.date | None] = mapped_column(Date())
|
||||
escort_end_date: Mapped[datetime.date | None] = mapped_column(Date())
|
||||
online_date: Mapped[datetime.date | None] = mapped_column(Date())
|
||||
offline_date: Mapped[datetime.date | None] = mapped_column(Date())
|
||||
development_ks: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
development_psi: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
max_lift: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
scoring_logic_storage_object_id: Mapped[str | None] = mapped_column(String(26))
|
||||
source_system: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, default="model_platform"
|
||||
)
|
||||
source_version_ref: Mapped[str | None] = mapped_column(String(128))
|
||||
source_updated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime())
|
||||
|
||||
|
||||
__all__ = ["OpsModelInstance", "OpsModelVersion"]
|
||||
@@ -0,0 +1,182 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import Date, DateTime, Index, Integer, JSON, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from common.db.models.operations.base import (
|
||||
CreatedAtMixin,
|
||||
OperationsBase,
|
||||
SoftDeleteMixin,
|
||||
TimestampMixin,
|
||||
)
|
||||
|
||||
|
||||
class OpsMonitorBatch(TimestampMixin, SoftDeleteMixin, OperationsBase):
|
||||
__tablename__ = "ops_monitor_batches"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"uk_ops_monitor_batches_source_no",
|
||||
"workspace_id",
|
||||
"source_system",
|
||||
"source_batch_no",
|
||||
unique=True,
|
||||
),
|
||||
Index(
|
||||
"uk_ops_monitor_batches_revision",
|
||||
"workspace_id",
|
||||
"source_system",
|
||||
"monitor_month",
|
||||
"revision_no",
|
||||
unique=True,
|
||||
),
|
||||
Index(
|
||||
"idx_ops_monitor_batches_publish",
|
||||
"workspace_id",
|
||||
"batch_status",
|
||||
"monitor_month",
|
||||
"revision_no",
|
||||
),
|
||||
{"comment": "月度监控写入批次与发布门闩"},
|
||||
)
|
||||
|
||||
batch_id: Mapped[str] = mapped_column(String(26), primary_key=True)
|
||||
workspace_id: Mapped[str] = mapped_column(String(26), nullable=False)
|
||||
source_system: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, default="model_platform"
|
||||
)
|
||||
source_batch_no: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
monitor_month: Mapped[datetime.date] = mapped_column(Date(), nullable=False)
|
||||
revision_no: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
supersedes_batch_id: Mapped[str | None] = mapped_column(String(26))
|
||||
batch_status: Mapped[str] = mapped_column(
|
||||
String(24), nullable=False, default="writing"
|
||||
)
|
||||
expected_model_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
written_model_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
feature_row_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
distribution_row_count: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0
|
||||
)
|
||||
checksum_sha256: Mapped[str | None] = mapped_column(String(64))
|
||||
generated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime())
|
||||
published_at: Mapped[datetime.datetime | None] = mapped_column(DateTime())
|
||||
published_by: Mapped[str | None] = mapped_column(String(26))
|
||||
failed_reason: Mapped[str | None] = mapped_column(String(2000))
|
||||
|
||||
|
||||
class OpsMonitorResult(CreatedAtMixin, SoftDeleteMixin, OperationsBase):
|
||||
__tablename__ = "ops_monitor_results"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"uk_ops_monitor_results_batch_model",
|
||||
"batch_id",
|
||||
"model_instance_id",
|
||||
unique=True,
|
||||
),
|
||||
Index(
|
||||
"idx_ops_monitor_results_month",
|
||||
"workspace_id",
|
||||
"monitor_month",
|
||||
"model_instance_id",
|
||||
),
|
||||
{"comment": "单模型单月原始监控结果;发布后不可更新"},
|
||||
)
|
||||
|
||||
monitor_result_id: Mapped[str] = mapped_column(String(26), primary_key=True)
|
||||
workspace_id: Mapped[str] = mapped_column(String(26), nullable=False)
|
||||
batch_id: Mapped[str] = mapped_column(String(26), nullable=False, index=True)
|
||||
model_instance_id: Mapped[str] = mapped_column(String(26), nullable=False, index=True)
|
||||
model_version_id: Mapped[str] = mapped_column(String(26), nullable=False, index=True)
|
||||
monitor_month: Mapped[datetime.date] = mapped_column(Date(), nullable=False)
|
||||
source_result_ref: Mapped[str | None] = mapped_column(String(128))
|
||||
ranking_result: Mapped[str | None] = mapped_column(String(24))
|
||||
ks_value: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
psi_value: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
sample_count: Mapped[int | None] = mapped_column(Integer)
|
||||
good_count: Mapped[int | None] = mapped_column(Integer)
|
||||
bad_count: Mapped[int | None] = mapped_column(Integer)
|
||||
source_result_json: Mapped[dict[str, Any] | None] = mapped_column(JSON())
|
||||
source_calculated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime())
|
||||
|
||||
|
||||
class OpsMonitorFeatureMetric(CreatedAtMixin, SoftDeleteMixin, OperationsBase):
|
||||
__tablename__ = "ops_monitor_feature_metrics"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"uk_ops_feature_metrics_result_feature",
|
||||
"monitor_result_id",
|
||||
"feature_code",
|
||||
unique=True,
|
||||
),
|
||||
Index("idx_ops_feature_metrics_iv_drop", "monitor_result_id", "iv_drop_rate"),
|
||||
Index(
|
||||
"idx_ops_feature_metrics_csi_rise", "monitor_result_id", "csi_rise_rate"
|
||||
),
|
||||
{"comment": "单月特征级 IV/CSI 与贡献变化"},
|
||||
)
|
||||
|
||||
feature_metric_id: Mapped[str] = mapped_column(String(26), primary_key=True)
|
||||
monitor_result_id: Mapped[str] = mapped_column(String(26), nullable=False, index=True)
|
||||
feature_code: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
feature_name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
iv_value: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
previous_iv_value: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
iv_drop_rate: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
csi_value: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
previous_csi_value: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
csi_rise_rate: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
ks_contribution_change: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
psi_contribution_change: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
source_metric_json: Mapped[dict[str, Any] | None] = mapped_column(JSON())
|
||||
|
||||
|
||||
class OpsMonitorDistribution(CreatedAtMixin, SoftDeleteMixin, OperationsBase):
|
||||
__tablename__ = "ops_monitor_distributions"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"uk_ops_distributions_bin",
|
||||
"monitor_result_id",
|
||||
"dimension_type",
|
||||
"feature_code",
|
||||
"bin_order",
|
||||
unique=True,
|
||||
),
|
||||
Index(
|
||||
"idx_ops_distributions_feature",
|
||||
"monitor_result_id",
|
||||
"feature_code",
|
||||
"bin_order",
|
||||
),
|
||||
{"comment": "排序性评分分箱及特征分布"},
|
||||
)
|
||||
|
||||
distribution_id: Mapped[str] = mapped_column(String(26), primary_key=True)
|
||||
monitor_result_id: Mapped[str] = mapped_column(String(26), nullable=False, index=True)
|
||||
dimension_type: Mapped[str] = mapped_column(String(24), nullable=False)
|
||||
feature_code: Mapped[str] = mapped_column(String(128), nullable=False, default="")
|
||||
feature_name: Mapped[str | None] = mapped_column(String(200))
|
||||
bin_order: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
bin_code: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
bin_label: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
reference_period_label: Mapped[str | None] = mapped_column(String(64))
|
||||
reference_count: Mapped[int | None] = mapped_column(Integer)
|
||||
reference_share: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
current_count: Mapped[int | None] = mapped_column(Integer)
|
||||
current_share: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
good_count: Mapped[int | None] = mapped_column(Integer)
|
||||
bad_count: Mapped[int | None] = mapped_column(Integer)
|
||||
bad_rate: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
psi_component: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
csi_component: Mapped[Decimal | None] = mapped_column(Numeric(12, 8))
|
||||
|
||||
|
||||
__all__ = [
|
||||
"OpsMonitorBatch",
|
||||
"OpsMonitorDistribution",
|
||||
"OpsMonitorFeatureMetric",
|
||||
"OpsMonitorResult",
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Index, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from common.db.models.operations.base import (
|
||||
OperationsBase,
|
||||
SoftDeleteMixin,
|
||||
TimestampMixin,
|
||||
)
|
||||
|
||||
|
||||
class OpsModelCategory(TimestampMixin, SoftDeleteMixin, OperationsBase):
|
||||
__tablename__ = "ops_model_categories"
|
||||
__table_args__ = (
|
||||
Index("uk_ops_categories_name", "category_name", unique=True),
|
||||
Index("idx_ops_categories_status", "status", "sort_order"),
|
||||
{"comment": "固定模型大类字典,由运维模块维护"},
|
||||
)
|
||||
|
||||
category_code: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
category_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||
|
||||
|
||||
class OpsBank(TimestampMixin, SoftDeleteMixin, OperationsBase):
|
||||
__tablename__ = "ops_banks"
|
||||
__table_args__ = (
|
||||
Index("uk_ops_banks_code", "workspace_id", "bank_code", unique=True),
|
||||
Index(
|
||||
"uk_ops_banks_source",
|
||||
"workspace_id",
|
||||
"source_system",
|
||||
"source_bank_ref",
|
||||
unique=True,
|
||||
),
|
||||
Index(
|
||||
"idx_ops_banks_workspace_status",
|
||||
"workspace_id",
|
||||
"bank_status",
|
||||
"is_deleted",
|
||||
),
|
||||
{"comment": "银行主数据;模型平台可受控直写"},
|
||||
)
|
||||
|
||||
bank_id: Mapped[str] = mapped_column(String(26), primary_key=True)
|
||||
workspace_id: Mapped[str] = mapped_column(String(26), nullable=False)
|
||||
bank_code: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
bank_name: Mapped[str] = mapped_column(String(150), nullable=False)
|
||||
is_wuji_bank: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
bank_status: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, default="active"
|
||||
)
|
||||
source_system: Mapped[str] = mapped_column(
|
||||
String(64), nullable=False, default="model_platform"
|
||||
)
|
||||
source_bank_ref: Mapped[str | None] = mapped_column(String(128))
|
||||
source_updated_at: Mapped[datetime.datetime | None] = mapped_column(DateTime())
|
||||
|
||||
|
||||
__all__ = ["OpsBank", "OpsModelCategory"]
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncEngine,
|
||||
AsyncSession,
|
||||
@@ -55,3 +56,18 @@ async def session_scope(
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def readonly_session_scope(
|
||||
factory: AsyncSessionFactory,
|
||||
) -> AsyncIterator[AsyncSession]:
|
||||
"""Yield a session whose MySQL transaction is explicitly read-only."""
|
||||
async with factory() as session:
|
||||
try:
|
||||
bind = session.get_bind()
|
||||
if bind.dialect.name == "mysql":
|
||||
await session.execute(text("SET TRANSACTION READ ONLY"))
|
||||
yield session
|
||||
finally:
|
||||
await session.rollback()
|
||||
|
||||
Reference in New Issue
Block a user