feat: 数据库设计

This commit is contained in:
R524809
2025-11-11 17:25:25 +08:00
parent e71b5e5bc0
commit 75ca7f10be
3 changed files with 1373 additions and 2 deletions

View File

@@ -0,0 +1,885 @@
# 数据库设计文档
## 表结构总览
数据库使用 PostgreSQL
```
用户相关
├── users (用户表)
持仓相关
├── brokers (券商表)
├── positions (持仓表)
└── 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 '最后登录时间';
```
### daily_snapshots 表设计
**表结构**
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| id | BIGSERIAL | PRIMARY KEY | 快照ID自增 |
| user_id | BIGINT | NOT NULL, FOREIGN KEY | 用户ID关联 users 表 |
| date | DATE | NOT NULL | 快照日期 |
| total_asset | DECIMAL(18, 2) | NOT NULL | 总资产(所有持仓市值 + 现金余额) |
| total_invested | DECIMAL(18, 2) | NOT NULL | 累计投入金额(所有投入的资金总和) |
| time_weighted_return | DECIMAL(10, 6) | NOT NULL | 时间加权收益率(累计收益率) |
| 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 | 创建时间 |
| UNIQUE(user_id, date) | | | 同一用户同一日期只能有一条快照 |
**创建语句**
```sql
CREATE TABLE daily_snapshots (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
date DATE NOT NULL,
total_asset DECIMAL(18, 2) NOT NULL,
total_invested DECIMAL(18, 2) NOT NULL,
time_weighted_return DECIMAL(10, 6) NOT NULL DEFAULT 0,
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,
UNIQUE(user_id, date)
);
-- 创建索引
CREATE INDEX idx_daily_snapshots_user_id ON daily_snapshots(user_id);
CREATE INDEX idx_daily_snapshots_date ON daily_snapshots(date);
CREATE INDEX idx_daily_snapshots_user_date ON daily_snapshots(user_id, date DESC);
-- 添加注释
COMMENT ON TABLE daily_snapshots IS '每日资产快照表,用于基金净值法计算收益率';
COMMENT ON COLUMN daily_snapshots.total_asset IS '总资产:所有持仓市值 + 现金余额';
COMMENT ON COLUMN daily_snapshots.total_invested IS '累计投入金额:所有投入的资金总和';
COMMENT ON COLUMN daily_snapshots.time_weighted_return IS '时间加权收益率(累计收益率)';
COMMENT ON COLUMN daily_snapshots.positions_data IS '持仓明细快照JSON格式存储';
COMMENT ON COLUMN daily_snapshots.cash_data IS '现金账户明细快照JSON格式存储';
```
#### 基础字段计算
**total_asset总资产**
```
total_asset = Σ(持仓市值) + 现金余额
其中:
- 持仓市值 = 持仓份额 × 当前价格
- 现金余额 = 所有现金账户余额之和
```
**total_invested累计投入金额**
```
total_invested = 初始投入 + 后续投入 - 提取金额
说明:
- 初始投入:账户创建时的初始资金
- 后续投入:用户手动记录的资金投入
- 提取金额:用户提取的资金(提现等)
- 注意:买入股票的资金不算"投入",因为只是资产形式转换
```
#### 净值计算(不存储,查询时计算)
**net_value单位净值**
```sql
-- 查询时计算
net_value = total_asset / NULLIF(total_invested, 0)
-- 示例 SQL
SELECT
date,
total_asset,
total_invested,
total_asset / NULLIF(total_invested, 0) as net_value
FROM daily_snapshots
WHERE user_id = :user_id
ORDER BY date;
```
**total_profit总收益**
```sql
-- 查询时计算
total_profit = total_asset - total_invested
-- 示例 SQL
SELECT
date,
total_asset,
total_invested,
total_asset - total_invested as total_profit
FROM daily_snapshots
WHERE user_id = :user_id
ORDER BY date;
```
#### 收益率计算
**time_weighted_return时间加权收益率**
基于基金净值法,通过相邻日期的净值变化计算:
```sql
-- 计算逻辑
-- 1. 计算每日净值
net_value_today = total_asset_today / total_invested_today
net_value_yesterday = total_asset_yesterday / total_invested_yesterday
-- 2. 计算日收益率
daily_return = (net_value_today - net_value_yesterday) / net_value_yesterday
-- 3. 累计收益率(复利)
time_weighted_return = (1 + r1) × (1 + r2) × ... × (1 + rn) - 1
-- 实际存储时,存储累计收益率
```
**annualized_return年化收益率**
```sql
-- 计算逻辑
annualized_return = (1 + time_weighted_return)^(365 / 投资天数) - 1
-- 投资天数 = 当前日期 - 首次投入日期
```
**year_to_date_return当年收益率**
```sql
-- 计算逻辑
-- 1. 获取年初快照
year_start_snapshot = SELECT * FROM daily_snapshots
WHERE user_id = :user_id
AND date = DATE_TRUNC('year', CURRENT_DATE)
-- 2. 计算当年收益率
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
},
{
"position_id": 2,
"symbol": "00700",
"name": "腾讯控股",
"shares": 200,
"cost_price": 320.00,
"current_price": 300.00,
"market_value": 60000.00,
"profit": -4000.00
}
]
```
**cash_data现金账户明细快照**
```json
[
{
"account_id": 1,
"currency": "CNY",
"balance": 50000.00
},
{
"account_id": 2,
"currency": "USD",
"balance": 1000.00,
"exchange_rate": 7.2,
"balance_cny": 7200.00
}
]
```
#### 每日快照生成流程
```sql
-- 伪代码流程
1. 获取用户所有持仓
SELECT * FROM positions WHERE user_id = :user_id AND status = 'active'
2. 获取用户所有现金账户
SELECT * FROM cash_accounts WHERE account_id IN (用户账户列表)
3. 计算总资产
total_asset = Σ(持仓市值) + Σ(现金余额)
4. 计算累计投入(从资金变动记录表获取)
total_invested = SELECT SUM(amount) FROM cash_flows
WHERE user_id = :user_id AND flow_type = 'deposit'
- SELECT SUM(amount) FROM cash_flows
WHERE user_id = :user_id AND flow_type = 'withdraw'
5. 计算时间加权收益率
- 获取昨日快照
- 计算净值变化
- 更新累计收益率
6. 计算年化收益率和当年收益率
7. 生成持仓和现金明细快照(JSON格式
8. 插入或更新快照记录
INSERT INTO daily_snapshots (...)
ON CONFLICT (user_id, date) DO UPDATE SET ...
```
#### 时间加权收益率更新逻辑
```sql
-- 获取昨日快照
WITH yesterday_snapshot AS (
SELECT * FROM daily_snapshots
WHERE user_id = :user_id
AND date = CURRENT_DATE - INTERVAL '1 day'
),
today_data AS (
SELECT
:total_asset as total_asset,
:total_invested as total_invested
)
-- 计算今日净值
SELECT
(today_data.total_asset / NULLIF(today_data.total_invested, 0)) as today_net_value,
(yesterday_snapshot.total_asset / NULLIF(yesterday_snapshot.total_invested, 0)) as yesterday_net_value
FROM today_data, yesterday_snapshot;
-- 计算日收益率
daily_return = (today_net_value - yesterday_net_value) / yesterday_net_value
-- 更新累计收益率
new_time_weighted_return = (1 + yesterday_snapshot.time_weighted_return) × (1 + daily_return) - 1
```
#### 查询用户净值曲线
```sql
SELECT
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 daily_snapshots
WHERE user_id = :user_id
ORDER BY date DESC
LIMIT 365; -- 最近一年
```
#### 查询收益率统计
```sql
SELECT
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 daily_snapshots
WHERE user_id = :user_id
ORDER BY date DESC
LIMIT 30; -- 最近30天
```
#### 查询持仓明细历史
```sql
SELECT
date,
positions_data
FROM daily_snapshots
WHERE user_id = :user_id
AND positions_data IS NOT NULL
ORDER BY date DESC
LIMIT 10;
```
#### 注意事项
1. **数据一致性**
- `total_invested` 必须大于 0否则净值计算会出错
- 使用 `NULLIF(total_invested, 0)` 避免除零错误
2. **性能优化**
- 每日快照在收盘后批量生成(如 18:00
- 使用索引加速查询user_id, date
- positions_data 和 cash_data 使用 JSONB 类型,支持高效查询
3. **数据完整性**
- 确保每日都有快照(即使没有交易)
- 如果某日没有快照,使用最近一次快照的数据
4. **净值计算**
- 净值不存储,查询时计算,保证数据一致性
- 总收益也不存储,查询时计算
5. **收益率计算**
- 时间加权收益率需要每日更新
- 年化收益率需要定期重新计算(因为投资天数在变化)
- 当年收益率需要每年重置计算基准
---
## 持仓相关表设计
### 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);
```
---
### 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 | 汇率(用于多货币) |
| 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,
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_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 '最新市场价,系统每日自动更新';
COMMENT ON COLUMN positions.exchange_rate IS '汇率,用于多货币资产,如港股、美股';
```
---
### 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 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