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,482 @@
订阅相关表设计
---
## 表结构总览
数据库使用 PostgreSQL
```
订阅相关
├── subscription_plans (订阅计划表)
├── subscriptions (用户订阅表)
├── subscription_history (订阅历史记录表)
├── payments (支付记录表)
└── subscription_features (订阅功能权限表)
```
## 订阅相关表设计
### subscription_plans 订阅计划表
**表结构**
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| plan_id | BIGSERIAL | PRIMARY KEY | 计划ID自增 |
| plan_code | VARCHAR(50) | NOT NULL, UNIQUE | 计划代码annual_19 |
| plan_name | VARCHAR(100) | NOT NULL | 计划名称(如:年付会员) |
| plan_type | VARCHAR(20) | NOT NULL, CHECK | 计划类型monthly/yearly/lifetime |
| price | DECIMAL(10, 2) | NOT NULL | 价格(元) |
| duration_days | INTEGER | NOT NULL | 订阅时长(天) |
| features | JSONB | | 包含的功能列表JSON格式 |
| is_active | BOOLEAN | NOT NULL, DEFAULT true | 是否启用 |
| sort_order | INTEGER | DEFAULT 0 | 排序顺序 |
| description | TEXT | | 计划描述 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
**创建语句**
```sql
CREATE TABLE subscription_plans (
plan_id BIGSERIAL PRIMARY KEY,
plan_code VARCHAR(50) NOT NULL UNIQUE,
plan_name VARCHAR(100) NOT NULL,
plan_type VARCHAR(20) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
duration_days INTEGER NOT NULL,
features JSONB,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INTEGER DEFAULT 0,
description TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT check_plan_type CHECK (plan_type IN ('monthly', 'yearly', 'lifetime')),
CONSTRAINT check_price_positive CHECK (price >= 0),
CONSTRAINT check_duration_positive CHECK (duration_days > 0)
);
-- 创建索引
CREATE INDEX idx_subscription_plans_code ON subscription_plans(plan_code);
CREATE INDEX idx_subscription_plans_type ON subscription_plans(plan_type);
CREATE INDEX idx_subscription_plans_active ON subscription_plans(is_active);
-- 添加注释
COMMENT ON TABLE subscription_plans IS '订阅计划表,定义可购买的订阅套餐';
COMMENT ON COLUMN subscription_plans.plan_code IS '计划代码用于系统识别annual_19';
COMMENT ON COLUMN subscription_plans.features IS '包含的功能列表JSON格式["advanced_analytics", "export_data"]';
```
**示例数据**
```sql
-- 年付19元计划
INSERT INTO subscription_plans (plan_code, plan_name, plan_type, price, duration_days, features, description)
VALUES (
'annual_19',
'年付会员',
'yearly',
19.00,
365,
'["advanced_analytics", "export_data", "unlimited_accounts", "priority_support"]'::jsonb,
'年付19元解锁高级功能'
);
```
---
### subscriptions 用户订阅表
**表结构**
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| subscription_id | BIGSERIAL | PRIMARY KEY | 订阅ID自增 |
| user_id | BIGINT | NOT NULL, FOREIGN KEY | 用户ID关联 users 表 |
| plan_id | BIGINT | NOT NULL, FOREIGN KEY | 订阅计划ID |
| status | VARCHAR(20) | NOT NULL, CHECK | 订阅状态active/expired/cancelled |
| start_date | DATE | NOT NULL | 订阅开始日期 |
| end_date | DATE | NOT NULL | 订阅结束日期 |
| auto_renew | BOOLEAN | NOT NULL, DEFAULT false | 是否自动续费 |
| cancelled_at | TIMESTAMP | | 取消时间 |
| cancelled_reason | VARCHAR(255) | | 取消原因 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
**创建语句**
```sql
CREATE TABLE subscriptions (
subscription_id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
plan_id BIGINT NOT NULL REFERENCES subscription_plans(plan_id),
status VARCHAR(20) NOT NULL DEFAULT 'active',
start_date DATE NOT NULL,
end_date DATE NOT NULL,
auto_renew BOOLEAN NOT NULL DEFAULT false,
cancelled_at TIMESTAMP,
cancelled_reason VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT check_subscription_status CHECK (status IN ('active', 'expired', 'cancelled', 'pending')),
CONSTRAINT check_date_order CHECK (end_date >= start_date)
);
-- 创建索引
CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id);
CREATE INDEX idx_subscriptions_plan_id ON subscriptions(plan_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
CREATE INDEX idx_subscriptions_end_date ON subscriptions(end_date);
CREATE INDEX idx_subscriptions_user_status ON subscriptions(user_id, status);
-- 创建触发器自动更新 updated_at
CREATE TRIGGER update_subscriptions_updated_at
BEFORE UPDATE ON subscriptions
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- 添加注释
COMMENT ON TABLE subscriptions IS '用户订阅表,记录用户当前的订阅状态';
COMMENT ON COLUMN subscriptions.status IS '订阅状态active(活跃)/expired(已过期)/cancelled(已取消)/pending(待支付)';
COMMENT ON COLUMN subscriptions.auto_renew IS '是否自动续费true表示到期自动续费';
```
---
### subscription_history 订阅历史记录表
**表结构**
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| history_id | BIGSERIAL | PRIMARY KEY | 历史记录ID |
| subscription_id | BIGINT | NOT NULL, FOREIGN KEY | 订阅ID |
| user_id | BIGINT | NOT NULL, FOREIGN KEY | 用户ID |
| plan_id | BIGINT | NOT NULL, FOREIGN KEY | 订阅计划ID |
| action | VARCHAR(20) | NOT NULL, CHECK | 操作类型purchase/renew/cancel/expire |
| old_status | VARCHAR(20) | | 原状态 |
| new_status | VARCHAR(20) | | 新状态 |
| old_end_date | DATE | | 原结束日期 |
| new_end_date | DATE | | 新结束日期 |
| amount | DECIMAL(10, 2) | | 金额(如果是购买/续费) |
| payment_id | BIGINT | FOREIGN KEY | 关联的支付记录ID |
| notes | TEXT | | 备注 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
**创建语句**
```sql
CREATE TABLE subscription_history (
history_id BIGSERIAL PRIMARY KEY,
subscription_id BIGINT NOT NULL REFERENCES subscriptions(subscription_id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
plan_id BIGINT NOT NULL REFERENCES subscription_plans(plan_id),
action VARCHAR(20) NOT NULL,
old_status VARCHAR(20),
new_status VARCHAR(20),
old_end_date DATE,
new_end_date DATE,
amount DECIMAL(10, 2),
payment_id BIGINT REFERENCES payments(payment_id),
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT check_action_type CHECK (action IN ('purchase', 'renew', 'cancel', 'expire', 'upgrade', 'downgrade'))
);
-- 创建索引
CREATE INDEX idx_subscription_history_user_id ON subscription_history(user_id);
CREATE INDEX idx_subscription_history_subscription_id ON subscription_history(subscription_id);
CREATE INDEX idx_subscription_history_action ON subscription_history(action);
CREATE INDEX idx_subscription_history_created_at ON subscription_history(created_at);
-- 添加注释
COMMENT ON TABLE subscription_history IS '订阅历史记录表,记录所有订阅状态变更';
COMMENT ON COLUMN subscription_history.action IS '操作类型purchase(购买)/renew(续费)/cancel(取消)/expire(过期)/upgrade(升级)/downgrade(降级)';
```
---
### payments 支付记录表
**表结构**
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| payment_id | BIGSERIAL | PRIMARY KEY | 支付ID自增 |
| user_id | BIGINT | NOT NULL, FOREIGN KEY | 用户ID |
| subscription_id | BIGINT | FOREIGN KEY | 关联的订阅ID |
| plan_id | BIGINT | NOT NULL, FOREIGN KEY | 订阅计划ID |
| payment_method | VARCHAR(20) | NOT NULL, CHECK | 支付方式wechat/alipay/apple/google |
| payment_channel | VARCHAR(50) | | 支付渠道微信小程序、H5等 |
| transaction_id | VARCHAR(100) | UNIQUE | 第三方交易号 |
| order_no | VARCHAR(100) | NOT NULL, UNIQUE | 内部订单号 |
| amount | DECIMAL(10, 2) | NOT NULL | 支付金额 |
| currency | VARCHAR(10) | NOT NULL, DEFAULT 'CNY' | 货币类型 |
| status | VARCHAR(20) | NOT NULL, CHECK | 支付状态pending/success/failed/refunded |
| paid_at | TIMESTAMP | | 支付完成时间 |
| refunded_at | TIMESTAMP | | 退款时间 |
| refund_amount | DECIMAL(10, 2) | | 退款金额 |
| refund_reason | VARCHAR(255) | | 退款原因 |
| metadata | JSONB | | 额外信息(如:微信支付回调数据) |
| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
**创建语句**
```sql
CREATE TABLE payments (
payment_id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
subscription_id BIGINT REFERENCES subscriptions(subscription_id),
plan_id BIGINT NOT NULL REFERENCES subscription_plans(plan_id),
payment_method VARCHAR(20) NOT NULL,
payment_channel VARCHAR(50),
transaction_id VARCHAR(100) UNIQUE,
order_no VARCHAR(100) NOT NULL UNIQUE,
amount DECIMAL(10, 2) NOT NULL,
currency VARCHAR(10) NOT NULL DEFAULT 'CNY',
status VARCHAR(20) NOT NULL DEFAULT 'pending',
paid_at TIMESTAMP,
refunded_at TIMESTAMP,
refund_amount DECIMAL(10, 2),
refund_reason VARCHAR(255),
metadata JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT check_payment_method CHECK (payment_method IN ('wechat', 'alipay', 'apple', 'google', 'other')),
CONSTRAINT check_payment_status CHECK (status IN ('pending', 'success', 'failed', 'refunded', 'cancelled')),
CONSTRAINT check_amount_positive CHECK (amount > 0)
);
-- 创建索引
CREATE INDEX idx_payments_user_id ON payments(user_id);
CREATE INDEX idx_payments_subscription_id ON payments(subscription_id);
CREATE INDEX idx_payments_order_no ON payments(order_no);
CREATE INDEX idx_payments_transaction_id ON payments(transaction_id);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_created_at ON payments(created_at);
-- 创建触发器自动更新 updated_at
CREATE TRIGGER update_payments_updated_at
BEFORE UPDATE ON payments
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- 添加注释
COMMENT ON TABLE payments IS '支付记录表,记录所有支付交易';
COMMENT ON COLUMN payments.order_no IS '内部订单号,系统生成,唯一';
COMMENT ON COLUMN payments.transaction_id IS '第三方支付平台的交易号';
COMMENT ON COLUMN payments.metadata IS '额外信息JSON格式存储支付回调数据等';
```
---
### subscription_features 订阅功能权限表
**表结构**
| 字段名 | 数据类型 | 约束 | 说明 |
|--------|---------|------|------|
| feature_id | BIGSERIAL | PRIMARY KEY | 功能ID |
| feature_code | VARCHAR(50) | NOT NULL, UNIQUE | 功能代码 |
| feature_name | VARCHAR(100) | NOT NULL | 功能名称 |
| feature_type | VARCHAR(20) | NOT NULL, CHECK | 功能类型basic/premium |
| description | TEXT | | 功能描述 |
| is_active | BOOLEAN | NOT NULL, DEFAULT true | 是否启用 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
**创建语句**
```sql
CREATE TABLE subscription_features (
feature_id BIGSERIAL PRIMARY KEY,
feature_code VARCHAR(50) NOT NULL UNIQUE,
feature_name VARCHAR(100) NOT NULL,
feature_type VARCHAR(20) NOT NULL,
description TEXT,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT check_feature_type CHECK (feature_type IN ('basic', 'premium'))
);
-- 创建索引
CREATE INDEX idx_subscription_features_code ON subscription_features(feature_code);
CREATE INDEX idx_subscription_features_type ON subscription_features(feature_type);
-- 添加注释
COMMENT ON TABLE subscription_features IS '订阅功能权限表,定义所有可用的功能';
COMMENT ON COLUMN subscription_features.feature_code IS '功能代码用于系统识别advanced_analytics';
```
**示例数据**
```sql
-- 基础功能(免费)
INSERT INTO subscription_features (feature_code, feature_name, feature_type, description)
VALUES
('basic_records', '基础记录', 'basic', '记录持仓和交易'),
('basic_stats', '基础统计', 'basic', '查看基础收益统计');
-- 高级功能(付费)
INSERT INTO subscription_features (feature_code, feature_name, feature_type, description)
VALUES
('advanced_analytics', '高级分析', 'premium', '深度收益分析和图表'),
('export_data', '数据导出', 'premium', '导出Excel/CSV数据'),
('unlimited_accounts', '无限账户', 'premium', '支持多个券商账户'),
('priority_support', '优先支持', 'premium', '优先客服支持');
```
---
## 订阅系统业务流程
### 1. 用户购买订阅流程
```sql
-- 步骤1创建支付记录
INSERT INTO payments (user_id, plan_id, payment_method, order_no, amount, status)
VALUES (:user_id, :plan_id, 'wechat', :order_no, 19.00, 'pending');
-- 步骤2支付成功后创建订阅
INSERT INTO subscriptions (user_id, plan_id, status, start_date, end_date)
VALUES (
:user_id,
:plan_id,
'active',
CURRENT_DATE,
CURRENT_DATE + INTERVAL '365 days'
);
-- 步骤3更新支付记录
UPDATE payments
SET status = 'success', paid_at = CURRENT_TIMESTAMP, subscription_id = :subscription_id
WHERE payment_id = :payment_id;
-- 步骤4记录订阅历史
INSERT INTO subscription_history (
subscription_id, user_id, plan_id, action,
new_status, new_end_date, amount, payment_id
)
VALUES (
:subscription_id, :user_id, :plan_id, 'purchase',
'active', CURRENT_DATE + INTERVAL '365 days', 19.00, :payment_id
);
```
### 2. 检查用户功能权限
```sql
-- 查询用户是否有某个功能权限
SELECT
CASE
WHEN s.status = 'active' AND s.end_date >= CURRENT_DATE THEN true
ELSE false
END as has_access,
sp.features
FROM subscriptions s
INNER JOIN subscription_plans sp ON s.plan_id = sp.plan_id
WHERE s.user_id = :user_id
AND s.status = 'active'
AND s.end_date >= CURRENT_DATE
ORDER BY s.end_date DESC
LIMIT 1;
-- 或者检查特定功能
SELECT
CASE
WHEN s.status = 'active'
AND s.end_date >= CURRENT_DATE
AND sp.features @> '["advanced_analytics"]'::jsonb
THEN true
ELSE false
END as has_advanced_analytics
FROM subscriptions s
INNER JOIN subscription_plans sp ON s.plan_id = sp.plan_id
WHERE s.user_id = :user_id
ORDER BY s.end_date DESC
LIMIT 1;
```
### 3. 订阅过期处理
```sql
-- 定时任务:检查并更新过期订阅
UPDATE subscriptions
SET status = 'expired'
WHERE status = 'active'
AND end_date < CURRENT_DATE;
-- 记录过期历史
INSERT INTO subscription_history (
subscription_id, user_id, plan_id, action,
old_status, new_status, old_end_date
)
SELECT
subscription_id, user_id, plan_id, 'expire',
'active', 'expired', end_date
FROM subscriptions
WHERE status = 'expired'
AND end_date < CURRENT_DATE
AND NOT EXISTS (
SELECT 1 FROM subscription_history
WHERE subscription_id = subscriptions.subscription_id
AND action = 'expire'
AND created_at::date = CURRENT_DATE
);
```
### 4. 自动续费处理
```sql
-- 查询即将到期且开启自动续费的订阅
SELECT s.*, sp.price, sp.plan_code
FROM subscriptions s
INNER JOIN subscription_plans sp ON s.plan_id = sp.plan_id
WHERE s.status = 'active'
AND s.auto_renew = true
AND s.end_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '3 days';
-- 执行自动续费(创建新的支付记录和延长订阅)
BEGIN;
-- 创建支付记录
INSERT INTO payments (user_id, plan_id, payment_method, order_no, amount, status)
VALUES (:user_id, :plan_id, 'auto_renew', :order_no, :price, 'pending');
-- 延长订阅
UPDATE subscriptions
SET end_date = end_date + INTERVAL '365 days',
updated_at = CURRENT_TIMESTAMP
WHERE subscription_id = :subscription_id;
-- 记录历史
INSERT INTO subscription_history (...)
VALUES (...);
COMMIT;
```
---
## 注意事项
1. **数据一致性**
- 订阅状态变更时,必须同时更新 subscriptions 和 subscription_history
- 支付成功后才创建订阅,避免未支付就开通
2. **功能权限检查**
- 每次使用功能前都要检查订阅状态和功能权限
- 可以缓存用户权限,但需要设置合理的过期时间
3. **订阅过期处理**
- 建议使用定时任务每日检查过期订阅
- 过期后立即更新状态,避免用户继续使用付费功能
4. **自动续费**
- 需要在订阅到期前3-7天提醒用户
- 自动续费失败时,需要通知用户并更新订阅状态
5. **退款处理**
- 退款时需要更新支付状态和订阅状态
- 按比例计算退款金额(已使用天数)
6. **数据安全**
- 支付相关数据需要加密存储
- 交易号等敏感信息需要脱敏处理

View File

@@ -1,5 +1,6 @@
# 投资记录模块-产品设计 # 投资记录模块-产品设计
## 核心设计思路 ## 一、核心设计思路
### 1.1 持仓变更设计
1. 把所有的表更分为两种:`用户驱动(主动)``系统驱动(被动)` 1. 把所有的表更分为两种:`用户驱动(主动)``系统驱动(被动)`
2. 系统驱动包含:现金分红、送股、拆股、汇率变动等 2. 系统驱动包含:现金分红、送股、拆股、汇率变动等
3. 用户驱动包含:初始买入、追加买入、卖出; 3. 用户驱动包含:初始买入、追加买入、卖出;
@@ -12,7 +13,10 @@
- 分红:收盘后获取每股分红金额,最新成本价=原成本价 - 分红,市场价逻辑保持不变(使用不复权的股价) - 分红:收盘后获取每股分红金额,最新成本价=原成本价 - 分红,市场价逻辑保持不变(使用不复权的股价)
- 送股、拆股等都变更最新的成本价和份额,并记录。 - 送股、拆股等都变更最新的成本价和份额,并记录。
### 1.2 收益记录设计
1. 使用基金净值法(时间加权收益率)来统计收益;
2. 每次主动和被动变更,重新计算总体的资产金额和份额,记录到 daily_snapshots 表。
3. 忘记记录的情况:如果用户忘记记录当日交易,过一段时间后再来记录,需要删除期间的快照数据,并保留期间的交易数据。然后删除期间的快照数据,重新生成。
## 想法 ## 想法
- 私密分享:可以将自己的交易计划和复盘,通过小程序私密分享-分享给其他人,这样即保障了裂变属性,有增加了隐私安全。 - 私密分享:可以将自己的交易计划和复盘,通过小程序私密分享-分享给其他人,这样即保障了裂变属性,有增加了隐私安全。

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