feat: 数据库设计
This commit is contained in:
482
我编写的文档/付费订阅数据库设计.md
Normal file
482
我编写的文档/付费订阅数据库设计.md
Normal 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. **数据安全**
|
||||||
|
- 支付相关数据需要加密存储
|
||||||
|
- 交易号等敏感信息需要脱敏处理
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# 投资记录模块-产品设计
|
# 投资记录模块-产品设计
|
||||||
## 核心设计思路
|
## 一、核心设计思路
|
||||||
|
### 1.1 持仓变更设计
|
||||||
1. 把所有的表更分为两种:`用户驱动(主动)` 和 `系统驱动(被动)`
|
1. 把所有的表更分为两种:`用户驱动(主动)` 和 `系统驱动(被动)`
|
||||||
2. 系统驱动包含:现金分红、送股、拆股、汇率变动等
|
2. 系统驱动包含:现金分红、送股、拆股、汇率变动等
|
||||||
3. 用户驱动包含:初始买入、追加买入、卖出;
|
3. 用户驱动包含:初始买入、追加买入、卖出;
|
||||||
@@ -12,7 +13,10 @@
|
|||||||
- 分红:收盘后获取每股分红金额,最新成本价=原成本价 - 分红,市场价逻辑保持不变(使用不复权的股价)
|
- 分红:收盘后获取每股分红金额,最新成本价=原成本价 - 分红,市场价逻辑保持不变(使用不复权的股价)
|
||||||
- 送股、拆股等都变更最新的成本价和份额,并记录。
|
- 送股、拆股等都变更最新的成本价和份额,并记录。
|
||||||
|
|
||||||
|
### 1.2 收益记录设计
|
||||||
|
1. 使用基金净值法(时间加权收益率)来统计收益;
|
||||||
|
2. 每次主动和被动变更,重新计算总体的资产金额和份额,记录到 daily_snapshots 表。
|
||||||
|
3. 忘记记录的情况:如果用户忘记记录当日交易,过一段时间后再来记录,需要删除期间的快照数据,并保留期间的交易数据。然后删除期间的快照数据,重新生成。
|
||||||
|
|
||||||
## 想法
|
## 想法
|
||||||
- 私密分享:可以将自己的交易计划和复盘,通过小程序私密分享-分享给其他人,这样即保障了裂变属性,有增加了隐私安全。
|
- 私密分享:可以将自己的交易计划和复盘,通过小程序私密分享-分享给其他人,这样即保障了裂变属性,有增加了隐私安全。
|
||||||
885
我编写的文档/数据库设计.md
Normal file
885
我编写的文档/数据库设计.md
Normal 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
|
||||||
|
|
||||||
Reference in New Issue
Block a user