## 问题 产品列表与排行页的「最近涨跌」全是"暂无"。原因是它只能由**两个交易日的收盘价** 现算,而 `fin_market_price` 每只产品每天只有一行 —— 库里头一天只有一天数据时 根本算不出来。 ## 但行情源本来就给了这个数 腾讯行情接口的第 32 位就是**当日涨跌幅**(适配器此前只解析了开高低收量额,没取它)。 所以不必等第二个交易日去算,把源给的数存下来即可。 ## 改动 - **迁移** `20260913_market_price_change_pct`:给 `fin_market_price` **新增一个可空列** `change_pct DECIMAL(10,4)`。只加列、不改任何已有字段(AGENTS.md 规则 2 允许), 可空且不回填,既有读写方全部不受影响。 - **适配器**:`_parse_tencent` 增解析位置 4(昨收)与位置 32(涨跌幅)。 - **同步服务**:写入 `change_pct`;降级路径(净值兜底)没有这个数就写 **NULL**。 - **接口**:`_resolve_change_pct` **优先用存下来的值**;迁移前的历史行没有该列的值, 才回退到"今日收盘 vs 昨收"现算。仍为 `null` 时前端显示"暂无" —— **不得当成 0**(`formatPercent(null)` 会渲染成 `+0.00%`,那等于说"今天平盘")。 ## 契约测试同步 两处契约断言因结构演进需要跟进 —— 它们的作用正是拦住这类变更: - `test_fund_readonly_contract.py`:`fin_market_price` 列数 **13 → 14** - `test_advisor_migration_contract.py`:迁移 head 更新为新版本 (核心断言仍是"链收敛到唯一 head",此处只是钉住末端) 验证:实测 20 只产品**全部有真实涨跌幅**(如 515450 −0.43%、159511 +1.23%); unit+contract **1397 passed**;integration **110 passed**;ruff 通过; mypy 251 文件 0 错;`audit_schema` 与 `migration_state_check` 均通过;e2e 冒烟 40/40。
397 lines
24 KiB
Python
397 lines
24 KiB
Python
"""场内基金模拟交易域(fin_*)只读 ORM 映射。
|
||
|
||
设计约定(强约束):
|
||
|
||
1. 本模块只做「列映射」:不提供任何写辅助方法(无 insert/update/delete/save/merge),
|
||
底座、Agent 与业务组员都不得借本模块下单、改持仓或改交易数据(项目硬性业务红线)。
|
||
2. 列名、类型、可空性严格对齐 MySQL 实际结构(information_schema.COLUMNS 逐列核对),
|
||
不允许任何 DDL / Alembic 迁移改动库结构;回归校验由 tools/audit_constraints.py 负责。
|
||
3. 不声明 ForeignKey:跨表关系由 app/repository/fund_query_repository.py 的
|
||
客户数据范围解析器表达,避免 ORM 引入 flush 期级联写语义。
|
||
4. 未映射 MySQL 生成列(GENERATION_EXPRESSION 非空)——本域当前没有生成列。
|
||
5. 只读查询统一走 app/repository/fund_query_repository.py,Agent(Service 层)
|
||
不得直接访问本模块的 ORM 实体。
|
||
"""
|
||
|
||
from datetime import date, datetime
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from sqlalchemy import JSON, Date, Integer, Numeric, String, Text
|
||
from sqlalchemy.dialects.mysql import BIGINT, DATETIME, TINYINT
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from app.model.base import Base
|
||
|
||
__all__ = [
|
||
"FundCapitalFlow",
|
||
"FundCashLedger",
|
||
"FundCustomerProfile",
|
||
"FundFeeRule",
|
||
"FundHolding",
|
||
"FundMarketPrice",
|
||
"FundNavHistory",
|
||
"FundProduct",
|
||
"FundRiskAlert",
|
||
"FundRiskAssessment",
|
||
"FundRiskNotification",
|
||
"FundSimAccount",
|
||
"FundSimOrder",
|
||
"FundTransaction",
|
||
]
|
||
|
||
|
||
class FundProduct(Base):
|
||
"""场内基金产品主数据(fin_product)。"""
|
||
|
||
__tablename__ = "fin_product"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
product_code: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
product_name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
exchange_code: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
product_category: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
risk_level: Mapped[str] = mapped_column(String(8), nullable=False)
|
||
fund_manager: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
currency: Mapped[str] = mapped_column(String(8), nullable=False)
|
||
lot_size: Mapped[Decimal] = mapped_column(Numeric(18, 4), nullable=False)
|
||
price_tick: Mapped[Decimal] = mapped_column(Numeric(18, 6), nullable=False)
|
||
current_nav: Mapped[Decimal | None] = mapped_column(Numeric(18, 6), nullable=True)
|
||
current_nav_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
min_amount: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
open_start_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
open_end_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
open_period_start: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||
open_period_end: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||
transaction_fee_rate: Mapped[Decimal | None] = mapped_column(Numeric(10, 6), nullable=True)
|
||
single_investor_max_holding_ratio: Mapped[Decimal] = mapped_column(
|
||
Numeric(7, 4), nullable=False
|
||
)
|
||
management_fee_rate: Mapped[Decimal | None] = mapped_column(Numeric(7, 4), nullable=True)
|
||
custodian_fee_rate: Mapped[Decimal | None] = mapped_column(Numeric(7, 4), nullable=True)
|
||
risk_disclosure_required: Mapped[int] = mapped_column(TINYINT(display_width=1), nullable=False)
|
||
second_confirmation_required: Mapped[int] = mapped_column(
|
||
TINYINT(display_width=1), nullable=False
|
||
)
|
||
recording_required: Mapped[int] = mapped_column(TINYINT(display_width=1), nullable=False)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
updated_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundMarketPrice(Base):
|
||
"""场内日行情(fin_market_price),行情为公共数据,无客户归属。"""
|
||
|
||
__tablename__ = "fin_market_price"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
product_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
trade_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||
open_price: Mapped[Decimal] = mapped_column(Numeric(18, 6), nullable=False)
|
||
high_price: Mapped[Decimal] = mapped_column(Numeric(18, 6), nullable=False)
|
||
low_price: Mapped[Decimal] = mapped_column(Numeric(18, 6), nullable=False)
|
||
close_price: Mapped[Decimal] = mapped_column(Numeric(18, 6), nullable=False)
|
||
#: 当日涨跌幅(%),来自行情源。**可空**:源未提供、或该行由不提供涨跌幅的降级路径
|
||
#: (如 `eastmoney_nav_fallback`)写入时为 NULL —— 调用方必须显示"暂无"而不是当成 0。
|
||
change_pct: Mapped[Decimal | None] = mapped_column(Numeric(10, 4), nullable=True)
|
||
volume: Mapped[Decimal | None] = mapped_column(Numeric(24, 4), nullable=True)
|
||
turnover_amount: Mapped[Decimal | None] = mapped_column(Numeric(24, 2), nullable=True)
|
||
total_fund_shares: Mapped[Decimal] = mapped_column(Numeric(24, 4), nullable=False)
|
||
source: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
source_updated_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundNavHistory(Base):
|
||
"""基金净值历史(fin_nav_history),公共数据。"""
|
||
|
||
__tablename__ = "fin_nav_history"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
product_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
nav_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||
nav: Mapped[Decimal] = mapped_column(Numeric(12, 6), nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundSimAccount(Base):
|
||
"""模拟资金账户(fin_sim_account)。"""
|
||
|
||
__tablename__ = "fin_sim_account"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
account_no: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
customer_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
currency: Mapped[str] = mapped_column(String(8), nullable=False)
|
||
cash_balance: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
available_cash: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
frozen_cash: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
initial_balance: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
version: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
updated_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundCashLedger(Base):
|
||
"""资金流水(fin_cash_ledger)。无 customer_id,客户归属经 account_id 间接解析。"""
|
||
|
||
__tablename__ = "fin_cash_ledger"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
ledger_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
account_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
transaction_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
entry_type: Mapped[str] = mapped_column(String(24), nullable=False)
|
||
amount: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
balance_after: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
available_cash_after: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
frozen_cash_after: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
idempotency_key: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
occurred_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundCapitalFlow(Base):
|
||
"""资金划转/出入金流水(fin_capital_flow)。"""
|
||
|
||
__tablename__ = "fin_capital_flow"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
flow_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
customer_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
account_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
transaction_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
flow_type: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
amount: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
balance_after: Mapped[Decimal | None] = mapped_column(Numeric(18, 2), nullable=True)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
settled_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
occurred_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
payer_name: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
source_type: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
related_work_order_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
match_status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
updated_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundFeeRule(Base):
|
||
"""费率规则(fin_fee_rule),配置型公共数据。"""
|
||
|
||
__tablename__ = "fin_fee_rule"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
rule_code: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
product_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
exchange_code: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||
order_side: Mapped[str] = mapped_column(String(8), nullable=False)
|
||
customer_tier: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||
min_trade_amount: Mapped[Decimal | None] = mapped_column(Numeric(18, 2), nullable=True)
|
||
max_trade_amount: Mapped[Decimal | None] = mapped_column(Numeric(18, 2), nullable=True)
|
||
fee_rate: Mapped[Decimal] = mapped_column(Numeric(10, 6), nullable=False)
|
||
minimum_fee: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
fixed_fee: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
priority: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
effective_from: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
effective_until: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
updated_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundSimOrder(Base):
|
||
"""模拟委托单(fin_sim_order)。只读查询,禁止在此写入委托。"""
|
||
|
||
__tablename__ = "fin_sim_order"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
order_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
customer_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
account_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
product_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
order_side: Mapped[str] = mapped_column(String(8), nullable=False)
|
||
price_type: Mapped[str] = mapped_column(String(8), nullable=False)
|
||
quantity: Mapped[Decimal] = mapped_column(Numeric(18, 4), nullable=False)
|
||
limit_price: Mapped[Decimal | None] = mapped_column(Numeric(18, 6), nullable=True)
|
||
quote_price: Mapped[Decimal] = mapped_column(Numeric(18, 6), nullable=False)
|
||
quote_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
quote_source: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
channel: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||
advisor_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
filled_quantity: Mapped[Decimal] = mapped_column(Numeric(18, 4), nullable=False)
|
||
average_executed_price: Mapped[Decimal | None] = mapped_column(Numeric(18, 6), nullable=True)
|
||
status: Mapped[str] = mapped_column(String(24), nullable=False)
|
||
risk_rule_hits: Mapped[Any | None] = mapped_column(JSON, nullable=True)
|
||
risk_disclosure_ack_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
second_confirmation_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
recording_reference: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
ops_handler_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
ops_handled_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
compliance_handler_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
compliance_handled_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
reject_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
submitted_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
cancelled_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
updated_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundTransaction(Base):
|
||
"""成交记录(fin_transaction)。只读查询,禁止在此写入成交。"""
|
||
|
||
__tablename__ = "fin_transaction"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
transaction_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
order_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
work_order_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
customer_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
account_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
product_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
order_side: Mapped[str] = mapped_column(String(8), nullable=False)
|
||
transaction_type: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
executed_price: Mapped[Decimal] = mapped_column(Numeric(18, 6), nullable=False)
|
||
nav: Mapped[Decimal] = mapped_column(Numeric(12, 6), nullable=False)
|
||
executed_quantity: Mapped[Decimal] = mapped_column(Numeric(18, 4), nullable=False)
|
||
shares: Mapped[Decimal] = mapped_column(Numeric(18, 4), nullable=False)
|
||
gross_amount: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
amount: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
fee_rule_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
fee_rate_snapshot: Mapped[Decimal] = mapped_column(Numeric(10, 6), nullable=False)
|
||
fee_amount: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
fee: Mapped[Decimal | None] = mapped_column(Numeric(18, 2), nullable=True)
|
||
net_amount: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
quote_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
quote_source: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
executed_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
confirmed_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
confirmed_by: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||
auto_confirmed: Mapped[int] = mapped_column(TINYINT(display_width=1), nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundHolding(Base):
|
||
"""持仓(fin_holding)。只读查询,禁止在此修改持仓。"""
|
||
|
||
__tablename__ = "fin_holding"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
customer_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
trade_account: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
product_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
total_quantity: Mapped[Decimal] = mapped_column(Numeric(18, 4), nullable=False)
|
||
shares: Mapped[Decimal] = mapped_column(Numeric(18, 4), nullable=False)
|
||
available_quantity: Mapped[Decimal] = mapped_column(Numeric(18, 4), nullable=False)
|
||
frozen_quantity: Mapped[Decimal] = mapped_column(Numeric(18, 4), nullable=False)
|
||
average_cost: Mapped[Decimal] = mapped_column(Numeric(18, 6), nullable=False)
|
||
cost_amount: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
market_value: Mapped[Decimal | None] = mapped_column(Numeric(18, 2), nullable=True)
|
||
current_value: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
profit_loss: Mapped[Decimal | None] = mapped_column(Numeric(18, 2), nullable=True)
|
||
profit_loss_ratio: Mapped[Decimal | None] = mapped_column(Numeric(10, 4), nullable=True)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
first_acquired_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
version: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
updated_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundCustomerProfile(Base):
|
||
"""客户画像(fin_customer_profile),主键为 customer_id。"""
|
||
|
||
__tablename__ = "fin_customer_profile"
|
||
|
||
customer_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
trade_account: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
real_name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
birth_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||
occupation: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
mobile_masked: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||
investor_type: Mapped[str] = mapped_column(String(8), nullable=False)
|
||
investment_horizon: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||
preferred_asset_class: Mapped[Any | None] = mapped_column(JSON, nullable=True)
|
||
trading_frequency: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||
last_active_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
total_asset: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
|
||
behavior_score: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
risk_tags: Mapped[Any | None] = mapped_column(JSON, nullable=True)
|
||
opened_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
updated_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundRiskAssessment(Base):
|
||
"""风险测评记录(fin_risk_assessment)。"""
|
||
|
||
__tablename__ = "fin_risk_assessment"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
customer_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
questionnaire_version: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
answers: Mapped[Any] = mapped_column(JSON, nullable=False)
|
||
total_score: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
investor_type: Mapped[str] = mapped_column(String(8), nullable=False)
|
||
assessed_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
valid_until: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundRiskAlert(Base):
|
||
"""风险预警事件(fin_risk_alert)。"""
|
||
|
||
__tablename__ = "fin_risk_alert"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
alert_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
customer_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
related_transaction_id: Mapped[int | None] = mapped_column(
|
||
BIGINT(unsigned=True), nullable=True
|
||
)
|
||
related_order_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
related_work_order_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
primary_risk_work_order_id: Mapped[int | None] = mapped_column(
|
||
BIGINT(unsigned=True), nullable=True
|
||
)
|
||
alert_type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
alert_level: Mapped[str] = mapped_column(String(8), nullable=False)
|
||
trigger_rule_codes: Mapped[Any] = mapped_column(JSON, nullable=False)
|
||
evidence_summary: Mapped[str] = mapped_column(Text, nullable=False)
|
||
evidence_snapshot: Mapped[Any] = mapped_column(JSON, nullable=False)
|
||
priority_score: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
event_status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
ack_status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
ack_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
handler_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
due_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
is_escalated: Mapped[int] = mapped_column(TINYINT(display_width=1), nullable=False)
|
||
escalated_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
escalation_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
handle_result: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
closed_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
close_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
ai_analysis: Mapped[Any | None] = mapped_column(JSON, nullable=True)
|
||
manual_remark: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
updated_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|
||
|
||
|
||
class FundRiskNotification(Base):
|
||
"""风险通知(fin_risk_notification)。无 customer_id,客户归属经 alert_id 间接解析。"""
|
||
|
||
__tablename__ = "fin_risk_notification"
|
||
|
||
id: Mapped[int] = mapped_column(BIGINT(unsigned=True), primary_key=True)
|
||
notification_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
alert_id: Mapped[int] = mapped_column(BIGINT(unsigned=True), nullable=False)
|
||
channel: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
receiver_user_id: Mapped[int | None] = mapped_column(BIGINT(unsigned=True), nullable=True)
|
||
receiver_email: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
title: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||
send_status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
sent_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
read_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
acknowledged_at: Mapped[datetime | None] = mapped_column(DATETIME(fsp=0), nullable=True)
|
||
fail_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||
created_at: Mapped[datetime] = mapped_column(DATETIME(fsp=0), nullable=False)
|