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

514 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

订阅相关表设计
---
## 表结构总览
数据库使用 PostgreSQL
```
订阅相关
├── 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. **数据安全**
- 支付相关数据需要加密存储
- 交易号等敏感信息需要脱敏处理
7. **持仓自动价格更新功能联动**
- 用户购买订阅后,需要批量更新该用户所有持仓的 `auto_price_update = true`
- 用户订阅过期或取消后,需要批量更新该用户所有持仓的 `auto_price_update = false`
- 建议在订阅状态变更时,同步更新 positions 表的 auto_price_update 字段
**示例SQL**
```sql
-- 用户购买订阅后,启用自动价格更新
UPDATE positions
SET auto_price_update = true
WHERE user_id = :user_id
AND asset_type IN ('stock', 'fund', 'bond');
-- 用户订阅过期后,禁用自动价格更新
UPDATE positions
SET auto_price_update = false
WHERE user_id = :user_id;
-- 查询付费用户的股票持仓(用于自动价格更新)
SELECT
p.position_id,
p.symbol,
p.market,
p.asset_type,
p.current_price
FROM positions p
WHERE p.auto_price_update = true
AND p.asset_type = 'stock'
AND p.status = 'active';
```