Files
group_xinghuo_jinrong/app/model/entities.py
T
GaoYiYuan_0626 fe4801bc0a feat: C4 FR-8 持仓集中度预警(RISK-006)
依据《实现方案-风控追加需求v1.1-C4C6.md》§2;不改表结构(alert_type/status
复用 payload 承载,audit_log.event_type 为 VARCHAR 可直接扩)。

1. settings.py + .env.example:一次性加齐风控追加 v1.1 共 11 项配置(C4~C6 共用)。
2. core_ro.concentration_profile(customer_id, limit=500):一次 SQL 取明细
   (LIMIT limit+1 探测截断)+ Python 端按 min_risk_code in (R4,R5) 聚合;
   收口挂账 #1(PRD 字面为 list_holdings,改聚合封装,docstring 注明偏离)。
3. rules.py:RULE_SCORES/RULE_ALERT_TYPES 加 RISK-006=60/pattern;RuleHit 加
   alert_subtype;RiskThresholds 加 concentration_threshold 且 from_settings
   必须补读(评审 P1-2:漏读会让 conftest monkeypatch 失效打穿现有断言);
   新增纯函数 rule_concentration——空仓不触发、截断视同达标(保守告警)、
   阈值边界 79.9% 不触发 / 80% 触发、R4+R5 为 0 不触发。
4. engine.process_trade_event:run_rules 之后、record_trade_alerts 之前并入
   集中度命中(不动 run_rules 签名);命中后 L3 打 high_risk_concentration
   标签 + 写 risk_concentration 审计(金额只落合计与前 5 条摘要)。
5. risk_repository:find_pending_event_alert 改候选 LIMIT 50 + Python 过滤掉
   payload.alert_subtype 含 agent_behavior 的单(评审 P0-1:代理人维度行为链单
   不得充当客户维度事件单的聚合锚点);append_alert_event 加 extra_subtypes
   合并进 payload.alert_subtype(不传时行为与原先一致,向后兼容)。
6. alert_service:subtypes 集合维护(空集不注入 payload,评审 P2-3);
   追加时 alert_type 按「老单规则 ∪ 本批规则」重算(评审 P1-3,修掉既有
   large_amount 单被本批仅 RISK-006(60) 翻转为 pattern 的缺陷);
   _publish_alert 加 notify_role/extra 可选参数(C5/C6 复用)。
7. 对话线:chat_tools.customer_context 加 profile(concentration_ratio/
   r45_value/total_value/holdings_truncated),tool_service.summarize 加
   「高风险持仓占比 X%(仅供参考)」;不新增意图词。
8. 02-redis-keys.md 增补 alert_subtype / escalation_level 附加推送字段。

测试:conftest 加 autouse _disable_concentration_rule(阈值推 1.01 做回归隔离,
现有用例断言零改动);test_risk_rules 加 RISK-006 纯函数 6 例;新建
tests/test_concentration_c4.py 11 例(与 RISK-001 同单聚合、score max=70、
L3 tag、risk_concentration 审计、仅集中度也出单、subtype 合并、P0-1 回归、
alert_type 不翻转、对话线 ratio)。全量 453 绿(436 + 17)。
2026-09-07 19:05:02 +08:00

836 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""全库 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)