Files
group_xinghuo_jinrong/scripts/core/01-ddl.sql
T
GaoYiYuan_0626 c5182f1910 feat(convert): 基金转换 T-0/T-0b 门禁 + T-1 数据层 + T-2/T-2b 纯函数与实算回填
T-0 / T-0b(门禁 · 2026-09-10)
- T-0:sqlite 与 MySQL 结构对齐 —— core_holding 统一为 qty/cost_amount/as_of/pnl_pct
  + PK + UNIQUE(customer_id, product_id);补 core_product_nav;新增建库自校验
  _assert_ddl_aligned()(R-g);test_db.py 增 3 条门禁用例(含反向验证门禁失效)
- T-0b:DB 账号分离(D20)—— 新增 scripts/core/00-grant.sql(三账号逐表授权);
  settings.py 增 3 组账号;db.py 改 get_engine(db, role),缓存键改为 (库名, 角色),
  账号未配置回退单账号;core_ro→ro / gateway_repository→rw / risk·session_repository→rw;
  tests/conftest.py 四处显式 role="admin"(R-e)

T-1(数据层)
- scripts/core/01-ddl.sql:新建 core_fee_rule / core_share_lot / core_convert_lot_detail;
  core_trade 加 convert_group_id + idx_convert_group;core_product 加 8 列 + fee_rate 补 COMMENT
- 新增 07-seed-fee-rule.sql(赎回费 5 档 × 14 产品,按 22 号文 §10)/ 08-seed-share-lot.sql
  (58 行持仓 → 61 行批次,Σ remain_qty 恒等于 qty)/ 09-seed-org.sql(管理人 + TA +
  申购费率 + 最低持有余额,v1.1 按「管理人全产品线」重排)
- reset.ps1 追加 07/08/09;02-mysql-agent专用.sql 追加 risk_convert_detail
- tests/_ddl.py 同步 4 表 + 新增 REQUIRED_CONVERT_TABLES 建库门禁
- 新增 scripts/dev/verify_convert_seed.py(pymysql 等价 reset 流程 + 8 条 DoD 断言,
  含断言 ⑧「费率档 ↔ product_type 匹配」,越档即 FAIL)

T-2 / T-2b(纯函数包 + 示例实算回填)
- 新增 app/service/convert/ 7 文件:__init__ / types / calc / fee / nav / lot_bootstrap / errors
  (纯函数,不查库、不碰 SQL;所有量化显式 ROUND_HALF_UP;lot_bootstrap 用 zlib.crc32
   保证 D18 跨进程同源)
- 新增 tests/test_convert_calc.py 93 用例(12 类:HALF_UP 反向自证 / 分档边界 /
  FIFO 含同 confirmed_at 兜底 / 双口径 / 强制全转与强制赎回 / PRD §5.3 全链自证 /
  纯函数零 IO 依赖断言)
- 重写 scripts/dev/calc_convert_demo.py:去掉脚本内公式副本,改为调用生产 calc.py,
  末尾与 PRD §5.3 逐项比对(不一致即退出码 1),兼作一致性门禁

验证
- pytest 609 passed / 3 skipped(516 → +93,零回归)
- verify_convert_seed.py 8/8 PASS;calc_convert_demo.py 15/15 与 PRD §5.3 一致

文档:PRD v0.9.1(费率分类修正)· 架构 §7 签名回填 / §8.3 错误码注 / §15 T-2 完成 ·
开发计划 §1.5 新增 R-h + §4.2·§4.3 执行记录 · AGENTS.md · docs/memory
2026-09-10 14:45:55 +08:00

259 lines
17 KiB
SQL
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.
-- jinrong_core 表结构 · 对齐客户资料(用户信息示例 / 适当性指南 / AML 规则)
USE jinrong_core;
-- 风险等级字典(客户 C1~C5 · 产品 R1~R5)
CREATE TABLE core_risk_grade (
code VARCHAR(4) NOT NULL PRIMARY KEY COMMENT 'C1~C5 或 R1~R5',
grade_type ENUM('customer','product') NOT NULL,
display_name VARCHAR(32) NOT NULL,
sort_order TINYINT UNSIGNED NOT NULL
) ENGINE=InnoDB COMMENT='风险等级字典';
CREATE TABLE core_industry (
industry_code VARCHAR(16) NOT NULL PRIMARY KEY,
industry_name VARCHAR(64) NOT NULL
) ENGINE=InnoDB COMMENT='行业分类';
-- C×R 适当性匹配矩阵(《个人投资者适当性管理指南》第十二条)
CREATE TABLE core_suitability_rule (
id TINYINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
customer_risk_code CHAR(2) NOT NULL COMMENT 'C1~C5',
product_risk_code CHAR(2) NOT NULL COMMENT 'R1~R5',
match_result ENUM('allowed','allowed_with_disclosure','forbidden') NOT NULL,
rule_ref VARCHAR(32) NOT NULL DEFAULT 'JR-AST-012',
UNIQUE KEY uk_cx_r (customer_risk_code, product_risk_code),
CONSTRAINT fk_suit_cust_risk FOREIGN KEY (customer_risk_code) REFERENCES core_risk_grade(code),
CONSTRAINT fk_suit_prod_risk FOREIGN KEY (product_risk_code) REFERENCES core_risk_grade(code)
) ENGINE=InnoDB COMMENT='适当性匹配规则(L0 权威)';
-- 内部员工(含 RBAC 角色种子,供 JWT/鉴权联调对照)
CREATE TABLE core_staff (
staff_id VARCHAR(64) NOT NULL PRIMARY KEY,
display_name VARCHAR(64) NOT NULL,
staff_type ENUM('advisor','analyst','risk_officer','compliance','ops') NOT NULL,
roles JSON NOT NULL COMMENT 'JWT roles 数组,如 ["advisor"]',
tenant_id VARCHAR(32) NOT NULL DEFAULT 'TENANT-001',
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
) ENGINE=InnoDB COMMENT='内部员工主档(模拟 IdP 账号源)';
-- 客户主档 L0(KYC + 财务概况 · 对齐用户信息数据示例)
CREATE TABLE core_customer (
customer_id VARCHAR(64) NOT NULL PRIMARY KEY,
display_name VARCHAR(64) NOT NULL COMMENT '脱敏展示名',
gender ENUM('M','F','U') NOT NULL DEFAULT 'U',
birth_date DATE NULL,
age TINYINT UNSIGNED NULL,
id_no_mask VARCHAR(32) NULL COMMENT '310101199903XXXXXX',
occupation VARCHAR(64) NULL,
employer VARCHAR(128) NULL,
education ENUM('high_school','associate','bachelor','master','doctor','other') NULL,
marital_status ENUM('single','married','widowed','divorced','other') NULL,
city VARCHAR(64) NULL,
address_mask VARCHAR(256) NULL,
phone_mask VARCHAR(16) NULL,
email_mask VARCHAR(64) NULL,
annual_income DECIMAL(18,2) NULL COMMENT '家庭年收入(元)',
financial_asset DECIMAL(18,2) NULL COMMENT '金融资产规模(不含房产)',
monthly_investable DECIMAL(18,2) NULL COMMENT '月可投资金额',
is_hnw TINYINT(1) NOT NULL DEFAULT 0 COMMENT '高净值客户',
service_tier ENUM('normal','vip','diamond') NOT NULL DEFAULT 'normal',
is_pep TINYINT(1) NOT NULL DEFAULT 0 COMMENT '政治公众人物',
aml_risk_level ENUM('low','medium','high') NOT NULL DEFAULT 'low',
invest_experience_years TINYINT UNSIGNED NULL,
first_invest_date DATE NULL,
tenant_id VARCHAR(32) NOT NULL DEFAULT 'TENANT-001',
open_date DATE NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
KEY idx_service_tier (service_tier),
KEY idx_hnw (is_hnw),
KEY idx_aml (aml_risk_level)
) ENGINE=InnoDB COMMENT='客户主档 L0 · KYC';
-- 正式风险测评 L0(对齐适当性指南 16 题问卷 + FM-03 风评过期)
CREATE TABLE core_customer_risk (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
customer_id VARCHAR(64) NOT NULL,
risk_code CHAR(2) NOT NULL COMMENT 'C1~C5',
questionnaire_score SMALLINT UNSIGNED NULL COMMENT '问卷总分 20-100',
max_loss_tolerance_pct DECIMAL(5,2) NULL COMMENT '最大可承受亏损比例',
investment_goal VARCHAR(128) NULL,
investment_horizon ENUM('short','medium','long','flexible') NULL,
investor_category ENUM('ordinary','professional','professional_pending') NOT NULL DEFAULT 'ordinary',
professional_approved_at DATE NULL,
is_authoritative TINYINT(1) NOT NULL DEFAULT 1,
evaluated_at DATE NOT NULL,
expires_at DATE NOT NULL COMMENT '风评有效期(通常 evaluated_at+12 月)',
source VARCHAR(32) NOT NULL DEFAULT 'risk_questionnaire',
UNIQUE KEY uk_customer_current (customer_id),
KEY idx_risk (risk_code),
KEY idx_expires (expires_at),
KEY idx_investor_cat (investor_category),
CONSTRAINT fk_cust_risk_customer FOREIGN KEY (customer_id) REFERENCES core_customer(customer_id),
CONSTRAINT fk_cust_risk_code FOREIGN KEY (risk_code) REFERENCES core_risk_grade(code)
) ENGINE=InnoDB COMMENT='客户正式风险测评 L0';
CREATE TABLE core_customer_advisor (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
customer_id VARCHAR(64) NOT NULL,
advisor_id VARCHAR(64) NOT NULL COMMENT '对应 core_staff.staff_id',
rel_status ENUM('active','transferred','closed') NOT NULL DEFAULT 'active',
effective_from DATE NOT NULL,
effective_to DATE NULL,
UNIQUE KEY uk_cust_advisor_from (customer_id, advisor_id, effective_from),
KEY idx_advisor (advisor_id, rel_status),
CONSTRAINT fk_ca_customer FOREIGN KEY (customer_id) REFERENCES core_customer(customer_id),
CONSTRAINT fk_ca_advisor FOREIGN KEY (advisor_id) REFERENCES core_staff(staff_id)
) ENGINE=InnoDB COMMENT='客户-代理人归属';
-- 产品主档(对齐 C-11:风险等级 + 期限 + 起购金额)
CREATE TABLE core_product (
product_id VARCHAR(64) NOT NULL PRIMARY KEY,
product_name VARCHAR(128) NOT NULL,
product_type ENUM(
'money','bond','mixed','stock','index',
'wealth_mgmt','private_fund','insurance','structured'
) NOT NULL,
min_risk_code CHAR(2) NOT NULL COMMENT 'R1~R5 最低适配',
min_subscribe_amount DECIMAL(18,2) NOT NULL DEFAULT 1.00 COMMENT '起购金额(元)',
term_days INT UNSIGNED NULL COMMENT '产品期限(天),NULL=灵活开放',
requires_disclosure TINYINT(1) NOT NULL DEFAULT 0 COMMENT '购买前需签署风险揭示书',
industry_code VARCHAR(16) NULL,
fee_rate DECIMAL(6,4) NULL COMMENT '历史费率展示位(换算逻辑不读;申购费率见 subscribe_fee_rate)',
-- ===== 基金转换(convert)扩展列 · T-1 =====
can_subscribe TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否可申购(转换转入前置校验)',
can_redeem TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否可赎回(转换转出前置校验)',
min_hold_qty DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '最低持有份额阈值',
min_redeem_qty DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '最低赎回份额',
min_hold_action VARCHAR(32) NOT NULL DEFAULT 'force_transfer' COMMENT '低于 min_hold_qty 的动作:force_transfer=强制全转 / force_redeem=强制赎回剩余;用 VARCHAR 非 ENUM,加动作免 ALTER',
subscribe_fee_rate DECIMAL(6,4) NOT NULL DEFAULT 0 COMMENT '申购费率(补差费计算源,价外法)。22 号文 §8 上限:指数/债券 ≤0.0030、其他混合 ≤0.0050、主动偏股 ≤0.0080、货基 0',
fund_company VARCHAR(64) NULL COMMENT '基金管理人(同一笔转换需同管理人)',
ta_code VARCHAR(32) NULL COMMENT '注册登记机构代码(同一笔转换需同 TA)',
is_open TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
KEY idx_min_risk (min_risk_code),
KEY idx_product_type (product_type),
CONSTRAINT fk_product_risk FOREIGN KEY (min_risk_code) REFERENCES core_risk_grade(code),
CONSTRAINT fk_product_industry FOREIGN KEY (industry_code) REFERENCES core_industry(industry_code)
) ENGINE=InnoDB COMMENT='产品主档';
CREATE TABLE core_holding (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
customer_id VARCHAR(64) NOT NULL,
product_id VARCHAR(64) NOT NULL,
qty DECIMAL(18,4) NOT NULL,
cost_amount DECIMAL(18,2) NOT NULL,
market_value DECIMAL(18,2) NOT NULL,
pnl_pct DECIMAL(8,4) NOT NULL COMMENT '盈亏比例',
as_of DATE NOT NULL,
UNIQUE KEY uk_cust_product (customer_id, product_id),
KEY idx_customer (customer_id),
CONSTRAINT fk_hold_customer FOREIGN KEY (customer_id) REFERENCES core_customer(customer_id),
CONSTRAINT fk_hold_product FOREIGN KEY (product_id) REFERENCES core_product(product_id)
) ENGINE=InnoDB COMMENT='持仓快照';
-- 交易流水(扩展 AML 字段 · 对齐反洗钱规则 RW-001~020)
CREATE TABLE core_trade (
trade_id VARCHAR(64) NOT NULL PRIMARY KEY,
customer_id VARCHAR(64) NOT NULL,
product_id VARCHAR(64) NOT NULL,
trade_type ENUM('subscribe','redeem','convert') NOT NULL,
amount DECIMAL(18,2) NOT NULL,
qty DECIMAL(18,4) NULL,
convert_group_id VARCHAR(64) NULL COMMENT '基金转换组 ID:同组两条流水(转出 redeem / 转入 subscribe)共享;非 convert 场景为 NULL',
channel ENUM('online','mobile','counter','other') NOT NULL DEFAULT 'online',
counterparty_account_mask VARCHAR(32) NULL,
counterparty_name VARCHAR(64) NULL,
is_cash TINYINT(1) NOT NULL DEFAULT 0,
payer_name VARCHAR(64) NULL COMMENT '代付人(RW-014)',
is_third_party_pay TINYINT(1) NOT NULL DEFAULT 0,
trade_status ENUM('confirmed','pending','cancelled') NOT NULL DEFAULT 'confirmed',
traded_at DATETIME(3) NOT NULL,
KEY idx_customer_time (customer_id, traded_at),
KEY idx_convert_group (convert_group_id),
KEY idx_amount (amount, traded_at),
KEY idx_channel_time (channel, traded_at),
CONSTRAINT fk_trade_customer FOREIGN KEY (customer_id) REFERENCES core_customer(customer_id),
CONSTRAINT fk_trade_product FOREIGN KEY (product_id) REFERENCES core_product(product_id)
) ENGINE=InnoDB COMMENT='交易流水';
CREATE TABLE core_cash_flow (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
customer_id VARCHAR(64) NOT NULL,
flow_type ENUM('in','out') NOT NULL,
flow_subtype ENUM('deposit','withdraw','transfer_in','transfer_out','subscribe','redeem','other') NOT NULL DEFAULT 'other',
amount DECIMAL(18,2) NOT NULL,
channel ENUM('online','mobile','counter','other') NOT NULL DEFAULT 'online',
counterparty_account_mask VARCHAR(32) NULL,
counterparty_name VARCHAR(64) NULL,
remark VARCHAR(128) NULL,
occurred_at DATETIME(3) NOT NULL,
KEY idx_customer (customer_id, occurred_at),
KEY idx_flow_subtype (flow_subtype, occurred_at),
CONSTRAINT fk_cf_customer FOREIGN KEY (customer_id) REFERENCES core_customer(customer_id)
) ENGINE=InnoDB COMMENT='资金进出';
CREATE TABLE core_product_nav (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
product_id VARCHAR(64) NOT NULL,
nav DECIMAL(10,4) NOT NULL,
daily_chg_pct DECIMAL(8,4) NOT NULL,
nav_date DATE NOT NULL,
UNIQUE KEY uk_product_date (product_id, nav_date),
CONSTRAINT fk_nav_product FOREIGN KEY (product_id) REFERENCES core_product(product_id)
) ENGINE=InnoDB COMMENT='产品净值';
-- ============================================================
-- 基金转换(convert)新增表 · T-1(2026-09-10)
-- 依据:docs/PRD/PRD-基金转换交易.md §4.1 · 架构设计-基金转换交易.md §9
-- ============================================================
-- 费率规则:redeem 按持有期分档(本期启用);subscribe 金额分档为真实 Core 接入扩展位(本期不启用)
CREATE TABLE core_fee_rule (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
product_id VARCHAR(64) NOT NULL,
fee_type ENUM('subscribe','redeem') NOT NULL,
min_hold_days INT NULL COMMENT 'redeem:持有期下界(含)',
max_hold_days INT NULL COMMENT 'redeem:持有期上界(不含);NULL=无上限',
min_amount DECIMAL(18,2) NULL COMMENT 'subscribe:金额下界(含);本期不启用',
max_amount DECIMAL(18,2) NULL COMMENT 'subscribe:金额上界(不含);本期不启用',
rate DECIMAL(6,4) NOT NULL COMMENT '费率;redeem 档位按 22 号文 §10 下限(<7d 1.5% / 7-30d 1% / 30-180d 0.5% / ≥180d 合同约定)',
to_fund_ratio DECIMAL(5,4) NOT NULL DEFAULT 1.0000 COMMENT '赎回费计入基金财产比例。22 号文 §10:赎回费全额计入基金财产 → 恒 1.0(旧规 75%/50%/25% 分级已废止)。本期仅留痕、不参与计算',
KEY idx_product_type (product_id, fee_type),
CONSTRAINT fk_fee_rule_product FOREIGN KEY (product_id) REFERENCES core_product(product_id)
) ENGINE=InnoDB COMMENT='费率规则(赎回费分档 / 申购费扩展位)';
-- 份额批次:FIFO 计费的权威源(普通申赎无批次时由 lot_bootstrap 兜底补建,D8/D18)
CREATE TABLE core_share_lot (
lot_id VARCHAR(64) NOT NULL PRIMARY KEY,
customer_id VARCHAR(64) NOT NULL,
product_id VARCHAR(64) NOT NULL,
qty DECIMAL(18,4) NOT NULL COMMENT '原始份额',
remain_qty DECIMAL(18,4) NOT NULL COMMENT '剩余份额(FIFO 扣减后;归零保留行不删,保可追溯)',
nav DECIMAL(10,4) NOT NULL COMMENT '成交净值',
confirmed_at DATETIME(3) NOT NULL COMMENT '份额确认时间(持有期自 T+1 起算,D14)',
source_trade_id VARCHAR(64) NULL COMMENT '来源流水(兜底补建时为 NULL)',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
KEY idx_cust_prod_time (customer_id, product_id, confirmed_at),
CONSTRAINT fk_lot_customer FOREIGN KEY (customer_id) REFERENCES core_customer(customer_id),
CONSTRAINT fk_lot_product FOREIGN KEY (product_id) REFERENCES core_product(product_id)
) ENGINE=InnoDB COMMENT='份额批次(FIFO 计费权威源)';
-- 转换批次计费明细:含 nav/nav_date 使 Core 侧自包含,供 rebuild_alerts 补偿(D6)
CREATE TABLE core_convert_lot_detail (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
convert_group_id VARCHAR(64) NOT NULL,
lot_id VARCHAR(64) NOT NULL COMMENT '被扣减的原批次(对应 core_share_lot.lot_id)',
qty DECIMAL(18,4) NOT NULL COMMENT '本批转出份额',
hold_days INT NOT NULL COMMENT '本批持有天数(自然日,不含申请日)',
amount DECIMAL(18,2) NOT NULL COMMENT '本批金额(已按 2 位四舍五入)',
fee_rate DECIMAL(6,4) NOT NULL COMMENT '本批适用费率',
fee_amount DECIMAL(18,2) NOT NULL COMMENT '本批费用(已按 2 位四舍五入)',
nav DECIMAL(10,4) NOT NULL COMMENT '本批成交净值',
nav_date DATE NOT NULL COMMENT '净值日期',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
KEY idx_group (convert_group_id),
KEY idx_lot (lot_id)
) ENGINE=InnoDB COMMENT='转换批次计费明细(Core 侧自包含,补偿数据源)';