"""全库 ORM 实体定义(29 张表 · 代码内可读的表结构参照)。 **定位(先读这一段):** - **权威表结构 = SQL 文件**:`docs/项目框架设计/表设计/01-mysql-共用底座.sql`、 `02-mysql-agent专用.sql`(jinrong_agent 17 张)+ `scripts/core/01-ddl.sql` (jinrong_core 12 张)。本文件与之**人工同步**,仅供 IDE 浏览 / 结构检索 / 新人理解数据模型,**不用于建表**(建库走 `scripts/core/reset.ps1` + `mysql < *.sql`)。 - **运行时读写不走 ORM**:repository 层统一用 SQLAlchemy `text()` 原生 SQL + dict(见 `app/repository/*`、`app/gateway/gateway_repository.py`); 禁止用 `Base.metadata.create_all()` 建表(会绕过 SQL 单一事实源)。 - **jinrong_core 为只读库**:Core 正式 C1~C5 / 持仓 / 流水不可被画像覆盖; 唯一例外 `app/gateway/gateway_repository.py` 仅可 INSERT `core_trade`(B5)。 - 列注释、枚举值集、索引名 / 唯一键名均与 DDL **字面对齐**; MySQL 专属精度(如 DATETIME(3)、UNSIGNED)在列 comment 里标注。 - 每张表的 DDL 出处以类 docstring 标注,改动表结构时两处同步(改表需用户确认)。 """ from __future__ import annotations from datetime import date, datetime from decimal import Decimal from typing import Any from sqlalchemy import ( CHAR, JSON, BigInteger, Boolean, Date, DateTime, Enum, ForeignKey, Index, Integer, Numeric, SmallInteger, String, Text, UniqueConstraint, ) from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column # ============================================================================= # 两个库 → 两个独立 metadata # ============================================================================= class AgentBase(DeclarativeBase): """jinrong_agent 库元数据(四 Agent 共用底座 + 各 Agent 专用表,17 张)。""" class CoreBase(DeclarativeBase): """jinrong_core 库元数据(Core 模拟底座,12 张,只读)。""" # ============================================================================= # jinrong_agent · 共用底座第一批(6 张) # 权威 DDL:docs/项目框架设计/表设计/01-mysql-共用底座.sql # ============================================================================= class AgentSession(AgentBase): """【共用】Agent 会话主表 · jinrong_agent.agent_session""" __tablename__ = "agent_session" __table_args__ = ( UniqueConstraint("session_id", name="uk_session_id"), Index("idx_trace", "trace_id"), Index("idx_actor", "agent_type", "actor_id", "created_at"), Index("idx_customer", "customer_id", "created_at"), Index("idx_advisor_customer", "advisor_id", "customer_id"), {"comment": "【共用】Agent 会话主表", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True, comment="DDL: BIGINT UNSIGNED AUTO_INCREMENT") session_id: Mapped[str] = mapped_column(String(64), comment="对外会话 UUID") trace_id: Mapped[str] = mapped_column(String(64), comment="全链路追踪 ID") agent_type: Mapped[str] = mapped_column( Enum("customer", "advisor", "analyst", "risk"), comment="Agent 类型" ) actor_id: Mapped[str] = mapped_column(String(64), comment="操作者:customer_id / staff_id / SYSTEM") actor_role: Mapped[str] = mapped_column(String(32), comment="customer/advisor/analyst/risk_officer/compliance") customer_id: Mapped[str | None] = mapped_column(String(64), comment="会话关联客户") advisor_id: Mapped[str | None] = mapped_column(String(64), comment="代理人归属校验用") title: Mapped[str | None] = mapped_column(String(256)) status: Mapped[str] = mapped_column( Enum("active", "closed", "blocked"), comment="DDL: DEFAULT 'active'" ) # 列名 metadata 与 Declarative 基类属性冲突,Python 侧别名 metadata_ metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") updated_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) ON UPDATE CURRENT_TIMESTAMP(3)") closed_at: Mapped[datetime | None] = mapped_column(DateTime) class AgentMessage(AgentBase): """【共用】消息明细 · jinrong_agent.agent_message""" __tablename__ = "agent_message" __table_args__ = ( Index("idx_session_seq", "session_id", "seq_no"), Index("idx_trace", "trace_id"), Index("idx_created", "created_at"), {"comment": "【共用】消息明细", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) session_id: Mapped[str] = mapped_column(String(64)) trace_id: Mapped[str] = mapped_column(String(64)) seq_no: Mapped[int] = mapped_column(Integer, comment="DDL: INT UNSIGNED;同会话内递增序号") role: Mapped[str] = mapped_column(Enum("user", "assistant", "system", "tool")) content: Mapped[str] = mapped_column(Text, comment="DDL: MEDIUMTEXT") content_hash: Mapped[str | None] = mapped_column(CHAR(64)) token_est: Mapped[int | None] = mapped_column(Integer, comment="DDL: INT UNSIGNED") has_disclaimer: Mapped[bool] = mapped_column(Boolean, default=False, comment="DDL: TINYINT(1) DEFAULT 0") created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") class AgentToolCall(AgentBase): """【共用】Tool 调用审计(T-04 对话链路留痕)· jinrong_agent.agent_tool_call""" __tablename__ = "agent_tool_call" __table_args__ = ( Index("idx_session", "session_id", "created_at"), Index("idx_trace", "trace_id"), Index("idx_tool", "tool_name", "created_at"), {"comment": "【共用】Tool 调用审计", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) session_id: Mapped[str] = mapped_column(String(64)) trace_id: Mapped[str] = mapped_column(String(64)) message_id: Mapped[int | None] = mapped_column(BigInteger, comment="一期 NULL:Tool 先于 LLM 执行(见 session_repository.insert_tool_call)") tool_name: Mapped[str] = mapped_column(String(128)) tool_input: Mapped[dict[str, Any]] = mapped_column(JSON, comment="JSON,调用方序列化传入") tool_output: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) status: Mapped[str] = mapped_column(Enum("success", "error", "blocked", "timeout")) error_code: Mapped[str | None] = mapped_column(String(64)) latency_ms: Mapped[int | None] = mapped_column(Integer, comment="DDL: INT UNSIGNED") created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") class AuditLog(AgentBase): """【共用】审计总账(只 INSERT,禁止 UPDATE/DELETE)· jinrong_agent.audit_log""" __tablename__ = "audit_log" __table_args__ = ( Index("idx_trace", "trace_id"), Index("idx_event_time", "event_type", "created_at"), Index("idx_customer", "customer_id", "created_at"), Index("idx_actor", "actor_id", "created_at"), {"comment": "【共用】审计总账(只 INSERT)", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) trace_id: Mapped[str] = mapped_column(String(64)) event_type: Mapped[str] = mapped_column(String(64), comment="非枚举:VARCHAR(64),事件类型可扩展") agent_type: Mapped[str] = mapped_column(Enum("customer", "advisor", "analyst", "risk", "platform")) actor_id: Mapped[str] = mapped_column(String(64)) customer_id: Mapped[str | None] = mapped_column(String(64)) rule_id: Mapped[str | None] = mapped_column(String(64)) input_summary: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) decision: Mapped[str | None] = mapped_column(String(64)) risk_score: Mapped[int | None] = mapped_column(SmallInteger, comment="DDL: SMALLINT UNSIGNED") handler_id: Mapped[str | None] = mapped_column(String(64)) handler_result: Mapped[str | None] = mapped_column(String(64)) handler_comment: Mapped[str | None] = mapped_column(String(512)) created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") class InputGuardLog(AgentBase): """【共用】输入安全防护留痕(T-03 四类 guard)· jinrong_agent.input_guard_log""" __tablename__ = "input_guard_log" __table_args__ = ( Index("idx_session", "session_id"), Index("idx_time", "created_at"), {"comment": "【共用】输入安全防护", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) trace_id: Mapped[str] = mapped_column(String(64)) session_id: Mapped[str | None] = mapped_column(String(64)) agent_type: Mapped[str] = mapped_column(Enum("customer", "advisor", "analyst", "risk")) actor_id: Mapped[str] = mapped_column(String(64)) guard_type: Mapped[str] = mapped_column( Enum("prompt_injection", "oversize", "illegal_param", "rate_limit"), comment="ENUM 四值已用满(T-03)", ) raw_excerpt: Mapped[str | None] = mapped_column(String(1024)) action: Mapped[str] = mapped_column(Enum("blocked", "sanitized", "passed")) created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") class CustomerAdvisorRel(AgentBase): """【共用】客户-代理人归属(Core 同步,sync_advisor_rel.py 灌)· jinrong_agent.customer_advisor_rel""" __tablename__ = "customer_advisor_rel" __table_args__ = ( UniqueConstraint("customer_id", "advisor_id", "effective_from", name="uk_customer_advisor"), Index("idx_advisor", "advisor_id", "rel_status"), {"comment": "【共用】客户-代理人归属(Core 同步)", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) customer_id: Mapped[str] = mapped_column(String(64)) advisor_id: Mapped[str] = mapped_column(String(64)) rel_status: Mapped[str] = mapped_column( Enum("active", "transferred", "closed"), comment="DDL: DEFAULT 'active'" ) effective_from: Mapped[date] = mapped_column(Date) effective_to: Mapped[date | None] = mapped_column(Date) synced_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") # ============================================================================= # jinrong_agent · 共用底座第二批:跨 Agent 交换(5 张) # ============================================================================= class CustomerProfileL1(AgentBase): """【交换】L1 客户画像 · 客户 Agent 写(禁止覆盖 Core L0 正式测评)· jinrong_agent.customer_profile_l1""" __tablename__ = "customer_profile_l1" __table_args__ = ({"comment": "【交换】L1 客户画像 · 客户 Agent 写", "mysql_engine": "InnoDB"},) customer_id: Mapped[str] = mapped_column(String(64), primary_key=True) style_tags: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) style_questionnaire: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) allocation_plan: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) behavior_tags: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) attribution_pref: Mapped[str | None] = mapped_column(String(32)) version: Mapped[int] = mapped_column(Integer, default=1, comment="DDL: INT UNSIGNED DEFAULT 1") updated_by: Mapped[str] = mapped_column( Enum("customer_agent", "system"), comment="DDL: DEFAULT 'customer_agent'" ) updated_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) ON UPDATE CURRENT_TIMESTAMP(3)") class CustomerProfileL2(AgentBase): """【交换】L2 服务画像 · 代理人 Agent 写 · jinrong_agent.customer_profile_l2""" __tablename__ = "customer_profile_l2" __table_args__ = ( UniqueConstraint("customer_id", "advisor_id", name="uk_customer_advisor"), Index("idx_advisor", "advisor_id"), {"comment": "【交换】L2 服务画像 · 代理人 Agent 写", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) customer_id: Mapped[str] = mapped_column(String(64)) advisor_id: Mapped[str] = mapped_column(String(64)) asset_snapshot: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) demands: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) follow_up_todos: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) service_tags: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) source_session_id: Mapped[str | None] = mapped_column(String(64)) version: Mapped[int] = mapped_column(Integer, default=1, comment="DDL: INT UNSIGNED DEFAULT 1") updated_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) ON UPDATE CURRENT_TIMESTAMP(3)") class CustomerProfileL3(AgentBase): """【交换】L3 监测画像 · 风控 Agent 写 · jinrong_agent.customer_profile_l3""" __tablename__ = "customer_profile_l3" __table_args__ = ({"comment": "【交换】L3 监测画像 · 风控 Agent 写", "mysql_engine": "InnoDB"},) customer_id: Mapped[str] = mapped_column(String(64), primary_key=True) monitor_tier: Mapped[str] = mapped_column( Enum("normal", "watch", "high"), comment="DDL: DEFAULT 'normal'" ) risk_score: Mapped[int | None] = mapped_column(SmallInteger, comment="DDL: SMALLINT UNSIGNED") score_dimensions: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) monitor_tags: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) last_alert_id: Mapped[str | None] = mapped_column(String(64)) computed_at: Mapped[datetime] = mapped_column(DateTime) updated_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) ON UPDATE CURRENT_TIMESTAMP(3)") class RiskAlert(AgentBase): """【交换】预警单 · 风控写 / 分析读(仅 R-02 可阻断交易)· jinrong_agent.risk_alert""" __tablename__ = "risk_alert" __table_args__ = ( Index("idx_status_time", "status", "created_at"), Index("idx_customer", "customer_id", "created_at"), Index("idx_type", "alert_type", "created_at"), {"comment": "【交换】预警单 · 风控写 / 分析读", "mysql_engine": "InnoDB"}, ) alert_id: Mapped[str] = mapped_column(String(64), primary_key=True) trace_id: Mapped[str] = mapped_column(String(64)) customer_id: Mapped[str] = mapped_column(String(64)) trade_id: Mapped[str | None] = mapped_column(String(64)) alert_type: Mapped[str] = mapped_column( Enum("large_amount", "freq_trade", "suitability", "aml", "pattern"), comment="C4 集中度事件复用 'pattern' + payload.alert_subtype(FR-8)", ) triggered_rules: Mapped[dict[str, Any]] = mapped_column(JSON) risk_score: Mapped[int | None] = mapped_column(SmallInteger, comment="DDL: SMALLINT UNSIGNED") status: Mapped[str] = mapped_column( Enum("pending_review", "confirmed_normal", "confirmed_suspicious", "reported"), comment="DDL: DEFAULT 'pending_review'", ) payload: Mapped[dict[str, Any]] = mapped_column(JSON, comment="C5 升级信息由 payload.escalation_level/escalated_at 承载(FR-9)") handler_id: Mapped[str | None] = mapped_column(String(64)) handler_result: Mapped[str | None] = mapped_column(String(64)) handler_comment: Mapped[str | None] = mapped_column(String(512)) created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") handled_at: Mapped[datetime | None] = mapped_column(DateTime) class RiskSuitabilityLog(AgentBase): """【交换】适当性记录 · 风控写 / 客户+代理人读(AL-02 对齐 main 21 列契约; 字段契约详见 docs/项目框架设计/表设计/07-risk_suitability_log说明.md) · jinrong_agent.risk_suitability_log""" __tablename__ = "risk_suitability_log" __table_args__ = ( Index("idx_trace", "trace_id"), Index("idx_customer", "customer_id", "created_at"), Index("idx_product", "product_id", "created_at"), Index("idx_blocked", "is_blocked", "created_at"), Index("idx_match", "match_result", "created_at"), Index("idx_mismatch", "mismatch_type", "created_at"), {"comment": "【交换】适当性记录 · 风控写 / 客户+代理人读", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) trace_id: Mapped[str] = mapped_column(String(64)) customer_id: Mapped[str] = mapped_column(String(64)) product_id: Mapped[str] = mapped_column(String(64)) product_name: Mapped[str | None] = mapped_column(String(128), comment="判定时产品名快照") customer_risk_level: Mapped[str] = mapped_column(CHAR(2), comment="L0 C1~C5") product_risk_level: Mapped[str] = mapped_column(CHAR(2), comment="产品最低 R1~R5") investor_category: Mapped[str] = mapped_column( Enum("ordinary", "professional", "professional_pending"), comment="DDL: DEFAULT 'ordinary'", ) match_result: Mapped[str] = mapped_column( Enum( "allowed", "allowed_with_disclosure", "forbidden", "risk_expired", "professional_exempt", ), comment="与 Core check_suitability 输出一致(五值)", ) mismatch_type: Mapped[str] = mapped_column( Enum( "none", "risk_level", "risk_expired", "age_branch_confirm", "min_subscribe", "not_found", "professional_exempt", ), comment="阻断/特殊处理原因分类(七值),DDL: DEFAULT 'none'", ) is_matched: Mapped[bool] = mapped_column(Boolean) is_blocked: Mapped[bool] = mapped_column(Boolean, default=False, comment="DDL: TINYINT(1) DEFAULT 0") requires_disclosure: Mapped[bool] = mapped_column(Boolean, default=False, comment="DDL: TINYINT(1) DEFAULT 0") needs_branch_confirm: Mapped[bool] = mapped_column(Boolean, default=False, comment="FM-01 年龄≥70 买 R3+;DDL: TINYINT(1) DEFAULT 0") risk_was_expired: Mapped[bool] = mapped_column(Boolean, default=False, comment="判定时风评是否已过期;DDL: TINYINT(1) DEFAULT 0") block_reason: Mapped[str | None] = mapped_column(String(512), comment="对人可读原因") block_response_code: Mapped[str | None] = mapped_column(String(32), comment="API 机器码,如 SUIT_RISK_MISMATCH") check_source: Mapped[str] = mapped_column( Enum("r02_trade", "r02_chat", "c11_inquiry", "manual"), comment="DDL: DEFAULT 'r02_trade'", ) actor_id: Mapped[str] = mapped_column(String(64), comment="发起者 customer_id / staff_id / svc-trade-suitability") request_ref: Mapped[str | None] = mapped_column(String(64), comment="交易单号 / session_id") profile_l1_version: Mapped[int | None] = mapped_column(Integer, comment="DDL: INT UNSIGNED") rule_refs: Mapped[dict[str, Any] | None] = mapped_column(JSON, comment='依据规则,如 ["JR-AST-012","FM-03"]') created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") # ============================================================================= # jinrong_agent · 各 Agent 专用(6 张) # 权威 DDL:docs/项目框架设计/表设计/02-mysql-agent专用.sql # ============================================================================= class CustomerThresholdConfig(AgentBase): """【客户专用】亏损阈值配置 · jinrong_agent.customer_threshold_config""" __tablename__ = "customer_threshold_config" __table_args__ = ( Index("idx_customer", "customer_id", "is_enabled"), {"comment": "【客户专用】亏损阈值配置", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) customer_id: Mapped[str] = mapped_column(String(64)) scope_type: Mapped[str] = mapped_column( Enum("portfolio", "product"), comment="DDL: DEFAULT 'portfolio'" ) scope_ref: Mapped[str | None] = mapped_column(String(64)) loss_threshold_pct: Mapped[Decimal] = mapped_column(Numeric(5, 2)) notify_channel: Mapped[str] = mapped_column(String(32), comment="DDL: SET('app','sms','email') DEFAULT 'app'") is_enabled: Mapped[bool] = mapped_column(Boolean, default=True, comment="DDL: TINYINT(1) DEFAULT 1") created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") updated_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) ON UPDATE CURRENT_TIMESTAMP(3)") class CustomerNotifyLog(AgentBase): """【客户专用】提醒留痕 · jinrong_agent.customer_notify_log""" __tablename__ = "customer_notify_log" __table_args__ = ( Index("idx_customer_time", "customer_id", "created_at"), {"comment": "【客户专用】提醒留痕", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) customer_id: Mapped[str] = mapped_column(String(64)) trace_id: Mapped[str] = mapped_column(String(64)) notify_type: Mapped[str] = mapped_column(Enum("loss_threshold", "market_volatility")) threshold_config_id: Mapped[int | None] = mapped_column(BigInteger) payload: Mapped[dict[str, Any]] = mapped_column(JSON) channel: Mapped[str] = mapped_column(String(16)) send_status: Mapped[str] = mapped_column(Enum("sent", "failed")) created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") class AdvisorDraft(AgentBase): """【代理人专用】话术/跟进草稿(草稿不外发客户)· jinrong_agent.advisor_draft""" __tablename__ = "advisor_draft" __table_args__ = ( UniqueConstraint("draft_id", name="uk_draft_id"), Index("idx_advisor_customer", "advisor_id", "customer_id", "created_at"), Index("idx_review", "review_status", "created_at"), {"comment": "【代理人专用】话术/跟进草稿", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) draft_id: Mapped[str] = mapped_column(String(64)) session_id: Mapped[str] = mapped_column(String(64)) trace_id: Mapped[str] = mapped_column(String(64)) advisor_id: Mapped[str] = mapped_column(String(64)) customer_id: Mapped[str] = mapped_column(String(64)) draft_type: Mapped[str] = mapped_column(Enum("script", "follow_up")) content: Mapped[str] = mapped_column(Text, comment="DDL: MEDIUMTEXT") review_status: Mapped[str] = mapped_column( Enum("pending", "approved", "rejected"), comment="DDL: DEFAULT 'pending'" ) reviewer_id: Mapped[str | None] = mapped_column(String(64)) reviewed_at: Mapped[datetime | None] = mapped_column(DateTime) created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") class ComplianceHitLog(AgentBase): """【代理人专用】违规话术命中 · jinrong_agent.compliance_hit_log""" __tablename__ = "compliance_hit_log" __table_args__ = ( Index("idx_severity_time", "severity", "created_at"), Index("idx_session", "session_id"), {"comment": "【代理人专用】违规话术命中", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) session_id: Mapped[str] = mapped_column(String(64)) trace_id: Mapped[str] = mapped_column(String(64)) agent_type: Mapped[str] = mapped_column(Enum("customer", "advisor")) actor_id: Mapped[str] = mapped_column(String(64)) hit_category: Mapped[str] = mapped_column( Enum("return_promise", "principal_guarantee", "buy_sell_guide", "product_recommend", "other") ) matched_terms: Mapped[dict[str, Any]] = mapped_column(JSON) severity: Mapped[str] = mapped_column(Enum("low", "medium", "high")) action_taken: Mapped[str] = mapped_column(Enum("flagged", "blocked", "alerted")) created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") class AnalyticsQueryLog(AgentBase): """【分析专用】查数 SQL 留痕(NL2SQL 审计)· jinrong_agent.analytics_query_log""" __tablename__ = "analytics_query_log" __table_args__ = ( Index("idx_staff_time", "staff_id", "created_at"), Index("idx_trace", "trace_id"), Index("idx_sql_hash", "sql_hash"), {"comment": "【分析专用】查数 SQL 留痕", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) session_id: Mapped[str] = mapped_column(String(64)) trace_id: Mapped[str] = mapped_column(String(64)) staff_id: Mapped[str] = mapped_column(String(64)) nl_question: Mapped[str] = mapped_column(Text) generated_sql: Mapped[str] = mapped_column(Text) sql_hash: Mapped[str] = mapped_column(CHAR(64)) row_count: Mapped[int | None] = mapped_column(Integer, comment="DDL: INT UNSIGNED") exec_status: Mapped[str] = mapped_column(Enum("success", "error", "blocked")) exec_latency_ms: Mapped[int | None] = mapped_column(Integer, comment="DDL: INT UNSIGNED") result_summary: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True) has_disclaimer: Mapped[bool] = mapped_column(Boolean, default=False, comment="DDL: TINYINT(1) DEFAULT 0") error_message: Mapped[str | None] = mapped_column(String(512)) created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") class RiskAmlList(AgentBase): """【风控专用】AML 名单本地镜像(seed-aml-list.sql 灌)· jinrong_agent.risk_aml_list""" __tablename__ = "risk_aml_list" __table_args__ = ( UniqueConstraint("list_id", name="uk_list_id"), Index("idx_name", "full_name"), Index("idx_active", "is_active"), {"comment": "【风控专用】AML 名单本地镜像 · 依据 docs/PRD/PRD-风控监测Agent.md §6.2", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) list_id: Mapped[str] = mapped_column(String(64)) list_type: Mapped[str] = mapped_column(Enum("sanction", "terror", "pep")) full_name: Mapped[str] = mapped_column(String(128), comment="与 core_customer.display_name 同为脱敏展示名口径") id_no: Mapped[str | None] = mapped_column(String(32), comment="预留:待 Core 提供证件数据后启用匹配") bank_card_no: Mapped[str | None] = mapped_column(String(32), comment="预留:同上") match_threshold: Mapped[Decimal] = mapped_column(Numeric(3, 2), default=Decimal("0.85")) source: Mapped[str] = mapped_column(String(64)) list_version: Mapped[str] = mapped_column(String(16)) effective_date: Mapped[date] = mapped_column(Date) is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="DDL: TINYINT(1) DEFAULT 1") created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") # ============================================================================= # jinrong_core · Core 模拟底座(12 张,只读) # 权威 DDL:scripts/core/01-ddl.sql(AL-01 对齐 main:is_hnw / 风评新列 / 矩阵表) # ============================================================================= class CoreRiskGrade(CoreBase): """风险等级字典(客户 C1~C5 · 产品 R1~R5)· jinrong_core.core_risk_grade""" __tablename__ = "core_risk_grade" __table_args__ = ({"comment": "风险等级字典", "mysql_engine": "InnoDB"},) code: Mapped[str] = mapped_column(CHAR(4), primary_key=True, comment="C1~C5 或 R1~R5") grade_type: Mapped[str] = mapped_column(Enum("customer", "product")) display_name: Mapped[str] = mapped_column(String(32)) sort_order: Mapped[int] = mapped_column(SmallInteger, comment="DDL: TINYINT UNSIGNED") class CoreIndustry(CoreBase): """行业分类 · jinrong_core.core_industry""" __tablename__ = "core_industry" __table_args__ = ({"comment": "行业分类", "mysql_engine": "InnoDB"},) industry_code: Mapped[str] = mapped_column(String(16), primary_key=True) industry_name: Mapped[str] = mapped_column(String(64)) class CoreSuitabilityRule(CoreBase): """适当性匹配规则(L0 权威 · C×R 矩阵,《个人投资者适当性管理指南》第十二条) · jinrong_core.core_suitability_rule""" __tablename__ = "core_suitability_rule" __table_args__ = ( UniqueConstraint("customer_risk_code", "product_risk_code", name="uk_cx_r"), {"comment": "适当性匹配规则(L0 权威)", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(SmallInteger, primary_key=True, autoincrement=True, comment="DDL: TINYINT UNSIGNED AUTO_INCREMENT") customer_risk_code: Mapped[str] = mapped_column( CHAR(2), ForeignKey("core_risk_grade.code", name="fk_suit_cust_risk"), comment="C1~C5" ) product_risk_code: Mapped[str] = mapped_column( CHAR(2), ForeignKey("core_risk_grade.code", name="fk_suit_prod_risk"), comment="R1~R5" ) match_result: Mapped[str] = mapped_column(Enum("allowed", "allowed_with_disclosure", "forbidden")) rule_ref: Mapped[str] = mapped_column(String(32), comment="DDL: DEFAULT 'JR-AST-012'") class CoreStaff(CoreBase): """内部员工主档(模拟 IdP 账号源,含 RBAC 角色种子)· jinrong_core.core_staff""" __tablename__ = "core_staff" __table_args__ = ({"comment": "内部员工主档(模拟 IdP 账号源)", "mysql_engine": "InnoDB"},) staff_id: Mapped[str] = mapped_column(String(64), primary_key=True) display_name: Mapped[str] = mapped_column(String(64)) staff_type: Mapped[str] = mapped_column( Enum("advisor", "analyst", "risk_officer", "compliance", "ops") ) roles: Mapped[dict[str, Any]] = mapped_column(JSON, comment='JWT roles 数组,如 ["advisor"]') tenant_id: Mapped[str] = mapped_column(String(32), comment="DDL: DEFAULT 'TENANT-001'") is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="DDL: TINYINT(1) DEFAULT 1") created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") class CoreCustomer(CoreBase): """客户主档 L0 · KYC(对齐用户信息数据示例)· jinrong_core.core_customer""" __tablename__ = "core_customer" __table_args__ = ( Index("idx_service_tier", "service_tier"), Index("idx_hnw", "is_hnw"), Index("idx_aml", "aml_risk_level"), {"comment": "客户主档 L0 · KYC", "mysql_engine": "InnoDB"}, ) customer_id: Mapped[str] = mapped_column(String(64), primary_key=True) display_name: Mapped[str] = mapped_column(String(64), comment="脱敏展示名") gender: Mapped[str] = mapped_column(Enum("M", "F", "U"), comment="DDL: DEFAULT 'U'") birth_date: Mapped[date | None] = mapped_column(Date) age: Mapped[int | None] = mapped_column(SmallInteger, comment="DDL: TINYINT UNSIGNED") id_no_mask: Mapped[str | None] = mapped_column(String(32), comment="如 310101199903XXXXXX") occupation: Mapped[str | None] = mapped_column(String(64)) employer: Mapped[str | None] = mapped_column(String(128)) education: Mapped[str | None] = mapped_column( Enum("high_school", "associate", "bachelor", "master", "doctor", "other") ) marital_status: Mapped[str | None] = mapped_column( Enum("single", "married", "widowed", "divorced", "other") ) city: Mapped[str | None] = mapped_column(String(64)) address_mask: Mapped[str | None] = mapped_column(String(256)) phone_mask: Mapped[str | None] = mapped_column(String(16)) email_mask: Mapped[str | None] = mapped_column(String(64)) annual_income: Mapped[Decimal | None] = mapped_column(Numeric(18, 2), comment="家庭年收入(元)") financial_asset: Mapped[Decimal | None] = mapped_column(Numeric(18, 2), comment="金融资产规模(不含房产)") monthly_investable: Mapped[Decimal | None] = mapped_column(Numeric(18, 2), comment="月可投资金额") is_hnw: Mapped[bool] = mapped_column(Boolean, default=False, comment="高净值客户;DDL: TINYINT(1) DEFAULT 0(AL-01)") service_tier: Mapped[str] = mapped_column( Enum("normal", "vip", "diamond"), comment="DDL: DEFAULT 'normal'" ) is_pep: Mapped[bool] = mapped_column(Boolean, default=False, comment="政治公众人物;DDL: TINYINT(1) DEFAULT 0") aml_risk_level: Mapped[str] = mapped_column( Enum("low", "medium", "high"), comment="DDL: DEFAULT 'low'" ) invest_experience_years: Mapped[int | None] = mapped_column(SmallInteger, comment="DDL: TINYINT UNSIGNED") first_invest_date: Mapped[date | None] = mapped_column(Date) tenant_id: Mapped[str] = mapped_column(String(32), comment="DDL: DEFAULT 'TENANT-001'") open_date: Mapped[date] = mapped_column(Date) is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="DDL: TINYINT(1) DEFAULT 1") created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") class CoreCustomerRisk(CoreBase): """客户正式风险测评 L0(对齐适当性指南 16 题问卷 + FM-03 风评过期) · jinrong_core.core_customer_risk""" __tablename__ = "core_customer_risk" __table_args__ = ( UniqueConstraint("customer_id", name="uk_customer_current"), Index("idx_risk", "risk_code"), Index("idx_expires", "expires_at"), Index("idx_investor_cat", "investor_category"), {"comment": "客户正式风险测评 L0", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) customer_id: Mapped[str] = mapped_column( String(64), ForeignKey("core_customer.customer_id", name="fk_cust_risk_customer") ) risk_code: Mapped[str] = mapped_column( CHAR(2), ForeignKey("core_risk_grade.code", name="fk_cust_risk_code"), comment="C1~C5" ) questionnaire_score: Mapped[int | None] = mapped_column(SmallInteger, comment="问卷总分 20-100;DDL: SMALLINT UNSIGNED") max_loss_tolerance_pct: Mapped[Decimal | None] = mapped_column(Numeric(5, 2), comment="最大可承受亏损比例") investment_goal: Mapped[str | None] = mapped_column(String(128)) investment_horizon: Mapped[str | None] = mapped_column( Enum("short", "medium", "long", "flexible") ) investor_category: Mapped[str] = mapped_column( Enum("ordinary", "professional", "professional_pending"), comment="DDL: DEFAULT 'ordinary'(AL-01 新列)", ) professional_approved_at: Mapped[date | None] = mapped_column(Date, comment="AL-01 新列") is_authoritative: Mapped[bool] = mapped_column(Boolean, default=True, comment="正式测评标记(画像不得覆盖);DDL: TINYINT(1) DEFAULT 1") evaluated_at: Mapped[date] = mapped_column(Date) expires_at: Mapped[date] = mapped_column(Date, comment="风评有效期(通常 evaluated_at+12 月,FM-03 过期判定依据;AL-01 新列)") source: Mapped[str] = mapped_column(String(32), comment="DDL: DEFAULT 'risk_questionnaire'") class CoreCustomerAdvisor(CoreBase): """客户-代理人归属 · jinrong_core.core_customer_advisor""" __tablename__ = "core_customer_advisor" __table_args__ = ( UniqueConstraint("customer_id", "advisor_id", "effective_from", name="uk_cust_advisor_from"), Index("idx_advisor", "advisor_id", "rel_status"), {"comment": "客户-代理人归属", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) customer_id: Mapped[str] = mapped_column( String(64), ForeignKey("core_customer.customer_id", name="fk_ca_customer") ) advisor_id: Mapped[str] = mapped_column( String(64), ForeignKey("core_staff.staff_id", name="fk_ca_advisor"), comment="对应 core_staff.staff_id" ) rel_status: Mapped[str] = mapped_column( Enum("active", "transferred", "closed"), comment="DDL: DEFAULT 'active'" ) effective_from: Mapped[date] = mapped_column(Date) effective_to: Mapped[date | None] = mapped_column(Date) class CoreProduct(CoreBase): """产品主档(对齐 C-11:风险等级 + 期限 + 起购金额)· jinrong_core.core_product""" __tablename__ = "core_product" __table_args__ = ( Index("idx_min_risk", "min_risk_code"), Index("idx_product_type", "product_type"), {"comment": "产品主档", "mysql_engine": "InnoDB"}, ) product_id: Mapped[str] = mapped_column(String(64), primary_key=True) product_name: Mapped[str] = mapped_column(String(128)) product_type: Mapped[str] = mapped_column( Enum( "money", "bond", "mixed", "stock", "index", "wealth_mgmt", "private_fund", "insurance", "structured", ) ) min_risk_code: Mapped[str] = mapped_column( CHAR(2), ForeignKey("core_risk_grade.code", name="fk_product_risk"), comment="R1~R5 最低适配" ) min_subscribe_amount: Mapped[Decimal] = mapped_column(Numeric(18, 2), comment="起购金额(元);DDL: DEFAULT 1.00") term_days: Mapped[int | None] = mapped_column(Integer, comment="产品期限(天),NULL=灵活开放;DDL: INT UNSIGNED(AL-01 新列)") requires_disclosure: Mapped[bool] = mapped_column(Boolean, default=False, comment="购买前需签署风险揭示书;DDL: TINYINT(1) DEFAULT 0(AL-01 新列)") industry_code: Mapped[str | None] = mapped_column( String(16), ForeignKey("core_industry.industry_code", name="fk_product_industry") ) fee_rate: Mapped[Decimal | None] = mapped_column(Numeric(6, 4)) is_open: Mapped[bool] = mapped_column(Boolean, default=True, comment="DDL: TINYINT(1) DEFAULT 1") created_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)") class CoreHolding(CoreBase): """持仓快照 · jinrong_core.core_holding""" __tablename__ = "core_holding" __table_args__ = ( UniqueConstraint("customer_id", "product_id", name="uk_cust_product"), Index("idx_customer", "customer_id"), {"comment": "持仓快照", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) customer_id: Mapped[str] = mapped_column( String(64), ForeignKey("core_customer.customer_id", name="fk_hold_customer") ) product_id: Mapped[str] = mapped_column( String(64), ForeignKey("core_product.product_id", name="fk_hold_product") ) qty: Mapped[Decimal] = mapped_column(Numeric(18, 4)) cost_amount: Mapped[Decimal] = mapped_column(Numeric(18, 2)) market_value: Mapped[Decimal] = mapped_column(Numeric(18, 2)) pnl_pct: Mapped[Decimal] = mapped_column(Numeric(8, 4), comment="盈亏比例") as_of: Mapped[date] = mapped_column(Date) class CoreTrade(CoreBase): """交易流水(扩展 AML 字段 · 对齐反洗钱规则 RW-001~020) 写入口唯一:app/gateway/gateway_repository.py(仅 INSERT,B5) · jinrong_core.core_trade""" __tablename__ = "core_trade" __table_args__ = ( Index("idx_customer_time", "customer_id", "traded_at"), Index("idx_amount", "amount", "traded_at"), Index("idx_channel_time", "channel", "traded_at"), {"comment": "交易流水", "mysql_engine": "InnoDB"}, ) trade_id: Mapped[str] = mapped_column(String(64), primary_key=True) customer_id: Mapped[str] = mapped_column( String(64), ForeignKey("core_customer.customer_id", name="fk_trade_customer") ) product_id: Mapped[str] = mapped_column( String(64), ForeignKey("core_product.product_id", name="fk_trade_product") ) trade_type: Mapped[str] = mapped_column(Enum("subscribe", "redeem", "convert")) amount: Mapped[Decimal] = mapped_column(Numeric(18, 2)) qty: Mapped[Decimal | None] = mapped_column(Numeric(18, 4)) channel: Mapped[str] = mapped_column( Enum("online", "mobile", "counter", "other"), comment="DDL: DEFAULT 'online'" ) counterparty_account_mask: Mapped[str | None] = mapped_column(String(32)) counterparty_name: Mapped[str | None] = mapped_column(String(64)) is_cash: Mapped[bool] = mapped_column(Boolean, default=False, comment="DDL: TINYINT(1) DEFAULT 0") payer_name: Mapped[str | None] = mapped_column(String(64), comment="代付人(RW-014)") is_third_party_pay: Mapped[bool] = mapped_column(Boolean, default=False, comment="DDL: TINYINT(1) DEFAULT 0") trade_status: Mapped[str] = mapped_column( Enum("confirmed", "pending", "cancelled"), comment="DDL: DEFAULT 'confirmed'" ) traded_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3)") class CoreCashFlow(CoreBase): """资金进出 · jinrong_core.core_cash_flow""" __tablename__ = "core_cash_flow" __table_args__ = ( Index("idx_customer", "customer_id", "occurred_at"), Index("idx_flow_subtype", "flow_subtype", "occurred_at"), {"comment": "资金进出", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) customer_id: Mapped[str] = mapped_column( String(64), ForeignKey("core_customer.customer_id", name="fk_cf_customer") ) flow_type: Mapped[str] = mapped_column(Enum("in", "out")) flow_subtype: Mapped[str] = mapped_column( Enum("deposit", "withdraw", "transfer_in", "transfer_out", "subscribe", "redeem", "other"), comment="DDL: DEFAULT 'other'", ) amount: Mapped[Decimal] = mapped_column(Numeric(18, 2)) channel: Mapped[str] = mapped_column( Enum("online", "mobile", "counter", "other"), comment="DDL: DEFAULT 'online'" ) counterparty_account_mask: Mapped[str | None] = mapped_column(String(32)) counterparty_name: Mapped[str | None] = mapped_column(String(64)) remark: Mapped[str | None] = mapped_column(String(128)) occurred_at: Mapped[datetime] = mapped_column(DateTime, comment="DDL: DATETIME(3)") class CoreProductNav(CoreBase): """产品净值 · jinrong_core.core_product_nav""" __tablename__ = "core_product_nav" __table_args__ = ( UniqueConstraint("product_id", "nav_date", name="uk_product_date"), {"comment": "产品净值", "mysql_engine": "InnoDB"}, ) id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) product_id: Mapped[str] = mapped_column( String(64), ForeignKey("core_product.product_id", name="fk_nav_product") ) nav: Mapped[Decimal] = mapped_column(Numeric(10, 4)) daily_chg_pct: Mapped[Decimal] = mapped_column(Numeric(8, 4)) nav_date: Mapped[date] = mapped_column(Date)