feat: add A-card operations frontend and backend foundation

This commit is contained in:
郑龙捷
2026-09-02 09:54:03 +08:00
parent 9badd3597f
commit ad71259ba5
106 changed files with 12461 additions and 1796 deletions
+55
View File
@@ -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",
+2
View File
@@ -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"]
+16
View File
@@ -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()