依据《实现方案-风控追加需求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)。
836 lines
43 KiB
Python
836 lines
43 KiB
Python
"""全库 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)
|