feat: 添加设计文件packages

This commit is contained in:
R524809
2026-02-11 16:01:42 +08:00
parent 161781cbbd
commit 571465cfbb
26 changed files with 15808 additions and 5 deletions

View File

@@ -1,5 +1,5 @@
# 开发环境配置
PORT=3200
PORT=3201
DB_HOST=localhost
DB_PORT=5432
@@ -21,4 +21,4 @@ ADMIN_NICKNAME=思考的Joey
ADMIN_ROLE=super_admin
STORAGE_PATH=./uploads # 存储路径(默认:./uploads
STORAGE_BASE_URL=http://localhost:3200/uploads # 访问URL默认http://localhost:3200/uploads
STORAGE_BASE_URL=http://localhost:3201/uploads # 访问URL默认http://localhost:3201/uploads

View File

@@ -2,10 +2,10 @@
# 运行 pnpm dev 时会加载此文件
# 开发服务器端口
VITE_PORT=3201
VITE_PORT=3200
# API 基础地址(开发环境)
VITE_API_BASE_URL=http://localhost:3200/api
VITE_API_BASE_URL=http://localhost:3201/api
# 应用名称
VITE_APP_NAME=投小记

View File

@@ -15,7 +15,7 @@ const StockDailyPricePage = lazy(() => import('@/pages/stock-daily-price'));
export const router = createBrowserRouter([
{
path: '/',
path: '/login',
element: <LoginPage />,
errorElement: <ErrorPage />,
},

View File

@@ -0,0 +1,513 @@
订阅相关表设计
---
## 表结构总览
数据库使用 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';
```

View File

@@ -0,0 +1,93 @@
# 投资记录模块-产品设计
## 一、投资记录设计思路
职责清晰:系统负责所有可规则化、基于市场公开事件的处理(被动变更);用户只负责输入自己主动发起的行为结果(主动变更)。
输入极简:对于任何一笔交易,用户最核心、最确定的输入就是最终的持仓成本和最新的持仓份额。让用户只修改这两项,避免了理解复杂规则(如加权平均计算)的负担,也减少了输入错误的可能性。
输入极简:对于任何一笔交易,用户最核心、最确定的输入就是最终的持仓成本和最新的持仓份额。让用户只修改这两项,避免了理解复杂规则(如加权平均计算)的负担,也减少了输入错误的可能性。
### 1.1 持仓变更设计
1. 把所有的表更分为两种:`用户驱动(主动)``系统驱动(被动)`
2. 系统驱动包含:现金分红、送股、拆股、汇率变动等
3. 用户驱动包含:初始买入、追加买入、卖出;
4. 所有的用户驱动,都只需要变更最终的持仓成本和最新的持仓份额,这两项。避免其他复杂的操作
5. 每次用户主动变更,记录成本价和份额的同时,还需要完成如下记录:
- 反向计算本次交易股价和份额,并记录;
- 统计最新的份额和净值,并记录
- 同时引导填下投资复盘和思考。
6. 系统驱动的变更:(万一无法实现,可以降级为用户驱动变更)
- 分红:收盘后获取每股分红金额,最新成本价 = 原成本价 - 分红,市场价逻辑保持不变(使用不复权的股价)
- 送股、拆股等都变更最新的成本价和份额,并记录。
### 1.2 收益记录设计
1. 使用基金净值法(时间加权收益率)来统计收益;
2. 每次主动和被动变更,重新计算总体的资产金额和份额,记录到 daily_snapshots 表。
3. 忘记记录的情况:如果用户忘记记录当日交易,过一段时间后再来记录,需要删除期间的快照数据,并保留期间的交易数据。然后删除期间的快照数据,重新生成。
### 1.3 系统驱动变价记录逻辑
1. 首先需要有一个表记录所有股票的基本信息方便用户在输入code或股票名是进行模糊匹配。这个数据因为不常变更可以放在CDN上。
2. 每日定时把所有用户需要自动变价的持仓(股票/基金)汇总,在收盘后查询这些股票的最新市价、市值、市盈率等信息(所有用的是持仓最新价格应该依赖这个表,减少直接查询外部证券接口的次数)
3. 更新完股票的最新市价后,在定时更新用户持仓表中的持仓数据、收益数据,对应 positions 和 asset_snapshots 表。
## 二、持仓页面详细设计
### 页面概述
页面整体展示三部分内容:
- 资产概览、
- 资产和收益图表(累计资产折线图、收益率折线图)、
- 持仓百分比图
- 我的持仓列表
### 功能需求
#### 资产概览
以卡片形式展示资产概览,左上角显示用户昵称,下边显示记账时长(例如 记账:300天右侧从上到下分别展示总金额大字红色展示、上一个交易日收益、累计收益、累计收益率、年化收益率。
#### 收益图表
- 第一个图表默认展示从记账以来每日总金额的折线图可以切换时间近5日、本月、近一个月、近一年、记账以来、自定义
- 第二个图表默认展示从记账以来每日累计收益率可以切换不同时间段近5日、本月、近一个月、近一年、记账以来、自定义
PS 你在输出设计稿的时候,需要使用 Echarts 来模拟)
#### 持仓百分比
使用环图,展示各个持仓资产项的金额百分比
PS 你在输出设计稿的时候,需要使用 Echarts 来模拟)
#### 我的持仓列表
**展示形式**
title 左侧展示 “我的持仓”,右侧展示一个 圆形的加号(添加资产项) 按钮,可以添加新的资产项。
以卡片形式展示 资产项:
- 左侧依次展示:公司简称(大号字体)、股票代码、市场、持股市场、证券公司简称(这一块你要帮我看看如何排列会更好)
- 中间展示:持股数、持股时长。
- 右侧展示:从上到下依次展示昨日市价、盈亏金额、收益率。
- 右小角展示一个 “更新资产”(名称可以你再帮我想一下) 的按钮点击从底部弹出Popup页变更资产页
**操作形式**
- 点击 圆形的“添加资产项”按钮,从底部弹出 新增资产页。
- 点击 资产项卡片总的“更新资产”的按钮,从底部弹出 变更资产页
## 三、新增资产页/变更资产页
这个页面有两种形态,新增和变更,差别是新增最上面有搜索框,变更没有搜索框。都是冲底部弹出,再上滑,可以变成全屏页面。
**新增资产页**
最上面可以选择资产类型,分别是:股票、基金、现金、其他
股票:
先展示 搜索框用户可以输入code或股票名称自动联想用户选择后确认股票。
然后选择券商,下拉列表展示。
选择后之后依次展示股票名称、股票code、市场。
之后展示成本价输入框和份数输入框。(输入时要考虑便捷性)
基金:
不展示搜索框,仅展示输入框,需要用户自己输入。
之后展示成本价输入框和份数输入框。(输入时要考虑便捷性)
现金和其他:
不展示搜索框,仅展示输入框,需要用户自己输入资产名称。
直接输入金额即可。
确认按钮:点击确认,提交数据到后台,按钮居中展示。
**变更资产页**
最上面不需要在选择资产类型,下边的展示和`新增资产页` 一样。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
# 整体产品设计思路
---
## 心智设计
让每一笔投资都经过思考
### 应用名思考
- 投小记-记录、计划、复盘你的投资
- 投小记-记录、决策、复盘你的投资
投小记 - 专业投资记账助手,助您记录每一笔交易、制定投资计划、进行投资复盘,轻松管理股票、基金等资产收益。
### 页面Title设计
每个页面除了标题外,下边附带一句话
首页 - 买股票就是买公司
交易计划 - 计划你的交易,交易你的计划
复盘页 - 回顾过去是为了更好应对将来
## 页面设计原则
### 配置方案
主题色:`#8b5cf6`,紫色主题
### 设计原则
- 简洁又不失个性
- 体现 安静与思考 原则
-
## 想法
- 私密分享:可以将自己的交易计划和复盘,通过小程序私密分享-分享给其他人,这样即保障了裂变属性,有增加了隐私安全。

View File

@@ -0,0 +1,6 @@
# 计划模块-产品设计
## 二、交易计划模块
### 1. 创建计划
核心思想:计划应当和估值合并,计划依赖于估值。
1. 交易计划列表页:

View File

@@ -0,0 +1,865 @@
# NestJS 与 PostgreSQL 集成方案
## 一、兼容性说明
### ✅ PostgreSQL 完全支持 NestJS
**简短回答**PostgreSQL 与 NestJS **完全兼容**,并且是 NestJS 官方推荐和广泛使用的数据库组合。
### 1.1 官方支持
NestJS 官方文档中明确支持 PostgreSQL主要通过以下 ORM 库:
1. **TypeORM** - NestJS 官方推荐,最常用
2. **Prisma** - 现代化 ORM类型安全
3. **Sequelize** - 成熟的 ORM 库
4. **Knex.js** - SQL 查询构建器
5. **原生 pg 驱动** - 直接使用 PostgreSQL 驱动
### 1.2 为什么选择 NestJS + PostgreSQL
1. **完美兼容**NestJS 的依赖注入和模块化架构与 PostgreSQL 配合良好
2. **TypeScript 支持**:两者都原生支持 TypeScript类型安全
3. **生态成熟**:有丰富的库和工具支持
4. **性能优秀**PostgreSQL 性能强大NestJS 框架高效
5. **开发体验好**:代码生成、迁移工具完善
---
## 二、推荐方案对比
### 2.1 TypeORM最推荐
**优势:**
- ✅ NestJS 官方文档示例主要使用 TypeORM
- ✅ 装饰器语法,与 NestJS 风格一致
- ✅ 支持数据库迁移Migration
- ✅ 支持实体关系映射ORM
- ✅ 支持事务管理
- ✅ 支持查询构建器
**适用场景:**
- 需要完整的 ORM 功能
- 团队熟悉装饰器语法
- 需要数据库迁移管理
### 2.2 Prisma现代化选择
**优势:**
- ✅ 类型安全TypeScript 支持最好
- ✅ 代码生成,自动生成类型定义
- ✅ 迁移工具强大
- ✅ 性能优秀
- ✅ 开发体验极佳
**适用场景:**
- 重视类型安全
- 需要快速开发
- 团队喜欢现代化工具
### 2.3 Sequelize成熟稳定
**优势:**
- ✅ 非常成熟,生态丰富
- ✅ 文档完善
- ✅ 支持多种数据库
**劣势:**
- ⚠️ TypeScript 支持不如 Prisma
- ⚠️ 语法相对传统
---
## 三、TypeORM 集成方案(推荐)
### 3.1 安装依赖
```bash
npm install @nestjs/typeorm typeorm pg
npm install --save-dev @types/pg
```
### 3.2 项目结构
```
src/
├── app.module.ts
├── database/
│ ├── database.module.ts
│ └── database.config.ts
├── users/
│ ├── entities/
│ │ └── user.entity.ts
│ ├── users.module.ts
│ ├── users.service.ts
│ └── users.controller.ts
├── accounts/
│ ├── entities/
│ │ └── account.entity.ts
│ └── ...
└── transactions/
├── entities/
│ └── transaction.entity.ts
└── ...
```
### 3.3 配置数据库连接
#### database/database.config.ts
```typescript
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';
export const getDatabaseConfig = (
configService: ConfigService,
): TypeOrmModuleOptions => ({
type: 'postgres',
host: configService.get('DB_HOST', 'localhost'),
port: configService.get('DB_PORT', 5432),
username: configService.get('DB_USERNAME', 'postgres'),
password: configService.get('DB_PASSWORD', 'password'),
database: configService.get('DB_NAME', 'vestmind'),
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
synchronize: configService.get('NODE_ENV') !== 'production', // 生产环境设为 false
logging: configService.get('NODE_ENV') === 'development',
migrations: [__dirname + '/../migrations/**/*{.ts,.js}'],
migrationsRun: false,
ssl: configService.get('DB_SSL', false),
});
```
#### database/database.module.ts
```typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { getDatabaseConfig } from './database.config';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: getDatabaseConfig,
inject: [ConfigService],
}),
],
})
export class DatabaseModule {}
```
#### app.module.ts
```typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';
import { UsersModule } from './users/users.module';
import { AccountsModule } from './accounts/accounts.module';
import { TransactionsModule } from './transactions/transactions.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
DatabaseModule,
UsersModule,
AccountsModule,
TransactionsModule,
],
})
export class AppModule {}
```
### 3.4 实体定义示例
#### users/entities/user.entity.ts
```typescript
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
} from 'typeorm';
import { Account } from '../../accounts/entities/account.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('increment')
id: number;
@Column({ type: 'varchar', length: 50, unique: true })
username: string;
@Column({ type: 'varchar', length: 100, unique: true, nullable: true })
email: string;
@Column({ type: 'varchar', length: 20, unique: true, nullable: true })
phone: string;
@Column({ type: 'varchar', length: 255 })
passwordHash: string;
@Column({ type: 'varchar', length: 50, nullable: true })
nickname: string;
@Column({ type: 'varchar', length: 255, nullable: true })
avatarUrl: string;
@Column({ type: 'varchar', length: 20, default: 'active' })
status: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@Column({ type: 'timestamp', nullable: true })
lastLoginAt: Date;
// 关系
@OneToMany(() => Account, (account) => account.user)
accounts: Account[];
}
```
#### accounts/entities/account.entity.ts
```typescript
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Position } from '../../positions/entities/position.entity';
import { Transaction } from '../../transactions/entities/transaction.entity';
@Entity('accounts')
export class Account {
@PrimaryGeneratedColumn('increment')
id: number;
@Column({ type: 'int' })
userId: number;
@Column({ type: 'varchar', length: 100 })
name: string;
@Column({ type: 'varchar', length: 20 })
type: string; // stock, fund, cash, mixed
@Column({ type: 'varchar', length: 10, default: 'CNY' })
currency: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({ type: 'varchar', length: 20, default: 'active' })
status: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
// 关系
@ManyToOne(() => User, (user) => user.accounts)
@JoinColumn({ name: 'userId' })
user: User;
@OneToMany(() => Position, (position) => position.account)
positions: Position[];
@OneToMany(() => Transaction, (transaction) => transaction.account)
transactions: Transaction[];
}
```
#### transactions/entities/transaction.entity.ts
```typescript
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Index,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Account } from '../../accounts/entities/account.entity';
import { Position } from '../../positions/entities/position.entity';
import { TradingPlan } from '../../trading-plans/entities/trading-plan.entity';
@Entity('transactions')
@Index(['userId', 'date', 'createdAt'])
@Index(['accountId'])
@Index(['positionId'])
export class Transaction {
@PrimaryGeneratedColumn('increment')
id: number;
@Column({ type: 'int' })
userId: number;
@Column({ type: 'int' })
accountId: number;
@Column({ type: 'int', nullable: true })
positionId: number;
@Column({ type: 'int', nullable: true })
tradingPlanId: number;
@Column({ type: 'varchar', length: 20 })
type: string; // buy, sell, dividend, split, bonus, rights, deposit, withdraw
@Column({ type: 'date' })
date: Date;
@Column({ type: 'varchar', length: 20, nullable: true })
symbol: string;
@Column({ type: 'varchar', length: 100, nullable: true })
name: string;
@Column({ type: 'varchar', length: 20, nullable: true })
market: string;
@Column({ type: 'decimal', precision: 18, scale: 4, nullable: true })
shares: number;
@Column({ type: 'decimal', precision: 18, scale: 4, nullable: true })
price: number;
@Column({ type: 'decimal', precision: 18, scale: 2 })
amount: number;
@Column({ type: 'decimal', precision: 18, scale: 2, default: 0 })
fee: number;
@Column({ type: 'varchar', length: 10, default: 'CNY' })
currency: string;
@Column({ type: 'decimal', precision: 10, scale: 6, default: 1 })
exchangeRate: number;
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, any>;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
// 关系
@ManyToOne(() => User)
@JoinColumn({ name: 'userId' })
user: User;
@ManyToOne(() => Account)
@JoinColumn({ name: 'accountId' })
account: Account;
@ManyToOne(() => Position, { nullable: true })
@JoinColumn({ name: 'positionId' })
position: Position;
@ManyToOne(() => TradingPlan, { nullable: true })
@JoinColumn({ name: 'tradingPlanId' })
tradingPlan: TradingPlan;
}
```
### 3.5 Service 使用示例
#### transactions/transactions.service.ts
```typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between } from 'typeorm';
import { Transaction } from './entities/transaction.entity';
import { CreateTransactionDto } from './dto/create-transaction.dto';
@Injectable()
export class TransactionsService {
constructor(
@InjectRepository(Transaction)
private transactionRepository: Repository<Transaction>,
) {}
async create(createTransactionDto: CreateTransactionDto): Promise<Transaction> {
const transaction = this.transactionRepository.create(createTransactionDto);
return await this.transactionRepository.save(transaction);
}
async findAll(userId: number, page: number = 1, limit: number = 20) {
const [data, total] = await this.transactionRepository.findAndCount({
where: { userId },
relations: ['account', 'position', 'tradingPlan'],
order: { date: 'DESC', createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async findOne(id: number, userId: number): Promise<Transaction> {
return await this.transactionRepository.findOne({
where: { id, userId },
relations: ['account', 'position', 'tradingPlan'],
});
}
async findByDateRange(
userId: number,
startDate: Date,
endDate: Date,
): Promise<Transaction[]> {
return await this.transactionRepository.find({
where: {
userId,
date: Between(startDate, endDate),
},
order: { date: 'DESC' },
});
}
async update(id: number, userId: number, updateData: Partial<Transaction>) {
await this.transactionRepository.update({ id, userId }, updateData);
return this.findOne(id, userId);
}
async remove(id: number, userId: number): Promise<void> {
await this.transactionRepository.delete({ id, userId });
}
}
```
### 3.6 Module 配置
#### transactions/transactions.module.ts
```typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TransactionsService } from './transactions.service';
import { TransactionsController } from './transactions.controller';
import { Transaction } from './entities/transaction.entity';
@Module({
imports: [TypeOrmModule.forFeature([Transaction])],
controllers: [TransactionsController],
providers: [TransactionsService],
exports: [TransactionsService],
})
export class TransactionsModule {}
```
### 3.7 事务处理示例
```typescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { Transaction } from './entities/transaction.entity';
import { Position } from '../positions/entities/position.entity';
import { CashAccount } from '../cash-accounts/entities/cash-account.entity';
@Injectable()
export class TradingService {
constructor(
private dataSource: DataSource,
@InjectRepository(Transaction)
private transactionRepository: Repository<Transaction>,
) {}
async buyStock(buyDto: CreateBuyDto) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. 创建交易记录
const transaction = this.transactionRepository.create({
...buyDto,
type: 'buy',
});
await queryRunner.manager.save(transaction);
// 2. 更新持仓
const position = await queryRunner.manager.findOne(Position, {
where: { accountId: buyDto.accountId, symbol: buyDto.symbol },
});
if (position) {
// 更新现有持仓
const newShares = position.shares + buyDto.shares;
const newCostPrice =
(position.shares * position.costPrice +
buyDto.shares * buyDto.price +
buyDto.fee) /
newShares;
position.shares = newShares;
position.costPrice = newCostPrice;
await queryRunner.manager.save(position);
} else {
// 创建新持仓
const newPosition = queryRunner.manager.create(Position, {
accountId: buyDto.accountId,
symbol: buyDto.symbol,
name: buyDto.name,
market: buyDto.market,
shares: buyDto.shares,
costPrice: buyDto.price,
currentPrice: buyDto.price,
});
await queryRunner.manager.save(newPosition);
}
// 3. 更新现金账户
const cashAccount = await queryRunner.manager.findOne(CashAccount, {
where: { accountId: buyDto.accountId },
});
cashAccount.balance -= buyDto.amount + buyDto.fee;
await queryRunner.manager.save(cashAccount);
await queryRunner.commitTransaction();
return transaction;
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
}
```
---
## 四、Prisma 集成方案(备选)
### 4.1 安装依赖
```bash
npm install @prisma/client
npm install -D prisma
npx prisma init
```
### 4.2 Prisma Schema 示例
#### prisma/schema.prisma
```prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id BigInt @id @default(autoincrement())
username String @unique @db.VarChar(50)
email String? @unique @db.VarChar(100)
phone String? @unique @db.VarChar(20)
passwordHash String @db.VarChar(255)
nickname String? @db.VarChar(50)
avatarUrl String? @db.VarChar(255)
status String @default("active") @db.VarChar(20)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastLoginAt DateTime?
accounts Account[]
transactions Transaction[]
@@map("users")
}
model Account {
id BigInt @id @default(autoincrement())
userId BigInt
name String @db.VarChar(100)
type String @db.VarChar(20)
currency String @default("CNY") @db.VarChar(10)
description String?
status String @default("active") @db.VarChar(20)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
positions Position[]
transactions Transaction[]
@@map("accounts")
}
model Transaction {
id BigInt @id @default(autoincrement())
userId BigInt
accountId BigInt
positionId BigInt?
tradingPlanId BigInt?
type String @db.VarChar(20)
date DateTime @db.Date
symbol String? @db.VarChar(20)
name String? @db.VarChar(100)
market String? @db.VarChar(20)
shares Decimal? @db.Decimal(18, 4)
price Decimal? @db.Decimal(18, 4)
amount Decimal @db.Decimal(18, 2)
fee Decimal @default(0) @db.Decimal(18, 2)
currency String @default("CNY") @db.VarChar(10)
exchangeRate Decimal @default(1) @db.Decimal(10, 6)
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
@@index([userId, date, createdAt])
@@index([accountId])
@@index([positionId])
@@map("transactions")
}
```
### 4.3 Prisma Service
#### prisma/prisma.service.ts
```typescript
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
```
---
## 五、环境配置
### 5.1 .env 文件
```env
# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=your_password
DB_NAME=vestmind
DB_SSL=false
# 或者使用连接字符串Prisma
DATABASE_URL="postgresql://postgres:password@localhost:5432/vestmind?schema=public"
# 应用配置
NODE_ENV=development
PORT=3000
```
### 5.2 package.json 脚本
```json
{
"scripts": {
"start": "nest start",
"start:dev": "nest start --watch",
"start:prod": "node dist/main",
"typeorm": "typeorm-ts-node-commonjs",
"migration:generate": "typeorm-ts-node-commonjs migration:generate",
"migration:run": "typeorm-ts-node-commonjs migration:run",
"migration:revert": "typeorm-ts-node-commonjs migration:revert",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:studio": "prisma studio"
}
}
```
---
## 六、数据库迁移
### 6.1 TypeORM 迁移
#### 创建迁移
```bash
npm run typeorm migration:generate -- -n CreateUsersTable
```
#### 迁移文件示例
```typescript
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateUsersTable1234567890 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE users;`);
}
}
```
### 6.2 Prisma 迁移
```bash
# 创建迁移
npx prisma migrate dev --name init
# 应用迁移
npx prisma migrate deploy
# 查看迁移状态
npx prisma migrate status
```
---
## 七、性能优化建议
### 7.1 连接池配置
```typescript
// TypeORM
{
type: 'postgres',
// ... 其他配置
extra: {
max: 20, // 最大连接数
min: 5, // 最小连接数
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
},
}
```
### 7.2 查询优化
```typescript
// 使用 select 只查询需要的字段
const users = await userRepository.find({
select: ['id', 'username', 'email'],
});
// 使用 relations 预加载关联数据
const transaction = await transactionRepository.findOne({
where: { id },
relations: ['account', 'position'],
});
// 使用 QueryBuilder 进行复杂查询
const result = await transactionRepository
.createQueryBuilder('transaction')
.leftJoinAndSelect('transaction.account', 'account')
.where('transaction.userId = :userId', { userId })
.andWhere('transaction.date BETWEEN :start AND :end', { start, end })
.orderBy('transaction.date', 'DESC')
.getMany();
```
---
## 八、总结
### 8.1 推荐方案
**对于您的项目,推荐使用 TypeORM + PostgreSQL**
1. ✅ NestJS 官方推荐,文档完善
2. ✅ 装饰器语法,与 NestJS 风格一致
3. ✅ 支持复杂的实体关系
4. ✅ 支持事务管理
5. ✅ 迁移工具完善
### 8.2 快速开始
```bash
# 1. 创建 NestJS 项目
npm i -g @nestjs/cli
nest new vest-mind-backend
# 2. 安装依赖
cd vest-mind-backend
npm install @nestjs/typeorm typeorm pg
npm install --save-dev @types/pg
# 3. 配置数据库连接
# 参考上面的 database.module.ts
# 4. 创建实体
nest g module users
nest g service users
nest g controller users
# 5. 运行项目
npm run start:dev
```
### 8.3 参考资料
- [NestJS 官方文档 - TypeORM](https://docs.nestjs.com/techniques/database)
- [TypeORM 官方文档](https://typeorm.io/)
- [PostgreSQL 官方文档](https://www.postgresql.org/docs/)
---
**文档版本**v1.0
**创建日期**2024年

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,409 @@
## 1. 产品概述
### 1.1 产品定位
**产品名称**:思投录 (VestMind)
**产品定位**:投资决策与复盘工具
**产品愿景**:让每笔投资都经得起思考
**目标用户**:个人投资者、价值投资者、投资新手
### 1.2 产品价值
- 帮助用户建立系统化的投资决策流程
- 通过复盘机制提升投资决策质量
- 提供专业的投资工具和检查清单
- 培养理性投资思维,避免冲动交易
### 1.3 设计原则
- **紫色主题**:营造安静思考的投资氛围
- **移动端优先**适配APP和小程序
- **简洁易用**:降低使用门槛
- **数据驱动**:基于数据做决策
## 2. 功能架构
### 2.1 核心功能模块
```
思投录
├── 持仓管理
├── 交易计划
├── 交易记录/复盘
└── 我的工具
```
### 2.2 功能优先级
- **P0**:持仓管理、交易记录
- **P1**:交易计划、基础工具
- **P2**:高级工具、分享功能
## 3. 页面详细设计
### 3.1 首页/持仓页面
#### 3.1.1 页面概述
用户进入应用后的主页面,展示整体投资概况和持仓详情。
#### 3.1.2 页面布局
```
┌─────────────────────────┐
│ 思投录 (顶部导航) │
│ 让每笔投资都经得起思考 │
├─────────────────────────┤
│ 持仓概览卡片 │
│ ┌─────────────────────┐ │
│ │ 总资产: ¥128,450.00 │ │
│ │ 今日收益: +¥1,250 │ │
│ │ 总收益率: +12.5% │ │
│ └─────────────────────┘ │
├─────────────────────────┤
│ 持仓列表 │
│ ┌─────────────────────┐ │
│ │ 贵州茅台 600519 │ │
│ │ 100股 ¥1,850.00 │ │
│ │ +¥2,500.00 (+15.6%) │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ 腾讯控股 00700 │ │
│ │ 200股 ¥320.00 │ │
│ │ -¥800.00 (-1.2%) │ │
│ └─────────────────────┘ │
├─────────────────────────┤
│ 底部导航栏 │
│ [持仓] [计划] [记录] [我的] │
└─────────────────────────┘
```
#### 3.1.3 功能需求
**持仓概览卡片**
- 显示总资产金额
- 显示今日收益(正负用颜色区分)
- 显示总收益率
- 支持点击查看详细统计
**持仓列表**
- 显示股票名称、代码
- 显示持股数量、当前价格
- 显示盈亏金额和收益率
- 支持点击查看单只股票详情
- 支持添加新持仓
**添加持仓功能**
- 股票名称输入
- 股票代码输入
- 持股数量输入
- 成本价格输入
- 仓位上限设置(预警功能)
#### 3.1.4 交互需求
- 下拉刷新更新数据
- 长按持仓项显示操作菜单
- 点击"+"按钮添加新持仓
- 滑动删除持仓(需确认)
### 3.2 交易计划页面
#### 3.2.1 页面概述
帮助用户制定和执行交易计划,实现"计划你的交易,交易你的计划"。
#### 3.2.2 页面布局
```
┌─────────────────────────┐
│ 交易计划 (顶部导航) │
│ [+ 新建] │
├─────────────────────────┤
│ 计划列表 │
│ ┌─────────────────────┐ │
│ │ 招商银行 600036 │ │
│ │ 状态: 进行中 │ │
│ │ 目标价格: ¥45.00 │ │
│ │ 计划金额: ¥10,000 │ │
│ │ 截止时间: 2024-03-15│ │
│ │ ████████░░ 60% │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ 中国平安 601318 │ │
│ │ 状态: 已完成 │ │
│ │ 目标价格: ¥55.00 │ │
│ │ 计划金额: ¥15,000 │ │
│ │ 截止时间: 2024-04-20│ │
│ │ ██████████ 100% │ │
│ └─────────────────────┘ │
├─────────────────────────┤
│ 底部导航栏 │
│ [持仓] [计划] [记录] [我的] │
└─────────────────────────┘
```
#### 3.2.3 功能需求
**计划列表**
- 显示计划状态(进行中/已完成/已取消)
- 显示股票信息、目标价格
- 显示计划金额、截止时间
- 显示完成进度条
- 支持点击查看计划详情
**新建计划功能**
- 股票选择(名称、代码)
- 市场选择A股/港股/美股)
- 目标价格设置
- 截止时间设置
- 投资金额或股份数选择
- 分步买入设置默认3步
- 每步买入价格设置
**计划执行**
- 到达目标价格提醒
- 支持手动标记完成
- 支持从计划跳转到交易记录
- 支持修改计划参数
#### 3.2.4 交互需求
- 点击计划项查看详情
- 长按显示操作菜单(编辑/删除/完成)
- 滑动标记为完成
- 支持计划搜索和筛选
### 3.3 交易记录/复盘页面
#### 3.3.1 页面概述
记录每笔交易的详细信息,通过时间线展示交易历史和思考过程。
#### 3.3.2 页面布局
```
┌─────────────────────────┐
│ 交易记录 (顶部导航) │
│ [+ 记录] │
├─────────────────────────┤
│ 时间线 │
│ ┌─────────────────────┐ │
│ │ 2024-01-15 │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 贵州茅台 600519 │ │ │
│ │ │ 买入 100股 │ │ │
│ │ │ 价格: ¥1,600.00 │ │ │
│ │ │ ┌─────────────┐ │ │ │
│ │ │ │ 交易思考: │ │ │ │
│ │ │ │ 基于茅台品牌 │ │ │ │
│ │ │ │ 价值和长期增 │ │ │ │
│ │ │ │ 长潜力... │ │ │ │
│ │ │ └─────────────┘ │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ 2024-01-10 │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 比亚迪 002594 │ │ │
│ │ │ 卖出 200股 │ │ │
│ │ │ 价格: ¥280.00 │ │ │
│ │ │ ┌─────────────┐ │ │ │
│ │ │ │ 交易思考: │ │ │ │
│ │ │ │ 新能源汽车行 │ │ │ │
│ │ │ │ 业竞争加剧...│ │ │ │
│ │ │ └─────────────┘ │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────┘ │
├─────────────────────────┤
│ 底部导航栏 │
│ [持仓] [计划] [记录] [我的] │
└─────────────────────────┘
```
#### 3.3.3 功能需求
**时间线展示**
- 按时间倒序显示交易记录
- 显示交易日期
- 显示交易类型(买入/卖出)
- 显示股票信息、数量、价格
- 显示交易思考内容
**记录交易功能**
- 交易类型选择(买入/卖出)
- 股票信息输入
- 交易数量输入
- 交易价格输入
- 交易思考记录(必填)
- 支持从计划跳转记录
**复盘功能**
- 定期弹出复盘提醒
- 支持为历史交易添加复盘
- 复盘内容记录
- 复盘时间记录
**分享功能**(可选)
- 支持分享单笔交易
- 支持分享交易时间线
- 隐私设置控制
#### 3.3.4 交互需求
- 点击交易记录查看详情
- 长按显示操作菜单(编辑/删除/复盘)
- 支持交易记录搜索
- 支持按股票筛选
- 支持按时间范围筛选
### 3.4 我的工具页面
#### 3.4.1 页面概述
提供各种投资工具和计算器,帮助用户做出更好的投资决策。
#### 3.4.2 页面布局
```
┌─────────────────────────┐
│ 我的工具 (顶部导航) │
├─────────────────────────┤
│ 工具网格 │
│ ┌─────────┐ ┌─────────┐ │
│ │ ✅ │ │ 🧮 │ │
│ │投资检查清单│ │复利计算器│ │
│ │买入卖出检查│ │计算未来收益│ │
│ └─────────┘ └─────────┘ │
│ ┌─────────┐ ┌─────────┐ │
│ │ 📈 │ │ 🎯 │ │
│ │ 估值工具 │ │ 自由目标 │ │
│ │企业价值评估│ │财务自由规划│ │
│ └─────────┘ └─────────┘ │
├─────────────────────────┤
│ 用户信息 │
│ ┌─────────────────────┐ │
│ │ 头像 | 用户名 │ │
│ │ 投资天数: 365天 │ │
│ │ 总交易次数: 25次 │ │
│ └─────────────────────┘ │
├─────────────────────────┤
│ 底部导航栏 │
│ [持仓] [计划] [记录] [我的] │
└─────────────────────────┘
```
#### 3.4.3 功能需求
**投资检查清单**
- 买入检查清单
- 企业基本面是否优秀?
- 估值是否合理?
- 行业前景如何?
- 管理层是否可信?
- 现金流是否健康?
- 卖出检查清单
- 基本面是否恶化?
- 估值是否过高?
- 是否有更好的投资机会?
- 是否需要资金配置?
- 支持在创建交易计划时自动弹出
**复利计算器**
- 初始金额输入
- 每年投入金额输入
- 年复合增长率设置
- 投资年限设置
- 计算总投入、最终金额、总收益
- 生成收益曲线图
**估值工具**
- 老唐估值法
- 净利润输入
- 无风险收益率设置
- 合理PE倍数计算
- 估值结果输出
- 现金流折现法
- 自由现金流输入
- 增长率设置
- 折现率设置
- 估值结果输出
**自由目标**
- 目标资产设置
- 当前资产输入
- 年复合增长率设置
- 每年投入金额设置
- 计算达成时间
- 与持仓数据联动
#### 3.4.4 交互需求
- 点击工具卡片打开对应工具
- 工具界面支持数据输入和计算
- 支持结果保存和分享
- 支持历史记录查看
## 4. 技术需求
### 4.1 平台支持
- **移动端APP**iOS、Android
- **小程序**:微信小程序
- **响应式设计**:适配不同屏幕尺寸
### 4.2 数据存储
- 本地存储:用户数据、设置
- 云端同步:多设备数据同步
- 数据备份:定期备份重要数据
### 4.3 性能要求
- 页面加载时间 < 2秒
- 操作响应时间 < 500ms
- 支持离线使用基础功能
## 5. 用户体验需求
### 5.1 易用性
- 界面简洁直观
- 操作流程简单
- 新手引导完善
- 错误提示友好
### 5.2 可访问性
- 支持字体大小调节
- 支持颜色对比度调节
- 支持语音输入
- 支持键盘导航
### 5.3 个性化
- 主题颜色自定义
- 功能模块自定义
- 提醒设置个性化
- 数据展示个性化
## 6. 安全需求
### 6.1 数据安全
- 本地数据加密存储
- 网络传输加密
- 用户隐私保护
- 数据访问权限控制
### 6.2 功能安全
- 重要操作二次确认
- 数据删除保护
- 异常操作监控
- 安全日志记录
## 7. 运营需求
### 7.1 数据统计
- 用户行为分析
- 功能使用统计
- 性能监控
- 错误日志收集
### 7.2 用户反馈
- 意见反馈入口
- 问题报告功能
- 用户满意度调查
- 功能建议收集
## 8. 开发计划
### 8.1 版本规划
- **V1.0**基础功能持仓记录
- **V1.1**交易计划功能
- **V1.2**工具集成
- **V2.0**高级功能和优化
### 8.2 里程碑
- 需求确认1周
- UI设计2周
- 开发实现8周
- 测试优化2周
- 上线发布1周
## 9. 成功指标
### 9.1 用户指标
- 日活跃用户数
- 用户留存率
- 功能使用率
- 用户满意度
### 9.2 产品指标
- 交易记录完成率
- 计划执行率
- 工具使用频率
- 数据准确性
---
*本文档版本V1.0*
*最后更新2024年1月*
*文档状态待评审*

View File

@@ -0,0 +1,409 @@
## 1. 产品概述
### 1.1 产品定位
**产品名称**:思投录 (VestMind)
**产品定位**:投资决策与复盘工具
**产品愿景**:让每笔投资都经得起思考
**目标用户**:个人投资者、价值投资者、投资新手
### 1.2 产品价值
- 帮助用户建立系统化的投资决策流程
- 通过复盘机制提升投资决策质量
- 提供专业的投资工具和检查清单
- 培养理性投资思维,避免冲动交易
### 1.3 设计原则
- **紫色主题**:营造安静思考的投资氛围
- **移动端优先**适配APP和小程序
- **简洁易用**:降低使用门槛
- **数据驱动**:基于数据做决策
## 2. 功能架构
### 2.1 核心功能模块
```
思投录
├── 持仓管理
├── 交易计划
├── 交易记录/复盘
└── 我的工具
```
### 2.2 功能优先级
- **P0**:持仓管理、交易记录
- **P1**:交易计划、基础工具
- **P2**:高级工具、分享功能
## 3. 页面详细设计
### 3.1 首页/持仓页面
#### 3.1.1 页面概述
用户进入应用后的主页面,展示整体投资概况和持仓详情。
#### 3.1.2 页面布局
```
┌─────────────────────────┐
│ 思投录 (顶部导航) │
│ 让每笔投资都经得起思考 │
├─────────────────────────┤
│ 持仓概览卡片 │
│ ┌─────────────────────┐ │
│ │ 总资产: ¥128,450.00 │ │
│ │ 今日收益: +¥1,250 │ │
│ │ 总收益率: +12.5% │ │
│ └─────────────────────┘ │
├─────────────────────────┤
│ 持仓列表 │
│ ┌─────────────────────┐ │
│ │ 贵州茅台 600519 │ │
│ │ 100股 ¥1,850.00 │ │
│ │ +¥2,500.00 (+15.6%) │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ 腾讯控股 00700 │ │
│ │ 200股 ¥320.00 │ │
│ │ -¥800.00 (-1.2%) │ │
│ └─────────────────────┘ │
├─────────────────────────┤
│ 底部导航栏 │
│ [持仓] [计划] [记录] [我的] │
└─────────────────────────┘
```
#### 3.1.3 功能需求
**持仓概览卡片**
- 显示总资产金额
- 显示今日收益(正负用颜色区分)
- 显示总收益率
- 支持点击查看详细统计
**持仓列表**
- 显示股票名称、代码
- 显示持股数量、当前价格
- 显示盈亏金额和收益率
- 支持点击查看单只股票详情
- 支持添加新持仓
**添加持仓功能**
- 股票名称输入
- 股票代码输入
- 持股数量输入
- 成本价格输入
- 仓位上限设置(预警功能)
#### 3.1.4 交互需求
- 下拉刷新更新数据
- 长按持仓项显示操作菜单
- 点击"+"按钮添加新持仓
- 滑动删除持仓(需确认)
### 3.2 交易计划页面
#### 3.2.1 页面概述
帮助用户制定和执行交易计划,实现"计划你的交易,交易你的计划"。
#### 3.2.2 页面布局
```
┌─────────────────────────┐
│ 交易计划 (顶部导航) │
│ [+ 新建] │
├─────────────────────────┤
│ 计划列表 │
│ ┌─────────────────────┐ │
│ │ 招商银行 600036 │ │
│ │ 状态: 进行中 │ │
│ │ 目标价格: ¥45.00 │ │
│ │ 计划金额: ¥10,000 │ │
│ │ 截止时间: 2024-03-15│ │
│ │ ████████░░ 60% │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ 中国平安 601318 │ │
│ │ 状态: 已完成 │ │
│ │ 目标价格: ¥55.00 │ │
│ │ 计划金额: ¥15,000 │ │
│ │ 截止时间: 2024-04-20│ │
│ │ ██████████ 100% │ │
│ └─────────────────────┘ │
├─────────────────────────┤
│ 底部导航栏 │
│ [持仓] [计划] [记录] [我的] │
└─────────────────────────┘
```
#### 3.2.3 功能需求
**计划列表**
- 显示计划状态(进行中/已完成/已取消)
- 显示股票信息、目标价格
- 显示计划金额、截止时间
- 显示完成进度条
- 支持点击查看计划详情
**新建计划功能**
- 股票选择(名称、代码)
- 市场选择A股/港股/美股)
- 目标价格设置
- 截止时间设置
- 投资金额或股份数选择
- 分步买入设置默认3步
- 每步买入价格设置
**计划执行**
- 到达目标价格提醒
- 支持手动标记完成
- 支持从计划跳转到交易记录
- 支持修改计划参数
#### 3.2.4 交互需求
- 点击计划项查看详情
- 长按显示操作菜单(编辑/删除/完成)
- 滑动标记为完成
- 支持计划搜索和筛选
### 3.3 交易记录/复盘页面
#### 3.3.1 页面概述
记录每笔交易的详细信息,通过时间线展示交易历史和思考过程。
#### 3.3.2 页面布局
```
┌─────────────────────────┐
│ 交易记录 (顶部导航) │
│ [+ 记录] │
├─────────────────────────┤
│ 时间线 │
│ ┌─────────────────────┐ │
│ │ 2024-01-15 │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 贵州茅台 600519 │ │ │
│ │ │ 买入 100股 │ │ │
│ │ │ 价格: ¥1,600.00 │ │ │
│ │ │ ┌─────────────┐ │ │ │
│ │ │ │ 交易思考: │ │ │ │
│ │ │ │ 基于茅台品牌 │ │ │ │
│ │ │ │ 价值和长期增 │ │ │ │
│ │ │ │ 长潜力... │ │ │ │
│ │ │ └─────────────┘ │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ 2024-01-10 │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 比亚迪 002594 │ │ │
│ │ │ 卖出 200股 │ │ │
│ │ │ 价格: ¥280.00 │ │ │
│ │ │ ┌─────────────┐ │ │ │
│ │ │ │ 交易思考: │ │ │ │
│ │ │ │ 新能源汽车行 │ │ │ │
│ │ │ │ 业竞争加剧...│ │ │ │
│ │ │ └─────────────┘ │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────┘ │
├─────────────────────────┤
│ 底部导航栏 │
│ [持仓] [计划] [记录] [我的] │
└─────────────────────────┘
```
#### 3.3.3 功能需求
**时间线展示**
- 按时间倒序显示交易记录
- 显示交易日期
- 显示交易类型(买入/卖出)
- 显示股票信息、数量、价格
- 显示交易思考内容
**记录交易功能**
- 交易类型选择(买入/卖出)
- 股票信息输入
- 交易数量输入
- 交易价格输入
- 交易思考记录(必填)
- 支持从计划跳转记录
**复盘功能**
- 定期弹出复盘提醒
- 支持为历史交易添加复盘
- 复盘内容记录
- 复盘时间记录
**分享功能**(可选)
- 支持分享单笔交易
- 支持分享交易时间线
- 隐私设置控制
#### 3.3.4 交互需求
- 点击交易记录查看详情
- 长按显示操作菜单(编辑/删除/复盘)
- 支持交易记录搜索
- 支持按股票筛选
- 支持按时间范围筛选
### 3.4 我的工具页面
#### 3.4.1 页面概述
提供各种投资工具和计算器,帮助用户做出更好的投资决策。
#### 3.4.2 页面布局
```
┌─────────────────────────┐
│ 我的工具 (顶部导航) │
├─────────────────────────┤
│ 工具网格 │
│ ┌─────────┐ ┌─────────┐ │
│ │ ✅ │ │ 🧮 │ │
│ │投资检查清单│ │复利计算器│ │
│ │买入卖出检查│ │计算未来收益│ │
│ └─────────┘ └─────────┘ │
│ ┌─────────┐ ┌─────────┐ │
│ │ 📈 │ │ 🎯 │ │
│ │ 估值工具 │ │ 自由目标 │ │
│ │企业价值评估│ │财务自由规划│ │
│ └─────────┘ └─────────┘ │
├─────────────────────────┤
│ 用户信息 │
│ ┌─────────────────────┐ │
│ │ 头像 | 用户名 │ │
│ │ 投资天数: 365天 │ │
│ │ 总交易次数: 25次 │ │
│ └─────────────────────┘ │
├─────────────────────────┤
│ 底部导航栏 │
│ [持仓] [计划] [记录] [我的] │
└─────────────────────────┘
```
#### 3.4.3 功能需求
**投资检查清单**
- 买入检查清单
- 企业基本面是否优秀?
- 估值是否合理?
- 行业前景如何?
- 管理层是否可信?
- 现金流是否健康?
- 卖出检查清单
- 基本面是否恶化?
- 估值是否过高?
- 是否有更好的投资机会?
- 是否需要资金配置?
- 支持在创建交易计划时自动弹出
**复利计算器**
- 初始金额输入
- 每年投入金额输入
- 年复合增长率设置
- 投资年限设置
- 计算总投入、最终金额、总收益
- 生成收益曲线图
**估值工具**
- 老唐估值法
- 净利润输入
- 无风险收益率设置
- 合理PE倍数计算
- 估值结果输出
- 现金流折现法
- 自由现金流输入
- 增长率设置
- 折现率设置
- 估值结果输出
**自由目标**
- 目标资产设置
- 当前资产输入
- 年复合增长率设置
- 每年投入金额设置
- 计算达成时间
- 与持仓数据联动
#### 3.4.4 交互需求
- 点击工具卡片打开对应工具
- 工具界面支持数据输入和计算
- 支持结果保存和分享
- 支持历史记录查看
## 4. 技术需求
### 4.1 平台支持
- **移动端APP**iOS、Android
- **小程序**:微信小程序
- **响应式设计**:适配不同屏幕尺寸
### 4.2 数据存储
- 本地存储:用户数据、设置
- 云端同步:多设备数据同步
- 数据备份:定期备份重要数据
### 4.3 性能要求
- 页面加载时间 < 2秒
- 操作响应时间 < 500ms
- 支持离线使用基础功能
## 5. 用户体验需求
### 5.1 易用性
- 界面简洁直观
- 操作流程简单
- 新手引导完善
- 错误提示友好
### 5.2 可访问性
- 支持字体大小调节
- 支持颜色对比度调节
- 支持语音输入
- 支持键盘导航
### 5.3 个性化
- 主题颜色自定义
- 功能模块自定义
- 提醒设置个性化
- 数据展示个性化
## 6. 安全需求
### 6.1 数据安全
- 本地数据加密存储
- 网络传输加密
- 用户隐私保护
- 数据访问权限控制
### 6.2 功能安全
- 重要操作二次确认
- 数据删除保护
- 异常操作监控
- 安全日志记录
## 7. 运营需求
### 7.1 数据统计
- 用户行为分析
- 功能使用统计
- 性能监控
- 错误日志收集
### 7.2 用户反馈
- 意见反馈入口
- 问题报告功能
- 用户满意度调查
- 功能建议收集
## 8. 开发计划
### 8.1 版本规划
- **V1.0**基础功能持仓记录
- **V1.1**交易计划功能
- **V1.2**工具集成
- **V2.0**高级功能和优化
### 8.2 里程碑
- 需求确认1周
- UI设计2周
- 开发实现8周
- 测试优化2周
- 上线发布1周
## 9. 成功指标
### 9.1 用户指标
- 日活跃用户数
- 用户留存率
- 功能使用率
- 用户满意度
### 9.2 产品指标
- 交易记录完成率
- 计划执行率
- 工具使用频率
- 数据准确性
---
*本文档版本V1.0*
*最后更新2024年1月*
*文档状态待评审*

View File

@@ -0,0 +1,11 @@
# 应用如何命名
## 参考命名
- 投资复盘笔记
## 审核是描述
这只是一个个人投资交易的记录与复盘工具
考虑更稳妥的命名:如果希望最大化降低审核风险,可以考虑对名称进行微调,使其更偏向“个人工具”属性,同时保留核心含义。例如:
- 增加个人化前缀/后缀:比如“我的复盘笔记”、“知行投资笔记”。
- 使用更中性的词汇:例如“资产轨迹日记”、“收益账本与思考”。

View File

@@ -0,0 +1,732 @@
# 投资收益记录系统设计文档
## 一、需求概述
### 1.1 核心目标
开发一个自动化的投资收益记录应用,**用户只需直接修改持仓的成本价和份额**,系统自动更新市场价格、计算收益和收益率,无需用户定期手动更新资产金额。
**设计原则:**
- **被动变更(分红、拆股、送股)**:系统自动完成
- **主动变更(买入、卖出、追加)**:用户直接修改成本价和份数,无需记录每次交易细节
- **适用场景**:多券商用户汇总统计,不是替代券商系统
### 1.2 支持的资产类型
- **股票**支持A股、港股、美股等
- **基金**:支持各类基金产品
- **现金**:证券账户留存现金、分红现金等
### 1.3 核心功能
- 自动更新市场价格(每日收盘后)
- 自动计算总资产、总收益
- 自动计算累计收益率、年化收益率、当年收益率
- 支持多种交易场景处理
- 使用基金净值法和资金加权法计算收益率(取两者低者)
---
## 二、设计分析与优化
### 2.1 用户提出的设计分析
#### ✅ 正确的设计点
1. **拆股处理**
- 自动变更股份数和成本价
- **实现方式**:股份数 = 原股份数 × 拆股比例,成本价 = 原成本价 ÷ 拆股比例
- **示例**10股拆成20股1:2成本价从100元变为50元
2. **分红处理**
- 股票成本价减去每股分红得到最新成本价
- **实现方式**:新成本价 = 原成本价 - 每股分红金额
- **注意**:分红需要同时记录到现金账户
3. **买入/卖出处理**
- 变更持仓份额和成本价
- **实现方式**
- **买入**:新成本价 = (原成本价 × 原份额 + 买入价 × 买入份额) / (原份额 + 买入份额)
- **卖出**:只减少份额,成本价不变(先进先出或加权平均)
4. **提现处理**
- 变更现金账户金额
- **实现方式**:现金余额 = 原余额 - 提现金额
#### ⚠️ 需要补充和优化的点
1. **分红处理需要完善**
- 需要区分现金分红和股票分红(送股)
- 现金分红:成本价降低,现金账户增加
- 股票分红(送股):股份数增加,成本价降低
- **公式**:新成本价 = (原成本价 × 原股份数) / (原股份数 + 送股数)
2. **买入/卖出成本价计算**
- 需要考虑交易费用(佣金、印花税等)
- **买入成本价** = (买入价 × 买入份额 + 交易费用) / 买入份额
- **卖出**:需要计算已实现收益,并更新持仓成本价
3. **基金的特殊处理**
- 基金分红通常有现金分红和红利再投资两种方式
- 需要支持基金份额的自动调整
---
## 三、遗漏场景分析
### 3.1 必须处理的场景
1. **送股(股票分红)**
- 场景公司送股如10送5
- 处理:股份数增加,成本价降低
- 公式:新股份数 = 原股份数 × (1 + 送股比例),新成本价 = 原成本价 / (1 + 送股比例)
2. **配股**
- 场景:公司配股,需要用户决定是否参与
- 处理:如果参与,需要记录配股价格和数量,重新计算成本价
3. **转增股本**
- 场景:资本公积转增股本
- 处理:类似送股,股份数增加,成本价降低
4. **股票合并(并股)**
- 场景如10股合并为1股
- 处理:股份数减少,成本价提高
- 公式:新股份数 = 原股份数 / 合并比例,新成本价 = 原成本价 × 合并比例
5. **现金分红再投资**
- 场景:用户选择将分红现金再次买入股票
- 处理:需要记录一笔买入交易
6. **基金分红再投资**
- 场景:基金分红选择红利再投资
- 处理:基金份额增加,成本价不变
7. **股票退市/摘牌**
- 场景:股票退市,无法获取市场价格
- 处理:需要标记为退市状态,使用最后已知价格或用户手动设置
8. **股票停牌**
- 场景:股票长期停牌
- 处理:使用停牌前最后价格,标记停牌状态
9. **汇率变动(港股、美股)**
- 场景:持有港股、美股,汇率变动影响资产价值
- 处理:需要记录买入时的汇率,计算时使用当前汇率
10. **交易费用**
- 场景:买入/卖出都有交易费用
- 处理:买入时计入成本,卖出时从收益中扣除
11. **现金账户利息**
- 场景:证券账户现金可能产生利息
- 处理:定期(如每月)更新现金余额,增加利息收入
12. **资产转移**
- 场景:从一个账户转移到另一个账户
- 处理:需要支持多账户管理
### 3.2 可选处理的场景
1. **股票期权/权证**
2. **可转债**
3. **分级基金**
4. **ETF套利**
---
## 四、收益率计算方法
### 4.1 基金净值法(时间加权收益率)
**原理**:消除资金流入流出的影响,只反映投资能力。
**计算公式**
```
收益率 = (期末净值 - 期初净值) / 期初净值
```
**每日净值计算**
```
当日净值 = (总资产价值) / (累计投入资金)
```
**累计净值**
```
累计净值 = 1 × (1 + r1) × (1 + r2) × ... × (1 + rn)
```
### 4.2 资金加权法(内部收益率 IRR
**原理**:考虑资金流入流出的时间点,计算真实的投资回报率。
**计算公式**
```
NPV = Σ(CFt / (1 + IRR)^t) = 0
```
其中:
- CFt第t期的现金流正数表示投入负数表示提取
- IRR内部收益率
**实现方式**
- 使用迭代法如牛顿法求解IRR
- 或使用Excel的XIRR函数考虑具体日期
### 4.3 取两者低者的实现
**策略**:保守计算,取两种方法中较低的收益率。
**实现逻辑**
```javascript
const timeWeightedReturn = calculateTimeWeightedReturn();
const moneyWeightedReturn = calculateIRR();
const finalReturn = Math.min(timeWeightedReturn, moneyWeightedReturn);
```
### 4.4 其他收益率指标
1. **累计收益率**
```
累计收益率 = (当前总资产 - 累计投入资金) / 累计投入资金
```
2. **年化收益率**
```
年化收益率 = (1 + 累计收益率)^(365 / 投资天数) - 1
```
3. **当年收益率**
```
当年收益率 = (当前总资产 - 年初总资产) / 年初总资产
```
---
## 五、数据模型设计
### 5.1 资产账户Account
```typescript
interface Account {
id: string; // 账户ID
name: string; // 账户名称
type: 'stock' | 'fund' | 'cash'; // 账户类型
currency: string; // 货币类型CNY/USD/HKD
createdAt: Date; // 创建时间
updatedAt: Date; // 更新时间
}
```
### 5.2 持仓Position
```typescript
interface Position {
id: string; // 持仓ID
accountId: string; // 所属账户ID
symbol: string; // 股票/基金代码
name: string; // 股票/基金名称
shares: number; // 持仓份额
costPrice: number; // 成本价(每股/每份)
currentPrice: number; // 当前价格
market: string; // 市场A股/港股/美股)
currency: string; // 货币类型
status: 'active' | 'suspended' | 'delisted'; // 状态
createdAt: Date;
updatedAt: Date;
}
```
### 5.3 持仓变更记录PositionChange
```typescript
interface PositionChange {
id: string; // 变更ID
positionId: string; // 持仓ID
changeDate: Date; // 变更日期
changeType: 'manual' | 'buy' | 'sell' | 'auto'; // 变更类型
beforeShares: number; // 变更前份数
beforeCostPrice: number; // 变更前成本价
afterShares: number; // 变更后份数
afterCostPrice: number; // 变更后成本价
notes?: string; // 备注/思考
createdAt: Date;
}
```
### 5.3.1 资金变动记录CashFlow- 用于计算 IRR
```typescript
interface CashFlow {
id: string; // 资金流ID
accountId: string; // 账户ID
flowDate: Date; // 资金变动日期
flowType: 'deposit' | 'withdraw' | 'dividend' | 'interest'; // 类型
amount: number; // 金额(正数表示投入,负数表示提取)
currency: string; // 货币类型
notes?: string; // 备注
createdAt: Date;
}
```
**说明:**
- 不再需要详细的交易记录表
- 持仓变更记录只记录成本价和份数的变化,不记录交易细节
- 资金变动记录用于计算 IRR用户只需记录投入/提取的时间和金额
### 5.4 现金账户CashAccount
```typescript
interface CashAccount {
id: string; // 现金账户ID
accountId: string; // 所属账户ID
balance: number; // 余额
currency: string; // 货币类型
interestRate?: number; // 利率(年化)
updatedAt: Date;
}
```
### 5.5 每日资产快照DailySnapshot
```typescript
interface DailySnapshot {
id: string; // 快照ID
date: Date; // 日期
totalAsset: number; // 总资产
totalCost: number; // 总成本(累计投入)
totalProfit: number; // 总收益
netValue: number; // 单位净值(总资产/总成本)
timeWeightedReturn: number; // 时间加权收益率
moneyWeightedReturn: number; // 资金加权收益率(基于资金流计算)
finalReturn: number; // 最终收益率(取两者低者)
annualizedReturn: number; // 年化收益率
yearToDateReturn: number; // 当年收益率
positions: Position[]; // 持仓明细JSON格式
cashAccounts: CashAccount[]; // 现金账户明细JSON格式
}
```
**说明:**
- 系统每日自动生成资产快照
- 用于计算时间加权收益率(基金净值法)
- 不依赖交易记录,只依赖持仓和资产快照
---
## 六、核心算法设计
### 6.1 成本价计算算法
#### 买入后成本价计算
```javascript
function calculateCostAfterBuy(originalShares, originalCost, buyShares, buyPrice, fee) {
const totalCost = (originalShares * originalCost) + (buyShares * buyPrice) + fee;
const totalShares = originalShares + buyShares;
return totalCost / totalShares;
}
```
#### 卖出后成本价计算
```javascript
// 卖出不改变成本价,只减少份额
function calculateCostAfterSell(originalShares, originalCost, sellShares) {
// 成本价不变
return originalCost;
}
```
#### 拆股后成本价计算
```javascript
function calculateCostAfterSplit(originalShares, originalCost, splitRatio) {
// splitRatio: 如 1:2 拆股splitRatio = 2
const newShares = originalShares * splitRatio;
const newCost = originalCost / splitRatio;
return { newShares, newCost };
}
```
#### 分红后成本价计算
```javascript
function calculateCostAfterDividend(originalCost, dividendPerShare) {
// 现金分红:成本价降低
return originalCost - dividendPerShare;
}
```
#### 送股后成本价计算
```javascript
function calculateCostAfterBonus(originalShares, originalCost, bonusRatio) {
// bonusRatio: 如 10送5bonusRatio = 0.5
const newShares = originalShares * (1 + bonusRatio);
const newCost = originalCost / (1 + bonusRatio);
return { newShares, newCost };
}
```
### 6.2 收益率计算算法
#### 时间加权收益率(基金净值法)
```javascript
function calculateTimeWeightedReturn(snapshots) {
let cumulativeReturn = 1;
for (let i = 1; i < snapshots.length; i++) {
const prevSnapshot = snapshots[i - 1];
const currSnapshot = snapshots[i];
// 计算期间收益率
const periodReturn = (currSnapshot.totalAsset - prevSnapshot.totalAsset) / prevSnapshot.totalAsset;
cumulativeReturn *= (1 + periodReturn);
}
return cumulativeReturn - 1;
}
```
#### 资金加权收益率IRR
```javascript
function calculateIRR(cashFlows) {
// cashFlows: 资金变动记录数组
// 正数投入资金deposit
// 负数提取资金withdraw
// 最后一条:当前资产价值(负数,表示"提取"
// 构建现金流数组
const flows = cashFlows.map(cf => ({
date: cf.flowDate,
amount: cf.amount
}));
// 添加当前资产价值(作为最后一笔现金流)
const currentAsset = calculateTotalAsset();
flows.push({
date: new Date(),
amount: -currentAsset // 负数表示"提取"
});
function npv(rate) {
let sum = 0;
const baseDate = flows[0].date;
for (let i = 0; i < flows.length; i++) {
const days = (flows[i].date - baseDate) / (1000 * 60 * 60 * 24);
sum += flows[i].amount / Math.pow(1 + rate, days / 365);
}
return sum;
}
// 使用二分法求解
let low = -0.99;
let high = 10;
let mid;
for (let i = 0; i < 100; i++) {
mid = (low + high) / 2;
const npvValue = npv(mid);
if (Math.abs(npvValue) < 0.0001) {
return mid;
}
if (npvValue > 0) {
low = mid;
} else {
high = mid;
}
}
return mid;
}
```
**说明:**
- 基于资金变动记录CashFlow计算不是基于交易记录
- 用户只需记录:什么时候投入多少钱、什么时候提取多少钱
- 比记录每笔交易简单很多
#### 年化收益率计算
```javascript
function calculateAnnualizedReturn(totalReturn, days) {
return Math.pow(1 + totalReturn, 365 / days) - 1;
}
```
### 6.3 每日资产更新流程
```javascript
async function updateDailyAssets(date) {
// 1. 获取所有持仓
const positions = await getActivePositions();
// 2. 更新每个持仓的市场价格
for (const position of positions) {
const currentPrice = await fetchMarketPrice(position.symbol, position.market, date);
position.currentPrice = currentPrice;
position.updatedAt = date;
}
// 3. 计算总资产
const totalAsset = calculateTotalAsset(positions, cashAccounts);
// 4. 计算总成本
const totalCost = calculateTotalCost(positions, cashAccounts);
// 5. 计算总收益
const totalProfit = totalAsset - totalCost;
// 6. 计算收益率
const timeWeightedReturn = calculateTimeWeightedReturn(snapshots);
const moneyWeightedReturn = calculateIRR(cashFlows, dates);
const finalReturn = Math.min(timeWeightedReturn, moneyWeightedReturn);
// 7. 保存资产快照
await saveAssetSnapshot({
date,
totalAsset,
totalCost,
totalProfit,
timeWeightedReturn,
moneyWeightedReturn,
finalReturn,
positions,
cashAccounts
});
}
```
---
## 七、交易场景处理流程
### 7.1 买入/卖出股票/基金(简化操作)
**新设计:用户直接修改持仓**
```
用户操作:
1. 打开持仓列表,找到对应股票
2. 点击"编辑"或"调整"
3. 直接修改:
- 成本价(如:从 100 改为 95表示加仓后新的加权成本价
- 份数(如:从 100 股改为 150 股)
4. 可选:添加备注/思考(如:"2024年1月加仓"
5. 保存
系统处理:
1. 记录变更前状态(用于变更历史)
2. 更新持仓(成本价、份数)
3. 记录持仓变更记录(简化版,不记录交易细节)
4. 更新总资产和收益率
5. 生成每日资产快照(用于收益率计算)
```
**优势:**
- ✅ 操作简单,只需修改两个数字
- ✅ 适合多券商用户快速汇总
- ✅ 用户可以根据券商系统的成本价直接输入
- ✅ 不需要记录每笔交易的费用、时间等细节
**注意事项:**
- 用户需要自己计算加权平均成本价(或使用系统提供的计算器)
- 系统会记录变更历史,但不会记录详细的交易信息
### 7.3 拆股处理
```
1. 系统检测或用户输入:拆股比例(如 1:2
2. 系统处理:
- 自动更新股份数 = 原股份数 × 拆股比例
- 自动更新成本价 = 原成本价 ÷ 拆股比例
- 记录拆股交易记录
3. 更新总资产(总价值不变)
```
### 7.4 现金分红处理
```
1. 系统检测或用户输入:每股分红金额
2. 系统处理:
- 计算分红总额 = 每股分红 × 持仓份额
- 更新成本价 = 原成本价 - 每股分红
- 增加现金账户余额(分红总额)
- 记录分红交易记录
3. 更新总资产和收益率
```
### 7.5 送股处理
```
1. 系统检测或用户输入:送股比例(如 10送5比例为0.5
2. 系统处理:
- 更新股份数 = 原股份数 × (1 + 送股比例)
- 更新成本价 = 原成本价 ÷ (1 + 送股比例)
- 记录送股交易记录
3. 更新总资产(总价值不变)
```
### 7.6 提现处理
```
1. 用户输入:提现金额
2. 系统处理:
- 检查现金账户余额是否足够
- 减少现金账户余额
- 记录提现交易记录
3. 更新总资产
```
---
## 八、系统架构设计
### 8.1 技术栈建议
**前端**
- React / Vue.js
- TypeScript
- 状态管理Redux / Zustand
- UI框架Ant Design / Material-UI
**后端**
- Node.js / Python
- 数据库PostgreSQL / MySQL
- 缓存Redis
**数据获取**
- 股票价格API腾讯财经、新浪财经、Yahoo Finance
- 基金净值API天天基金、晨星
### 8.2 模块划分
1. **资产管理模块**
- 账户管理
- 持仓管理
- 现金账户管理
2. **交易处理模块**
- 买入/卖出处理
- 分红处理
- 拆股/送股处理
- 交易记录管理
3. **价格更新模块**
- 定时任务(每日收盘后更新)
- 价格数据获取
- 价格数据缓存
4. **收益计算模块**
- 时间加权收益率计算
- 资金加权收益率计算
- 年化收益率计算
- 资产快照生成
5. **数据统计模块**
- 总资产统计
- 收益统计
- 收益率统计
- 持仓分析
### 8.3 数据存储设计
**表结构**
- accounts账户表
- positions持仓表
- transactions交易记录表
- cash_accounts现金账户表
- asset_snapshots资产快照表
- market_prices市场价格表用于缓存
---
## 九、实现建议
### 9.1 开发优先级
**第一阶段MVP**
1. 基本的资产账户管理
2. 持仓管理(买入/卖出)
3. 每日价格更新
4. 基本的收益计算(累计收益率)
**第二阶段**
1. 拆股、分红、送股处理
2. 时间加权收益率和资金加权收益率计算
3. 年化收益率计算
4. 资产快照功能
**第三阶段**
1. 多账户管理
2. 多货币支持
3. 高级统计和分析
4. 数据导出功能
### 9.2 注意事项
1. **数据准确性**
- 价格数据需要可靠的数据源
- 交易记录需要完整保存,不可修改
- 成本价计算需要精确
2. **性能优化**
- 价格数据缓存
- 资产快照定期生成,避免实时计算
- 收益率计算优化IRR计算可能较慢
3. **用户体验**
- 交易记录操作简单
- 自动处理拆股、分红等场景
- 清晰的收益展示
4. **数据备份**
- 定期备份交易记录
- 支持数据导出
---
## 十、总结
### 10.1 设计要点
1. ✅ **用户只需直接修改持仓**:买入/卖出时直接修改成本价和份数,无需记录交易细节
2. ✅ **自动处理特殊事件**:拆股、分红、送股等被动变更由系统自动处理
3. ✅ **自动更新市场价格**:每日收盘后自动更新,无需用户手动输入
4. ✅ **简化操作流程**:适合多券商用户快速汇总统计,不是替代券商系统
5. ✅ **保守的收益率计算**:使用时间加权(基于资产快照)和资金加权(基于资金流)两种方法,取较低者
6. ✅ **完整的收益统计**:累计收益率、年化收益率、当年收益率等
7. ✅ **支持计划和复盘**:通过持仓思考记录实现,关联持仓而非交易
### 10.2 需要补充的场景
1. 送股(股票分红)
2. 配股
3. 转增股本
4. 股票合并
5. 汇率变动(港股、美股)
6. 交易费用处理
7. 现金账户利息
8. 股票停牌/退市处理
### 10.3 实现建议
- 采用模块化设计,便于扩展
- 持仓变更记录不可修改,保证数据可追溯性
- 每日自动生成资产快照,用于收益率计算
- 提供成本价计算器工具,帮助用户计算加权平均成本价
- 支持批量导入功能,可以从券商系统导出后导入
- 提供清晰的数据展示和统计
- 持仓思考记录支持计划和复盘功能
### 10.4 数据模型总结
**核心表:**
- `positions` - 持仓表(用户直接修改成本价和份数)
- `position_changes` - 持仓变更记录(系统自动记录变更历史)
- `cash_flows` - 资金变动记录(用户记录投入/提取,用于 IRR 计算)
- `daily_snapshots` - 每日资产快照(系统自动生成,用于时间加权收益率)
- `position_thoughts` - 持仓思考记录(用户记录思考,用于计划和复盘)
**不再需要:**
- ~~详细的交易记录表~~(改为简化的持仓变更记录)
---
**文档版本**v1.0
**创建日期**2024年
**最后更新**2024年

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,832 @@
# 思投录数据库设计文档
## 一、数据库选型分析
### 1.1 业务特点分析
根据业务需求,系统需要处理以下类型的数据:
1. **结构化数据**
- 用户账户、持仓、交易记录
- 交易计划、计划步骤
- 资产净值、收益率等数值数据
2. **时间序列数据**
- 每日单位净值记录
- 资产快照(每日)
- 价格历史数据
3. **文本内容数据**
- 交易思考(交易记录时填写)
- 复盘内容(定期复盘时填写)
- 计划思考(创建计划时填写)
4. **关联关系**
- 交易记录与持仓的关联
- 交易计划与交易记录的关联
- 复盘与交易记录的关联
- 计划步骤与计划的关联
5. **查询需求**
- 复杂的时间范围查询
- 多表关联查询
- 聚合统计查询
- 时间线查询(按时间排序)
### 1.2 数据库选型推荐
#### 推荐方案PostgreSQL
**推荐理由:**
1. **关系型数据库优势**
- ✅ 数据结构清晰,关系明确
- ✅ 支持复杂查询和统计JOIN、聚合函数
- ✅ 支持事务,保证数据一致性
- ✅ ACID特性数据可靠性高
2. **PostgreSQL特有优势**
- ✅ 支持JSON/JSONB类型可灵活存储思考内容等文本
- ✅ 支持数组类型,适合存储计划步骤等
- ✅ 强大的时间序列查询能力
- ✅ 支持全文搜索(可用于搜索思考内容)
- ✅ 性能优秀,适合中小型应用
- ✅ 开源免费,社区活跃
3. **其他考虑**
- 如果团队更熟悉MySQLMySQL 8.0+ 也是不错的选择
- 对于时间序列数据可以考虑用TimescaleDB基于PostgreSQL的扩展
#### 备选方案MySQL 8.0+
**适用场景:**
- 团队更熟悉MySQL
- 需要更好的云服务支持如阿里云RDS
- 数据量不是特别大
**MySQL优势**
- ✅ 生态成熟,工具丰富
- ✅ 云服务支持好
- ✅ 性能稳定
- ⚠️ JSON支持不如PostgreSQL强大
- ⚠️ 复杂查询性能略逊于PostgreSQL
#### 不推荐方案
- **MongoDB等NoSQL**虽然可以存储JSON但关联查询复杂不适合这种关系明确的数据结构
- **Redis**:只适合缓存,不适合持久化存储
- **SQLite**:适合单机应用,不支持多用户并发
### 1.3 最终推荐
**推荐使用 PostgreSQL 14+**
理由:
1. 数据结构关系明确,适合关系型数据库
2. JSONB类型可以灵活存储思考内容
3. 强大的查询能力,适合复杂统计
4. 时间序列查询性能好
5. 支持全文搜索,方便搜索思考内容
---
## 二、数据库表设计
### 2.1 表结构总览
```
用户相关
├── users (用户表)
└── user_settings (用户设置表)
账户相关
├── accounts (账户表)
├── positions (持仓表)
├── cash_accounts (现金账户表)
└── position_warnings (持仓预警表)
交易相关
├── transactions (交易记录表)
├── transaction_thoughts (交易思考表)
└── transaction_reviews (交易复盘表)
计划相关
├── trading_plans (交易计划表)
├── plan_steps (计划步骤表)
└── plan_thoughts (计划思考表)
净值相关
├── net_value_snapshots (净值快照表)
└── daily_net_values (每日净值表)
市场数据
└── market_prices (市场价格表)
```
### 2.2 详细表结构设计
#### 2.2.1 用户相关表
##### users (用户表)
```sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE,
phone VARCHAR(20) UNIQUE,
password_hash VARCHAR(255) NOT NULL,
nickname VARCHAR(50),
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) DEFAULT 'active' -- active, inactive, deleted
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_phone ON users(phone);
CREATE INDEX idx_users_status ON users(status);
```
##### user_settings (用户设置表)
```sql
CREATE TABLE user_settings (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
setting_key VARCHAR(50) NOT NULL,
setting_value TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, setting_key)
);
CREATE INDEX idx_user_settings_user_id ON user_settings(user_id);
```
**常用设置项:**
- `position_limit_per_stock`: 单只股票仓位上限(百分比)
- `currency`: 默认货币
- `theme`: 主题设置
- `notification_enabled`: 是否启用通知
#### 2.2.2 账户相关表
##### accounts (账户表)
```sql
CREATE TABLE accounts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
type VARCHAR(20) NOT NULL, -- stock, fund, cash, mixed
currency VARCHAR(10) NOT NULL DEFAULT 'CNY', -- CNY, USD, HKD
description TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) DEFAULT 'active' -- active, archived, deleted
);
CREATE INDEX idx_accounts_user_id ON accounts(user_id);
CREATE INDEX idx_accounts_status ON accounts(status);
```
##### positions (持仓表)
```sql
CREATE TABLE positions (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
symbol VARCHAR(20) NOT NULL, -- 股票/基金代码
name VARCHAR(100) NOT NULL, -- 股票/基金名称
market VARCHAR(20) NOT NULL, -- 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',
status VARCHAR(20) DEFAULT 'active', -- active, suspended, delisted
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(account_id, symbol, market) -- 同一账户同一股票只能有一条持仓
);
CREATE INDEX idx_positions_account_id ON positions(account_id);
CREATE INDEX idx_positions_symbol ON positions(symbol);
CREATE INDEX idx_positions_status ON positions(status);
CREATE INDEX idx_positions_updated_at ON positions(updated_at);
```
##### cash_accounts (现金账户表)
```sql
CREATE TABLE cash_accounts (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
balance DECIMAL(18, 2) NOT NULL DEFAULT 0, -- 余额
currency VARCHAR(10) NOT NULL DEFAULT 'CNY',
interest_rate DECIMAL(5, 4) DEFAULT 0, -- 年化利率
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(account_id, currency) -- 同一账户同一货币只能有一条记录
);
CREATE INDEX idx_cash_accounts_account_id ON cash_accounts(account_id);
```
##### position_warnings (持仓预警表)
```sql
CREATE TABLE position_warnings (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
position_id BIGINT NOT NULL REFERENCES positions(id) ON DELETE CASCADE,
warning_type VARCHAR(20) NOT NULL, -- position_limit, price_alert
threshold_value DECIMAL(18, 4), -- 阈值
is_triggered BOOLEAN DEFAULT FALSE,
triggered_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_position_warnings_user_id ON position_warnings(user_id);
CREATE INDEX idx_position_warnings_position_id ON position_warnings(position_id);
```
#### 2.2.3 交易相关表
##### transactions (交易记录表)
```sql
CREATE TABLE transactions (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
position_id BIGINT REFERENCES positions(id) ON DELETE SET NULL, -- 可为空,因为可能是现金交易
trading_plan_id BIGINT REFERENCES trading_plans(id) ON DELETE SET NULL, -- 关联交易计划
type VARCHAR(20) NOT NULL, -- buy, sell, dividend, split, bonus, rights, deposit, withdraw
date DATE NOT NULL, -- 交易日期
symbol VARCHAR(20), -- 股票/基金代码
name VARCHAR(100), -- 股票/基金名称
market VARCHAR(20), -- 市场
shares DECIMAL(18, 4), -- 交易份额
price DECIMAL(18, 4), -- 交易价格
amount DECIMAL(18, 2) NOT NULL, -- 交易金额(正数表示收入,负数表示支出)
fee DECIMAL(18, 2) DEFAULT 0, -- 交易费用
currency VARCHAR(10) NOT NULL DEFAULT 'CNY',
exchange_rate DECIMAL(10, 6) DEFAULT 1, -- 汇率(用于多货币)
metadata JSONB, -- 存储额外信息,如拆股比例、分红金额等
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_transactions_user_id ON transactions(user_id);
CREATE INDEX idx_transactions_account_id ON transactions(account_id);
CREATE INDEX idx_transactions_position_id ON transactions(position_id);
CREATE INDEX idx_transactions_trading_plan_id ON transactions(trading_plan_id);
CREATE INDEX idx_transactions_date ON transactions(date);
CREATE INDEX idx_transactions_type ON transactions(type);
CREATE INDEX idx_transactions_symbol ON transactions(symbol);
CREATE INDEX idx_transactions_date_desc ON transactions(date DESC); -- 用于时间线查询
```
**metadata字段示例**
```json
{
"split_ratio": 2, // 拆股比例
"dividend_per_share": 0.5, // 每股分红
"bonus_ratio": 0.5, // 送股比例
"rights_price": 10.5, // 配股价格
"rights_shares": 100 // 配股数量
}
```
##### transaction_thoughts (交易思考表)
```sql
CREATE TABLE transaction_thoughts (
id BIGSERIAL PRIMARY KEY,
transaction_id BIGINT NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
content TEXT NOT NULL, -- 思考内容
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(transaction_id) -- 每笔交易只能有一条思考
);
CREATE INDEX idx_transaction_thoughts_transaction_id ON transaction_thoughts(transaction_id);
CREATE INDEX idx_transaction_thoughts_content ON transaction_thoughts USING gin(to_tsvector('jiebacfg', content)); -- 全文搜索索引需要安装pg_trgm扩展
```
##### transaction_reviews (交易复盘表)
```sql
CREATE TABLE transaction_reviews (
id BIGSERIAL PRIMARY KEY,
transaction_id BIGINT NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
review_type VARCHAR(20) DEFAULT 'manual', -- manual, scheduled
content TEXT NOT NULL, -- 复盘内容
review_date DATE NOT NULL, -- 复盘日期
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_transaction_reviews_transaction_id ON transaction_reviews(transaction_id);
CREATE INDEX idx_transaction_reviews_review_date ON transaction_reviews(review_date);
CREATE INDEX idx_transaction_reviews_content ON transaction_reviews USING gin(to_tsvector('jiebacfg', content)); -- 全文搜索索引
```
**说明:**
- 一笔交易可以有多次复盘(不同时间点)
- `review_type`: manual手动复盘、scheduled定期提醒复盘
#### 2.2.4 计划相关表
##### trading_plans (交易计划表)
```sql
CREATE TABLE trading_plans (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
symbol VARCHAR(20) NOT NULL, -- 股票代码
name VARCHAR(100) NOT NULL, -- 股票名称
market VARCHAR(20) NOT NULL, -- 市场
target_price DECIMAL(18, 4) NOT NULL, -- 目标价格
target_amount DECIMAL(18, 2), -- 目标金额
target_shares DECIMAL(18, 4), -- 目标份额(金额和份额二选一)
deadline DATE, -- 截止日期
status VARCHAR(20) DEFAULT 'pending', -- pending, in_progress, completed, cancelled
progress DECIMAL(5, 2) DEFAULT 0, -- 完成进度(百分比)
current_price DECIMAL(18, 4), -- 当前价格(用于提醒)
is_price_alert_enabled BOOLEAN DEFAULT TRUE, -- 是否启用价格提醒
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP
);
CREATE INDEX idx_trading_plans_user_id ON trading_plans(user_id);
CREATE INDEX idx_trading_plans_status ON trading_plans(status);
CREATE INDEX idx_trading_plans_deadline ON trading_plans(deadline);
CREATE INDEX idx_trading_plans_symbol ON trading_plans(symbol);
```
##### plan_steps (计划步骤表)
```sql
CREATE TABLE plan_steps (
id BIGSERIAL PRIMARY KEY,
trading_plan_id BIGINT NOT NULL REFERENCES trading_plans(id) ON DELETE CASCADE,
step_order INTEGER NOT NULL, -- 步骤顺序1, 2, 3...
target_price DECIMAL(18, 4) NOT NULL, -- 该步骤的目标价格
target_amount DECIMAL(18, 2), -- 该步骤的目标金额
target_shares DECIMAL(18, 4), -- 该步骤的目标份额
status VARCHAR(20) DEFAULT 'pending', -- pending, completed
completed_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(trading_plan_id, step_order)
);
CREATE INDEX idx_plan_steps_trading_plan_id ON plan_steps(trading_plan_id);
CREATE INDEX idx_plan_steps_status ON plan_steps(status);
```
##### plan_thoughts (计划思考表)
```sql
CREATE TABLE plan_thoughts (
id BIGSERIAL PRIMARY KEY,
trading_plan_id BIGINT NOT NULL REFERENCES trading_plans(id) ON DELETE CASCADE,
content TEXT NOT NULL, -- 思考内容
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(trading_plan_id) -- 每个计划只能有一条思考
);
CREATE INDEX idx_plan_thoughts_trading_plan_id ON plan_thoughts(trading_plan_id);
CREATE INDEX idx_plan_thoughts_content ON plan_thoughts USING gin(to_tsvector('jiebacfg', content)); -- 全文搜索索引
```
#### 2.2.5 净值相关表
##### net_value_snapshots (净值快照表)
```sql
CREATE TABLE net_value_snapshots (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
account_id BIGINT REFERENCES accounts(id) ON DELETE CASCADE, -- 可为空,表示总账户
snapshot_date DATE NOT NULL, -- 快照日期
total_asset DECIMAL(18, 2) NOT NULL, -- 总资产
total_cost DECIMAL(18, 2) NOT NULL, -- 总成本(累计投入)
total_profit DECIMAL(18, 2) NOT NULL, -- 总收益
net_value DECIMAL(18, 6) NOT NULL, -- 单位净值
cumulative_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, -- 现金明细
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, account_id, snapshot_date) -- 同一用户同一账户同一日期只能有一条快照
);
CREATE INDEX idx_net_value_snapshots_user_id ON net_value_snapshots(user_id);
CREATE INDEX idx_net_value_snapshots_account_id ON net_value_snapshots(account_id);
CREATE INDEX idx_net_value_snapshots_date ON net_value_snapshots(snapshot_date);
CREATE INDEX idx_net_value_snapshots_user_date ON net_value_snapshots(user_id, snapshot_date DESC);
```
**positions_data字段示例**
```json
[
{
"position_id": 1,
"symbol": "600519",
"name": "贵州茅台",
"shares": 100,
"cost_price": 1600,
"current_price": 1850,
"market_value": 185000,
"profit": 25000
}
]
```
##### daily_net_values (每日净值表)
```sql
CREATE TABLE daily_net_values (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
account_id BIGINT REFERENCES accounts(id) ON DELETE CASCADE, -- 可为空,表示总账户
value_date DATE NOT NULL, -- 净值日期
net_value DECIMAL(18, 6) NOT NULL, -- 单位净值
total_asset DECIMAL(18, 2) NOT NULL, -- 总资产
total_cost DECIMAL(18, 2) NOT NULL, -- 总成本
daily_return DECIMAL(10, 6), -- 日收益率
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, account_id, value_date)
);
CREATE INDEX idx_daily_net_values_user_id ON daily_net_values(user_id);
CREATE INDEX idx_daily_net_values_account_id ON daily_net_values(account_id);
CREATE INDEX idx_daily_net_values_date ON daily_net_values(value_date);
CREATE INDEX idx_daily_net_values_user_date ON daily_net_values(user_id, account_id, value_date DESC); -- 用于净值曲线查询
```
#### 2.2.6 市场数据表
##### market_prices (市场价格表)
```sql
CREATE TABLE market_prices (
id BIGSERIAL PRIMARY KEY,
symbol VARCHAR(20) NOT NULL,
market VARCHAR(20) NOT NULL,
price_date DATE NOT NULL,
open_price DECIMAL(18, 4), -- 开盘价
close_price DECIMAL(18, 4) NOT NULL, -- 收盘价
high_price DECIMAL(18, 4), -- 最高价
low_price DECIMAL(18, 4), -- 最低价
volume BIGINT, -- 成交量
amount DECIMAL(18, 2), -- 成交额
currency VARCHAR(10) DEFAULT 'CNY',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(symbol, market, price_date)
);
CREATE INDEX idx_market_prices_symbol ON market_prices(symbol, market);
CREATE INDEX idx_market_prices_date ON market_prices(price_date);
CREATE INDEX idx_market_prices_symbol_date ON market_prices(symbol, market, price_date DESC);
```
---
## 三、关键业务逻辑实现
### 3.1 基金净值法计算
#### 3.1.1 每日净值计算流程
```sql
-- 1. 计算当日总资产
-- 总资产 = 所有持仓市值 + 现金余额
-- 2. 计算累计投入资金
-- 累计投入 = 所有买入交易的金额总和 - 所有卖出交易的金额总和 + 初始资金
-- 3. 计算单位净值
-- 单位净值 = 总资产 / 累计投入资金
-- 4. 计算收益率
-- 累计收益率 = (当前净值 - 初始净值) / 初始净值
-- 日收益率 = (今日净值 - 昨日净值) / 昨日净值
```
#### 3.1.2 净值计算SQL示例
```sql
-- 计算用户某账户的当日净值
WITH asset_value AS (
-- 计算持仓市值
SELECT
COALESCE(SUM(p.shares * p.current_price), 0) as position_value
FROM positions p
WHERE p.account_id = :account_id
AND p.status = 'active'
),
cash_value AS (
-- 计算现金余额
SELECT
COALESCE(SUM(c.balance), 0) as cash_balance
FROM cash_accounts c
WHERE c.account_id = :account_id
),
total_cost AS (
-- 计算累计投入
SELECT
COALESCE(SUM(
CASE
WHEN t.type IN ('buy', 'deposit') THEN t.amount
WHEN t.type IN ('sell', 'withdraw') THEN -t.amount
ELSE 0
END
), 0) as total_invested
FROM transactions t
WHERE t.account_id = :account_id
)
SELECT
(av.position_value + cv.cash_balance) as total_asset,
tc.total_invested,
CASE
WHEN tc.total_invested > 0
THEN (av.position_value + cv.cash_balance) / tc.total_invested
ELSE 1.0
END as net_value
FROM asset_value av, cash_value cv, total_cost tc;
```
### 3.2 交易记录与思考关联查询
```sql
-- 查询交易记录及其思考(时间线)
SELECT
t.id,
t.date,
t.type,
t.symbol,
t.name,
t.shares,
t.price,
t.amount,
tt.content as thought_content,
tt.created_at as thought_created_at
FROM transactions t
LEFT JOIN transaction_thoughts tt ON t.id = tt.transaction_id
WHERE t.user_id = :user_id
ORDER BY t.date DESC, t.created_at DESC
LIMIT :limit OFFSET :offset;
```
### 3.3 交易复盘查询
```sql
-- 查询交易及其所有复盘记录
SELECT
t.id,
t.date,
t.symbol,
t.name,
t.type,
tr.id as review_id,
tr.review_date,
tr.content as review_content,
tr.created_at as review_created_at
FROM transactions t
LEFT JOIN transaction_reviews tr ON t.id = tr.transaction_id
WHERE t.user_id = :user_id
AND t.id = :transaction_id
ORDER BY tr.review_date DESC;
```
### 3.4 交易计划进度更新
```sql
-- 更新计划进度
UPDATE trading_plans tp
SET
progress = (
SELECT
CASE
WHEN tp.target_amount > 0
THEN LEAST(100, (COALESCE(SUM(CASE WHEN t.type = 'buy' THEN t.amount ELSE 0 END), 0) / tp.target_amount) * 100)
WHEN tp.target_shares > 0
THEN LEAST(100, (COALESCE(SUM(CASE WHEN t.type = 'buy' THEN t.shares ELSE 0 END), 0) / tp.target_shares) * 100)
ELSE 0
END
FROM transactions t
WHERE t.trading_plan_id = tp.id
),
status = CASE
WHEN progress >= 100 THEN 'completed'
WHEN progress > 0 THEN 'in_progress'
ELSE 'pending'
END,
updated_at = CURRENT_TIMESTAMP
WHERE tp.id = :plan_id;
```
---
## 四、索引优化建议
### 4.1 必须创建的索引
1. **外键索引**:所有外键字段都应创建索引
2. **查询字段索引**经常用于WHERE、JOIN、ORDER BY的字段
3. **时间字段索引**date、created_at等时间字段
### 4.2 全文搜索索引
对于思考内容、复盘内容等文本字段使用PostgreSQL的全文搜索功能
```sql
-- 安装扩展(需要管理员权限)
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS jieba; -- 中文分词(如果可用)
-- 创建全文搜索索引
CREATE INDEX idx_transaction_thoughts_content_search
ON transaction_thoughts USING gin(to_tsvector('jiebacfg', content));
```
### 4.3 复合索引
根据查询模式创建复合索引:
```sql
-- 用户时间线查询(最常用)
CREATE INDEX idx_transactions_user_date ON transactions(user_id, date DESC, created_at DESC);
-- 净值查询
CREATE INDEX idx_daily_net_values_user_account_date ON daily_net_values(user_id, account_id, value_date DESC);
-- 持仓查询
CREATE INDEX idx_positions_account_status ON positions(account_id, status);
```
---
## 五、数据维护建议
### 5.1 定期任务
1. **每日净值计算**
- 每日收盘后如18:00自动计算并保存净值
- 更新持仓的当前价格
2. **价格数据更新**
- 每日更新市场价格表
- 清理过期的价格数据保留最近2年
3. **计划提醒**
- 检查到达目标价格的计划
- 发送提醒通知
4. **复盘提醒**
- 定期(如每月)提醒用户进行复盘
### 5.2 数据清理
```sql
-- 清理过期的市场价格数据保留最近2年
DELETE FROM market_prices
WHERE price_date < CURRENT_DATE - INTERVAL '2 years';
-- 归档已删除的数据(软删除的数据)
-- 可以考虑定期归档到历史表
```
### 5.3 数据备份
1. **定期全量备份**:每日备份
2. **增量备份**:每小时备份事务日志
3. **重要数据备份**:交易记录、思考内容等关键数据单独备份
---
## 六、性能优化建议
### 6.1 查询优化
1. **分页查询**所有列表查询都应使用LIMIT和OFFSET
2. **避免N+1查询**使用JOIN一次性获取关联数据
3. **使用EXPLAIN分析**:定期分析慢查询
### 6.2 缓存策略
1. **净值数据缓存**最近30天的净值数据可以缓存
2. **持仓数据缓存**:当前持仓数据可以缓存,更新时失效
3. **市场价格缓存**使用Redis缓存常用股票的最新价格
### 6.3 读写分离
如果数据量大,可以考虑:
- 主库:写操作
- 从库:读操作(查询、统计)
---
## 七、数据迁移方案
### 7.1 初始化数据
```sql
-- 创建初始用户
INSERT INTO users (username, password_hash, nickname)
VALUES ('admin', 'hashed_password', '管理员');
-- 创建初始账户
INSERT INTO accounts (user_id, name, type, currency)
VALUES (1, '主账户', 'mixed', 'CNY');
-- 设置初始净值
INSERT INTO daily_net_values (user_id, account_id, value_date, net_value, total_asset, total_cost)
VALUES (1, 1, CURRENT_DATE, 1.0, 0, 0);
```
### 7.2 数据导入
如果从其他系统迁移数据,需要:
1. 清洗数据格式
2. 按依赖关系顺序导入(先用户,再账户,再交易)
3. 重新计算净值历史
---
## 八、安全考虑
### 8.1 数据安全
1. **敏感数据加密**密码使用bcrypt等加密
2. **SQL注入防护**:使用参数化查询
3. **权限控制**:用户只能访问自己的数据
### 8.2 数据完整性
1. **外键约束**:确保数据关联正确
2. **唯一约束**:防止重复数据
3. **检查约束**:确保数据有效性
```sql
-- 示例:检查约束
ALTER TABLE transactions
ADD CONSTRAINT check_transaction_amount
CHECK (amount != 0);
ALTER TABLE positions
ADD CONSTRAINT check_shares_positive
CHECK (shares >= 0);
```
---
## 九、总结
### 9.1 数据库选型
**推荐使用 PostgreSQL 14+**
优势:
- ✅ 关系型数据库,结构清晰
- ✅ JSONB支持灵活存储思考内容
- ✅ 强大的查询能力
- ✅ 全文搜索支持
- ✅ 时间序列查询性能好
### 9.2 核心表设计
1. **用户体系**users, user_settings
2. **账户体系**accounts, positions, cash_accounts
3. **交易体系**transactions, transaction_thoughts, transaction_reviews
4. **计划体系**trading_plans, plan_steps, plan_thoughts
5. **净值体系**daily_net_values, net_value_snapshots
6. **市场数据**market_prices
### 9.3 关键特性
1. **基金净值法**通过daily_net_values表记录每日净值
2. **思考记录**transaction_thoughts和plan_thoughts表
3. **复盘功能**transaction_reviews表支持多次复盘
4. **时间线查询**:通过索引优化时间线查询性能
### 9.4 后续优化
1. 根据实际使用情况调整索引
2. 监控慢查询,持续优化
3. 考虑使用TimescaleDB处理时间序列数据
4. 根据数据量考虑分表策略
---
**文档版本**v1.0
**创建日期**2024年
**数据库版本**PostgreSQL 14+

View File

@@ -0,0 +1,419 @@
# 设计优化评估:简化交易记录方案
## 一、优化方案概述
### 1.1 核心设计思路
**原设计(记录每次交易):**
- 用户每次买入/卖出都需要记录:价格、份额、费用、日期
- 系统自动计算加权平均成本价
- 保留完整的交易历史记录
**新设计(直接修改持仓):**
- 被动变更(分红、拆股、送股):系统自动完成 ✅
- 主动变更(买入、卖出、追加买入):用户直接修改成本价和份数
- 不记录每次交易的详细历史
### 1.2 用户场景分析
**目标用户:多券商用户**
- 在多个券商都有账户
- 需要汇总查看整体持仓和收益
- 不需要替代券商系统,只是汇总统计
**核心需求:**
- 简单快速更新持仓
- 自动计算收益和收益率
- 未来支持计划和复盘功能
---
## 二、方案评估
### 2.1 ✅ 优势分析
#### 1. **操作简单直接**
```
原设计:买入 → 输入价格、份额、费用 → 系统计算成本价
新设计:直接修改成本价和份数 → 完成
```
- ✅ 操作步骤减少 50% 以上
- ✅ 学习成本低,上手快
- ✅ 适合快速更新多券商汇总数据
#### 2. **符合多券商场景**
- ✅ 用户已经在券商系统完成交易,这里只是汇总
- ✅ 不需要重复录入交易细节
- ✅ 可以定期(如每周/每月)批量更新持仓
#### 3. **降低维护成本**
- ✅ 不需要记录每次交易的费用、时间等细节
- ✅ 减少数据录入错误
- ✅ 减少系统复杂度
#### 4. **灵活性更高**
- ✅ 用户可以手动调整成本价(如考虑交易费用后的实际成本)
- ✅ 可以快速修正错误
- ✅ 支持"模糊"记录(不需要精确到每笔交易)
### 2.2 ⚠️ 潜在问题分析
#### 1. **收益率计算准确性**
**问题:**
- 基金净值法需要知道每次资金流入流出的时间点
- 资金加权法IRR需要完整的现金流记录
- 如果只有持仓快照,无法准确计算时间加权收益率
**影响评估:**
- ⚠️ **时间加权收益率**:需要每日资产快照,如果只有持仓数据,可以计算,但精度可能受影响
- ⚠️ **资金加权收益率IRR**:需要现金流记录,如果只记录持仓,无法计算
-**累计收益率**:可以计算(当前资产 - 累计投入)
-**年化收益率**:可以计算(基于累计收益率)
**解决方案:**
- 保留"资金变动记录"(不是交易记录,而是资金流入流出)
- 记录:日期、金额、类型(投入/提取)
- 这样可以计算 IRR同时保持操作简单
#### 2. **计划和复盘功能的影响**
**问题:**
- PRD 中提到需要记录"交易思考"和"复盘"
- 如果只有持仓快照,如何关联思考和复盘?
**影响评估:**
- ⚠️ 无法关联到具体的某笔交易
- ✅ 可以关联到持仓(某只股票的整体思考)
- ✅ 可以记录时间点的思考(如"2024年1月加仓茅台"
**解决方案:**
- 持仓级别的思考记录(不是交易级别)
- 时间线记录(记录某个时间点的持仓变化和思考)
- 支持"持仓变更记录"(记录成本价和份数的变化,但不记录交易细节)
#### 3. **数据可追溯性**
**问题:**
- 如果只记录当前持仓,无法追溯历史
- 无法回答"什么时候买入的?"、"买入价格是多少?"
**影响评估:**
- ⚠️ 失去详细的交易历史
- ✅ 可以保留持仓变更历史(成本价和份数的变化)
- ✅ 对于多券商汇总场景,这个需求可能不是核心
**解决方案:**
- 保留"持仓变更记录"(简化版)
- 日期
- 变更类型(手动调整/买入/卖出)
- 变更前:成本价、份数
- 变更后:成本价、份数
- 可选:思考/备注
#### 4. **成本价计算的准确性**
**问题:**
- 用户手动输入成本价,可能不准确
- 多笔买入的加权平均成本价需要用户自己计算
**影响评估:**
- ⚠️ 用户需要自己计算加权平均成本价
- ⚠️ 可能输入错误
- ✅ 但用户可以根据券商系统的成本价直接输入
**解决方案:**
- 提供"成本价计算器"工具
- 支持批量导入(从券商系统导出后导入)
- 提供成本价验证提示
---
## 三、优化后的设计方案
### 3.1 数据模型调整
#### 持仓表Position- 保持不变
```typescript
interface Position {
id: string;
accountId: string;
symbol: string;
name: string;
shares: number; // 持仓份额
costPrice: number; // 成本价(用户直接修改)
currentPrice: number; // 当前价格(系统自动更新)
market: string;
currency: string;
status: string;
createdAt: Date;
updatedAt: Date;
}
```
#### 持仓变更记录表PositionChange- 新增
```typescript
interface PositionChange {
id: string;
positionId: string;
changeDate: Date; // 变更日期
changeType: 'manual' | 'buy' | 'sell' | 'auto'; // 变更类型
beforeShares: number; // 变更前份数
beforeCostPrice: number; // 变更前成本价
afterShares: number; // 变更后份数
afterCostPrice: number; // 变更后成本价
notes?: string; // 备注/思考
createdAt: Date;
}
```
#### 资金变动记录表CashFlow- 新增(用于计算 IRR
```typescript
interface CashFlow {
id: string;
accountId: string;
flowDate: Date; // 资金变动日期
flowType: 'deposit' | 'withdraw' | 'dividend' | 'interest'; // 类型
amount: number; // 金额(正数表示投入,负数表示提取)
currency: string;
notes?: string; // 备注
createdAt: Date;
}
```
#### 持仓思考表PositionThought- 新增(用于计划和复盘)
```typescript
interface PositionThought {
id: string;
positionId: string;
thoughtDate: Date; // 思考日期
thoughtType: 'plan' | 'review' | 'note'; // 类型
content: string; // 思考内容
createdAt: Date;
updatedAt: Date;
}
```
### 3.2 操作流程设计
#### 买入/卖出操作(简化版)
```
用户操作:
1. 打开持仓列表
2. 点击"编辑"或"调整"
3. 直接修改:
- 成本价(如:从 100 改为 95表示加仓后新的加权成本价
- 份数(如:从 100 股改为 150 股)
4. 可选:添加备注/思考
5. 保存
系统处理:
1. 记录变更前状态
2. 更新持仓(成本价、份数)
3. 记录持仓变更记录
4. 更新总资产和收益率
```
#### 被动变更(系统自动)
```
分红/拆股/送股:
1. 系统检测或用户触发
2. 自动计算新的成本价和份数
3. 更新持仓
4. 记录持仓变更记录changeType = 'auto'
```
### 3.3 收益率计算调整
#### 时间加权收益率(基金净值法)
**方案:基于每日资产快照**
```javascript
// 需要每日记录资产快照
interface DailySnapshot {
date: Date;
totalAsset: number; // 总资产
totalCost: number; // 总成本(累计投入)
netValue: number; // 单位净值
}
// 计算方式
function calculateTimeWeightedReturn(snapshots) {
let cumulativeReturn = 1;
for (let i = 1; i < snapshots.length; i++) {
const periodReturn = (snapshots[i].netValue - snapshots[i-1].netValue) / snapshots[i-1].netValue;
cumulativeReturn *= (1 + periodReturn);
}
return cumulativeReturn - 1;
}
```
**关键点:**
- ✅ 需要每日资产快照(系统自动生成)
- ✅ 不依赖交易记录,只依赖资产快照
- ✅ 可以准确计算
#### 资金加权收益率IRR
**方案:基于资金变动记录**
```javascript
// 使用 CashFlow 记录
function calculateIRR(cashFlows) {
// cashFlows: 资金流入流出记录
// 正数:投入资金
// 负数:提取资金
// 最后一条:当前资产价值(负数,表示"提取"
// 使用二分法或牛顿法求解 IRR
}
```
**关键点:**
- ✅ 需要记录资金变动(不是交易记录)
- ✅ 用户只需要记录:什么时候投入多少钱、什么时候提取多少钱
- ✅ 比记录每笔交易简单很多
---
## 四、方案对比总结
### 4.1 功能对比
| 功能 | 原设计(记录交易) | 新设计(直接修改) | 评估 |
|------|------------------|-------------------|------|
| 操作复杂度 | 高(每次交易需录入) | 低(直接修改) | ✅ 新设计更简单 |
| 数据准确性 | 高(系统计算) | 中(用户输入) | ⚠️ 需要验证 |
| 交易历史 | 完整 | 简化(变更记录) | ⚠️ 失去细节 |
| 收益率计算 | 精确 | 可接受 | ✅ 通过快照和资金流可计算 |
| 计划和复盘 | 可关联交易 | 可关联持仓 | ✅ 两种都支持 |
| 多券商场景 | 适合 | 更适合 | ✅ 新设计更适合 |
### 4.2 适用场景分析
**新设计更适合:**
- ✅ 多券商用户汇总统计
- ✅ 不需要详细交易历史的用户
- ✅ 希望快速更新持仓的用户
- ✅ 主要关注收益统计,不关注交易细节
**原设计更适合:**
- ✅ 需要完整交易历史的用户
- ✅ 需要精确计算每笔交易收益的用户
- ✅ 单券商用户可以对接券商API
### 4.3 推荐方案
**推荐采用新设计(直接修改持仓),但需要补充:**
1.**保留持仓变更记录**(简化版,不记录交易细节)
2.**保留资金变动记录**(用于计算 IRR
3.**每日资产快照**(用于计算时间加权收益率)
4.**持仓思考记录**(用于计划和复盘)
**这样既简化了操作,又保证了核心功能的实现。**
---
## 五、最终建议
### 5.1 核心设计原则
1. **被动变更 = 系统自动**
- 分红、拆股、送股等由系统自动处理
- 用户只需确认或触发
2. **主动变更 = 直接修改持仓**
- 买入/卖出:直接修改成本价和份数
- 可选:记录变更备注/思考
- 系统记录变更历史(简化版)
3. **资金变动 = 单独记录**
- 记录资金投入/提取(不是交易)
- 用于计算 IRR
- 操作简单(只需记录日期和金额)
4. **思考记录 = 关联持仓**
- 不是关联交易,而是关联持仓
- 支持时间线展示
- 支持计划和复盘
### 5.2 数据模型建议
**核心表:**
- `positions` - 持仓表(用户直接修改)
- `position_changes` - 持仓变更记录(系统自动记录)
- `cash_flows` - 资金变动记录(用户记录投入/提取)
- `daily_snapshots` - 每日资产快照(系统自动生成)
- `position_thoughts` - 持仓思考记录(用户记录思考)
**移除或简化:**
- ~~`transactions`~~ - 不再需要详细的交易记录表
- 或者保留但改为可选(高级用户可以使用)
### 5.3 用户体验优化
1. **提供成本价计算器**
- 用户输入多笔买入价格和份额
- 系统计算加权平均成本价
- 用户可以直接复制使用
2. **支持批量导入**
- 从券商系统导出持仓
- 批量导入到系统
- 减少手动输入
3. **变更历史展示**
- 展示持仓变更时间线
- 显示每次变更的成本价和份数变化
- 支持添加思考/备注
4. **智能提示**
- 成本价变化异常时提示
- 份数变化异常时提示
- 帮助用户发现输入错误
---
## 六、结论
### 6.1 方案评估结果
**✅ 新设计(直接修改持仓)更适合您的场景**
**理由:**
1. ✅ 操作简单,符合多券商汇总场景
2. ✅ 核心功能(收益计算)可以通过资产快照和资金流实现
3. ✅ 计划和复盘功能可以通过持仓思考记录实现
4. ✅ 用户体验更好,上手更快
**需要注意:**
1. ⚠️ 需要补充资金变动记录(用于 IRR 计算)
2. ⚠️ 需要每日资产快照(用于时间加权收益率)
3. ⚠️ 需要持仓变更记录(用于追溯和思考关联)
### 6.2 实施建议
**第一阶段:核心功能**
- 持仓直接编辑(成本价、份数)
- 持仓变更记录
- 每日资产快照
- 基础收益计算
**第二阶段:完善功能**
- 资金变动记录
- 时间加权收益率和 IRR 计算
- 持仓思考记录
**第三阶段:高级功能**
- 计划和复盘
- 批量导入
- 成本价计算器
---
**文档版本**v1.0
**创建日期**2024年
**评估结论**:✅ 推荐采用新设计,但需要补充资金流和快照功能

33
packages/design-ui/PRD.md Normal file
View File

@@ -0,0 +1,33 @@
## 核心思想
名称选择:思投录
愿景:让每笔投资都经得起思考
副标题:**投资决策与复盘工具**。
### 核心功能
#### 持仓
记录自己的股票持仓,统计各个股票持仓占比,按基金收益法统计的持仓收益。可以基于用户设置的单只股票仓位上限进行预警。
#### 交易计划
- “计划你的交易,交易你的计划”,让用户可以创建交易计划,选择股票、市场、目标价格、截止时间、投资金额或股份数(两者选一个即可)等。
- 之后到目标价后可以提醒用户。
- 用户可以为计划设置步骤,默认分三步进行买入等,可以分步设置买入价格。
#### 交易记录/复盘
- 引导用户记录每一笔交易,可以是从计划中点完成计划等操作跳转到记录页中,也可以是用户主动记录。用户可以记录每一笔交易的企业名称、买卖份数、买卖单价,最重要的是要引导用户写下买卖思考。
- 定期弹出页面应到用户写下复盘和思考。
- 用户可以参看每一笔交易的附带思考和复盘记录的时间线,让用户可以在交易中学习和成长,类似 QQ 空间的说说功能。
- 用户可以分享自己的交易时间线(不确定是否需要)。
#### 我的
不确定是否应该把 我的 作为一个 Tabbar。
- 展示一些我的信息。
- 最主要的是一些工具入口,例如 投资检查清单、复利计算器、估值工具(可以分老唐估值法和两段式现金流折现估值法)、自由目标等。
- 投资检查清单:可以分为买入和卖出两份检查清单,在每次设置交易计划时最后弹出,让用户每项检查。
- 复利计算器:侧重于投资者角度,可以填入初始金额,每年可投入的金额,调整预计的年复合增长率,然后查看未来的总收益。
- 自由目标:“让自己有的选”,自由目标可以和复利计算器和持仓结合起来,让用户自己设定一个总资产达成目标。把这个目标和财务自由、赎回自己的时间关联起来,让用户有“奔头”。
## App名称
中文名:思投录
英文名VestMind
“Vest”投资记录与管理和“Mind”思考与决策清单。用户能直观感受到这是一个与“投资”和“思考”相关的工具。
Mind”强调了投资不应是冲动行为而应是经过深思熟虑的、理性的“思维活动”这与你“让每笔投资都经得起思考”的愿景高度契合。
这个词组合起来听起来专业、简洁,且带有一种“智慧投资”的质感,能吸引那些希望提升自己投资决策质量的用户。
## 主题色
主题色需要偏紫色调,注重让人安静思考的色调。

View File

@@ -0,0 +1,124 @@
# 思投录 (VestMind) - 投资决策与复盘工具
## 项目概述
思投录是一款专注于投资决策与复盘的工具应用,旨在"让每笔投资都经得起思考"。本UI设计适用于移动端APP和小程序采用统一的紫色主题设计风格。
## 核心功能
### 1. 持仓管理
- 记录股票持仓信息
- 统计持仓占比和收益
- 基于仓位上限的预警功能
- 实时收益展示
### 2. 交易计划
- 创建详细的交易计划
- 设置目标价格和截止时间
- 分步骤买入计划
- 进度跟踪和提醒
### 3. 交易记录/复盘
- 记录每笔交易的详细信息
- 交易思考记录
- 时间线展示
- 定期复盘提醒
### 4. 我的工具
- 投资检查清单
- 复利计算器
- 估值工具(老唐估值法、现金流折现)
- 自由目标规划
## 设计特色
### 视觉设计
- **主题色**:紫色调(#8b5cf6, #7c3aed),营造安静思考的氛围
- **字体**Inter字体现代简洁
- **布局**:移动端优先,响应式设计
- **动效**:微妙的动画效果,提升用户体验
### 交互设计
- 底部导航栏,四个主要功能模块
- 模态框设计,支持复杂表单操作
- 卡片式布局,信息层次清晰
- 时间线展示,直观的交易历史
## 技术实现
### 文件结构
```
design-vest-mind/
├── index.html # 主页面
├── styles.css # 样式文件
├── script.js # 交互逻辑
└── README.md # 项目说明
```
### 技术栈
- **HTML5**:语义化标签,良好的可访问性
- **CSS3**Flexbox布局CSS Grid动画效果
- **JavaScript**ES6+语法,模块化设计
- **响应式设计**:适配不同屏幕尺寸
### 兼容性
- 移动端浏览器
- 微信小程序
- iOS Safari
- Android Chrome
## 使用说明
1. 打开 `index.html` 文件即可查看完整UI设计
2. 支持所有交互功能,包括:
- 页面切换
- 数据展示
- 模态框操作
- 工具计算
## 设计亮点
### 1. 紫色主题
- 主色调采用紫色渐变,符合"安静思考"的设计理念
- 渐变背景和阴影效果,营造层次感
### 2. 卡片设计
- 圆角卡片布局,现代简洁
- 悬停效果和阴影,增强交互反馈
### 3. 数据可视化
- 进度条展示计划完成度
- 颜色编码区分盈亏状态
- 时间线展示交易历史
### 4. 工具集成
- 投资检查清单,帮助理性决策
- 复利计算器,规划长期投资
- 估值工具,辅助价值判断
- 自由目标,激励投资动力
## 扩展功能
### 可添加的功能
- 数据持久化LocalStorage
- 图表展示Chart.js
- 推送通知
- 数据导入导出
- 多账户管理
### 小程序适配
- 使用微信小程序组件
- 适配小程序生命周期
- 集成微信API
## 开发建议
1. **数据管理**建议使用状态管理库如Vuex、Redux
2. **图表展示**集成ECharts或Chart.js
3. **数据持久化**使用IndexedDB或云数据库
4. **性能优化**:懒加载、虚拟滚动
5. **测试**:单元测试和端到端测试
## 总结
本UI设计完全基于PRD文档要求实现了思投录应用的核心功能界面。设计风格现代简洁交互流畅自然完全适配移动端和小程序使用场景。紫色主题营造了安静思考的投资氛围符合"让每笔投资都经得起思考"的产品愿景。

View File

@@ -0,0 +1,229 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>思投录 - 投资决策与复盘工具</title>
<link rel="stylesheet" href="styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<!-- 主容器 -->
<div class="app-container">
<!-- 顶部导航栏 -->
<header class="header">
<div class="header-content">
<h1 class="app-title">思投录</h1>
<p class="app-subtitle">让每笔投资都经得起思考</p>
</div>
</header>
<!-- 主要内容区域 -->
<main class="main-content">
<!-- 持仓概览卡片 -->
<section class="overview-card">
<div class="card-header">
<h2>持仓概览</h2>
<span class="total-value">¥128,450.00</span>
</div>
<div class="portfolio-summary">
<div class="summary-item">
<span class="label">今日收益</span>
<span class="value positive">+¥1,250.00</span>
</div>
<div class="summary-item">
<span class="label">总收益率</span>
<span class="value positive">+12.5%</span>
</div>
</div>
</section>
<!-- 底部导航栏 -->
<nav class="bottom-nav">
<div class="nav-item active" data-tab="portfolio">
<div class="nav-icon">📊</div>
<span>持仓</span>
</div>
<div class="nav-item" data-tab="plans">
<div class="nav-icon">📋</div>
<span>计划</span>
</div>
<div class="nav-item" data-tab="records">
<div class="nav-icon">📝</div>
<span>记录</span>
</div>
<div class="nav-item" data-tab="tools">
<div class="nav-icon">🛠️</div>
<span>我的</span>
</div>
</nav>
</main>
<!-- 持仓页面 -->
<div class="page" id="portfolio-page">
<div class="page-header">
<h2>我的持仓</h2>
<button class="add-btn">+ 添加</button>
</div>
<div class="holdings-list">
<div class="holding-item">
<div class="stock-info">
<div class="stock-name">贵州茅台</div>
<div class="stock-code">600519</div>
</div>
<div class="holding-details">
<div class="shares">100股</div>
<div class="current-price">¥1,850.00</div>
<div class="profit positive">+¥2,500.00 (+15.6%)</div>
</div>
</div>
<div class="holding-item">
<div class="stock-info">
<div class="stock-name">腾讯控股</div>
<div class="stock-code">00700</div>
</div>
<div class="holding-details">
<div class="shares">200股</div>
<div class="current-price">¥320.00</div>
<div class="profit negative">-¥800.00 (-1.2%)</div>
</div>
</div>
</div>
</div>
<!-- 交易计划页面 -->
<div class="page hidden" id="plans-page">
<div class="page-header">
<h2>交易计划</h2>
<button class="add-btn">+ 新建</button>
</div>
<div class="plans-list">
<div class="plan-item">
<div class="plan-header">
<div class="stock-info">
<div class="stock-name">招商银行</div>
<div class="stock-code">600036</div>
</div>
<div class="plan-status pending">进行中</div>
</div>
<div class="plan-details">
<div class="plan-row">
<span class="label">目标价格:</span>
<span class="value">¥45.00</span>
</div>
<div class="plan-row">
<span class="label">计划金额:</span>
<span class="value">¥10,000</span>
</div>
<div class="plan-row">
<span class="label">截止时间:</span>
<span class="value">2024-03-15</span>
</div>
</div>
<div class="plan-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: 60%"></div>
</div>
<span class="progress-text">已完成 60%</span>
</div>
</div>
</div>
</div>
<!-- 交易记录页面 -->
<div class="page hidden" id="records-page">
<div class="page-header">
<h2>交易记录</h2>
<button class="add-btn">+ 记录</button>
</div>
<div class="timeline">
<div class="timeline-item">
<div class="timeline-date">2024-01-15</div>
<div class="timeline-content">
<div class="transaction-card">
<div class="transaction-header">
<div class="stock-info">
<div class="stock-name">贵州茅台</div>
<div class="stock-code">600519</div>
</div>
<div class="transaction-type buy">买入</div>
</div>
<div class="transaction-details">
<div class="detail-row">
<span>数量:</span>
<span>100股</span>
</div>
<div class="detail-row">
<span>价格:</span>
<span>¥1,600.00</span>
</div>
</div>
<div class="transaction-thoughts">
<h4>交易思考:</h4>
<p>基于茅台品牌价值和长期增长潜力,认为当前价格具有投资价值。白酒行业龙头地位稳固,现金流优秀。</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 我的页面 -->
<div class="page hidden" id="tools-page">
<div class="page-header">
<h2>我的工具</h2>
</div>
<div class="tools-grid">
<div class="tool-card" data-tool="checklist">
<div class="tool-icon"></div>
<div class="tool-name">投资检查清单</div>
<div class="tool-desc">买入卖出检查项</div>
</div>
<div class="tool-card" data-tool="calculator">
<div class="tool-icon">🧮</div>
<div class="tool-name">复利计算器</div>
<div class="tool-desc">计算未来收益</div>
</div>
<div class="tool-card" data-tool="valuation">
<div class="tool-icon">📈</div>
<div class="tool-name">估值工具</div>
<div class="tool-desc">企业价值评估</div>
</div>
<div class="tool-card" data-tool="freedom">
<div class="tool-icon">🎯</div>
<div class="tool-name">自由目标</div>
<div class="tool-desc">财务自由规划</div>
</div>
</div>
</div>
</div>
<!-- 模态框 -->
<div class="modal" id="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">标题</h3>
<button class="close-btn" id="close-modal">&times;</button>
</div>
<div class="modal-body" id="modal-body">
<!-- 动态内容 -->
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,851 @@
// 思投录 - 投资决策与复盘工具
class VestMindApp {
constructor() {
this.currentTab = 'portfolio';
this.modal = document.getElementById('modal');
this.modalTitle = document.getElementById('modal-title');
this.modalBody = document.getElementById('modal-body');
this.closeModalBtn = document.getElementById('close-modal');
this.init();
}
init() {
this.bindEvents();
this.showPage('portfolio');
this.loadData();
}
bindEvents() {
// 底部导航栏事件
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', (e) => {
const tab = e.currentTarget.dataset.tab;
this.switchTab(tab);
});
});
// 模态框关闭事件
this.closeModalBtn.addEventListener('click', () => {
this.hideModal();
});
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) {
this.hideModal();
}
});
// 工具卡片点击事件
document.querySelectorAll('.tool-card').forEach(card => {
card.addEventListener('click', (e) => {
const tool = e.currentTarget.dataset.tool;
this.openTool(tool);
});
});
// 添加按钮事件
document.querySelectorAll('.add-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const page = e.currentTarget.closest('.page').id;
this.showAddModal(page);
});
});
}
switchTab(tab) {
// 更新导航栏状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
// 显示对应页面
this.showPage(tab);
this.currentTab = tab;
}
showPage(pageId) {
// 隐藏所有页面
document.querySelectorAll('.page').forEach(page => {
page.classList.add('hidden');
});
// 显示目标页面
const targetPage = document.getElementById(`${pageId}-page`);
if (targetPage) {
targetPage.classList.remove('hidden');
}
}
loadData() {
// 模拟数据加载
this.loadPortfolioData();
this.loadPlansData();
this.loadRecordsData();
}
loadPortfolioData() {
// 模拟持仓数据
const holdings = [
{
name: '贵州茅台',
code: '600519',
shares: 100,
currentPrice: 1850.00,
profit: 2500.00,
profitRate: 15.6
},
{
name: '腾讯控股',
code: '00700',
shares: 200,
currentPrice: 320.00,
profit: -800.00,
profitRate: -1.2
},
{
name: '招商银行',
code: '600036',
shares: 500,
currentPrice: 42.50,
profit: 1250.00,
profitRate: 6.2
}
];
this.renderHoldings(holdings);
}
renderHoldings(holdings) {
const container = document.querySelector('.holdings-list');
if (!container) return;
container.innerHTML = holdings.map(holding => `
<div class="holding-item">
<div class="stock-info">
<div class="stock-name">${holding.name}</div>
<div class="stock-code">${holding.code}</div>
</div>
<div class="holding-details">
<div class="shares">${holding.shares}股</div>
<div class="current-price">¥${holding.currentPrice.toFixed(2)}</div>
<div class="profit ${holding.profit >= 0 ? 'positive' : 'negative'}">
${holding.profit >= 0 ? '+' : ''}¥${holding.profit.toFixed(2)} (${holding.profitRate >= 0 ? '+' : ''}${holding.profitRate}%)
</div>
</div>
</div>
`).join('');
}
loadPlansData() {
// 模拟交易计划数据
const plans = [
{
name: '招商银行',
code: '600036',
targetPrice: 45.00,
amount: 10000,
deadline: '2024-03-15',
progress: 60,
status: 'pending'
},
{
name: '中国平安',
code: '601318',
targetPrice: 55.00,
amount: 15000,
deadline: '2024-04-20',
progress: 30,
status: 'pending'
}
];
this.renderPlans(plans);
}
renderPlans(plans) {
const container = document.querySelector('.plans-list');
if (!container) return;
container.innerHTML = plans.map(plan => `
<div class="plan-item">
<div class="plan-header">
<div class="stock-info">
<div class="stock-name">${plan.name}</div>
<div class="stock-code">${plan.code}</div>
</div>
<div class="plan-status ${plan.status}">进行中</div>
</div>
<div class="plan-details">
<div class="plan-row">
<span class="label">目标价格:</span>
<span class="value">¥${plan.targetPrice.toFixed(2)}</span>
</div>
<div class="plan-row">
<span class="label">计划金额:</span>
<span class="value">¥${plan.amount.toLocaleString()}</span>
</div>
<div class="plan-row">
<span class="label">截止时间:</span>
<span class="value">${plan.deadline}</span>
</div>
</div>
<div class="plan-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${plan.progress}%"></div>
</div>
<span class="progress-text">已完成 ${plan.progress}%</span>
</div>
</div>
`).join('');
}
loadRecordsData() {
// 模拟交易记录数据
const records = [
{
date: '2024-01-15',
type: 'buy',
name: '贵州茅台',
code: '600519',
shares: 100,
price: 1600.00,
thoughts: '基于茅台品牌价值和长期增长潜力,认为当前价格具有投资价值。白酒行业龙头地位稳固,现金流优秀。'
},
{
date: '2024-01-10',
type: 'sell',
name: '比亚迪',
code: '002594',
shares: 200,
price: 280.00,
thoughts: '新能源汽车行业竞争加剧,估值偏高,选择获利了结。'
}
];
this.renderRecords(records);
}
renderRecords(records) {
const container = document.querySelector('.timeline');
if (!container) return;
container.innerHTML = records.map(record => `
<div class="timeline-item">
<div class="timeline-date">${record.date}</div>
<div class="timeline-content">
<div class="transaction-card">
<div class="transaction-header">
<div class="stock-info">
<div class="stock-name">${record.name}</div>
<div class="stock-code">${record.code}</div>
</div>
<div class="transaction-type ${record.type}">${record.type === 'buy' ? '买入' : '卖出'}</div>
</div>
<div class="transaction-details">
<div class="detail-row">
<span>数量:</span>
<span>${record.shares}股</span>
</div>
<div class="detail-row">
<span>价格:</span>
<span>¥${record.price.toFixed(2)}</span>
</div>
</div>
<div class="transaction-thoughts">
<h4>交易思考:</h4>
<p>${record.thoughts}</p>
</div>
</div>
</div>
</div>
`).join('');
}
openTool(tool) {
switch (tool) {
case 'checklist':
this.showChecklistModal();
break;
case 'calculator':
this.showCalculatorModal();
break;
case 'valuation':
this.showValuationModal();
break;
case 'freedom':
this.showFreedomModal();
break;
}
}
showChecklistModal() {
this.modalTitle.textContent = '投资检查清单';
this.modalBody.innerHTML = `
<div class="checklist-container">
<div class="checklist-section">
<h4>买入检查清单</h4>
<div class="checklist-items">
<label class="checklist-item">
<input type="checkbox">
<span>企业基本面是否优秀?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>估值是否合理?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>行业前景如何?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>管理层是否可信?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>现金流是否健康?</span>
</label>
</div>
</div>
<div class="checklist-section">
<h4>卖出检查清单</h4>
<div class="checklist-items">
<label class="checklist-item">
<input type="checkbox">
<span>基本面是否恶化?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>估值是否过高?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>是否有更好的投资机会?</span>
</label>
<label class="checklist-item">
<input type="checkbox">
<span>是否需要资金配置?</span>
</label>
</div>
</div>
</div>
`;
this.showModal();
}
showCalculatorModal() {
this.modalTitle.textContent = '复利计算器';
this.modalBody.innerHTML = `
<div class="calculator-container">
<div class="input-group">
<label>初始金额(元)</label>
<input type="number" id="initial-amount" placeholder="100000" value="100000">
</div>
<div class="input-group">
<label>每年投入(元)</label>
<input type="number" id="annual-investment" placeholder="50000" value="50000">
</div>
<div class="input-group">
<label>年复合增长率(%</label>
<input type="number" id="growth-rate" placeholder="10" value="10" step="0.1">
</div>
<div class="input-group">
<label>投资年限</label>
<input type="number" id="years" placeholder="10" value="10">
</div>
<button class="calculate-btn" onclick="app.calculateCompound()">计算</button>
<div class="result-container" id="calculator-result" style="display: none;">
<h4>计算结果</h4>
<div class="result-item">
<span>总投入:</span>
<span id="total-investment">¥0</span>
</div>
<div class="result-item">
<span>最终金额:</span>
<span id="final-amount">¥0</span>
</div>
<div class="result-item">
<span>总收益:</span>
<span id="total-profit">¥0</span>
</div>
</div>
</div>
`;
this.showModal();
}
calculateCompound() {
const initialAmount = parseFloat(document.getElementById('initial-amount').value) || 0;
const annualInvestment = parseFloat(document.getElementById('annual-investment').value) || 0;
const growthRate = parseFloat(document.getElementById('growth-rate').value) || 0;
const years = parseInt(document.getElementById('years').value) || 0;
const rate = growthRate / 100;
let totalInvestment = initialAmount + annualInvestment * years;
let finalAmount = initialAmount * Math.pow(1 + rate, years);
// 计算每年投入的复利
for (let i = 1; i <= years; i++) {
finalAmount += annualInvestment * Math.pow(1 + rate, years - i);
}
const totalProfit = finalAmount - totalInvestment;
document.getElementById('total-investment').textContent = `¥${totalInvestment.toLocaleString()}`;
document.getElementById('final-amount').textContent = `¥${finalAmount.toLocaleString()}`;
document.getElementById('total-profit').textContent = `¥${totalProfit.toLocaleString()}`;
document.getElementById('calculator-result').style.display = 'block';
}
showValuationModal() {
this.modalTitle.textContent = '估值工具';
this.modalBody.innerHTML = `
<div class="valuation-container">
<div class="valuation-tabs">
<button class="tab-btn active" data-method="tang">老唐估值法</button>
<button class="tab-btn" data-method="dcf">现金流折现</button>
</div>
<div class="valuation-content">
<div class="method-content" id="tang-method">
<div class="input-group">
<label>净利润(亿元)</label>
<input type="number" id="net-profit" placeholder="100" value="100">
</div>
<div class="input-group">
<label>无风险收益率(%</label>
<input type="number" id="risk-free-rate" placeholder="3" value="3" step="0.1">
</div>
<div class="input-group">
<label>合理PE倍数</label>
<input type="number" id="pe-ratio" placeholder="25" value="25">
</div>
<button class="calculate-btn" onclick="app.calculateTangValuation()">计算估值</button>
<div class="result-container" id="tang-result" style="display: none;">
<h4>估值结果</h4>
<div class="result-item">
<span>合理估值:</span>
<span id="tang-value">¥0亿</span>
</div>
</div>
</div>
</div>
</div>
`;
this.showModal();
}
calculateTangValuation() {
const netProfit = parseFloat(document.getElementById('net-profit').value) || 0;
const riskFreeRate = parseFloat(document.getElementById('risk-free-rate').value) || 0;
const peRatio = parseFloat(document.getElementById('pe-ratio').value) || 0;
const reasonablePE = 1 / (riskFreeRate / 100);
const valuation = netProfit * Math.min(peRatio, reasonablePE);
document.getElementById('tang-value').textContent = `¥${valuation.toFixed(2)}亿`;
document.getElementById('tang-result').style.display = 'block';
}
showFreedomModal() {
this.modalTitle.textContent = '自由目标';
this.modalBody.innerHTML = `
<div class="freedom-container">
<div class="input-group">
<label>目标资产(万元)</label>
<input type="number" id="target-assets" placeholder="1000" value="1000">
</div>
<div class="input-group">
<label>当前资产(万元)</label>
<input type="number" id="current-assets" placeholder="100" value="100">
</div>
<div class="input-group">
<label>年复合增长率(%</label>
<input type="number" id="freedom-growth-rate" placeholder="10" value="10" step="0.1">
</div>
<div class="input-group">
<label>每年投入(万元)</label>
<input type="number" id="freedom-annual-investment" placeholder="20" value="20">
</div>
<button class="calculate-btn" onclick="app.calculateFreedom()">计算达成时间</button>
<div class="result-container" id="freedom-result" style="display: none;">
<h4>自由目标分析</h4>
<div class="result-item">
<span>预计达成时间:</span>
<span id="freedom-years">0年</span>
</div>
<div class="result-item">
<span>届时年龄:</span>
<span id="freedom-age">0岁</span>
</div>
<div class="result-item">
<span>总投入:</span>
<span id="freedom-total-investment">¥0万</span>
</div>
</div>
</div>
`;
this.showModal();
}
calculateFreedom() {
const targetAssets = parseFloat(document.getElementById('target-assets').value) || 0;
const currentAssets = parseFloat(document.getElementById('current-assets').value) || 0;
const growthRate = parseFloat(document.getElementById('freedom-growth-rate').value) || 0;
const annualInvestment = parseFloat(document.getElementById('freedom-annual-investment').value) || 0;
const rate = growthRate / 100;
let years = 0;
let assets = currentAssets;
// 模拟逐年增长
while (assets < targetAssets && years < 50) {
assets = assets * (1 + rate) + annualInvestment;
years++;
}
const totalInvestment = currentAssets + annualInvestment * years;
const currentAge = 30; // 假设当前年龄
const targetAge = currentAge + years;
document.getElementById('freedom-years').textContent = `${years}`;
document.getElementById('freedom-age').textContent = `${targetAge}`;
document.getElementById('freedom-total-investment').textContent = `¥${totalInvestment.toFixed(2)}`;
document.getElementById('freedom-result').style.display = 'block';
}
showAddModal(page) {
switch (page) {
case 'portfolio-page':
this.showAddHoldingModal();
break;
case 'plans-page':
this.showAddPlanModal();
break;
case 'records-page':
this.showAddRecordModal();
break;
}
}
showAddHoldingModal() {
this.modalTitle.textContent = '添加持仓';
this.modalBody.innerHTML = `
<div class="form-container">
<div class="input-group">
<label>股票名称</label>
<input type="text" id="stock-name" placeholder="请输入股票名称">
</div>
<div class="input-group">
<label>股票代码</label>
<input type="text" id="stock-code" placeholder="请输入股票代码">
</div>
<div class="input-group">
<label>持股数量</label>
<input type="number" id="shares" placeholder="请输入持股数量">
</div>
<div class="input-group">
<label>成本价格</label>
<input type="number" id="cost-price" placeholder="请输入成本价格" step="0.01">
</div>
<button class="submit-btn" onclick="app.addHolding()">添加持仓</button>
</div>
`;
this.showModal();
}
showAddPlanModal() {
this.modalTitle.textContent = '新建交易计划';
this.modalBody.innerHTML = `
<div class="form-container">
<div class="input-group">
<label>股票名称</label>
<input type="text" id="plan-stock-name" placeholder="请输入股票名称">
</div>
<div class="input-group">
<label>股票代码</label>
<input type="text" id="plan-stock-code" placeholder="请输入股票代码">
</div>
<div class="input-group">
<label>目标价格</label>
<input type="number" id="target-price" placeholder="请输入目标价格" step="0.01">
</div>
<div class="input-group">
<label>计划金额</label>
<input type="number" id="plan-amount" placeholder="请输入计划金额">
</div>
<div class="input-group">
<label>截止时间</label>
<input type="date" id="deadline">
</div>
<button class="submit-btn" onclick="app.addPlan()">创建计划</button>
</div>
`;
this.showModal();
}
showAddRecordModal() {
this.modalTitle.textContent = '记录交易';
this.modalBody.innerHTML = `
<div class="form-container">
<div class="input-group">
<label>交易类型</label>
<select id="transaction-type">
<option value="buy">买入</option>
<option value="sell">卖出</option>
</select>
</div>
<div class="input-group">
<label>股票名称</label>
<input type="text" id="record-stock-name" placeholder="请输入股票名称">
</div>
<div class="input-group">
<label>股票代码</label>
<input type="text" id="record-stock-code" placeholder="请输入股票代码">
</div>
<div class="input-group">
<label>交易数量</label>
<input type="number" id="record-shares" placeholder="请输入交易数量">
</div>
<div class="input-group">
<label>交易价格</label>
<input type="number" id="record-price" placeholder="请输入交易价格" step="0.01">
</div>
<div class="input-group">
<label>交易思考</label>
<textarea id="transaction-thoughts" placeholder="请记录您的交易思考..." rows="4"></textarea>
</div>
<button class="submit-btn" onclick="app.addRecord()">记录交易</button>
</div>
`;
this.showModal();
}
addHolding() {
// 模拟添加持仓
this.showToast('持仓添加成功!');
this.hideModal();
}
addPlan() {
// 模拟添加计划
this.showToast('交易计划创建成功!');
this.hideModal();
}
addRecord() {
// 模拟添加记录
this.showToast('交易记录添加成功!');
this.hideModal();
}
showModal() {
this.modal.classList.add('show');
document.body.style.overflow = 'hidden';
}
hideModal() {
this.modal.classList.remove('show');
document.body.style.overflow = 'auto';
}
showToast(message) {
// 创建提示框
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
toast.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease;
`;
document.body.appendChild(toast);
// 显示动画
setTimeout(() => {
toast.style.opacity = '1';
}, 100);
// 自动隐藏
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(toast);
}, 300);
}, 2000);
}
}
// 初始化应用
const app = new VestMindApp();
// 添加一些额外的样式
const additionalStyles = `
<style>
.checklist-container {
max-height: 400px;
overflow-y: auto;
}
.checklist-section {
margin-bottom: 20px;
}
.checklist-section h4 {
font-size: 16px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e5e7eb;
}
.checklist-item {
display: flex;
align-items: center;
padding: 8px 0;
cursor: pointer;
font-size: 14px;
color: #374151;
}
.checklist-item input[type="checkbox"] {
margin-right: 12px;
width: 16px;
height: 16px;
accent-color: #8b5cf6;
}
.calculator-container,
.valuation-container,
.freedom-container,
.form-container {
max-height: 400px;
overflow-y: auto;
}
.input-group {
margin-bottom: 16px;
}
.input-group label {
display: block;
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 6px;
}
.input-group input,
.input-group select,
.input-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.input-group input:focus,
.input-group select:focus,
.input-group textarea:focus {
outline: none;
border-color: #8b5cf6;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1);
}
.calculate-btn,
.submit-btn {
width: 100%;
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
border: none;
border-radius: 8px;
padding: 12px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 8px;
}
.calculate-btn:hover,
.submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}
.result-container {
margin-top: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border-left: 3px solid #8b5cf6;
}
.result-container h4 {
font-size: 16px;
font-weight: 600;
color: #374151;
margin-bottom: 12px;
}
.result-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
}
.result-item span:first-child {
color: #6b7280;
}
.result-item span:last-child {
font-weight: 600;
color: #374151;
}
.valuation-tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #e5e7eb;
}
.tab-btn {
flex: 1;
padding: 10px;
background: none;
border: none;
font-size: 14px;
font-weight: 500;
color: #6b7280;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.3s ease;
}
.tab-btn.active {
color: #8b5cf6;
border-bottom-color: #8b5cf6;
}
.tab-btn:hover {
color: #8b5cf6;
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', additionalStyles);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,702 @@
/* 基础样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
overflow-x: hidden;
}
/* 主容器 */
.app-container {
max-width: 414px;
margin: 0 auto;
min-height: 100vh;
background: #f8f9fa;
position: relative;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
/* 顶部导航栏 */
.header {
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
padding: 20px;
text-align: center;
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
50% { transform: translate(-50%, -50%) rotate(180deg); }
}
.header-content {
position: relative;
z-index: 1;
}
.app-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 4px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.app-subtitle {
font-size: 14px;
opacity: 0.9;
font-weight: 300;
}
/* 主要内容区域 */
.main-content {
padding: 20px;
padding-bottom: 100px;
}
/* 概览卡片 */
.overview-card {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.card-header h2 {
font-size: 18px;
font-weight: 600;
color: #374151;
}
.total-value {
font-size: 24px;
font-weight: 700;
color: #8b5cf6;
}
.portfolio-summary {
display: flex;
gap: 20px;
}
.summary-item {
flex: 1;
text-align: center;
}
.summary-item .label {
display: block;
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.summary-item .value {
display: block;
font-size: 16px;
font-weight: 600;
}
.summary-item .value.positive {
color: #10b981;
}
.summary-item .value.negative {
color: #ef4444;
}
/* 底部导航栏 */
.bottom-nav {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 414px;
background: white;
border-top: 1px solid #e5e7eb;
display: flex;
padding: 8px 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 4px;
cursor: pointer;
transition: all 0.3s ease;
border-radius: 8px;
margin: 0 4px;
}
.nav-item:hover {
background: rgba(139, 92, 246, 0.1);
}
.nav-item.active {
background: rgba(139, 92, 246, 0.1);
color: #8b5cf6;
}
.nav-icon {
font-size: 20px;
margin-bottom: 4px;
}
.nav-item span {
font-size: 12px;
font-weight: 500;
}
/* 页面样式 */
.page {
padding: 20px;
padding-bottom: 100px;
min-height: calc(100vh - 120px);
}
.page.hidden {
display: none;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-header h2 {
font-size: 20px;
font-weight: 600;
color: #374151;
}
.add-btn {
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
border: none;
border-radius: 20px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.add-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
}
/* 持仓列表 */
.holdings-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.holding-item {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #f3f4f6;
transition: all 0.3s ease;
}
.holding-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.stock-info {
margin-bottom: 8px;
}
.stock-name {
font-size: 16px;
font-weight: 600;
color: #374151;
margin-bottom: 2px;
}
.stock-code {
font-size: 12px;
color: #6b7280;
}
.holding-details {
display: flex;
justify-content: space-between;
align-items: center;
}
.shares {
font-size: 14px;
color: #6b7280;
}
.current-price {
font-size: 16px;
font-weight: 600;
color: #374151;
}
.profit {
font-size: 14px;
font-weight: 500;
text-align: right;
}
.profit.positive {
color: #10b981;
}
.profit.negative {
color: #ef4444;
}
/* 计划列表 */
.plans-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.plan-item {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #f3f4f6;
}
.plan-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.plan-status {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.plan-status.pending {
background: rgba(251, 191, 36, 0.1);
color: #f59e0b;
}
.plan-status.completed {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.plan-details {
margin-bottom: 12px;
}
.plan-row {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
font-size: 14px;
}
.plan-row .label {
color: #6b7280;
}
.plan-row .value {
font-weight: 500;
color: #374151;
}
.plan-progress {
display: flex;
align-items: center;
gap: 8px;
}
.progress-bar {
flex: 1;
height: 6px;
background: #e5e7eb;
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #8b5cf6 0%, #7c3aed 100%);
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 12px;
color: #6b7280;
white-space: nowrap;
}
/* 时间线 */
.timeline {
position: relative;
padding-left: 20px;
}
.timeline::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(180deg, #8b5cf6 0%, #7c3aed 100%);
}
.timeline-item {
position: relative;
margin-bottom: 24px;
}
.timeline-item::before {
content: '';
position: absolute;
left: -16px;
top: 8px;
width: 12px;
height: 12px;
background: #8b5cf6;
border-radius: 50%;
border: 3px solid white;
box-shadow: 0 0 0 2px #8b5cf6;
}
.timeline-date {
font-size: 12px;
color: #6b7280;
margin-bottom: 8px;
font-weight: 500;
}
.timeline-content {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #f3f4f6;
}
.transaction-card {
width: 100%;
}
.transaction-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.transaction-type {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.transaction-type.buy {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.transaction-type.sell {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.transaction-details {
margin-bottom: 12px;
}
.detail-row {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
font-size: 14px;
}
.transaction-thoughts {
background: #f8f9fa;
border-radius: 8px;
padding: 12px;
border-left: 3px solid #8b5cf6;
}
.transaction-thoughts h4 {
font-size: 14px;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.transaction-thoughts p {
font-size: 13px;
color: #6b7280;
line-height: 1.5;
}
/* 工具网格 */
.tools-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.tool-card {
background: white;
border-radius: 12px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #f3f4f6;
cursor: pointer;
transition: all 0.3s ease;
}
.tool-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(139, 92, 246, 0.15);
border-color: #8b5cf6;
}
.tool-icon {
font-size: 32px;
margin-bottom: 12px;
}
.tool-name {
font-size: 16px;
font-weight: 600;
color: #374151;
margin-bottom: 4px;
}
.tool-desc {
font-size: 12px;
color: #6b7280;
}
/* 模态框 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.modal.show {
opacity: 1;
visibility: visible;
}
.modal-content {
background: white;
border-radius: 16px;
width: 90%;
max-width: 400px;
max-height: 80vh;
overflow-y: auto;
transform: scale(0.9);
transition: transform 0.3s ease;
}
.modal.show .modal-content {
transform: scale(1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e5e7eb;
}
.modal-header h3 {
font-size: 18px;
font-weight: 600;
color: #374151;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
color: #6b7280;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s ease;
}
.close-btn:hover {
background: #f3f4f6;
color: #374151;
}
.modal-body {
padding: 20px;
}
/* 响应式设计 */
@media (max-width: 375px) {
.app-container {
max-width: 100%;
}
.main-content {
padding: 16px;
}
.page {
padding: 16px;
}
.tools-grid {
grid-template-columns: 1fr;
}
}
/* 小程序适配 */
@media (max-width: 320px) {
.app-title {
font-size: 24px;
}
.nav-item span {
font-size: 11px;
}
.nav-icon {
font-size: 18px;
}
}
/* 动画效果 */
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.holding-item,
.plan-item,
.timeline-content,
.tool-card {
animation: slideInUp 0.3s ease forwards;
}
/* 加载状态 */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
color: #6b7280;
}
.loading::after {
content: '';
width: 20px;
height: 20px;
border: 2px solid #e5e7eb;
border-top: 2px solid #8b5cf6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 40px 20px;
color: #6b7280;
}
.empty-state-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-state-text {
font-size: 16px;
margin-bottom: 8px;
}
.empty-state-desc {
font-size: 14px;
opacity: 0.7;
}