515 lines
24 KiB
Python
515 lines
24 KiB
Python
"""PRD-defined core PostgreSQL models for the risk prediction platform."""
|
|
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
|
|
from sqlalchemy import (
|
|
BigInteger,
|
|
Boolean,
|
|
Date,
|
|
DateTime,
|
|
ForeignKey,
|
|
Index,
|
|
Integer,
|
|
Numeric,
|
|
String,
|
|
Text,
|
|
UniqueConstraint,
|
|
func,
|
|
)
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.database.base import Base
|
|
|
|
|
|
class AnomalyCase(Base):
|
|
"""Approved historical anomaly case used by the experience engine."""
|
|
|
|
__tablename__ = "anomaly_cases"
|
|
__table_args__ = (
|
|
Index("ix_anomaly_cases_date", "date"),
|
|
Index("ix_anomaly_cases_product_category", "product_category"),
|
|
Index("ix_anomaly_cases_supplier", "supplier"),
|
|
Index("ix_anomaly_cases_factory", "factory"),
|
|
Index("ix_anomaly_cases_material_type", "material_type"),
|
|
Index("ix_anomaly_cases_defect_level", "defect_level"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
date: Mapped[date | None] = mapped_column(Date)
|
|
product_code: Mapped[str | None] = mapped_column(String(100))
|
|
product_name: Mapped[str | None] = mapped_column(String(500))
|
|
brand: Mapped[str | None] = mapped_column(String(100))
|
|
specification: Mapped[str | None] = mapped_column(String(100))
|
|
product_category: Mapped[str | None] = mapped_column(String(100))
|
|
stage_new: Mapped[str | None] = mapped_column(String(100))
|
|
stage_old: Mapped[str | None] = mapped_column(String(100))
|
|
anomaly_type: Mapped[str | None] = mapped_column(String(200))
|
|
material_type: Mapped[str | None] = mapped_column(String(200))
|
|
supplier: Mapped[str | None] = mapped_column(String(300))
|
|
factory: Mapped[str | None] = mapped_column(String(300))
|
|
person: Mapped[str | None] = mapped_column(String(200))
|
|
description: Mapped[str | None] = mapped_column(Text)
|
|
root_cause: Mapped[str | None] = mapped_column(Text)
|
|
solution: Mapped[str | None] = mapped_column(Text)
|
|
result: Mapped[str | None] = mapped_column(Text)
|
|
responsible: Mapped[str | None] = mapped_column(String(300))
|
|
defect_level: Mapped[str | None] = mapped_column(String(10))
|
|
is_resolved: Mapped[str | None] = mapped_column(Text)
|
|
source_file: Mapped[str | None] = mapped_column(String(300))
|
|
source_type: Mapped[str | None] = mapped_column(String(30))
|
|
remark: Mapped[str | None] = mapped_column(Text)
|
|
import_fingerprint: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
|
standard_rule_id: Mapped[int | None] = mapped_column(
|
|
BigInteger,
|
|
ForeignKey("standard_rules.id", ondelete="SET NULL"),
|
|
)
|
|
# pgvector is not installed on the target PostgreSQL server. Store vectors as JSONB until it is available.
|
|
embedding: Mapped[list[float] | None] = mapped_column(JSONB)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
|
|
standard_rule: Mapped["StandardRule | None"] = relationship(back_populates="anomaly_cases")
|
|
review_records: Mapped[list["ReviewRecord"]] = relationship(back_populates="case")
|
|
supplier_links: Mapped[list["AnomalyCaseSupplier"]] = relationship(
|
|
back_populates="case", cascade="all, delete-orphan"
|
|
)
|
|
|
|
|
|
class SupplierMaster(Base):
|
|
"""Canonical supplier entity independent from raw workbook text."""
|
|
|
|
__tablename__ = "supplier_master"
|
|
__table_args__ = (
|
|
Index("ix_supplier_master_city", "city"),
|
|
Index("ix_supplier_master_normalized_name", "normalized_name", unique=True),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
canonical_name: Mapped[str] = mapped_column(String(200), nullable=False)
|
|
short_name: Mapped[str | None] = mapped_column(String(100))
|
|
normalized_name: Mapped[str] = mapped_column(String(200), nullable=False)
|
|
city: Mapped[str | None] = mapped_column(String(50))
|
|
province: Mapped[str | None] = mapped_column(String(50))
|
|
supplier_type: Mapped[str | None] = mapped_column(String(50))
|
|
aliases: Mapped[list[str]] = mapped_column(JSONB, default=list, server_default="[]", nullable=False)
|
|
status: Mapped[str] = mapped_column(String(20), default="启用", server_default="启用", nullable=False)
|
|
source: Mapped[str | None] = mapped_column(String(100))
|
|
review_status: Mapped[str] = mapped_column(String(20), default="待确认", server_default="待确认", nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
|
)
|
|
|
|
anomaly_links: Mapped[list["AnomalyCaseSupplier"]] = relationship(
|
|
back_populates="supplier", cascade="all, delete-orphan"
|
|
)
|
|
|
|
|
|
class AnomalyCaseSupplier(Base):
|
|
"""Many-to-many mapping from an anomaly case to canonical suppliers."""
|
|
|
|
__tablename__ = "anomaly_case_suppliers"
|
|
__table_args__ = (
|
|
UniqueConstraint("case_id", "supplier_id", "supplier_role", name="uq_case_supplier_role"),
|
|
Index("ix_case_suppliers_supplier", "supplier_id"),
|
|
Index("ix_case_suppliers_case", "case_id"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
case_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("anomaly_cases.id", ondelete="CASCADE"), nullable=False)
|
|
supplier_id: Mapped[int] = mapped_column(
|
|
BigInteger, ForeignKey("supplier_master.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
supplier_role: Mapped[str | None] = mapped_column(String(50))
|
|
material_type: Mapped[str | None] = mapped_column(String(200))
|
|
source_text: Mapped[str | None] = mapped_column(Text)
|
|
match_method: Mapped[str] = mapped_column(String(30), default="manual", server_default="manual", nullable=False)
|
|
match_confidence: Mapped[Decimal | None] = mapped_column(Numeric(5, 4))
|
|
review_status: Mapped[str] = mapped_column(String(20), default="待确认", server_default="待确认", nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
|
|
case: Mapped[AnomalyCase] = relationship(back_populates="supplier_links")
|
|
supplier: Mapped[SupplierMaster] = relationship(back_populates="anomaly_links")
|
|
|
|
|
|
class SupplierProfile(Base):
|
|
"""Aggregated supplier risk profile."""
|
|
|
|
__tablename__ = "supplier_profiles"
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
supplier_name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
|
total_anomalies: Mapped[int] = mapped_column(Integer, default=0, server_default="0", nullable=False)
|
|
frequency_coef: Mapped[Decimal] = mapped_column(Numeric(4, 2), default=Decimal("1.00"), server_default="1.00")
|
|
tier: Mapped[str | None] = mapped_column(String(1))
|
|
category_breakdown: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, server_default="{}", nullable=False)
|
|
product_cat_breakdown: Mapped[dict[str, Any]] = mapped_column(
|
|
JSONB,
|
|
default=dict,
|
|
server_default="{}",
|
|
nullable=False,
|
|
)
|
|
stage_breakdown: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, server_default="{}", nullable=False)
|
|
monthly_trend: Mapped[list[dict[str, Any]]] = mapped_column(
|
|
JSONB,
|
|
default=list,
|
|
server_default="[]",
|
|
nullable=False,
|
|
)
|
|
risk_score: Mapped[int | None] = mapped_column(Integer)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
server_default=func.now(),
|
|
onupdate=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
|
|
class PurchaseOrderLine(Base):
|
|
"""A source row from the procurement order ledger."""
|
|
|
|
__tablename__ = "purchase_order_lines"
|
|
__table_args__ = (
|
|
Index("ix_purchase_order_lines_supplier_date", "supplier", "purchase_date"),
|
|
Index("ix_purchase_order_lines_document", "document_number"),
|
|
Index("uq_purchase_order_lines_source", "import_fingerprint", unique=True),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
document_number: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
purchase_date: Mapped[date | None] = mapped_column(Date)
|
|
supplier: Mapped[str] = mapped_column(String(300), nullable=False)
|
|
document_status: Mapped[str | None] = mapped_column(String(30))
|
|
close_status: Mapped[str | None] = mapped_column(String(30))
|
|
material_code: Mapped[str | None] = mapped_column(String(200))
|
|
material_name: Mapped[str | None] = mapped_column(Text)
|
|
purchase_unit: Mapped[str | None] = mapped_column(String(30))
|
|
purchase_quantity: Mapped[Decimal | None] = mapped_column(Numeric(18, 4))
|
|
delivery_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
tax_inclusive_unit_price: Mapped[Decimal | None] = mapped_column(Numeric(18, 6))
|
|
is_gift: Mapped[str | None] = mapped_column(String(10))
|
|
business_close: Mapped[str | None] = mapped_column(String(30))
|
|
remark: Mapped[str | None] = mapped_column(Text)
|
|
source_file: Mapped[str] = mapped_column(String(300), nullable=False)
|
|
import_fingerprint: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
|
|
|
|
class RiskAssessment(Base):
|
|
"""A user-triggered risk assessment and its structured result."""
|
|
|
|
__tablename__ = "risk_assessments"
|
|
__table_args__ = (
|
|
Index("ix_risk_assessments_user_id", "user_id"),
|
|
Index("ix_risk_assessments_created_at", "created_at"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
user_id: Mapped[str | None] = mapped_column(String(50))
|
|
product_name: Mapped[str | None] = mapped_column(String(200))
|
|
params: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, server_default="{}", nullable=False)
|
|
risk_score: Mapped[int | None] = mapped_column(Integer)
|
|
risk_level: Mapped[str | None] = mapped_column(String(10))
|
|
dimension_scores: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, server_default="{}", nullable=False)
|
|
similar_cases: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, server_default="{}", nullable=False)
|
|
ai_suggestions: Mapped[str | None] = mapped_column(Text)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
|
|
|
|
class Conversation(Base):
|
|
"""A persistent assistant conversation owned by one authenticated user."""
|
|
|
|
__tablename__ = "conversations"
|
|
__table_args__ = (Index("ix_conversations_user_updated", "user_id", "updated_at"),)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
user_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
ForeignKey("user_accounts.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
title: Mapped[str] = mapped_column(String(120), nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
server_default=func.now(),
|
|
onupdate=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
messages: Mapped[list["ConversationMessage"]] = relationship(
|
|
back_populates="conversation",
|
|
cascade="all, delete-orphan",
|
|
order_by="ConversationMessage.created_at",
|
|
)
|
|
|
|
|
|
class ConversationMessage(Base):
|
|
"""One user or assistant message, including optional structured results."""
|
|
|
|
__tablename__ = "conversation_messages"
|
|
__table_args__ = (Index("ix_conversation_messages_conversation_created", "conversation_id", "created_at"),)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
conversation_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
ForeignKey("conversations.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
role: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
message_type: Mapped[str] = mapped_column(String(20), default="text", server_default="text", nullable=False)
|
|
content: Mapped[str] = mapped_column(Text, nullable=False)
|
|
structured_data: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, server_default="{}", nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
|
|
conversation: Mapped[Conversation] = relationship(back_populates="messages")
|
|
|
|
|
|
class IntegrationNonce(Base):
|
|
"""Durable replay protection shared by all API workers."""
|
|
|
|
__tablename__ = "integration_nonces"
|
|
__table_args__ = (
|
|
UniqueConstraint("client_id", "nonce", name="uq_integration_nonce"),
|
|
Index("ix_integration_nonce_expires", "expires_at"),
|
|
Index("ix_integration_nonce_client_created", "client_id", "created_at"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
client_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
nonce: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
|
|
|
|
class IntegrationAssessment(Base):
|
|
"""Immutable successful reports and recoverable requests from external systems."""
|
|
|
|
__tablename__ = "integration_assessments"
|
|
__table_args__ = (
|
|
UniqueConstraint("client_id", "request_id", name="uq_integration_assessment_request"),
|
|
Index("ix_integration_assessment_user", "user_id"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
client_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
request_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
# Logical reference, like OperationAudit: migration does not require
|
|
# REFERENCES on the pre-existing user table, and reports survive account removal.
|
|
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
request_hash: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
request_data: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
|
as_of_date: Mapped[date] = mapped_column(Date, nullable=False)
|
|
status: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
owner_token: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
lease_until: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
result_data: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
|
report_markdown: Mapped[str | None] = mapped_column(Text)
|
|
error_message: Mapped[str | None] = mapped_column(String(200))
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
|
|
|
|
class RiskItem(Base):
|
|
"""Risk item submitted through form, voice, OCR, or import before approval."""
|
|
|
|
__tablename__ = "risk_items"
|
|
__table_args__ = (
|
|
Index("ix_risk_items_status", "status"),
|
|
Index("ix_risk_items_submitted_by", "submitted_by"),
|
|
Index("ix_risk_items_submitted_at", "submitted_at"),
|
|
Index("ix_risk_items_occurred_on", "occurred_on"),
|
|
Index("ix_risk_items_material_type", "material_type"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
source_type: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
source_file_id: Mapped[str | None] = mapped_column(String(50))
|
|
occurred_on: Mapped[date | None] = mapped_column(Date)
|
|
product_name: Mapped[str | None] = mapped_column(String(200))
|
|
product_category: Mapped[str | None] = mapped_column(String(50))
|
|
material_type: Mapped[str | None] = mapped_column(String(200))
|
|
supplier: Mapped[str | None] = mapped_column(String(100))
|
|
factory: Mapped[str | None] = mapped_column(String(100))
|
|
stage_new: Mapped[str | None] = mapped_column(String(50))
|
|
description: Mapped[str | None] = mapped_column(Text)
|
|
defect_level: Mapped[str | None] = mapped_column(String(5))
|
|
solution: Mapped[str | None] = mapped_column(Text)
|
|
photos: Mapped[list[str]] = mapped_column(JSONB, default=list, server_default="[]", nullable=False)
|
|
recognition_confidence: Mapped[Decimal | None] = mapped_column(Numeric(5, 4))
|
|
ocr_payload: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict, server_default="{}", nullable=False)
|
|
submitted_by: Mapped[str | None] = mapped_column(String(50))
|
|
submitted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
status: Mapped[str] = mapped_column(String(20), default="待预审", server_default="待预审", nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
|
|
review_records: Mapped[list["ReviewRecord"]] = relationship(
|
|
back_populates="risk_item",
|
|
cascade="all, delete-orphan",
|
|
)
|
|
|
|
|
|
class ReviewRecord(Base):
|
|
"""Immutable review trail for a submitted risk item."""
|
|
|
|
__tablename__ = "review_records"
|
|
__table_args__ = (
|
|
Index("ix_review_records_risk_item_id", "risk_item_id"),
|
|
Index("ix_review_records_reviewer", "reviewer"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
risk_item_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
ForeignKey("risk_items.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
)
|
|
pre_review_result: Mapped[str | None] = mapped_column(String(20))
|
|
pre_review_note: Mapped[str | None] = mapped_column(Text)
|
|
reviewer: Mapped[str | None] = mapped_column(String(50))
|
|
review_action: Mapped[str | None] = mapped_column(String(20))
|
|
review_note: Mapped[str | None] = mapped_column(Text)
|
|
reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
case_id: Mapped[int | None] = mapped_column(
|
|
BigInteger,
|
|
ForeignKey("anomaly_cases.id", ondelete="SET NULL"),
|
|
)
|
|
|
|
risk_item: Mapped[RiskItem] = relationship(back_populates="review_records")
|
|
case: Mapped[AnomalyCase | None] = relationship(back_populates="review_records")
|
|
|
|
|
|
class StandardRule(Base):
|
|
"""Four-level judgment standard used by the rule engine."""
|
|
|
|
__tablename__ = "standard_rules"
|
|
__table_args__ = (
|
|
Index("ix_standard_rules_categories", "category_lv1", "category_lv2", "category_lv3"),
|
|
Index("ix_standard_rules_status", "status"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
category_lv1: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
category_lv2: Mapped[str | None] = mapped_column(String(60))
|
|
category_lv3: Mapped[str | None] = mapped_column(String(60))
|
|
risk_point: Mapped[str] = mapped_column(Text, nullable=False)
|
|
judge_standard: Mapped[str] = mapped_column(Text, nullable=False)
|
|
regulation: Mapped[str | None] = mapped_column(String(300))
|
|
attachment: Mapped[str | None] = mapped_column(String(500))
|
|
status: Mapped[str] = mapped_column(String(10), default="启用", server_default="启用", nullable=False)
|
|
import_fingerprint: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
server_default=func.now(),
|
|
onupdate=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
anomaly_cases: Mapped[list[AnomalyCase]] = relationship(back_populates="standard_rule")
|
|
|
|
|
|
class UserAccount(Base):
|
|
"""Enterprise user account and RBAC role assignment."""
|
|
|
|
__tablename__ = "user_accounts"
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
emp_no: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
|
|
email: Mapped[str | None] = mapped_column(String(128), unique=True)
|
|
name: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
dept: Mapped[str | None] = mapped_column(String(50))
|
|
position: Mapped[str | None] = mapped_column(String(50))
|
|
role: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
password_hash: Mapped[str | None] = mapped_column(String(255))
|
|
status: Mapped[str] = mapped_column(String(10), default="启用", server_default="启用", nullable=False)
|
|
login_failed_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0", nullable=False)
|
|
locked_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
dingtalk_id: Mapped[str | None] = mapped_column(String(50), unique=True)
|
|
dingtalk_union_id: Mapped[str | None] = mapped_column(String(64))
|
|
dingtalk_avatar: Mapped[str | None] = mapped_column(String(512))
|
|
dingtalk_job_number: Mapped[str | None] = mapped_column(String(64))
|
|
dingtalk_title: Mapped[str | None] = mapped_column(String(128))
|
|
dingtalk_dept_id: Mapped[int | None] = mapped_column(BigInteger)
|
|
dingtalk_is_dept_manager: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
default=False,
|
|
server_default="false",
|
|
nullable=False,
|
|
)
|
|
dingtalk_role_codes: Mapped[str | None] = mapped_column(String(512))
|
|
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
extra_perms: Mapped[list[dict[str, Any]]] = mapped_column(JSONB, default=list, server_default="[]", nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
server_default=func.now(),
|
|
onupdate=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
|
|
class RolePermission(Base):
|
|
"""Function and data-level permissions assigned to a role."""
|
|
|
|
__tablename__ = "role_permissions"
|
|
__table_args__ = (
|
|
Index("uq_role_permissions_role_func", "role", "func_key", unique=True),
|
|
Index("ix_role_permissions_role", "role"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
role: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
func_key: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
data_level: Mapped[str] = mapped_column(String(10), default="L1", server_default="L1", nullable=False)
|
|
can_view: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false", nullable=False)
|
|
can_operate: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false", nullable=False)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
server_default=func.now(),
|
|
onupdate=func.now(),
|
|
nullable=False,
|
|
)
|
|
|
|
|
|
class RiskModelConfig(Base):
|
|
"""Administrator-managed seven-dimension weights and frequency parameters."""
|
|
|
|
__tablename__ = "risk_model_configs"
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, default=1)
|
|
weights: Mapped[dict[str, float]] = mapped_column(JSONB, nullable=False)
|
|
frequency_coefficient: Mapped[Decimal] = mapped_column(Numeric(5, 4), nullable=False)
|
|
frequency_cap: Mapped[Decimal] = mapped_column(Numeric(4, 2), nullable=False)
|
|
updated_by: Mapped[str | None] = mapped_column(String(20))
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
|
)
|
|
|
|
|
|
class OperationAudit(Base):
|
|
"""Append-only audit record for authentication, permission, and review actions."""
|
|
|
|
__tablename__ = "operation_audits"
|
|
__table_args__ = (
|
|
Index("ix_operation_audits_actor_created", "actor_user_id", "created_at"),
|
|
Index("ix_operation_audits_action_created", "action", "created_at"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
actor_user_id: Mapped[int | None] = mapped_column(BigInteger)
|
|
actor_emp_no: Mapped[str | None] = mapped_column(String(20))
|
|
action: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
resource_type: Mapped[str | None] = mapped_column(String(64))
|
|
resource_id: Mapped[str | None] = mapped_column(String(128))
|
|
result: Mapped[str] = mapped_column(String(20), default="success", server_default="success", nullable=False)
|
|
audit_metadata: Mapped[dict[str, Any]] = mapped_column(
|
|
"metadata",
|
|
JSONB,
|
|
default=dict,
|
|
server_default="{}",
|
|
nullable=False,
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|