# 数据库设计文档 ## 表结构总览 数据库使用 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 | **创建语句** ```sql 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) | | | 同一地区券商名称唯一 | **创建语句** ```sql 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(其他)'; ``` **示例数据** ```sql -- 中国券商 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) | | | 同一市场股票代码唯一 | **创建语句** ```sql 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 '股票代码,如:600519(A股)、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) | | | 同一股票同一日期只能有一条记录 | **创建语句** ```sql 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. **查询最新价格(用于更新持仓)** ```sql -- 查询指定股票的最新收盘价 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; ``` 2. **查询历史价格(用于图表展示)** ```sql -- 查询指定股票的历史价格 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; ``` 3. **每日价格更新流程** ```sql -- 步骤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 | 更新时间 | **创建语句** ```sql 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) | | | 同一用户同一日期只能有一条快照 | **创建语句** ```sql 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) ); ``` -- 创建索引 ```sql 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); ``` -- 添加注释 ```sql 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(总收益)** - 两者不再存储字段,需通过查询实时计算(避免冗余数据和不一致): ```sql -- 单位净值(需要时动态计算) net_value = total_asset / NULLIF(total_invested, 0) -- 总收益(需要时动态计算) total_profit = total_asset - total_invested ``` **时间加权收益率(time_weighted_return)逻辑** ```sql -- 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)** ```sql annualized_return = (1 + time_weighted_return) ^ (365 / 投资天数) - 1 ``` **当年收益率(year_to_date_return)** ```sql -- 获取年初快照,计算净值 year_to_date_return = (current_net_value - year_start_net_value) / year_start_net_value ``` #### 数据结构示例 **positions_data(持仓明细快照)** ```json [ { "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(现金账户明细快照)** ```json [ { "account_id": 1, "currency": "CNY", "balance": 50000.00 } ] ``` #### 每日快照主要流程(伪代码) ```sql 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_data(JSONB) 9. 插入或更新快照 INSERT INTO asset_snapshots (...) ON CONFLICT (user_id, snapshot_date) DO UPDATE SET ... ``` #### 查询与统计示例 **查询用户净值曲线** ```sql 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; -- 最近一年 ``` **查询收益率统计** ```sql 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; ``` **查询持仓明细历史** ```sql 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 | 更新时间 | **创建语句** ```sql 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个买点)** ```sql -- 为持仓创建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个卖点)** ```sql -- 为持仓创建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股 ``` **查询持仓及其价格计划** ```sql -- 查询持仓及其所有价格计划(包含券商信息) 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; ``` **查询可用券商列表** ```sql -- 查询所有启用的券商(用于下拉选择) 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; ``` **检查价格计划是否触发** ```sql -- 查询已达到计划价格但未完成的买入计划 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. 持仓更新流程 **用户修改持仓(主动变更)** ```sql -- 用户直接修改成本价和份数 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 表中记录) ``` **系统更新市场价格(被动变更)** ```sql -- 每日收盘后自动更新市场价格(仅更新启用自动更新的持仓) 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 控制券商显示顺序 2. **价格计划管理** - 默认支持3个买点和3个卖点 - 可以通过 step_order 扩展更多计划 - 完成后的计划可以保留作为历史记录 3. **现金资产处理** - 现金的 cost_price 固定为1 - shares 字段存储现金余额 - current_price 也为1(现金无价格波动) 4. **多货币支持** - 通过 exchange_rate 字段处理汇率 - 计算总资产时需要统一货币单位 5. **价格计划提醒** - 需要定时任务检查价格计划 - 当 current_price 达到 plan_price 时发送提醒 - 买入:current_price <= plan_price - 卖出:current_price >= plan_price 6. **自动价格更新功能** - `auto_price_update` 字段控制是否启用自动价格更新 - 付费用户创建持仓时,自动设置为 true - 用户订阅过期时,批量更新为 false - 未付费用户创建持仓时,默认为 false - 系统每日收盘后,仅更新 `auto_price_update = true` 的持仓价格