Files
design-vest-mind/我编写的文档/数据库设计.md
2025-11-12 18:06:09 +08:00

38 KiB
Raw Blame History

数据库设计文档

表结构总览

数据库使用 PostgreSQL

用户相关
├── users (用户表)

基础数据相关
├── brokers (券商表)
├── stock_info (股票基本信息表)
└── stock_daily_price (股票每日收盘价表)

账户相关
├── positions (持仓表)
├── asset_snapshots (资产快照表)
└── position_price_plans (持仓价格计划表)

用户相关表

users 用户表

表结构

字段名 数据类型 约束 说明
user_id BIGSERIAL PRIMARY KEY 用户ID自增
open_id VARCHAR(100) UNIQUE 微信体系的 openId小程序/公众号)
union_id VARCHAR(100) UNIQUE 微信体系的 unionId跨应用统一标识
username VARCHAR(100) UNIQUE 用户名(可选,用于账号密码登录)
email VARCHAR(100) UNIQUE 邮箱(可选,用于邮箱登录)
phone VARCHAR(20) UNIQUE 电话号码(可选,用于手机号登录)
nickname VARCHAR(100) 用户昵称(显示名称)
avatar_url VARCHAR(255) 头像URL
created_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 创建时间
updated_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 修改时间(自动更新)
last_login_at TIMESTAMP 最后登录时间
status VARCHAR(20) NOT NULL, DEFAULT 'active', CHECK 状态active/inactive/deleted

创建语句

CREATE TABLE users (
    user_id BIGSERIAL PRIMARY KEY,
    open_id VARCHAR(100) UNIQUE,
    union_id VARCHAR(100) UNIQUE,
    username VARCHAR(100) UNIQUE,
    email VARCHAR(100) UNIQUE,
    phone VARCHAR(20) UNIQUE,
    nickname VARCHAR(100),
    avatar_url VARCHAR(255),
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    last_login_at TIMESTAMP,
    status VARCHAR(20) NOT NULL DEFAULT 'active',
    -- 约束:至少要有一种登录方式
    CONSTRAINT check_login_method CHECK (
        open_id IS NOT NULL OR 
        username IS NOT NULL OR 
        email IS NOT NULL OR 
        phone IS NOT NULL
    ),
    -- 约束:状态值限制
    CONSTRAINT check_status CHECK (status IN ('active', 'inactive', 'deleted'))
);

-- 创建索引
CREATE INDEX idx_users_open_id ON users(open_id);
CREATE INDEX idx_users_union_id ON users(union_id);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_phone ON users(phone);
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_users_last_login_at ON users(last_login_at);

-- 创建自动更新 updated_at 的触发器
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = CURRENT_TIMESTAMP;
    RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_users_updated_at 
    BEFORE UPDATE ON users 
    FOR EACH ROW 
    EXECUTE FUNCTION update_updated_at_column();

-- 添加注释
COMMENT ON TABLE users IS '用户表,支持微信登录和账号密码登录';
COMMENT ON COLUMN users.user_id IS '用户ID主键自增';
COMMENT ON COLUMN users.open_id IS '微信 openId用于小程序/公众号登录,唯一';
COMMENT ON COLUMN users.union_id IS '微信 unionId用于跨应用统一标识唯一';
COMMENT ON COLUMN users.username IS '用户名,用于账号密码登录,唯一,可选';
COMMENT ON COLUMN users.email IS '邮箱,用于邮箱登录,唯一,可选';
COMMENT ON COLUMN users.phone IS '手机号,用于手机号登录,唯一,可选';
COMMENT ON COLUMN users.nickname IS '用户昵称,显示名称';
COMMENT ON COLUMN users.avatar_url IS '头像URL';
COMMENT ON COLUMN users.status IS '用户状态active(活跃)/inactive(非活跃)/deleted(已删除)';
COMMENT ON COLUMN users.created_at IS '创建时间,自动设置';
COMMENT ON COLUMN users.updated_at IS '更新时间,自动更新';
COMMENT ON COLUMN users.last_login_at IS '最后登录时间';

基础数据相关表设计

brokers 券商表

表结构

字段名 数据类型 约束 说明
broker_id BIGSERIAL PRIMARY KEY 券商ID自增
broker_code VARCHAR(50) NOT NULL 券商代码HTZQ、ZSZQ等
broker_name VARCHAR(100) NOT NULL 券商名称(如:华泰证券、招商证券等)
region VARCHAR(50) NOT NULL, DEFAULT 'CN' 地区/国家CN/US/HK等
sort_order INTEGER DEFAULT 0 排序顺序
is_active BOOLEAN NOT NULL, DEFAULT true 是否启用
created_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 创建时间
updated_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 更新时间
UNIQUE(broker_code, region) 同一地区券商代码唯一
UNIQUE(broker_name, region) 同一地区券商名称唯一

创建语句

CREATE TABLE brokers (
    broker_id BIGSERIAL PRIMARY KEY,
    broker_code VARCHAR(50) NOT NULL,
    broker_name VARCHAR(100) NOT NULL,
    region VARCHAR(50) NOT NULL DEFAULT 'CN',
    sort_order INTEGER DEFAULT 0,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    -- 同一地区券商代码唯一(不同地区可以有相同代码)
    UNIQUE(broker_code, region),
    -- 同一地区券商名称唯一(不同地区可以有相同名称)
    UNIQUE(broker_name, region),
    CONSTRAINT check_region CHECK (region IN ('CN', 'US', 'HK', 'SG', 'JP', 'UK', 'AU', 'CA', 'OTHER'))
);

-- 创建索引
CREATE INDEX idx_brokers_code ON brokers(broker_code);
CREATE INDEX idx_brokers_name ON brokers(broker_name);
CREATE INDEX idx_brokers_region ON brokers(region);
CREATE INDEX idx_brokers_active ON brokers(is_active);
CREATE INDEX idx_brokers_sort ON brokers(sort_order);
CREATE INDEX idx_brokers_region_active ON brokers(region, is_active);

-- 创建触发器自动更新 updated_at
CREATE TRIGGER update_brokers_updated_at 
    BEFORE UPDATE ON brokers 
    FOR EACH ROW 
    EXECUTE FUNCTION update_updated_at_column();

-- 添加注释
COMMENT ON TABLE brokers IS '券商表,记录所有可用的券商信息';
COMMENT ON COLUMN brokers.broker_code IS '券商代码用于系统识别HTZQ华泰证券';
COMMENT ON COLUMN brokers.broker_name IS '券商名称,显示给用户的名称,如:华泰证券';
COMMENT ON COLUMN brokers.region IS '地区/国家代码CN(中国)/US(美国)/HK(香港)/SG(新加坡)/JP(日本)/UK(英国)/AU(澳大利亚)/CA(加拿大)/OTHER(其他)';

示例数据

-- 中国券商
INSERT INTO brokers (broker_code, broker_name, region, sort_order) VALUES
    ('HTZQ', '华泰证券', 'CN', 1),
    ('ZSZQ', '招商证券', 'CN', 2),
    ('ZXZQ', '中信证券', 'CN', 3),
    ('GJZQ', '国金证券', 'CN', 4),
    ('DFZQ', '东方证券', 'CN', 5),
    ('GTZQ', '国泰君安', 'CN', 6),
    ('HXZQ', '华西证券', 'CN', 7),
    ('ZJZQ', '中金公司', 'CN', 8),
    ('PZQ', '平安证券', 'CN', 9),
    ('GFZQ', '广发证券', 'CN', 10);

-- 美国券商
INSERT INTO brokers (broker_code, broker_name, region, sort_order) VALUES
    ('SCHW', 'Charles Schwab', 'US', 1),
    ('FID', 'Fidelity', 'US', 2),
    ('IB', 'Interactive Brokers', 'US', 3),
    ('TD', 'TD Ameritrade', 'US', 4),
    ('ETRADE', 'E*TRADE', 'US', 5);

-- 香港券商
INSERT INTO brokers (broker_code, broker_name, region, sort_order) VALUES
    ('FUTU', '富途证券', 'HK', 1),
    ('TIGER', '老虎证券', 'HK', 2),
    ('HSBC', '汇丰银行', 'HK', 3),
    ('CITI', '花旗银行', 'HK', 4);

-- 其他地区券商示例
INSERT INTO brokers (broker_code, broker_name, region, sort_order) VALUES
    ('DBS', '星展银行', 'SG', 1),
    ('NOMURA', '野村证券', 'JP', 1);

stock_info 股票基本信息表

表结构

字段名 数据类型 约束 说明
id BIGSERIAL PRIMARY KEY 主键ID自增
stock_code VARCHAR(20) NOT NULL 股票代码600519, 00700.HK
stock_name VARCHAR(100) NOT NULL 股票名称
market VARCHAR(20) NOT NULL 市场标识A股: sh/sz/bj, 港股: hk, 美股: us等
full_name VARCHAR(200) 公司全称
industry VARCHAR(100) 所属行业
listing_date DATE 上市日期
status VARCHAR(20) NOT NULL, DEFAULT 'active', CHECK 状态active(正常)/suspended(停牌)/delisted(退市)
created_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 创建时间
updated_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 更新时间
UNIQUE(stock_code, market) 同一市场股票代码唯一

创建语句

CREATE TABLE stock_info (
    id BIGSERIAL PRIMARY KEY,
    stock_code VARCHAR(20) NOT NULL,
    stock_name VARCHAR(100) NOT NULL,
    market VARCHAR(20) NOT NULL,
    full_name VARCHAR(200),
    industry VARCHAR(100),
    listing_date DATE,
    status VARCHAR(20) NOT NULL DEFAULT 'active',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    -- 同一市场股票代码唯一
    UNIQUE(stock_code, market),
    CONSTRAINT check_stock_status CHECK (status IN ('active', 'suspended', 'delisted'))
);

-- 创建索引
CREATE INDEX idx_stock_info_code ON stock_info(stock_code);
CREATE INDEX idx_stock_info_market ON stock_info(market);
CREATE INDEX idx_stock_info_name ON stock_info(stock_name);
CREATE INDEX idx_stock_info_status ON stock_info(status);
CREATE INDEX idx_stock_info_code_market ON stock_info(stock_code, market);
-- 全文搜索索引(用于股票名称模糊匹配,需要先启用 pg_trgm 扩展)
-- CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- CREATE INDEX idx_stock_info_name_trgm ON stock_info USING gin(stock_name gin_trgm_ops);

-- 创建触发器自动更新 updated_at
CREATE TRIGGER update_stock_info_updated_at 
    BEFORE UPDATE ON stock_info 
    FOR EACH ROW 
    EXECUTE FUNCTION update_updated_at_column();

-- 添加注释
COMMENT ON TABLE stock_info IS '股票基本信息表,存储所有市场的股票基本静态信息';
COMMENT ON COLUMN stock_info.stock_code IS '股票代码600519A股、00700.HK港股、AAPL美股';
COMMENT ON COLUMN stock_info.stock_name IS '股票名称,用于显示和搜索';
COMMENT ON COLUMN stock_info.market IS '市场标识sh(上海)/sz(深圳)/bj(北京)/hk(香港)/us(美国)等';
COMMENT ON COLUMN stock_info.status IS '状态active(正常交易)/suspended(停牌)/delisted(退市)';

说明:

  • 此表存储所有市场的股票基本信息,用于用户输入时的模糊匹配
  • 数据不常变更,可以定期从外部数据源同步
  • 支持全文搜索,方便用户通过股票名称快速查找

stock_daily_price 股票每日收盘价表

表结构

字段名 数据类型 约束 说明
id BIGSERIAL PRIMARY KEY 主键ID自增
stock_code VARCHAR(20) NOT NULL 股票代码
market VARCHAR(20) NOT NULL 市场标识
trade_date DATE NOT NULL 交易日期
open_price DECIMAL(18, 4) 开盘价
close_price DECIMAL(18, 4) NOT NULL 收盘价
high_price DECIMAL(18, 4) 最高价
low_price DECIMAL(18, 4) 最低价
volume BIGINT 成交量(单位:手)
amount DECIMAL(20, 2) 成交额(单位:元)
change_amount DECIMAL(18, 4) 涨跌额
change_percent DECIMAL(10, 6) 涨跌幅(%
turnover_rate DECIMAL(10, 6) 换手率(%
pe_ratio DECIMAL(12, 4) 市盈率
pb_ratio DECIMAL(12, 4) 市净率
market_cap DECIMAL(20, 2) 总市值(单位:元)
created_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 创建时间
UNIQUE(stock_code, market, trade_date) 同一股票同一日期只能有一条记录

创建语句

CREATE TABLE stock_daily_price (
    id BIGSERIAL PRIMARY KEY,
    stock_code VARCHAR(20) NOT NULL,
    market VARCHAR(20) NOT NULL,
    trade_date DATE NOT NULL,
    open_price DECIMAL(18, 4),
    close_price DECIMAL(18, 4) NOT NULL,
    high_price DECIMAL(18, 4),
    low_price DECIMAL(18, 4),
    volume BIGINT,
    amount DECIMAL(20, 2),
    change_amount DECIMAL(18, 4),
    change_percent DECIMAL(10, 6),
    turnover_rate DECIMAL(10, 6),
    pe_ratio DECIMAL(12, 4),
    pb_ratio DECIMAL(12, 4),
    market_cap DECIMAL(20, 2),
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    -- 同一股票同一日期只能有一条记录
    UNIQUE(stock_code, market, trade_date),
    -- 外键关联股票基本信息表(可选,如果数据源可靠可以不加)
    CONSTRAINT fk_stock_price_info FOREIGN KEY (stock_code, market) 
        REFERENCES stock_info(stock_code, market) ON DELETE CASCADE
);

-- 创建索引
CREATE INDEX idx_stock_daily_price_code ON stock_daily_price(stock_code);
CREATE INDEX idx_stock_daily_price_market ON stock_daily_price(market);
CREATE INDEX idx_stock_daily_price_date ON stock_daily_price(trade_date);
CREATE INDEX idx_stock_daily_price_code_market ON stock_daily_price(stock_code, market);
CREATE INDEX idx_stock_daily_price_code_date ON stock_daily_price(stock_code, market, trade_date DESC);
-- 用于查询最新价格的索引
CREATE INDEX idx_stock_daily_price_latest ON stock_daily_price(stock_code, market, trade_date DESC);

-- 添加注释
COMMENT ON TABLE stock_daily_price IS '股票每日收盘价表,存储所有市场的股票每日收盘价及相关交易数据';
COMMENT ON COLUMN stock_daily_price.stock_code IS '股票代码,关联 stock_info 表';
COMMENT ON COLUMN stock_daily_price.close_price IS '收盘价,用于更新持仓的 current_price';
COMMENT ON COLUMN stock_daily_price.trade_date IS '交易日期,用于查询历史价格';
COMMENT ON COLUMN stock_daily_price.market_cap IS '总市值,单位:元';

使用说明:

  1. 查询最新价格(用于更新持仓)
-- 查询指定股票的最新收盘价
SELECT 
    stock_code,
    market,
    close_price,
    trade_date
FROM stock_daily_price
WHERE stock_code = :stock_code
  AND market = :market
ORDER BY trade_date DESC
LIMIT 1;

-- 批量查询多个股票的最新价格
SELECT DISTINCT ON (stock_code, market)
    stock_code,
    market,
    close_price,
    trade_date
FROM stock_daily_price
WHERE (stock_code, market) IN (
    ('600519', 'sh'),
    ('00700', 'hk'),
    ('AAPL', 'us')
)
ORDER BY stock_code, market, trade_date DESC;
  1. 查询历史价格(用于图表展示)
-- 查询指定股票的历史价格
SELECT 
    trade_date,
    open_price,
    close_price,
    high_price,
    low_price,
    volume,
    change_percent
FROM stock_daily_price
WHERE stock_code = :stock_code
  AND market = :market
  AND trade_date >= :start_date
ORDER BY trade_date;
  1. 每日价格更新流程
-- 步骤1从外部数据源获取最新价格数据
-- 步骤2批量插入或更新价格数据
INSERT INTO stock_daily_price (
    stock_code, market, trade_date, open_price, close_price,
    high_price, low_price, volume, amount, change_amount,
    change_percent, turnover_rate, pe_ratio, pb_ratio, market_cap
)
VALUES (...)
ON CONFLICT (stock_code, market, trade_date) 
DO UPDATE SET
    open_price = EXCLUDED.open_price,
    close_price = EXCLUDED.close_price,
    high_price = EXCLUDED.high_price,
    low_price = EXCLUDED.low_price,
    volume = EXCLUDED.volume,
    amount = EXCLUDED.amount,
    change_amount = EXCLUDED.change_amount,
    change_percent = EXCLUDED.change_percent,
    turnover_rate = EXCLUDED.turnover_rate,
    pe_ratio = EXCLUDED.pe_ratio,
    pb_ratio = EXCLUDED.pb_ratio,
    market_cap = EXCLUDED.market_cap;

-- 步骤3更新持仓表的 current_price仅更新 auto_price_update = true 的持仓)
UPDATE positions p
SET current_price = (
    SELECT close_price 
    FROM stock_daily_price sdp
    WHERE sdp.stock_code = p.symbol
      AND sdp.market = p.market
    ORDER BY sdp.trade_date DESC
    LIMIT 1
),
updated_at = CURRENT_TIMESTAMP
WHERE p.auto_price_update = true
  AND p.asset_type IN ('stock', 'fund')
  AND p.status = 'active';

账户相关表设计

positions 持仓表

记录用户持仓情况,包含数量、成本价、最新市场价、券商等 表结构

字段名 数据类型 约束 说明
position_id BIGSERIAL PRIMARY KEY 持仓ID自增
user_id BIGINT NOT NULL, FOREIGN KEY 用户ID关联 users 表
broker_id BIGINT NOT NULL, FOREIGN KEY 券商ID关联 brokers 表
asset_type VARCHAR(20) NOT NULL, CHECK 资产类型stock/fund/cash/bond
symbol VARCHAR(50) NOT NULL 资产代码(股票代码、基金代码等)
name VARCHAR(100) NOT NULL 资产名称
market VARCHAR(20) 市场A股/港股/美股等)
shares DECIMAL(18, 4) NOT NULL, DEFAULT 0 持仓份额/数量
cost_price DECIMAL(18, 4) NOT NULL 成本价(每股/每份)
current_price DECIMAL(18, 4) 最新市场价(系统自动更新)
currency VARCHAR(10) NOT NULL, DEFAULT 'CNY' 货币类型
exchange_rate DECIMAL(10, 6) DEFAULT 1 汇率(用于多货币)
auto_price_update BOOLEAN NOT NULL, DEFAULT false 是否自动更新价格(付费用户功能)
status VARCHAR(20) NOT NULL, DEFAULT 'active', CHECK 状态active/suspended/delisted
created_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 创建时间
updated_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 更新时间

创建语句

CREATE TABLE positions (
    position_id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    broker_id BIGINT NOT NULL REFERENCES brokers(broker_id),
    asset_type VARCHAR(20) NOT NULL,
    symbol VARCHAR(50) NOT NULL,
    name VARCHAR(100) NOT NULL,
    market VARCHAR(20),
    shares DECIMAL(18, 4) NOT NULL DEFAULT 0,
    cost_price DECIMAL(18, 4) NOT NULL,
    current_price DECIMAL(18, 4),
    currency VARCHAR(10) NOT NULL DEFAULT 'CNY',
    exchange_rate DECIMAL(10, 6) DEFAULT 1,
    auto_price_update BOOLEAN NOT NULL DEFAULT false,
    status VARCHAR(20) NOT NULL DEFAULT 'active',
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT check_asset_type CHECK (asset_type IN ('stock', 'fund', 'cash', 'bond', 'other')),
    CONSTRAINT check_status CHECK (status IN ('active', 'suspended', 'delisted')),
    CONSTRAINT check_shares_non_negative CHECK (shares >= 0),
    CONSTRAINT check_cost_price_positive CHECK (cost_price > 0),
    -- 同一用户同一券商同一资产只能有一条持仓(通过 user_id + broker_id + symbol + market + asset_type 唯一)
    UNIQUE(user_id, broker_id, symbol, market, asset_type)
);

-- 创建索引
CREATE INDEX idx_positions_user_id ON positions(user_id);
CREATE INDEX idx_positions_broker_id ON positions(broker_id);
CREATE INDEX idx_positions_symbol ON positions(symbol);
CREATE INDEX idx_positions_asset_type ON positions(asset_type);
CREATE INDEX idx_positions_status ON positions(status);
CREATE INDEX idx_positions_user_status ON positions(user_id, status);
CREATE INDEX idx_positions_user_broker ON positions(user_id, broker_id);
CREATE INDEX idx_positions_auto_price_update ON positions(auto_price_update);
CREATE INDEX idx_positions_auto_price_asset ON positions(auto_price_update, asset_type, status);
CREATE INDEX idx_positions_updated_at ON positions(updated_at);

-- 创建触发器自动更新 updated_at
CREATE TRIGGER update_positions_updated_at 
    BEFORE UPDATE ON positions 
    FOR EACH ROW 
    EXECUTE FUNCTION update_updated_at_column();

-- 添加注释
COMMENT ON TABLE positions IS '持仓表,记录用户的资产持仓';
COMMENT ON COLUMN positions.broker_id IS '券商ID关联 brokers 表,支持多券商';
COMMENT ON COLUMN positions.asset_type IS '资产类型stock(股票)/fund(基金)/cash(现金)/bond(国债)/other(其他)';
COMMENT ON COLUMN positions.symbol IS '资产代码如股票代码600519、基金代码等';
COMMENT ON COLUMN positions.shares IS '持仓份额/数量,股票为股数,基金为份数,现金为金额';
COMMENT ON COLUMN positions.cost_price IS '成本价,用户直接修改,系统不自动计算';
COMMENT ON COLUMN positions.current_price IS '最新市场价系统每日自动更新仅auto_price_update=true的持仓';
COMMENT ON COLUMN positions.exchange_rate IS '汇率,用于多货币资产,如港股、美股';
COMMENT ON COLUMN positions.auto_price_update IS '是否自动更新价格true表示系统每日自动更新市场价格付费用户功能';

asset_snapshots (资产快照表)

表结构(新版)

字段名 数据类型 约束 说明
id BIGSERIAL PRIMARY KEY 快照ID自增
user_id BIGINT NOT NULL, FOREIGN KEY 用户ID关联 users 表
snapshot_date DATE NOT NULL 快照日期
total_asset DECIMAL(18,2) NOT NULL 总资产(所有持仓市值 + 现金余额)
total_invested DECIMAL(18,2) NOT NULL 累计投入金额(初始投入 + 后续投入 - 提取金额)
time_weighted_return DECIMAL(12,8) 时间加权收益率(复利累计收益率)
annualized_return DECIMAL(10,6) 年化收益率
year_to_date_return DECIMAL(10,6) 当年收益率(年初至今)
positions_data JSONB 持仓明细快照JSON格式可选
cash_data JSONB 现金账户明细快照JSON格式可选
created_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 创建时间
updated_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 更新时间
UNIQUE(user_id, snapshot_date) 同一用户同一日期只能有一条快照

创建语句

CREATE TABLE asset_snapshots (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
    snapshot_date DATE NOT NULL,
    total_asset DECIMAL(18,2) NOT NULL,
    total_invested DECIMAL(18,2) NOT NULL,
    time_weighted_return DECIMAL(12,8),
    annualized_return DECIMAL(10,6),
    year_to_date_return DECIMAL(10,6),
    positions_data JSONB,
    cash_data JSONB,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(user_id, snapshot_date)
);

-- 创建索引

CREATE INDEX idx_as_user_id ON asset_snapshots(user_id);
CREATE INDEX idx_as_snapshot_date ON asset_snapshots(snapshot_date);
CREATE INDEX idx_as_user_date ON asset_snapshots(user_id, snapshot_date DESC);

-- 添加注释

COMMENT ON TABLE asset_snapshots IS '每日资产快照表,记录总资产、累计投入、收益率等,用于计算净值、收益';
COMMENT ON COLUMN asset_snapshots.user_id IS '用户ID关联 users 表';
COMMENT ON COLUMN asset_snapshots.snapshot_date IS '快照日期';
COMMENT ON COLUMN asset_snapshots.total_asset IS '总资产:所有持仓市值 + 现金余额';
COMMENT ON COLUMN asset_snapshots.total_invested IS '累计投入金额:初始资金 + 后续投入 - 提取金额';
COMMENT ON COLUMN asset_snapshots.time_weighted_return IS '时间加权收益率(复利累计收益率)';
COMMENT ON COLUMN asset_snapshots.annualized_return IS '年化收益率';
COMMENT ON COLUMN asset_snapshots.year_to_date_return IS '当年收益率';
COMMENT ON COLUMN asset_snapshots.positions_data IS '持仓明细快照JSON格式存储';
COMMENT ON COLUMN asset_snapshots.cash_data IS '现金账户明细快照JSON格式存储';
COMMENT ON COLUMN asset_snapshots.created_at IS '快照创建时间';
COMMENT ON COLUMN asset_snapshots.updated_at IS '快照更新时间';

字段计算说明与用法

total_asset总资产

total_asset = Σ(持仓市值) + 现金余额
其中:
- 持仓市值 = 持仓份额 × 当前价格
- 现金余额 = 所有现金账户余额之和

total_invested累计投入金额

total_invested = 初始投入 + 后续投入 - 提取金额
说明:
- 初始投入:账户创建时的初始资金
- 后续投入:用户手动记录的资金投入
- 提取金额:用户提取的资金(提现、转出)
- 注意:买入证券只是资金流转,非"投入"

net_value单位净值及 total_profit总收益

  • 两者不再存储字段,需通过查询实时计算(避免冗余数据和不一致):
-- 单位净值(需要时动态计算)
net_value = total_asset / NULLIF(total_invested, 0)

-- 总收益(需要时动态计算)
total_profit = total_asset - total_invested

时间加权收益率time_weighted_return逻辑

-- 1. 计算每日净值
net_value_today = total_asset_today / NULLIF(total_invested_today, 0)
net_value_yesterday = total_asset_yesterday / NULLIF(total_invested_yesterday, 0)

-- 2. 日收益率
daily_return = (net_value_today - net_value_yesterday) / NULLIF(net_value_yesterday, 0)

-- 3. 累计收益率(复利)
new_twr = (1 + 昨日 time_weighted_return) × (1 + daily_return) - 1

年化收益率annualized_return

annualized_return = (1 + time_weighted_return) ^ (365 / 投资天数) - 1

当年收益率year_to_date_return

-- 获取年初快照,计算净值
year_to_date_return = (current_net_value - year_start_net_value) / year_start_net_value

数据结构示例

positions_data持仓明细快照

[
  {
    "position_id": 1,
    "symbol": "600519",
    "name": "贵州茅台",
    "shares": 100,
    "cost_price": 1600.00,
    "current_price": 1850.00,
    "market_value": 185000.00,
    "profit": 25000.00
  }
]

cash_data现金账户明细快照

[
  {
    "account_id": 1,
    "currency": "CNY",
    "balance": 50000.00
  }
]

每日快照主要流程(伪代码)

1. 获取用户所有持仓
   SELECT * FROM positions WHERE user_id = :user_id AND status = 'active';

2. 获取用户现金账户
   SELECT * FROM cash_accounts WHERE user_id = :user_id;

3. 计算总资产
   total_asset = Σ(持仓市值) + Σ(现金余额)

4. 汇总累计投入
   total_invested = 
       SELECT COALESCE(SUM(amount),0) FROM cash_flows 
       WHERE user_id = :user_id AND flow_type = 'deposit'
     - SELECT COALESCE(SUM(amount),0) FROM cash_flows 
       WHERE user_id = :user_id AND flow_type = 'withdraw'

5. 计算净值、总收益(不入库,仅查询时用)

6. 计算时间加权收益率
   - 获取昨日快照
   - 计算净值变化
   - 复利累计

7. 计算年化和当年收益率

8. 组装positions_data, cash_dataJSONB

9. 插入或更新快照
   INSERT INTO asset_snapshots (...) 
   ON CONFLICT (user_id, snapshot_date) DO UPDATE SET ...

查询与统计示例

查询用户净值曲线

SELECT 
    snapshot_date,
    total_asset,
    total_invested,
    (total_asset / NULLIF(total_invested, 0)) AS net_value,
    (total_asset - total_invested) AS total_profit,
    time_weighted_return,
    annualized_return
FROM asset_snapshots
WHERE user_id = :user_id
ORDER BY snapshot_date DESC
LIMIT 365;  -- 最近一年

查询收益率统计

SELECT 
    snapshot_date,
    time_weighted_return * 100 as return_rate_percent,
    annualized_return * 100 as annualized_return_percent,
    year_to_date_return * 100 as ytd_return_percent
FROM asset_snapshots
WHERE user_id = :user_id
ORDER BY snapshot_date DESC
LIMIT 30;

查询持仓明细历史

SELECT 
    snapshot_date,
    positions_data
FROM asset_snapshots
WHERE user_id = :user_id
  AND positions_data IS NOT NULL
ORDER BY snapshot_date DESC
LIMIT 10;

注意事项

  1. 数据一致性
    • total_invested 必须大于 0否则净值计算会除零出错
    • 使用 NULLIF 防止除零
  2. 性能优化
    • 定时批量生成快照
    • 索引user_id, snapshot_date
    • JSONB 字段支持结构化、高效存储
  3. 数据完整性
    • 每日有且仅有一条快照,断档时补最近快照
  4. 净值及收益实时计算,不入库,仅金额、收益率相关存库
  5. 收益率需每日、每年维护和更新

position_price_plans 持仓价格计划表

表结构

字段名 数据类型 约束 说明
plan_id BIGSERIAL PRIMARY KEY 计划ID自增
position_id BIGINT NOT NULL, FOREIGN KEY 持仓ID关联 positions 表
action_type VARCHAR(20) NOT NULL, CHECK 操作类型buy/sell
plan_price DECIMAL(18, 4) NOT NULL 计划价格
plan_shares DECIMAL(18, 4) 计划份额(可选)
plan_amount DECIMAL(18, 2) 计划金额(可选,与份额二选一)
step_order INTEGER NOT NULL, DEFAULT 1 步骤顺序1, 2, 3...
is_completed BOOLEAN NOT NULL, DEFAULT false 是否已完成
completed_at TIMESTAMP 完成时间
notes TEXT 备注
created_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 创建时间
updated_at TIMESTAMP NOT NULL, DEFAULT CURRENT_TIMESTAMP 更新时间

创建语句

CREATE TABLE position_price_plans (
    plan_id BIGSERIAL PRIMARY KEY,
    position_id BIGINT NOT NULL REFERENCES positions(position_id) ON DELETE CASCADE,
    action_type VARCHAR(20) NOT NULL,
    plan_price DECIMAL(18, 4) NOT NULL,
    plan_shares DECIMAL(18, 4),
    plan_amount DECIMAL(18, 2),
    step_order INTEGER NOT NULL DEFAULT 1,
    is_completed BOOLEAN NOT NULL DEFAULT false,
    completed_at TIMESTAMP,
    notes TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT check_action_type CHECK (action_type IN ('buy', 'sell')),
    CONSTRAINT check_plan_price_positive CHECK (plan_price > 0),
    CONSTRAINT check_step_order_positive CHECK (step_order > 0),
    -- 同一持仓同一操作类型同一顺序只能有一个计划
    UNIQUE(position_id, action_type, step_order)
);

-- 创建索引
CREATE INDEX idx_position_price_plans_position_id ON position_price_plans(position_id);
CREATE INDEX idx_position_price_plans_action_type ON position_price_plans(action_type);
CREATE INDEX idx_position_price_plans_completed ON position_price_plans(is_completed);
CREATE INDEX idx_position_price_plans_position_action ON position_price_plans(position_id, action_type, step_order);

-- 创建触发器自动更新 updated_at
CREATE TRIGGER update_position_price_plans_updated_at 
    BEFORE UPDATE ON position_price_plans 
    FOR EACH ROW 
    EXECUTE FUNCTION update_updated_at_column();

-- 添加注释
COMMENT ON TABLE position_price_plans IS '持仓价格计划表,记录买入和卖出计划价格';
COMMENT ON COLUMN position_price_plans.plan_id IS '计划ID主键自增';
COMMENT ON COLUMN position_price_plans.action_type IS '操作类型buy(买入)/sell(卖出)';
COMMENT ON COLUMN position_price_plans.plan_price IS '计划价格,当市场价格达到此价格时触发提醒';
COMMENT ON COLUMN position_price_plans.plan_shares IS '计划份额,计划买入/卖出的数量';
COMMENT ON COLUMN position_price_plans.plan_amount IS '计划金额,计划买入/卖出的金额(与份额二选一)';
COMMENT ON COLUMN position_price_plans.step_order IS '步骤顺序默认3个买点和3个卖点1, 2, 3';

持仓表使用说明

1. 资产类型说明

stock股票

  • symbol: 股票代码600519
  • market: 市场A股/港股/美股)
  • shares: 股数
  • cost_price: 每股成本价

fund基金

  • symbol: 基金代码
  • market: 市场(如:场内/场外)
  • shares: 基金份数
  • cost_price: 每份成本价

cash现金

  • symbol: 可自定义CASH_CNY
  • name: 现金账户名称
  • shares: 现金余额(金额)
  • cost_price: 固定为1现金无成本价概念

bond国债

  • symbol: 债券代码
  • market: 市场
  • shares: 债券数量
  • cost_price: 每张成本价

2. 价格计划使用示例

创建买入计划3个买点

-- 为持仓创建3个买入计划
INSERT INTO position_price_plans (position_id, action_type, plan_price, plan_shares, step_order)
VALUES 
    (1, 'buy', 100.00, 100, 1),  -- 第一个买点100元买入100股
    (1, 'buy', 95.00, 100, 2),   -- 第二个买点95元买入100股
    (1, 'buy', 90.00, 100, 3);   -- 第三个买点90元买入100股

创建卖出计划3个卖点

-- 为持仓创建3个卖出计划
INSERT INTO position_price_plans (position_id, action_type, plan_price, plan_shares, step_order)
VALUES 
    (1, 'sell', 120.00, 50, 1),  -- 第一个卖点120元卖出50股
    (1, 'sell', 130.00, 50, 2),   -- 第二个卖点130元卖出50股
    (1, 'sell', 150.00, 50, 3);  -- 第三个卖点150元卖出50股

查询持仓及其价格计划

-- 查询持仓及其所有价格计划(包含券商信息)
SELECT 
    p.position_id,
    b.broker_name,
    p.name,
    p.shares,
    p.cost_price,
    p.current_price,
    t.action_type,
    t.plan_price,
    t.plan_shares,
    t.step_order,
    t.is_completed
FROM positions p
INNER JOIN brokers b ON p.broker_id = b.broker_id
LEFT JOIN position_price_plans t ON p.position_id = t.position_id
WHERE p.user_id = :user_id
  AND p.status = 'active'
ORDER BY b.sort_order, b.broker_name, p.position_id, t.action_type, t.step_order;

-- 按券商汇总查询持仓
SELECT 
    b.broker_name,
    COUNT(*) as position_count,
    SUM(p.shares * p.current_price) as total_value
FROM positions p
INNER JOIN brokers b ON p.broker_id = b.broker_id
WHERE p.user_id = :user_id
  AND p.status = 'active'
GROUP BY b.broker_id, b.broker_name
ORDER BY b.sort_order;

查询可用券商列表

-- 查询所有启用的券商(用于下拉选择)
SELECT 
    broker_id,
    broker_code,
    broker_name,
    region
FROM brokers
WHERE is_active = true
ORDER BY region, sort_order, broker_name;

-- 按地区查询券商
SELECT 
    broker_id,
    broker_code,
    broker_name,
    region
FROM brokers
WHERE is_active = true
  AND region = 'CN'  -- 查询中国券商
ORDER BY sort_order, broker_name;

-- 查询所有地区列表
SELECT DISTINCT region
FROM brokers
WHERE is_active = true
ORDER BY region;

检查价格计划是否触发

-- 查询已达到计划价格但未完成的买入计划
SELECT 
    p.position_id,
    p.name,
    p.current_price,
    t.plan_price,
    t.plan_shares,
    t.step_order
FROM positions p
INNER JOIN position_price_plans t ON p.position_id = t.position_id
WHERE p.user_id = :user_id
  AND t.action_type = 'buy'
  AND t.is_completed = false
  AND p.current_price <= t.plan_price
  AND p.status = 'active';

-- 查询已达到计划价格但未完成的卖出计划
SELECT 
    p.position_id,
    p.name,
    p.current_price,
    t.plan_price,
    t.plan_shares,
    t.step_order
FROM positions p
INNER JOIN position_price_plans t ON p.position_id = t.position_id
WHERE p.user_id = :user_id
  AND t.action_type = 'sell'
  AND t.is_completed = false
  AND p.current_price >= t.plan_price
  AND p.status = 'active';

3. 持仓更新流程

用户修改持仓(主动变更)

-- 用户直接修改成本价和份数
UPDATE positions 
SET 
    cost_price = :new_cost_price,
    shares = :new_shares,
    notes = :notes,
    updated_at = CURRENT_TIMESTAMP
WHERE position_id = :position_id
  AND user_id = :user_id;

-- 记录持仓变更历史(需要在 position_changes 表中记录)

系统更新市场价格(被动变更)

-- 每日收盘后自动更新市场价格(仅更新启用自动更新的持仓)
UPDATE positions 
SET 
    current_price = :market_price,
    updated_at = CURRENT_TIMESTAMP
WHERE status = 'active'
  AND auto_price_update = true
  AND asset_type IN ('stock', 'fund', 'bond');

-- 查询需要自动更新价格的持仓(用于批量更新)
SELECT 
    p.position_id,
    p.symbol,
    p.market,
    p.asset_type
FROM positions p
WHERE p.status = 'active'
  AND p.auto_price_update = true
  AND p.asset_type IN ('stock', 'fund', 'bond');

4. 注意事项

  1. 唯一性约束

    • 同一用户同一券商同一资产user_id + broker_id + symbol + market + asset_type只能有一条持仓
    • 支持用户在不同券商持有同一资产(如:华泰证券持有茅台、招商证券也持有茅台)
    • 避免同一券商重复持仓
  2. 券商管理

    • 通过 brokers 表统一管理券商信息
    • 用户添加持仓时从券商列表中选择,保证数据一致性
    • 支持自定义券商(可以添加用户自定义券商)
    • 通过 sort_order 控制券商显示顺序
  3. 价格计划管理

    • 默认支持3个买点和3个卖点
    • 可以通过 step_order 扩展更多计划
    • 完成后的计划可以保留作为历史记录
  4. 现金资产处理

    • 现金的 cost_price 固定为1
    • shares 字段存储现金余额
    • current_price 也为1现金无价格波动
  5. 多货币支持

    • 通过 exchange_rate 字段处理汇率
    • 计算总资产时需要统一货币单位
  6. 价格计划提醒

    • 需要定时任务检查价格计划
    • 当 current_price 达到 plan_price 时发送提醒
    • 买入current_price <= plan_price
    • 卖出current_price >= plan_price
  7. 自动价格更新功能

    • auto_price_update 字段控制是否启用自动价格更新
    • 付费用户创建持仓时,自动设置为 true
    • 用户订阅过期时,批量更新为 false
    • 未付费用户创建持仓时,默认为 false
    • 系统每日收盘后,仅更新 auto_price_update = true 的持仓价格