Files
invest-mind-store/packages/design-document/我编写的文档/数据库设计.md
2026-02-11 16:01:42 +08:00

1320 lines
48 KiB
Markdown
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.

# 数据库设计文档
## 表结构总览
数据库使用 PostgreSQL
```
用户相关
├── user (用户表)
基础数据相关
├── brokers (券商表)
├── stock_info (股票基本信息表)
└── stock_daily_price (股票每日收盘价表)
账户相关
├── positions (持仓表)
├── position_changes (持仓变更记录表)
├── 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 | 用户名(必选,用于账号密码登录) |
| password_hash | VARCHAR(255) | | 密码哈希值bcrypt加密仅用户名登录时需要 |
| 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 |
| role | VARCHAR(20) | NOT NULL, DEFAULT 'user', CHECK | 用户角色user(普通用户)/admin(管理员)/super_admin(超级管理员) |
**创建语句**
```sql
CREATE TABLE user (
user_id BIGSERIAL PRIMARY KEY,
open_id VARCHAR(100) UNIQUE,
union_id VARCHAR(100) UNIQUE,
username VARCHAR(100) UNIQUE,
password_hash VARCHAR(255),
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',
role VARCHAR(20) NOT NULL DEFAULT 'user',
-- 约束:至少要有一种登录方式
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_username_password CHECK (
username IS NULL OR password_hash IS NOT NULL
),
-- 约束:状态值限制
CONSTRAINT check_status CHECK (status IN ('active', 'inactive', 'deleted')),
-- 约束:角色值限制
CONSTRAINT check_role CHECK (role IN ('user', 'admin', 'super_admin'))
);
-- 创建索引
CREATE INDEX idx_users_open_id ON user(open_id);
CREATE INDEX idx_users_union_id ON user(union_id);
CREATE INDEX idx_users_username ON user(username);
CREATE INDEX idx_users_email ON user(email);
CREATE INDEX idx_users_phone ON user(phone);
CREATE INDEX idx_users_status ON user(status);
CREATE INDEX idx_users_last_login_at ON user(last_login_at);
CREATE INDEX idx_users_role ON user(role);
-- 创建自动更新 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 user
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- 添加注释
COMMENT ON TABLE user IS '用户表,支持微信登录和账号密码登录';
COMMENT ON COLUMN user.user_id IS '用户ID主键自增';
COMMENT ON COLUMN user.open_id IS '微信 openId用于小程序/公众号登录,唯一';
COMMENT ON COLUMN user.union_id IS '微信 unionId用于跨应用统一标识唯一';
COMMENT ON COLUMN user.username IS '用户名,用于账号密码登录,唯一,可选';
COMMENT ON COLUMN user.password_hash IS '密码哈希值,使用 bcrypt 算法加密存储,仅当使用用户名登录时必填';
COMMENT ON COLUMN user.email IS '邮箱,用于邮箱登录,唯一,可选';
COMMENT ON COLUMN user.phone IS '手机号,用于手机号登录,唯一,可选';
COMMENT ON COLUMN user.nickname IS '用户昵称,显示名称';
COMMENT ON COLUMN user.avatar_url IS '头像URL';
COMMENT ON COLUMN user.status IS '用户状态active(活跃)/inactive(非活跃)/deleted(已删除)';
COMMENT ON COLUMN user.role IS '用户角色user(普通用户)/admin(管理员)/super_admin(超级管理员)';
COMMENT ON COLUMN user.created_at IS '创建时间,自动设置';
COMMENT ON COLUMN user.updated_at IS '更新时间,自动更新';
COMMENT ON COLUMN user.last_login_at IS '最后登录时间';
```
#### 密码存储说明
**密码安全规范:**
1. **加密算法**:使用 `bcrypt` 算法对密码进行哈希加密(推荐使用 cost factor 10-12
2. **存储字段**:密码哈希值存储在 `password_hash` 字段中,**绝不存储明文密码**
3. **字段长度**`VARCHAR(255)` 足够存储 bcrypt 哈希值(通常为 60 字符)
4. **约束规则**
- 如果用户设置了 `username`,则必须设置 `password_hash`
- 微信登录用户不需要密码(`password_hash` 可为 NULL
- 密码字段允许为 NULL支持多种登录方式
**密码验证流程:**
```sql
-- 用户登录时验证密码
-- 1. 根据用户名查询用户
SELECT user_id, password_hash, status
FROM user
WHERE username = :username AND status = 'active';
-- 2. 在应用层使用 bcrypt 验证密码
-- bcrypt.compare(用户输入的明文密码, 数据库中的 password_hash)
-- 3. 验证成功后更新最后登录时间
UPDATE user
SET last_login_at = CURRENT_TIMESTAMP
WHERE user_id = :user_id;
```
**密码设置/修改流程:**
```sql
-- 注册时设置密码(应用层先使用 bcrypt 加密)
INSERT INTO user (username, password_hash, nickname, ...)
VALUES (:username, :password_hash_bcrypt, :nickname, ...);
-- 修改密码(应用层先使用 bcrypt 加密新密码)
UPDATE user
SET password_hash = :new_password_hash_bcrypt,
updated_at = CURRENT_TIMESTAMP
WHERE user_id = :user_id;
-- 重置密码(忘记密码场景)
UPDATE user
SET password_hash = :new_password_hash_bcrypt,
updated_at = CURRENT_TIMESTAMP
WHERE username = :username OR email = :email;
```
**注意事项:**
- 密码哈希值一旦生成,无法逆向还原为明文密码
- 忘记密码时,只能通过重置密码流程(发送验证码到邮箱/手机)来设置新密码
- 建议实现密码强度验证(长度、复杂度等)在应用层完成
- 建议实现登录失败次数限制,防止暴力破解
## 基础数据相关表设计
### broker 券商表
**表结构**
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| 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 broker (
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_broker_code ON broker(broker_code);
CREATE INDEX idx_broker_name ON broker(broker_name);
CREATE INDEX idx_broker_region ON broker(region);
CREATE INDEX idx_broker_active ON broker(is_active);
CREATE INDEX idx_broker_sort ON broker(sort_order);
CREATE INDEX idx_broker_region_active ON broker(region, is_active);
-- 创建触发器自动更新 updated_at
CREATE TRIGGER update_broker_updated_at
BEFORE UPDATE ON broker
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- 添加注释
COMMENT ON TABLE broker IS '券商表,记录所有可用的券商信息';
COMMENT ON COLUMN broker.broker_code IS '券商代码用于系统识别HTZQ华泰证券';
COMMENT ON COLUMN broker.broker_name IS '券商名称,显示给用户的名称,如:华泰证券';
COMMENT ON COLUMN broker.region IS '地区/国家代码CN(中国)/US(美国)/HK(香港)/SG(新加坡)/JP(日本)/UK(英国)/AU(澳大利亚)/CA(加拿大)/OTHER(其他)';
```
**示例数据**
```sql
-- 中国券商
INSERT INTO broker (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 broker (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 broker (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 '股票代码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 | 股票代码 |
| stock_name | VARCHAR(100) | 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) | | 涨跌幅(% |
| change_percent_60day | DECIMAL(10, 6) | | 60日涨跌幅% |
| change_percent_ytd | 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,
stock_name VARCHAR(100) 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),
change_percent_60day DECIMAL(10, 6),
change_percent_ytd 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.change_percent_60day IS '60日涨跌幅%';
COMMENT ON COLUMN stock_daily_price.change_percent_ytd 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, change_percent_60day, change_percent_ytd,
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,
change_percent_60day = EXCLUDED.change_percent_60day,
change_percent_ytd = EXCLUDED.change_percent_ytd,
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关联 user 表 |
| 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) | | 最新市场价(系统自动更新) |
| previous_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 user(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),
previous_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.previous_price IS '上一次的价格,用户手动更新价格时自动保存上一次价格,用于前端显示红绿色(上涨/下跌)';
COMMENT ON COLUMN positions.exchange_rate IS '汇率,用于多货币资产,如港股、美股';
COMMENT ON COLUMN positions.auto_price_update IS '是否自动更新价格true表示系统每日自动更新市场价格付费用户功能';
```
**示例数据**
```sql
-- 假设 user_id = 1broker_id 1=华泰证券(HTZQ)broker_id 4=富途证券(FUTU)broker_id 3=Interactive Brokers(IB)
-- 插入三条持仓数据
-- 1. A股股票贵州茅台上海市场
INSERT INTO positions (
user_id, broker_id, asset_type, symbol, name, market,
shares, cost_price, current_price, previous_price,
currency, exchange_rate, auto_price_update, status
) VALUES (
1, 1, 'stock', '600519', '贵州茅台', 'sh',
100.0000, 1600.0000, 1850.0000, 1800.0000,
'CNY', 1.000000, false, 'active'
);
-- 2. 港股股票:腾讯控股(香港市场)
INSERT INTO positions (
user_id, broker_id, asset_type, symbol, name, market,
shares, cost_price, current_price, previous_price,
currency, exchange_rate, auto_price_update, status
) VALUES (
1, 4, 'stock', '00700', '腾讯控股', 'hk',
200.0000, 320.0000, 350.0000, 340.0000,
'HKD', 0.920000, false, 'active'
);
-- 3. 美股股票:苹果公司(美国市场)
INSERT INTO positions (
user_id, broker_id, asset_type, symbol, name, market,
shares, cost_price, current_price, previous_price,
currency, exchange_rate, auto_price_update, status
) VALUES (
1, 3, 'stock', 'AAPL', '苹果公司', 'us',
50.0000, 150.0000, 175.0000, 170.0000,
'USD', 7.200000, false, 'active'
);
```
---
### position_changes 持仓变更记录表
记录每次持仓变更的详细信息,用于展示持仓变更历史。
**表结构**
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| change_id | BIGSERIAL | PRIMARY KEY | 变更记录ID自增 |
| position_id | BIGINT | NOT NULL, FOREIGN KEY | 持仓ID关联 positions 表 |
| change_date | DATE | NOT NULL | 变更日期 |
| change_type | VARCHAR(20) | NOT NULL, CHECK | 变更类型buy(买入)/sell(卖出)/auto(系统自动) |
| before_shares | DECIMAL(18, 4) | NOT NULL | 变更前份额/数量 |
| before_cost_price | DECIMAL(18, 4) | NOT NULL | 变更前成本价 |
| after_shares | DECIMAL(18, 4) | NOT NULL | 变更后份额/数量 |
| after_cost_price | DECIMAL(18, 4) | NOT NULL | 变更后成本价 |
| notes | TEXT | | 备注/思考 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
**创建语句**
```sql
CREATE TABLE position_changes (
change_id BIGSERIAL PRIMARY KEY,
position_id BIGINT NOT NULL REFERENCES positions(position_id) ON DELETE CASCADE,
change_date DATE NOT NULL,
change_type VARCHAR(20) NOT NULL,
before_shares DECIMAL(18, 4) NOT NULL,
before_cost_price DECIMAL(18, 4) NOT NULL,
after_shares DECIMAL(18, 4) NOT NULL,
after_cost_price DECIMAL(18, 4) NOT NULL,
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT check_change_type CHECK (change_type IN ('buy', 'sell', 'auto'))
);
-- 创建索引
CREATE INDEX idx_position_changes_position_id ON position_changes(position_id);
CREATE INDEX idx_position_changes_change_date ON position_changes(change_date);
CREATE INDEX idx_position_changes_position_date ON position_changes(position_id, change_date DESC);
-- 添加注释
COMMENT ON TABLE position_changes IS '持仓变更记录表,记录每次持仓变更的详细信息,用于展示持仓变更历史';
COMMENT ON COLUMN position_changes.position_id IS '持仓ID关联 positions 表';
COMMENT ON COLUMN position_changes.change_date IS '变更日期';
COMMENT ON COLUMN position_changes.change_type IS '变更类型buy(买入)/sell(卖出)/auto(系统自动,如分红、拆股等)';
COMMENT ON COLUMN position_changes.before_shares IS '变更前份额/数量';
COMMENT ON COLUMN position_changes.before_cost_price IS '变更前成本价';
COMMENT ON COLUMN position_changes.after_shares IS '变更后份额/数量';
COMMENT ON COLUMN position_changes.after_cost_price IS '变更后成本价';
COMMENT ON COLUMN position_changes.notes IS '备注/思考记录';
```
**使用说明:**
1. **记录买入变更**
```sql
-- 用户买入时,记录变更前后状态
INSERT INTO position_changes (
position_id, change_date, change_type,
before_shares, before_cost_price,
after_shares, after_cost_price,
notes
)
VALUES (
:position_id, :change_date, 'buy',
:before_shares, :before_cost_price,
:after_shares, :after_cost_price,
:notes
);
```
2. **记录卖出变更**
```sql
-- 用户卖出时,记录变更前后状态
INSERT INTO position_changes (
position_id, change_date, change_type,
before_shares, before_cost_price,
after_shares, after_cost_price,
notes
)
VALUES (
:position_id, :change_date, 'sell',
:before_shares, :before_cost_price,
:after_shares, :after_cost_price,
:notes
);
```
3. **记录系统自动变更**
```sql
-- 系统自动变更(分红、拆股、送股等)
INSERT INTO position_changes (
position_id, change_date, change_type,
before_shares, before_cost_price,
after_shares, after_cost_price,
notes
)
VALUES (
:position_id, :change_date, 'auto',
:before_shares, :before_cost_price,
:after_shares, :after_cost_price,
'分红/拆股/送股等系统自动变更'
);
```
4. **查询持仓变更历史**
```sql
-- 查询指定持仓的所有变更记录
SELECT
change_id,
change_date,
change_type,
before_shares,
before_cost_price,
after_shares,
after_cost_price,
notes,
created_at
FROM position_changes
WHERE position_id = :position_id
ORDER BY change_date DESC, created_at DESC;
```
---
### asset_snapshots (资产快照表)
**表结构(新版)**
| 字段名 | 数据类型 | 约束 | 说明 |
|---------------------|----------------|----------------------------------|---------------------------------------------------------|
| id | BIGSERIAL | PRIMARY KEY | 快照ID自增 |
| user_id | BIGINT | NOT NULL, FOREIGN KEY | 用户ID关联 user 表 |
| 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 user(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关联 user 表';
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_dataJSONB
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;
-- 用户手动更新价格时,将当前价格保存到 previous_price
UPDATE positions
SET
previous_price = current_price, -- 先将当前价格保存为上一次价格
current_price = :new_price, -- 更新为新价格
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` 的持仓价格