"""风控查询所需的补充只读 ORM 映射。 第二版 `app/model/fund.py` 已覆盖多数 `fin_*` 表;本模块只补充风控查询需要、 但当前尚未映射的三张已有表,不负责建表、迁移或写入。 安全边界: 1. 只声明列映射,不提供写入方法。 2. `sys_user.password_hash` 和 `email` 不进入风控只读模型。 3. 不声明 ForeignKey,避免 ORM 写入时产生级联语义。 """ from datetime import datetime from decimal import Decimal from typing import Any from sqlalchemy import JSON, Numeric, String, Text from sqlalchemy.dialects.mysql import BIGINT, DATETIME, TINYINT from sqlalchemy.orm import Mapped, mapped_column from app.model.base import Base __all__ = ["RiskLoginRecord", "RiskUser", "RiskWorkOrder"] class RiskUser(Base): """统一身份表中的风控只读字段(sys_user)。""" __tablename__ = "sys_user" id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True) user_no: Mapped[str] = mapped_column(String(32), nullable=False) username: Mapped[str] = mapped_column(String(64), nullable=False) user_type: Mapped[str] = mapped_column(String(16), nullable=False) employee_role: Mapped[str | None] = mapped_column(String(32), nullable=True) customer_tier: Mapped[str | None] = mapped_column(String(16), nullable=True) investor_type: Mapped[str | None] = mapped_column(String(8), nullable=True) investor_type_assessed_at: Mapped[datetime | None] = mapped_column( DATETIME(fsp=0), nullable=True ) is_professional_investor: Mapped[int] = mapped_column( TINYINT(display_width=1), nullable=False ) professional_investor_status: Mapped[str] = mapped_column(String(16), nullable=False) professional_investor_certified_at: Mapped[datetime | None] = mapped_column( DATETIME(fsp=0), nullable=True ) fund_account_status: Mapped[str] = mapped_column(String(16), nullable=False) fund_account_opened_at: Mapped[datetime | None] = mapped_column( DATETIME(fsp=0), nullable=True ) status: Mapped[str] = mapped_column(String(16), nullable=False) created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False) updated_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False) class RiskLoginRecord(Base): """登录记录只读映射(sys_login_record)。""" __tablename__ = "sys_login_record" id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True) user_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False) login_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False) login_result: Mapped[str] = mapped_column(String(16), nullable=False) ip_region: Mapped[str | None] = mapped_column(String(64), nullable=True) device_id: Mapped[str | None] = mapped_column(String(128), nullable=True) is_common_device: Mapped[int] = mapped_column(TINYINT(display_width=1), nullable=False) failure_reason: Mapped[str | None] = mapped_column(String(128), nullable=True) created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False) class RiskWorkOrder(Base): """风险处置及交易申请兼容工单只读映射(biz_work_order)。""" __tablename__ = "biz_work_order" id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True) work_order_no: Mapped[str] = mapped_column(String(64), nullable=False) customer_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False) order_type: Mapped[str | None] = mapped_column(String(16), nullable=True) product_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True) amount: Mapped[Decimal | None] = mapped_column(Numeric(18, 2), nullable=True) channel: Mapped[str | None] = mapped_column(String(32), nullable=True) advisor_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True) risk_rule_hits: Mapped[Any | None] = mapped_column(JSON, nullable=True) risk_disclosure_ack_at: Mapped[datetime | None] = mapped_column( DATETIME(fsp=0), nullable=True ) second_confirmation_at: Mapped[datetime | None] = mapped_column( DATETIME(fsp=0), nullable=True ) recording_reference: Mapped[str | None] = mapped_column(String(64), nullable=True) ops_handler_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True) ops_handled_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True) compliance_handler_id: Mapped[int | None] = mapped_column( BIGINT(unsigned=True), nullable=True ) compliance_handled_at: Mapped[datetime | None] = mapped_column( DATETIME(fsp=0), nullable=True ) reject_reason: Mapped[str | None] = mapped_column(Text, nullable=True) alert_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True) work_order_type: Mapped[str | None] = mapped_column(String(32), nullable=True) submitter_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True) handler_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True) priority: Mapped[str | None] = mapped_column(String(8), nullable=True) status: Mapped[str] = mapped_column(String(24), nullable=False) request_detail: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) handle_result: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) submitted_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True) accepted_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True) completed_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True) created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False) updated_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)