diff --git a/apps/api/.env.development b/apps/api/.env.development index b4412a3..ed95045 100644 --- a/apps/api/.env.development +++ b/apps/api/.env.development @@ -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) \ No newline at end of file +STORAGE_BASE_URL=http://localhost:3201/uploads # 访问URL(默认:http://localhost:3201/uploads) \ No newline at end of file diff --git a/apps/web/.env.development b/apps/web/.env.development index 2da767c..8b67e70 100644 --- a/apps/web/.env.development +++ b/apps/web/.env.development @@ -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=投小记 diff --git a/apps/web/src/router/index.tsx b/apps/web/src/router/index.tsx index f6f4e94..5e2f23d 100644 --- a/apps/web/src/router/index.tsx +++ b/apps/web/src/router/index.tsx @@ -15,7 +15,7 @@ const StockDailyPricePage = lazy(() => import('@/pages/stock-daily-price')); export const router = createBrowserRouter([ { - path: '/', + path: '/login', element: , errorElement: , }, diff --git a/packages/design-document/我编写的文档/付费订阅数据库设计.md b/packages/design-document/我编写的文档/付费订阅数据库设计.md new file mode 100644 index 0000000..2337e07 --- /dev/null +++ b/packages/design-document/我编写的文档/付费订阅数据库设计.md @@ -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'; + ``` + diff --git a/packages/design-document/我编写的文档/投资记录模块-产品设计.md b/packages/design-document/我编写的文档/投资记录模块-产品设计.md new file mode 100644 index 0000000..56f7dfb --- /dev/null +++ b/packages/design-document/我编写的文档/投资记录模块-产品设计.md @@ -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、市场。 +之后展示成本价输入框和份数输入框。(输入时要考虑便捷性) + +基金: +不展示搜索框,仅展示输入框,需要用户自己输入。 +之后展示成本价输入框和份数输入框。(输入时要考虑便捷性) + +现金和其他: +不展示搜索框,仅展示输入框,需要用户自己输入资产名称。 +直接输入金额即可。 + +确认按钮:点击确认,提交数据到后台,按钮居中展示。 + +**变更资产页** +最上面不需要在选择资产类型,下边的展示和`新增资产页` 一样。 + + diff --git a/packages/design-document/我编写的文档/数据库设计.md b/packages/design-document/我编写的文档/数据库设计.md new file mode 100644 index 0000000..0c045d5 --- /dev/null +++ b/packages/design-document/我编写的文档/数据库设计.md @@ -0,0 +1,1319 @@ +# 数据库设计文档 + +## 表结构总览 +数据库使用 PostgreSQL + +``` +用户相关 +├── user (用户表) + +基础数据相关 +├── brokers (券商表) +├── stock_info (股票基本信息表) +└── stock_daily_price (股票每日收盘价表) + +账户相关 +├── positions (持仓表) +├── position_changes (持仓变更记录表) +├── asset_snapshots (资产快照表) +└── position_price_plans (持仓价格计划表) +``` + +## 用户相关表 +### users 用户表 + +**表结构** + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|---------|------|------| +| user_id | BIGSERIAL | PRIMARY KEY | 用户ID,自增 | +| open_id | VARCHAR(100) | UNIQUE | 可选,微信体系的 openId(小程序/公众号) | +| union_id | VARCHAR(100) | UNIQUE | 可选,微信体系的 unionId(跨应用统一标识) | +| username | VARCHAR(100) | UNIQUE | 用户名(必选,用于账号密码登录) | +| password_hash | VARCHAR(255) | | 密码哈希值(bcrypt加密,仅用户名登录时需要) | +| email | VARCHAR(100) | UNIQUE | 邮箱(可选,用于邮箱登录) | +| phone | VARCHAR(20) | UNIQUE | 电话号码(可选,用于手机号登录) | +| nickname | VARCHAR(100) | | 用户昵称(显示名称) | +| avatar_url | VARCHAR(255) | | 头像URL | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 修改时间(自动更新) | +| last_login_at | TIMESTAMP | | 最后登录时间 | +| status | VARCHAR(20) | NOT NULL, DEFAULT 'active', CHECK | 状态:active/inactive/deleted | +| role | VARCHAR(20) | NOT NULL, DEFAULT 'user', CHECK | 用户角色:user(普通用户)/admin(管理员)/super_admin(超级管理员) | + +**创建语句** + +```sql +CREATE TABLE user ( + user_id BIGSERIAL PRIMARY KEY, + open_id VARCHAR(100) UNIQUE, + union_id VARCHAR(100) UNIQUE, + username VARCHAR(100) UNIQUE, + password_hash VARCHAR(255), + email VARCHAR(100) UNIQUE, + phone VARCHAR(20) UNIQUE, + nickname VARCHAR(100), + avatar_url VARCHAR(255), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_login_at TIMESTAMP, + status VARCHAR(20) NOT NULL DEFAULT 'active', + role VARCHAR(20) NOT NULL DEFAULT 'user', + -- 约束:至少要有一种登录方式 + CONSTRAINT check_login_method CHECK ( + open_id IS NOT NULL OR + username IS NOT NULL OR + email IS NOT NULL OR + phone IS NOT NULL + ), + -- 约束:如果使用用户名登录,则必须有密码 + CONSTRAINT check_username_password CHECK ( + username IS NULL OR password_hash IS NOT NULL + ), + -- 约束:状态值限制 + CONSTRAINT check_status CHECK (status IN ('active', 'inactive', 'deleted')), + -- 约束:角色值限制 + CONSTRAINT check_role CHECK (role IN ('user', 'admin', 'super_admin')) +); + +-- 创建索引 +CREATE INDEX idx_users_open_id ON user(open_id); +CREATE INDEX idx_users_union_id ON user(union_id); +CREATE INDEX idx_users_username ON user(username); +CREATE INDEX idx_users_email ON user(email); +CREATE INDEX idx_users_phone ON user(phone); +CREATE INDEX idx_users_status ON user(status); +CREATE INDEX idx_users_last_login_at ON user(last_login_at); +CREATE INDEX idx_users_role ON user(role); + +-- 创建自动更新 updated_at 的触发器 +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER update_users_updated_at + BEFORE UPDATE ON user + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- 添加注释 +COMMENT ON TABLE user IS '用户表,支持微信登录和账号密码登录'; +COMMENT ON COLUMN user.user_id IS '用户ID,主键,自增'; +COMMENT ON COLUMN user.open_id IS '微信 openId,用于小程序/公众号登录,唯一'; +COMMENT ON COLUMN user.union_id IS '微信 unionId,用于跨应用统一标识,唯一'; +COMMENT ON COLUMN user.username IS '用户名,用于账号密码登录,唯一,可选'; +COMMENT ON COLUMN user.password_hash IS '密码哈希值,使用 bcrypt 算法加密存储,仅当使用用户名登录时必填'; +COMMENT ON COLUMN user.email IS '邮箱,用于邮箱登录,唯一,可选'; +COMMENT ON COLUMN user.phone IS '手机号,用于手机号登录,唯一,可选'; +COMMENT ON COLUMN user.nickname IS '用户昵称,显示名称'; +COMMENT ON COLUMN user.avatar_url IS '头像URL'; +COMMENT ON COLUMN user.status IS '用户状态:active(活跃)/inactive(非活跃)/deleted(已删除)'; +COMMENT ON COLUMN user.role IS '用户角色:user(普通用户)/admin(管理员)/super_admin(超级管理员)'; +COMMENT ON COLUMN user.created_at IS '创建时间,自动设置'; +COMMENT ON COLUMN user.updated_at IS '更新时间,自动更新'; +COMMENT ON COLUMN user.last_login_at IS '最后登录时间'; +``` + +#### 密码存储说明 + +**密码安全规范:** +1. **加密算法**:使用 `bcrypt` 算法对密码进行哈希加密(推荐使用 cost factor 10-12) +2. **存储字段**:密码哈希值存储在 `password_hash` 字段中,**绝不存储明文密码** +3. **字段长度**:`VARCHAR(255)` 足够存储 bcrypt 哈希值(通常为 60 字符) +4. **约束规则**: + - 如果用户设置了 `username`,则必须设置 `password_hash` + - 微信登录用户不需要密码(`password_hash` 可为 NULL) + - 密码字段允许为 NULL,支持多种登录方式 + +**密码验证流程:** +```sql +-- 用户登录时验证密码 +-- 1. 根据用户名查询用户 +SELECT user_id, password_hash, status +FROM user +WHERE username = :username AND status = 'active'; + +-- 2. 在应用层使用 bcrypt 验证密码 +-- bcrypt.compare(用户输入的明文密码, 数据库中的 password_hash) + +-- 3. 验证成功后更新最后登录时间 +UPDATE user +SET last_login_at = CURRENT_TIMESTAMP +WHERE user_id = :user_id; +``` + +**密码设置/修改流程:** +```sql +-- 注册时设置密码(应用层先使用 bcrypt 加密) +INSERT INTO user (username, password_hash, nickname, ...) +VALUES (:username, :password_hash_bcrypt, :nickname, ...); + +-- 修改密码(应用层先使用 bcrypt 加密新密码) +UPDATE user +SET password_hash = :new_password_hash_bcrypt, + updated_at = CURRENT_TIMESTAMP +WHERE user_id = :user_id; + +-- 重置密码(忘记密码场景) +UPDATE user +SET password_hash = :new_password_hash_bcrypt, + updated_at = CURRENT_TIMESTAMP +WHERE username = :username OR email = :email; +``` + +**注意事项:** +- 密码哈希值一旦生成,无法逆向还原为明文密码 +- 忘记密码时,只能通过重置密码流程(发送验证码到邮箱/手机)来设置新密码 +- 建议实现密码强度验证(长度、复杂度等)在应用层完成 +- 建议实现登录失败次数限制,防止暴力破解 + + + + +## 基础数据相关表设计 + +### broker 券商表 + +**表结构** + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|---------|------|------| +| broker_id | BIGSERIAL | PRIMARY KEY | 券商ID,自增 | +| broker_code | VARCHAR(50) | NOT NULL | 券商代码(如:HTZQ、ZSZQ等) | +| broker_name | VARCHAR(100) | NOT NULL | 券商名称(如:华泰证券、招商证券等) | +| region | VARCHAR(50) | NOT NULL, DEFAULT 'CN' | 地区/国家(如:CN/US/HK等) | +| sort_order | INTEGER | DEFAULT 0 | 排序顺序 | +| is_active | BOOLEAN | NOT NULL, DEFAULT true | 是否启用 | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | +| UNIQUE(broker_code, region) | | | 同一地区券商代码唯一 | +| UNIQUE(broker_name, region) | | | 同一地区券商名称唯一 | + +**创建语句** + +```sql +CREATE TABLE broker ( + broker_id BIGSERIAL PRIMARY KEY, + broker_code VARCHAR(50) NOT NULL, + broker_name VARCHAR(100) NOT NULL, + region VARCHAR(50) NOT NULL DEFAULT 'CN', + sort_order INTEGER DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + -- 同一地区券商代码唯一(不同地区可以有相同代码) + UNIQUE(broker_code, region), + -- 同一地区券商名称唯一(不同地区可以有相同名称) + UNIQUE(broker_name, region), + CONSTRAINT check_region CHECK (region IN ('CN', 'US', 'HK', 'SG', 'JP', 'UK', 'AU', 'CA', 'OTHER')) +); + +-- 创建索引 +CREATE INDEX idx_broker_code ON broker(broker_code); +CREATE INDEX idx_broker_name ON broker(broker_name); +CREATE INDEX idx_broker_region ON broker(region); +CREATE INDEX idx_broker_active ON broker(is_active); +CREATE INDEX idx_broker_sort ON broker(sort_order); +CREATE INDEX idx_broker_region_active ON broker(region, is_active); + +-- 创建触发器自动更新 updated_at +CREATE TRIGGER update_broker_updated_at + BEFORE UPDATE ON broker + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- 添加注释 +COMMENT ON TABLE broker IS '券商表,记录所有可用的券商信息'; +COMMENT ON COLUMN broker.broker_code IS '券商代码,用于系统识别,如:HTZQ(华泰证券)'; +COMMENT ON COLUMN broker.broker_name IS '券商名称,显示给用户的名称,如:华泰证券'; +COMMENT ON COLUMN broker.region IS '地区/国家代码:CN(中国)/US(美国)/HK(香港)/SG(新加坡)/JP(日本)/UK(英国)/AU(澳大利亚)/CA(加拿大)/OTHER(其他)'; +``` + +**示例数据** + +```sql +-- 中国券商 +INSERT INTO broker (broker_code, broker_name, region, sort_order) VALUES + ('HTZQ', '华泰证券', 'CN', 1), + ('ZSZQ', '招商证券', 'CN', 2), + ('ZXZQ', '中信证券', 'CN', 3), + ('GJZQ', '国金证券', 'CN', 4), + ('DFZQ', '东方证券', 'CN', 5), + ('GTZQ', '国泰君安', 'CN', 6), + ('HXZQ', '华西证券', 'CN', 7), + ('ZJZQ', '中金公司', 'CN', 8), + ('PZQ', '平安证券', 'CN', 9), + ('GFZQ', '广发证券', 'CN', 10); + +-- 美国券商 +INSERT INTO broker (broker_code, broker_name, region, sort_order) VALUES + ('SCHW', 'Charles Schwab', 'US', 1), + ('FID', 'Fidelity', 'US', 2), + ('IB', 'Interactive Brokers', 'US', 3), + ('TD', 'TD Ameritrade', 'US', 4), + ('ETRADE', 'E*TRADE', 'US', 5); + +-- 香港券商 +INSERT INTO broker (broker_code, broker_name, region, sort_order) VALUES + ('FUTU', '富途证券', 'HK', 1), + ('TIGER', '老虎证券', 'HK', 2), + ('HSBC', '汇丰银行', 'HK', 3), + ('CITI', '花旗银行', 'HK', 4); + +-- 其他地区券商示例 +INSERT INTO brokers (broker_code, broker_name, region, sort_order) VALUES + ('DBS', '星展银行', 'SG', 1), + ('NOMURA', '野村证券', 'JP', 1); +``` + +--- + +### stock_info 股票基本信息表 + +**表结构** + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|---------|------|------| +| id | BIGSERIAL | PRIMARY KEY | 主键ID,自增 | +| stock_code | VARCHAR(20) | NOT NULL | 股票代码(如:600519, 00700.HK) | +| stock_name | VARCHAR(100) | NOT NULL | 股票名称 | +| market | VARCHAR(20) | NOT NULL | 市场标识(A股: sh/sz/bj, 港股: hk, 美股: us等) | +| full_name | VARCHAR(200) | | 公司全称 | +| industry | VARCHAR(100) | | 所属行业 | +| listing_date | DATE | | 上市日期 | +| status | VARCHAR(20) | NOT NULL, DEFAULT 'active', CHECK | 状态:active(正常)/suspended(停牌)/delisted(退市) | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | +| UNIQUE(stock_code, market) | | | 同一市场股票代码唯一 | + +**创建语句** + +```sql +CREATE TABLE stock_info ( + id BIGSERIAL PRIMARY KEY, + stock_code VARCHAR(20) NOT NULL, + stock_name VARCHAR(100) NOT NULL, + market VARCHAR(20) NOT NULL, + full_name VARCHAR(200), + industry VARCHAR(100), + listing_date DATE, + status VARCHAR(20) NOT NULL DEFAULT 'active', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + -- 同一市场股票代码唯一 + UNIQUE(stock_code, market), + CONSTRAINT check_stock_status CHECK (status IN ('active', 'suspended', 'delisted')) +); + +-- 创建索引 +CREATE INDEX idx_stock_info_code ON stock_info(stock_code); +CREATE INDEX idx_stock_info_market ON stock_info(market); +CREATE INDEX idx_stock_info_name ON stock_info(stock_name); +CREATE INDEX idx_stock_info_status ON stock_info(status); +CREATE INDEX idx_stock_info_code_market ON stock_info(stock_code, market); +-- 全文搜索索引(用于股票名称模糊匹配,需要先启用 pg_trgm 扩展) +-- CREATE EXTENSION IF NOT EXISTS pg_trgm; +-- CREATE INDEX idx_stock_info_name_trgm ON stock_info USING gin(stock_name gin_trgm_ops); + +-- 创建触发器自动更新 updated_at +CREATE TRIGGER update_stock_info_updated_at + BEFORE UPDATE ON stock_info + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- 添加注释 +COMMENT ON TABLE stock_info IS '股票基本信息表,存储所有市场的股票基本静态信息'; +COMMENT ON COLUMN stock_info.stock_code IS '股票代码,如:600519(A股)、00700.HK(港股)、AAPL(美股)'; +COMMENT ON COLUMN stock_info.stock_name IS '股票名称,用于显示和搜索'; +COMMENT ON COLUMN stock_info.market IS '市场标识:sh(上海)/sz(深圳)/bj(北京)/hk(香港)/us(美国)等'; +COMMENT ON COLUMN stock_info.status IS '状态:active(正常交易)/suspended(停牌)/delisted(退市)'; +``` + +**说明:** +- 此表存储所有市场的股票基本信息,用于用户输入时的模糊匹配 +- 数据不常变更,可以定期从外部数据源同步 +- 支持全文搜索,方便用户通过股票名称快速查找 + +--- + +### stock_daily_price 股票每日收盘价表 + +**表结构** + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|---------|------|------| +| id | BIGSERIAL | PRIMARY KEY | 主键ID,自增 | +| stock_code | VARCHAR(20) | NOT NULL | 股票代码 | +| stock_name | VARCHAR(100) | NOT NULL | 股票名称 | +| market | VARCHAR(20) | NOT NULL | 市场标识 | +| trade_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(20, 2) | | 成交额(单位:元) | +| change_amount | DECIMAL(18, 4) | | 涨跌额 | +| change_percent | DECIMAL(10, 6) | | 涨跌幅(%) | +| change_percent_60day | DECIMAL(10, 6) | | 60日涨跌幅(%) | +| change_percent_ytd | DECIMAL(10, 6) | | 年初至今涨跌幅(%) | +| turnover_rate | DECIMAL(10, 6) | | 换手率(%) | +| pe_ratio | DECIMAL(12, 4) | | 市盈率 | +| pb_ratio | DECIMAL(12, 4) | | 市净率 | +| market_cap | DECIMAL(20, 2) | | 总市值(单位:元) | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| UNIQUE(stock_code, market, trade_date) | | | 同一股票同一日期只能有一条记录 | + +**创建语句** + +```sql +CREATE TABLE stock_daily_price ( + id BIGSERIAL PRIMARY KEY, + stock_code VARCHAR(20) NOT NULL, + stock_name VARCHAR(100) NOT NULL, + market VARCHAR(20) NOT NULL, + trade_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(20, 2), + change_amount DECIMAL(18, 4), + change_percent DECIMAL(10, 6), + change_percent_60day DECIMAL(10, 6), + change_percent_ytd DECIMAL(10, 6), + turnover_rate DECIMAL(10, 6), + pe_ratio DECIMAL(12, 4), + pb_ratio DECIMAL(12, 4), + market_cap DECIMAL(20, 2), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + -- 同一股票同一日期只能有一条记录 + UNIQUE(stock_code, market, trade_date), + -- 外键关联股票基本信息表(可选,如果数据源可靠可以不加) + CONSTRAINT fk_stock_price_info FOREIGN KEY (stock_code, market) + REFERENCES stock_info(stock_code, market) ON DELETE CASCADE +); + +-- 创建索引 +CREATE INDEX idx_stock_daily_price_code ON stock_daily_price(stock_code); +CREATE INDEX idx_stock_daily_price_market ON stock_daily_price(market); +CREATE INDEX idx_stock_daily_price_date ON stock_daily_price(trade_date); +CREATE INDEX idx_stock_daily_price_code_market ON stock_daily_price(stock_code, market); +CREATE INDEX idx_stock_daily_price_code_date ON stock_daily_price(stock_code, market, trade_date DESC); +-- 用于查询最新价格的索引 +CREATE INDEX idx_stock_daily_price_latest ON stock_daily_price(stock_code, market, trade_date DESC); + +-- 添加注释 +COMMENT ON TABLE stock_daily_price IS '股票每日收盘价表,存储所有市场的股票每日收盘价及相关交易数据'; +COMMENT ON COLUMN stock_daily_price.stock_code IS '股票代码,关联 stock_info 表'; +COMMENT ON COLUMN stock_daily_price.close_price IS '收盘价,用于更新持仓的 current_price'; +COMMENT ON COLUMN stock_daily_price.trade_date IS '交易日期,用于查询历史价格'; +COMMENT ON COLUMN stock_daily_price.change_percent_60day IS '60日涨跌幅(%)'; +COMMENT ON COLUMN stock_daily_price.change_percent_ytd IS '年初至今涨跌幅(%)'; +COMMENT ON COLUMN stock_daily_price.market_cap IS '总市值,单位:元'; +``` + +**使用说明:** + +1. **查询最新价格(用于更新持仓)** +```sql +-- 查询指定股票的最新收盘价 +SELECT + stock_code, + market, + close_price, + trade_date +FROM stock_daily_price +WHERE stock_code = :stock_code + AND market = :market +ORDER BY trade_date DESC +LIMIT 1; + +-- 批量查询多个股票的最新价格 +SELECT DISTINCT ON (stock_code, market) + stock_code, + market, + close_price, + trade_date +FROM stock_daily_price +WHERE (stock_code, market) IN ( + ('600519', 'sh'), + ('00700', 'hk'), + ('AAPL', 'us') +) +ORDER BY stock_code, market, trade_date DESC; +``` + +2. **查询历史价格(用于图表展示)** +```sql +-- 查询指定股票的历史价格 +SELECT + trade_date, + open_price, + close_price, + high_price, + low_price, + volume, + change_percent +FROM stock_daily_price +WHERE stock_code = :stock_code + AND market = :market + AND trade_date >= :start_date +ORDER BY trade_date; +``` + +3. **每日价格更新流程** +```sql +-- 步骤1:从外部数据源获取最新价格数据 +-- 步骤2:批量插入或更新价格数据 +INSERT INTO stock_daily_price ( + stock_code, market, trade_date, open_price, close_price, + high_price, low_price, volume, amount, change_amount, + change_percent, change_percent_60day, change_percent_ytd, + turnover_rate, pe_ratio, pb_ratio, market_cap +) +VALUES (...) +ON CONFLICT (stock_code, market, trade_date) +DO UPDATE SET + open_price = EXCLUDED.open_price, + close_price = EXCLUDED.close_price, + high_price = EXCLUDED.high_price, + low_price = EXCLUDED.low_price, + volume = EXCLUDED.volume, + amount = EXCLUDED.amount, + change_amount = EXCLUDED.change_amount, + change_percent = EXCLUDED.change_percent, + change_percent_60day = EXCLUDED.change_percent_60day, + change_percent_ytd = EXCLUDED.change_percent_ytd, + turnover_rate = EXCLUDED.turnover_rate, + pe_ratio = EXCLUDED.pe_ratio, + pb_ratio = EXCLUDED.pb_ratio, + market_cap = EXCLUDED.market_cap; + +-- 步骤3:更新持仓表的 current_price(仅更新 auto_price_update = true 的持仓) +UPDATE positions p +SET current_price = ( + SELECT close_price + FROM stock_daily_price sdp + WHERE sdp.stock_code = p.symbol + AND sdp.market = p.market + ORDER BY sdp.trade_date DESC + LIMIT 1 +), +updated_at = CURRENT_TIMESTAMP +WHERE p.auto_price_update = true + AND p.asset_type IN ('stock', 'fund') + AND p.status = 'active'; +``` + +--- + +## 账户相关表设计 + +### positions 持仓表 +记录用户持仓情况,包含数量、成本价、最新市场价、券商等 +**表结构** + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|---------|------|------| +| position_id | BIGSERIAL | PRIMARY KEY | 持仓ID,自增 | +| user_id | BIGINT | NOT NULL, FOREIGN KEY | 用户ID,关联 user 表 | +| broker_id | BIGINT | NOT NULL, FOREIGN KEY | 券商ID,关联 brokers 表 | +| asset_type | VARCHAR(20) | NOT NULL, CHECK | 资产类型:stock/fund/cash/bond | +| symbol | VARCHAR(50) | NOT NULL | 资产代码(股票代码、基金代码等) | +| name | VARCHAR(100) | NOT NULL | 资产名称 | +| market | VARCHAR(20) | | 市场(A股/港股/美股等) | +| shares | DECIMAL(18, 4) | NOT NULL, DEFAULT 0 | 持仓份额/数量 | +| cost_price | DECIMAL(18, 4) | NOT NULL | 成本价(每股/每份) | +| current_price | DECIMAL(18, 4) | | 最新市场价(系统自动更新) | +| previous_price | DECIMAL(18, 4) | | 上一次的价格(用于对比显示红绿色) | +| currency | VARCHAR(10) | NOT NULL, DEFAULT 'CNY' | 货币类型 | +| exchange_rate | DECIMAL(10, 6) | DEFAULT 1 | 汇率(用于多货币) | +| auto_price_update | BOOLEAN | NOT NULL, DEFAULT false | 是否自动更新价格(付费用户功能) | +| status | VARCHAR(20) | NOT NULL, DEFAULT 'active', CHECK | 状态:active/suspended/delisted | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | + +**创建语句** + +```sql +CREATE TABLE positions ( + position_id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES user(user_id) ON DELETE CASCADE, + broker_id BIGINT NOT NULL REFERENCES brokers(broker_id), + asset_type VARCHAR(20) NOT NULL, + symbol VARCHAR(50) NOT NULL, + name VARCHAR(100) NOT NULL, + market VARCHAR(20), + shares DECIMAL(18, 4) NOT NULL DEFAULT 0, + cost_price DECIMAL(18, 4) NOT NULL, + current_price DECIMAL(18, 4), + previous_price DECIMAL(18, 4), + currency VARCHAR(10) NOT NULL DEFAULT 'CNY', + exchange_rate DECIMAL(10, 6) DEFAULT 1, + auto_price_update BOOLEAN NOT NULL DEFAULT false, + status VARCHAR(20) NOT NULL DEFAULT 'active', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT check_asset_type CHECK (asset_type IN ('stock', 'fund', 'cash', 'bond', 'other')), + CONSTRAINT check_status CHECK (status IN ('active', 'suspended', 'delisted')), + CONSTRAINT check_shares_non_negative CHECK (shares >= 0), + CONSTRAINT check_cost_price_positive CHECK (cost_price > 0), + -- 同一用户同一券商同一资产只能有一条持仓(通过 user_id + broker_id + symbol + market + asset_type 唯一) + UNIQUE(user_id, broker_id, symbol, market, asset_type) +); + +-- 创建索引 +CREATE INDEX idx_positions_user_id ON positions(user_id); +CREATE INDEX idx_positions_broker_id ON positions(broker_id); +CREATE INDEX idx_positions_symbol ON positions(symbol); +CREATE INDEX idx_positions_asset_type ON positions(asset_type); +CREATE INDEX idx_positions_status ON positions(status); +CREATE INDEX idx_positions_user_status ON positions(user_id, status); +CREATE INDEX idx_positions_user_broker ON positions(user_id, broker_id); +CREATE INDEX idx_positions_auto_price_update ON positions(auto_price_update); +CREATE INDEX idx_positions_auto_price_asset ON positions(auto_price_update, asset_type, status); +CREATE INDEX idx_positions_updated_at ON positions(updated_at); + +-- 创建触发器自动更新 updated_at +CREATE TRIGGER update_positions_updated_at + BEFORE UPDATE ON positions + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- 添加注释 +COMMENT ON TABLE positions IS '持仓表,记录用户的资产持仓'; +COMMENT ON COLUMN positions.broker_id IS '券商ID,关联 brokers 表,支持多券商'; +COMMENT ON COLUMN positions.asset_type IS '资产类型:stock(股票)/fund(基金)/cash(现金)/bond(国债)/other(其他)'; +COMMENT ON COLUMN positions.symbol IS '资产代码,如股票代码600519、基金代码等'; +COMMENT ON COLUMN positions.shares IS '持仓份额/数量,股票为股数,基金为份数,现金为金额'; +COMMENT ON COLUMN positions.cost_price IS '成本价,用户直接修改,系统不自动计算'; +COMMENT ON COLUMN positions.current_price IS '最新市场价,系统每日自动更新(仅auto_price_update=true的持仓)'; +COMMENT ON COLUMN positions.previous_price IS '上一次的价格,用户手动更新价格时自动保存上一次价格,用于前端显示红绿色(上涨/下跌)'; +COMMENT ON COLUMN positions.exchange_rate IS '汇率,用于多货币资产,如港股、美股'; +COMMENT ON COLUMN positions.auto_price_update IS '是否自动更新价格,true表示系统每日自动更新市场价格(付费用户功能)'; +``` + +**示例数据** + +```sql +-- 假设 user_id = 1,broker_id 1=华泰证券(HTZQ),broker_id 4=富途证券(FUTU),broker_id 3=Interactive Brokers(IB) +-- 插入三条持仓数据 + +-- 1. A股股票:贵州茅台(上海市场) +INSERT INTO positions ( + user_id, broker_id, asset_type, symbol, name, market, + shares, cost_price, current_price, previous_price, + currency, exchange_rate, auto_price_update, status +) VALUES ( + 1, 1, 'stock', '600519', '贵州茅台', 'sh', + 100.0000, 1600.0000, 1850.0000, 1800.0000, + 'CNY', 1.000000, false, 'active' +); + +-- 2. 港股股票:腾讯控股(香港市场) +INSERT INTO positions ( + user_id, broker_id, asset_type, symbol, name, market, + shares, cost_price, current_price, previous_price, + currency, exchange_rate, auto_price_update, status +) VALUES ( + 1, 4, 'stock', '00700', '腾讯控股', 'hk', + 200.0000, 320.0000, 350.0000, 340.0000, + 'HKD', 0.920000, false, 'active' +); + +-- 3. 美股股票:苹果公司(美国市场) +INSERT INTO positions ( + user_id, broker_id, asset_type, symbol, name, market, + shares, cost_price, current_price, previous_price, + currency, exchange_rate, auto_price_update, status +) VALUES ( + 1, 3, 'stock', 'AAPL', '苹果公司', 'us', + 50.0000, 150.0000, 175.0000, 170.0000, + 'USD', 7.200000, false, 'active' +); +``` + +--- + +### position_changes 持仓变更记录表 + +记录每次持仓变更的详细信息,用于展示持仓变更历史。 + +**表结构** + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|---------|------|------| +| change_id | BIGSERIAL | PRIMARY KEY | 变更记录ID,自增 | +| position_id | BIGINT | NOT NULL, FOREIGN KEY | 持仓ID,关联 positions 表 | +| change_date | DATE | NOT NULL | 变更日期 | +| change_type | VARCHAR(20) | NOT NULL, CHECK | 变更类型:buy(买入)/sell(卖出)/auto(系统自动) | +| before_shares | DECIMAL(18, 4) | NOT NULL | 变更前份额/数量 | +| before_cost_price | DECIMAL(18, 4) | NOT NULL | 变更前成本价 | +| after_shares | DECIMAL(18, 4) | NOT NULL | 变更后份额/数量 | +| after_cost_price | DECIMAL(18, 4) | NOT NULL | 变更后成本价 | +| notes | TEXT | | 备注/思考 | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | + +**创建语句** + +```sql +CREATE TABLE position_changes ( + change_id BIGSERIAL PRIMARY KEY, + position_id BIGINT NOT NULL REFERENCES positions(position_id) ON DELETE CASCADE, + change_date DATE NOT NULL, + change_type VARCHAR(20) NOT NULL, + before_shares DECIMAL(18, 4) NOT NULL, + before_cost_price DECIMAL(18, 4) NOT NULL, + after_shares DECIMAL(18, 4) NOT NULL, + after_cost_price DECIMAL(18, 4) NOT NULL, + notes TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT check_change_type CHECK (change_type IN ('buy', 'sell', 'auto')) +); + +-- 创建索引 +CREATE INDEX idx_position_changes_position_id ON position_changes(position_id); +CREATE INDEX idx_position_changes_change_date ON position_changes(change_date); +CREATE INDEX idx_position_changes_position_date ON position_changes(position_id, change_date DESC); + +-- 添加注释 +COMMENT ON TABLE position_changes IS '持仓变更记录表,记录每次持仓变更的详细信息,用于展示持仓变更历史'; +COMMENT ON COLUMN position_changes.position_id IS '持仓ID,关联 positions 表'; +COMMENT ON COLUMN position_changes.change_date IS '变更日期'; +COMMENT ON COLUMN position_changes.change_type IS '变更类型:buy(买入)/sell(卖出)/auto(系统自动,如分红、拆股等)'; +COMMENT ON COLUMN position_changes.before_shares IS '变更前份额/数量'; +COMMENT ON COLUMN position_changes.before_cost_price IS '变更前成本价'; +COMMENT ON COLUMN position_changes.after_shares IS '变更后份额/数量'; +COMMENT ON COLUMN position_changes.after_cost_price IS '变更后成本价'; +COMMENT ON COLUMN position_changes.notes IS '备注/思考记录'; +``` + +**使用说明:** + +1. **记录买入变更** +```sql +-- 用户买入时,记录变更前后状态 +INSERT INTO position_changes ( + position_id, change_date, change_type, + before_shares, before_cost_price, + after_shares, after_cost_price, + notes +) +VALUES ( + :position_id, :change_date, 'buy', + :before_shares, :before_cost_price, + :after_shares, :after_cost_price, + :notes +); +``` + +2. **记录卖出变更** +```sql +-- 用户卖出时,记录变更前后状态 +INSERT INTO position_changes ( + position_id, change_date, change_type, + before_shares, before_cost_price, + after_shares, after_cost_price, + notes +) +VALUES ( + :position_id, :change_date, 'sell', + :before_shares, :before_cost_price, + :after_shares, :after_cost_price, + :notes +); +``` + +3. **记录系统自动变更** +```sql +-- 系统自动变更(分红、拆股、送股等) +INSERT INTO position_changes ( + position_id, change_date, change_type, + before_shares, before_cost_price, + after_shares, after_cost_price, + notes +) +VALUES ( + :position_id, :change_date, 'auto', + :before_shares, :before_cost_price, + :after_shares, :after_cost_price, + '分红/拆股/送股等系统自动变更' +); +``` + +4. **查询持仓变更历史** +```sql +-- 查询指定持仓的所有变更记录 +SELECT + change_id, + change_date, + change_type, + before_shares, + before_cost_price, + after_shares, + after_cost_price, + notes, + created_at +FROM position_changes +WHERE position_id = :position_id +ORDER BY change_date DESC, created_at DESC; +``` + +--- + + +### asset_snapshots (资产快照表) +**表结构(新版)** + +| 字段名 | 数据类型 | 约束 | 说明 | +|---------------------|----------------|----------------------------------|---------------------------------------------------------| +| id | BIGSERIAL | PRIMARY KEY | 快照ID,自增 | +| user_id | BIGINT | NOT NULL, FOREIGN KEY | 用户ID,关联 user 表 | +| snapshot_date | DATE | NOT NULL | 快照日期 | +| total_asset | DECIMAL(18,2) | NOT NULL | 总资产(所有持仓市值 + 现金余额) | +| total_invested | DECIMAL(18,2) | NOT NULL | 累计投入金额(初始投入 + 后续投入 - 提取金额) | +| time_weighted_return| DECIMAL(12,8) | | 时间加权收益率(复利累计收益率) | +| annualized_return | DECIMAL(10,6) | | 年化收益率 | +| year_to_date_return | DECIMAL(10,6) | | 当年收益率(年初至今) | +| positions_data | JSONB | | 持仓明细快照(JSON格式,可选) | +| cash_data | JSONB | | 现金账户明细快照(JSON格式,可选) | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | +| UNIQUE(user_id, snapshot_date) | | | 同一用户同一日期只能有一条快照 | + +**创建语句** + +```sql +CREATE TABLE asset_snapshots ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES user(user_id) ON DELETE CASCADE, + snapshot_date DATE NOT NULL, + total_asset DECIMAL(18,2) NOT NULL, + total_invested DECIMAL(18,2) NOT NULL, + time_weighted_return DECIMAL(12,8), + annualized_return DECIMAL(10,6), + year_to_date_return DECIMAL(10,6), + positions_data JSONB, + cash_data JSONB, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, snapshot_date) +); +``` + +-- 创建索引 +```sql +CREATE INDEX idx_as_user_id ON asset_snapshots(user_id); +CREATE INDEX idx_as_snapshot_date ON asset_snapshots(snapshot_date); +CREATE INDEX idx_as_user_date ON asset_snapshots(user_id, snapshot_date DESC); +``` + +-- 添加注释 +```sql +COMMENT ON TABLE asset_snapshots IS '每日资产快照表,记录总资产、累计投入、收益率等,用于计算净值、收益'; +COMMENT ON COLUMN asset_snapshots.user_id IS '用户ID,关联 user 表'; +COMMENT ON COLUMN asset_snapshots.snapshot_date IS '快照日期'; +COMMENT ON COLUMN asset_snapshots.total_asset IS '总资产:所有持仓市值 + 现金余额'; +COMMENT ON COLUMN asset_snapshots.total_invested IS '累计投入金额:初始资金 + 后续投入 - 提取金额'; +COMMENT ON COLUMN asset_snapshots.time_weighted_return IS '时间加权收益率(复利累计收益率)'; +COMMENT ON COLUMN asset_snapshots.annualized_return IS '年化收益率'; +COMMENT ON COLUMN asset_snapshots.year_to_date_return IS '当年收益率'; +COMMENT ON COLUMN asset_snapshots.positions_data IS '持仓明细快照,JSON格式存储'; +COMMENT ON COLUMN asset_snapshots.cash_data IS '现金账户明细快照,JSON格式存储'; +COMMENT ON COLUMN asset_snapshots.created_at IS '快照创建时间'; +COMMENT ON COLUMN asset_snapshots.updated_at IS '快照更新时间'; +``` + +#### 字段计算说明与用法 + +**total_asset(总资产)** +``` +total_asset = Σ(持仓市值) + 现金余额 +其中: +- 持仓市值 = 持仓份额 × 当前价格 +- 现金余额 = 所有现金账户余额之和 +``` + +**total_invested(累计投入金额)** +``` +total_invested = 初始投入 + 后续投入 - 提取金额 +说明: +- 初始投入:账户创建时的初始资金 +- 后续投入:用户手动记录的资金投入 +- 提取金额:用户提取的资金(提现、转出) +- 注意:买入证券只是资金流转,非"投入" +``` + +**net_value(单位净值)及 total_profit(总收益)** +- 两者不再存储字段,需通过查询实时计算(避免冗余数据和不一致): + +```sql +-- 单位净值(需要时动态计算) +net_value = total_asset / NULLIF(total_invested, 0) + +-- 总收益(需要时动态计算) +total_profit = total_asset - total_invested +``` + +**时间加权收益率(time_weighted_return)逻辑** + +```sql +-- 1. 计算每日净值 +net_value_today = total_asset_today / NULLIF(total_invested_today, 0) +net_value_yesterday = total_asset_yesterday / NULLIF(total_invested_yesterday, 0) + +-- 2. 日收益率 +daily_return = (net_value_today - net_value_yesterday) / NULLIF(net_value_yesterday, 0) + +-- 3. 累计收益率(复利) +new_twr = (1 + 昨日 time_weighted_return) × (1 + daily_return) - 1 +``` + +**年化收益率(annualized_return)** +```sql +annualized_return = (1 + time_weighted_return) ^ (365 / 投资天数) - 1 +``` + +**当年收益率(year_to_date_return)** +```sql +-- 获取年初快照,计算净值 +year_to_date_return = (current_net_value - year_start_net_value) / year_start_net_value +``` + +#### 数据结构示例 + +**positions_data(持仓明细快照)** +```json +[ + { + "position_id": 1, + "symbol": "600519", + "name": "贵州茅台", + "shares": 100, + "cost_price": 1600.00, + "current_price": 1850.00, + "market_value": 185000.00, + "profit": 25000.00 + } +] +``` + +**cash_data(现金账户明细快照)** +```json +[ + { + "account_id": 1, + "currency": "CNY", + "balance": 50000.00 + } +] +``` + +#### 每日快照主要流程(伪代码) + +```sql +1. 获取用户所有持仓 + SELECT * FROM positions WHERE user_id = :user_id AND status = 'active'; + +2. 获取用户现金账户 + SELECT * FROM cash_accounts WHERE user_id = :user_id; + +3. 计算总资产 + total_asset = Σ(持仓市值) + Σ(现金余额) + +4. 汇总累计投入 + total_invested = + SELECT COALESCE(SUM(amount),0) FROM cash_flows + WHERE user_id = :user_id AND flow_type = 'deposit' + - SELECT COALESCE(SUM(amount),0) FROM cash_flows + WHERE user_id = :user_id AND flow_type = 'withdraw' + +5. 计算净值、总收益(不入库,仅查询时用) + +6. 计算时间加权收益率 + - 获取昨日快照 + - 计算净值变化 + - 复利累计 + +7. 计算年化和当年收益率 + +8. 组装positions_data, cash_data(JSONB) + +9. 插入或更新快照 + INSERT INTO asset_snapshots (...) + ON CONFLICT (user_id, snapshot_date) DO UPDATE SET ... +``` + +#### 查询与统计示例 + +**查询用户净值曲线** + +```sql +SELECT + snapshot_date, + total_asset, + total_invested, + (total_asset / NULLIF(total_invested, 0)) AS net_value, + (total_asset - total_invested) AS total_profit, + time_weighted_return, + annualized_return +FROM asset_snapshots +WHERE user_id = :user_id +ORDER BY snapshot_date DESC +LIMIT 365; -- 最近一年 +``` + +**查询收益率统计** + +```sql +SELECT + snapshot_date, + time_weighted_return * 100 as return_rate_percent, + annualized_return * 100 as annualized_return_percent, + year_to_date_return * 100 as ytd_return_percent +FROM asset_snapshots +WHERE user_id = :user_id +ORDER BY snapshot_date DESC +LIMIT 30; +``` + +**查询持仓明细历史** + +```sql +SELECT + snapshot_date, + positions_data +FROM asset_snapshots +WHERE user_id = :user_id + AND positions_data IS NOT NULL +ORDER BY snapshot_date DESC +LIMIT 10; +``` + +#### 注意事项 + +1. **数据一致性** + - total_invested 必须大于 0,否则净值计算会除零出错 + - 使用 NULLIF 防止除零 +2. **性能优化** + - 定时批量生成快照 + - 索引:user_id, snapshot_date + - JSONB 字段支持结构化、高效存储 +3. **数据完整性** + - 每日有且仅有一条快照,断档时补最近快照 +4. **净值及收益实时计算,不入库**,仅金额、收益率相关存库 +5. **收益率需每日、每年维护和更新** + +--- + +### position_price_plans 持仓价格计划表 + +**表结构** + +| 字段名 | 数据类型 | 约束 | 说明 | +|--------|---------|------|------| +| plan_id | BIGSERIAL | PRIMARY KEY | 计划ID,自增 | +| position_id | BIGINT | NOT NULL, FOREIGN KEY | 持仓ID,关联 positions 表 | +| action_type | VARCHAR(20) | NOT NULL, CHECK | 操作类型:buy/sell | +| plan_price | DECIMAL(18, 4) | NOT NULL | 计划价格 | +| plan_shares | DECIMAL(18, 4) | | 计划份额(可选) | +| plan_amount | DECIMAL(18, 2) | | 计划金额(可选,与份额二选一) | +| step_order | INTEGER | NOT NULL, DEFAULT 1 | 步骤顺序(1, 2, 3...) | +| is_completed | BOOLEAN | NOT NULL, DEFAULT false | 是否已完成 | +| completed_at | TIMESTAMP | | 完成时间 | +| notes | TEXT | | 备注 | +| created_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_at | TIMESTAMP | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 | + +**创建语句** + +```sql +CREATE TABLE position_price_plans ( + plan_id BIGSERIAL PRIMARY KEY, + position_id BIGINT NOT NULL REFERENCES positions(position_id) ON DELETE CASCADE, + action_type VARCHAR(20) NOT NULL, + plan_price DECIMAL(18, 4) NOT NULL, + plan_shares DECIMAL(18, 4), + plan_amount DECIMAL(18, 2), + step_order INTEGER NOT NULL DEFAULT 1, + is_completed BOOLEAN NOT NULL DEFAULT false, + completed_at TIMESTAMP, + notes TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT check_action_type CHECK (action_type IN ('buy', 'sell')), + CONSTRAINT check_plan_price_positive CHECK (plan_price > 0), + CONSTRAINT check_step_order_positive CHECK (step_order > 0), + -- 同一持仓同一操作类型同一顺序只能有一个计划 + UNIQUE(position_id, action_type, step_order) +); + +-- 创建索引 +CREATE INDEX idx_position_price_plans_position_id ON position_price_plans(position_id); +CREATE INDEX idx_position_price_plans_action_type ON position_price_plans(action_type); +CREATE INDEX idx_position_price_plans_completed ON position_price_plans(is_completed); +CREATE INDEX idx_position_price_plans_position_action ON position_price_plans(position_id, action_type, step_order); + +-- 创建触发器自动更新 updated_at +CREATE TRIGGER update_position_price_plans_updated_at + BEFORE UPDATE ON position_price_plans + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- 添加注释 +COMMENT ON TABLE position_price_plans IS '持仓价格计划表,记录买入和卖出计划价格'; +COMMENT ON COLUMN position_price_plans.plan_id IS '计划ID,主键,自增'; +COMMENT ON COLUMN position_price_plans.action_type IS '操作类型:buy(买入)/sell(卖出)'; +COMMENT ON COLUMN position_price_plans.plan_price IS '计划价格,当市场价格达到此价格时触发提醒'; +COMMENT ON COLUMN position_price_plans.plan_shares IS '计划份额,计划买入/卖出的数量'; +COMMENT ON COLUMN position_price_plans.plan_amount IS '计划金额,计划买入/卖出的金额(与份额二选一)'; +COMMENT ON COLUMN position_price_plans.step_order IS '步骤顺序,默认3个买点和3个卖点(1, 2, 3)'; +``` + +--- + +### 持仓表使用说明 + +#### 1. 资产类型说明 + +**stock(股票)** +- symbol: 股票代码(如:600519) +- market: 市场(A股/港股/美股) +- shares: 股数 +- cost_price: 每股成本价 + +**fund(基金)** +- symbol: 基金代码 +- market: 市场(如:场内/场外) +- shares: 基金份数 +- cost_price: 每份成本价 + +**cash(现金)** +- symbol: 可自定义(如:CASH_CNY) +- name: 现金账户名称 +- shares: 现金余额(金额) +- cost_price: 固定为1(现金无成本价概念) + +**bond(国债)** +- symbol: 债券代码 +- market: 市场 +- shares: 债券数量 +- cost_price: 每张成本价 + +#### 2. 价格计划使用示例 + +**创建买入计划(3个买点)** +```sql +-- 为持仓创建3个买入计划 +INSERT INTO position_price_plans (position_id, action_type, plan_price, plan_shares, step_order) +VALUES + (1, 'buy', 100.00, 100, 1), -- 第一个买点:100元买入100股 + (1, 'buy', 95.00, 100, 2), -- 第二个买点:95元买入100股 + (1, 'buy', 90.00, 100, 3); -- 第三个买点:90元买入100股 +``` + +**创建卖出计划(3个卖点)** +```sql +-- 为持仓创建3个卖出计划 +INSERT INTO position_price_plans (position_id, action_type, plan_price, plan_shares, step_order) +VALUES + (1, 'sell', 120.00, 50, 1), -- 第一个卖点:120元卖出50股 + (1, 'sell', 130.00, 50, 2), -- 第二个卖点:130元卖出50股 + (1, 'sell', 150.00, 50, 3); -- 第三个卖点:150元卖出50股 +``` + +**查询持仓及其价格计划** +```sql +-- 查询持仓及其所有价格计划(包含券商信息) +SELECT + p.position_id, + b.broker_name, + p.name, + p.shares, + p.cost_price, + p.current_price, + t.action_type, + t.plan_price, + t.plan_shares, + t.step_order, + t.is_completed +FROM positions p +INNER JOIN brokers b ON p.broker_id = b.broker_id +LEFT JOIN position_price_plans t ON p.position_id = t.position_id +WHERE p.user_id = :user_id + AND p.status = 'active' +ORDER BY b.sort_order, b.broker_name, p.position_id, t.action_type, t.step_order; + +-- 按券商汇总查询持仓 +SELECT + b.broker_name, + COUNT(*) as position_count, + SUM(p.shares * p.current_price) as total_value +FROM positions p +INNER JOIN brokers b ON p.broker_id = b.broker_id +WHERE p.user_id = :user_id + AND p.status = 'active' +GROUP BY b.broker_id, b.broker_name +ORDER BY b.sort_order; +``` + +**查询可用券商列表** +```sql +-- 查询所有启用的券商(用于下拉选择) +SELECT + broker_id, + broker_code, + broker_name, + region +FROM brokers +WHERE is_active = true +ORDER BY region, sort_order, broker_name; + +-- 按地区查询券商 +SELECT + broker_id, + broker_code, + broker_name, + region +FROM brokers +WHERE is_active = true + AND region = 'CN' -- 查询中国券商 +ORDER BY sort_order, broker_name; + +-- 查询所有地区列表 +SELECT DISTINCT region +FROM brokers +WHERE is_active = true +ORDER BY region; +``` + +**检查价格计划是否触发** +```sql +-- 查询已达到计划价格但未完成的买入计划 +SELECT + p.position_id, + p.name, + p.current_price, + t.plan_price, + t.plan_shares, + t.step_order +FROM positions p +INNER JOIN position_price_plans t ON p.position_id = t.position_id +WHERE p.user_id = :user_id + AND t.action_type = 'buy' + AND t.is_completed = false + AND p.current_price <= t.plan_price + AND p.status = 'active'; + +-- 查询已达到计划价格但未完成的卖出计划 +SELECT + p.position_id, + p.name, + p.current_price, + t.plan_price, + t.plan_shares, + t.step_order +FROM positions p +INNER JOIN position_price_plans t ON p.position_id = t.position_id +WHERE p.user_id = :user_id + AND t.action_type = 'sell' + AND t.is_completed = false + AND p.current_price >= t.plan_price + AND p.status = 'active'; +``` + +#### 3. 持仓更新流程 + +**用户修改持仓(主动变更)** +```sql +-- 用户直接修改成本价和份数 +UPDATE positions +SET + cost_price = :new_cost_price, + shares = :new_shares, + notes = :notes, + updated_at = CURRENT_TIMESTAMP +WHERE position_id = :position_id + AND user_id = :user_id; + +-- 用户手动更新价格时,将当前价格保存到 previous_price +UPDATE positions +SET + previous_price = current_price, -- 先将当前价格保存为上一次价格 + current_price = :new_price, -- 更新为新价格 + updated_at = CURRENT_TIMESTAMP +WHERE position_id = :position_id + AND user_id = :user_id; + +-- 记录持仓变更历史到 position_changes 表 +``` + +**系统更新市场价格(被动变更)** +```sql +-- 每日收盘后自动更新市场价格(仅更新启用自动更新的持仓) +UPDATE positions +SET + current_price = :market_price, + updated_at = CURRENT_TIMESTAMP +WHERE status = 'active' + AND auto_price_update = true + AND asset_type IN ('stock', 'fund', 'bond'); + +-- 查询需要自动更新价格的持仓(用于批量更新) +SELECT + p.position_id, + p.symbol, + p.market, + p.asset_type +FROM positions p +WHERE p.status = 'active' + AND p.auto_price_update = true + AND p.asset_type IN ('stock', 'fund', 'bond'); +``` + +#### 4. 注意事项 + +1. **唯一性约束** + - 同一用户同一券商同一资产(user_id + broker_id + symbol + market + asset_type)只能有一条持仓 + - 支持用户在不同券商持有同一资产(如:华泰证券持有茅台、招商证券也持有茅台) + - 避免同一券商重复持仓 + +2. **券商管理** + - 通过 brokers 表统一管理券商信息 + - 用户添加持仓时从券商列表中选择,保证数据一致性 + - 支持自定义券商(可以添加用户自定义券商) + - 通过 sort_order 控制券商显示顺序 + +2. **价格计划管理** + - 默认支持3个买点和3个卖点 + - 可以通过 step_order 扩展更多计划 + - 完成后的计划可以保留作为历史记录 + +3. **现金资产处理** + - 现金的 cost_price 固定为1 + - shares 字段存储现金余额 + - current_price 也为1(现金无价格波动) + +4. **多货币支持** + - 通过 exchange_rate 字段处理汇率 + - 计算总资产时需要统一货币单位 + +5. **价格计划提醒** + - 需要定时任务检查价格计划 + - 当 current_price 达到 plan_price 时发送提醒 + - 买入:current_price <= plan_price + - 卖出:current_price >= plan_price + +6. **自动价格更新功能** + - `auto_price_update` 字段控制是否启用自动价格更新 + - 付费用户创建持仓时,自动设置为 true + - 用户订阅过期时,批量更新为 false + - 未付费用户创建持仓时,默认为 false + - 系统每日收盘后,仅更新 `auto_price_update = true` 的持仓价格 + diff --git a/packages/design-document/我编写的文档/整体产品设计思路.md b/packages/design-document/我编写的文档/整体产品设计思路.md new file mode 100644 index 0000000..4664985 --- /dev/null +++ b/packages/design-document/我编写的文档/整体产品设计思路.md @@ -0,0 +1,28 @@ +# 整体产品设计思路 +--- +## 心智设计 +让每一笔投资都经过思考 +### 应用名思考 +- 投小记-记录、计划、复盘你的投资 +- 投小记-记录、决策、复盘你的投资 + +投小记 - 专业投资记账助手,助您记录每一笔交易、制定投资计划、进行投资复盘,轻松管理股票、基金等资产收益。 + +### 页面Title设计 +每个页面除了标题外,下边附带一句话 +首页 - 买股票就是买公司 +交易计划 - 计划你的交易,交易你的计划 +复盘页 - 回顾过去是为了更好应对将来 ?? + + +## 页面设计原则 +### 配置方案 +主题色:`#8b5cf6`,紫色主题 + +### 设计原则 +- 简洁又不失个性 +- 体现 安静与思考 原则 +- + +## 想法 +- 私密分享:可以将自己的交易计划和复盘,通过小程序私密分享-分享给其他人,这样即保障了裂变属性,有增加了隐私安全。 \ No newline at end of file diff --git a/packages/design-document/我编写的文档/计划模块-产品设计.md b/packages/design-document/我编写的文档/计划模块-产品设计.md new file mode 100644 index 0000000..864e0e7 --- /dev/null +++ b/packages/design-document/我编写的文档/计划模块-产品设计.md @@ -0,0 +1,6 @@ +# 计划模块-产品设计 + +## 二、交易计划模块 +### 1. 创建计划 +核心思想:计划应当和估值合并,计划依赖于估值。 +1. 交易计划列表页: \ No newline at end of file diff --git a/packages/design-document/机生文档/NestJS与PostgreSQL集成方案.md b/packages/design-document/机生文档/NestJS与PostgreSQL集成方案.md new file mode 100644 index 0000000..8f9dde3 --- /dev/null +++ b/packages/design-document/机生文档/NestJS与PostgreSQL集成方案.md @@ -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; + + @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, + ) {} + + async create(createTransactionDto: CreateTransactionDto): Promise { + 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 { + return await this.transactionRepository.findOne({ + where: { id, userId }, + relations: ['account', 'position', 'tradingPlan'], + }); + } + + async findByDateRange( + userId: number, + startDate: Date, + endDate: Date, + ): Promise { + return await this.transactionRepository.find({ + where: { + userId, + date: Between(startDate, endDate), + }, + order: { date: 'DESC' }, + }); + } + + async update(id: number, userId: number, updateData: Partial) { + await this.transactionRepository.update({ id, userId }, updateData); + return this.findOne(id, userId); + } + + async remove(id: number, userId: number): Promise { + 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, + ) {} + + 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 { + 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 { + 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年 + diff --git a/packages/design-document/机生文档/PostgreSQL-Mac使用手册.md b/packages/design-document/机生文档/PostgreSQL-Mac使用手册.md new file mode 100644 index 0000000..8eba8e6 --- /dev/null +++ b/packages/design-document/机生文档/PostgreSQL-Mac使用手册.md @@ -0,0 +1,1003 @@ +# PostgreSQL Mac 端使用手册 + +## 目录 + +1. [安装 PostgreSQL](#一安装-postgresql) +2. [连接数据库](#二连接数据库) +3. [查看所有数据库](#三查看所有数据库) +4. [创建数据库](#四创建数据库) +5. [切换数据库](#五切换数据库) +6. [查看表](#六查看表) +7. [创建表](#七创建表) +8. [常用操作命令](#八常用操作命令) + +--- + +## 一、安装 PostgreSQL + +### 1.1 使用 Homebrew 安装(推荐) + +```bash +# 安装 PostgreSQL(推荐使用最新版本) +brew install postgresql@18 + +# 或者安装其他版本 +brew install postgresql@18 +brew install postgresql@18 +``` + +### 1.2 启动 PostgreSQL 服务 + +```bash +# 启动服务(开机自启) +brew services start postgresql@18 + +# 停止服务 +brew services stop postgresql@18 + +# 重启服务 +brew services restart postgresql@18 + +# 查看服务状态 +brew services list +``` + +### 1.3 验证安装 + +```bash +# 查看版本 +psql --version + +# 查看 PostgreSQL 是否运行 +pg_isready +``` + +--- + +## 二、连接数据库 + +### 2.1 使用 psql 命令行连接 + +```bash +# 方式1:连接到默认数据库(使用当前系统用户名) +psql + +# 方式2:连接到指定数据库 +psql -d postgres + +# 方式3:指定用户名和数据库 +psql -U postgres -d postgres + +# 方式4:指定主机、端口、用户名和数据库 +psql -h localhost -p 5432 -U postgres -d postgres + +# 方式5:连接时输入密码(推荐用于脚本) +PGPASSWORD=your_password psql -U postgres -d postgres +``` + +### 2.2 连接后的提示符 + +连接成功后,你会看到类似这样的提示符: + +``` +postgres=# +``` + +- `postgres` 是当前连接的数据库名 +- `#` 表示你是超级用户(`$` 表示普通用户) + +### 2.3 退出连接 + +```sql +-- 方式1:使用命令 +\q + +-- 方式2:使用快捷键 +Ctrl + D +``` + +--- + +## 三、查看所有数据库 + +### 3.1 在 psql 中查看 + +```sql +-- 方式1:使用命令(推荐) +\l + +-- 方式2:使用详细模式 +\l+ + +-- 方式3:使用 SQL 查询 +SELECT datname FROM pg_database; + +-- 方式4:查看数据库详细信息 +SELECT + datname AS "数据库名", + pg_size_pretty(pg_database_size(datname)) AS "大小", + datcollate AS "排序规则", + datctype AS "字符集" +FROM pg_database +ORDER BY datname; +``` + +### 3.2 在命令行中查看(不进入 psql) + +```bash +# 列出所有数据库 +psql -l + +# 或者 +psql -U postgres -l +``` + +--- + +## 四、创建数据库 + +### 4.1 基本语法 + +```sql +CREATE DATABASE database_name; +``` + +### 4.2 创建数据库示例 + +```sql +-- 示例1:创建简单数据库 +CREATE DATABASE myapp; + +-- 示例2:创建数据库(指定编码和排序规则) +CREATE DATABASE myapp + WITH ENCODING 'UTF8' + LC_COLLATE='en_US.UTF-8' + LC_CTYPE='en_US.UTF-8'; + +-- 示例3:创建数据库(指定所有者) +CREATE DATABASE myapp OWNER username; + +-- 示例4:创建数据库(指定模板) +CREATE DATABASE myapp TEMPLATE template0; +``` + +### 4.3 创建数据库的完整选项 + +```sql +CREATE DATABASE database_name + [ [ WITH ] [ OWNER [=] user_name ] + [ TEMPLATE [=] template ] + [ ENCODING [=] encoding ] + [ LC_COLLATE [=] lc_collate ] + [ LC_CTYPE [=] lc_ctype ] + [ TABLESPACE [=] tablespace_name ] + [ ALLOW_CONNECTIONS [=] allowconn ] + [ CONNECTION LIMIT [=] connlimit ] + [ IS_TEMPLATE [=] istemplate ] ]; +``` + +### 4.4 常用参数说明 + +| 参数 | 说明 | 示例 | +|------|------|------| +| `OWNER` | 数据库所有者 | `OWNER myuser` | +| `TEMPLATE` | 模板数据库 | `TEMPLATE template0` | +| `ENCODING` | 字符编码 | `ENCODING 'UTF8'` | +| `LC_COLLATE` | 排序规则 | `LC_COLLATE='en_US.UTF-8'` | +| `LC_CTYPE` | 字符分类 | `LC_CTYPE='en_US.UTF-8'` | + +### 4.5 创建数据库的完整示例 + +```sql +-- 创建生产环境数据库 +CREATE DATABASE vest_mind_prod + WITH + OWNER = postgres + ENCODING = 'UTF8' + LC_COLLATE = 'en_US.UTF-8' + LC_CTYPE = 'en_US.UTF-8' + TEMPLATE = template0 + CONNECTION LIMIT = 100; + +-- 创建开发环境数据库 +CREATE DATABASE vest_mind_dev + WITH + OWNER = postgres + ENCODING = 'UTF8' + TEMPLATE = template0; +``` + +### 4.6 验证数据库是否创建成功 + +```sql +-- 方式1:查看所有数据库(推荐) +\l + +-- 方式2:使用 SQL 查询 +SELECT datname FROM pg_database WHERE datname = 'database_name'; + +-- 方式3:查看数据库详细信息 +SELECT + datname AS "数据库名", + pg_size_pretty(pg_database_size(datname)) AS "大小", + datcollate AS "排序规则", + datctype AS "字符集" +FROM pg_database +WHERE datname = 'database_name'; +``` + +### 4.7 创建数据库的注意事项 + +1. **数据库名称规则:** + - 必须以字母开头 + - 只能包含字母、数字、下划线和连字符 + - 不能使用 PostgreSQL 保留关键字 + - 建议使用小写字母和下划线 + +2. **权限要求:** + - 需要 `CREATEDB` 权限 + - 超级用户(如 `postgres`)可以创建任何数据库 + +3. **模板数据库:** + - `template0`:干净的模板,推荐使用 + - `template1`:默认模板,可以修改 + - 自定义模板:基于现有数据库创建 + +4. **字符编码:** + - 推荐使用 `UTF8` 编码 + - 确保 `LC_COLLATE` 和 `LC_CTYPE` 设置正确 + +### 4.8 创建数据库后立即使用 + +```sql +-- 创建数据库 +CREATE DATABASE myapp; + +-- 切换到新创建的数据库 +\c myapp + +-- 或者退出后重新连接 +\q +psql -d myapp +``` + +### 4.9 删除数据库 + +```sql +-- 删除数据库(注意:需要先断开所有连接) +DROP DATABASE database_name; + +-- 删除数据库(如果存在) +DROP DATABASE IF EXISTS database_name; + +-- 强制删除数据库(终止所有连接后删除) +-- 首先终止所有连接 +SELECT pg_terminate_backend(pid) +FROM pg_stat_activity +WHERE datname = 'database_name' AND pid <> pg_backend_pid(); + +-- 然后删除 +DROP DATABASE database_name; +``` + +### 4.10 重命名数据库 + +```sql +-- 重命名数据库 +ALTER DATABASE old_name RENAME TO new_name; +``` + +--- + +## 五、切换数据库 + +### 5.1 在 psql 中切换 + +```sql +-- 使用 \c 命令切换数据库 +\c database_name + +-- 切换到指定数据库并显示连接信息 +\c database_name username + +-- 查看当前连接的数据库 +SELECT current_database(); + +-- 或者使用命令 +\conninfo +``` + +### 5.2 退出后重新连接 + +```bash +# 退出当前连接 +\q + +# 重新连接到指定数据库 +psql -d database_name +``` + +--- + +## 六、查看表 + +### 6.1 查看当前数据库中的所有表 + +```sql +-- 方式1:使用命令(推荐) +\dt + +-- 方式2:查看详细信息(包括大小、描述等) +\dt+ + +-- 方式3:使用 SQL 查询 +SELECT + schemaname AS "模式", + tablename AS "表名" +FROM pg_tables +WHERE schemaname = 'public' +ORDER BY tablename; + +-- 方式4:查看所有表(包括系统表) +\dt *.* +``` + +### 6.2 查看表结构 + +```sql +-- 方式1:查看表结构(推荐) +\d table_name + +-- 方式2:查看详细信息(包括索引、约束等) +\d+ table_name + +-- 方式3:只查看列信息 +\d table_name + +-- 方式4:使用 SQL 查询列信息 +SELECT + column_name AS "列名", + data_type AS "数据类型", + character_maximum_length AS "最大长度", + is_nullable AS "可空", + column_default AS "默认值" +FROM information_schema.columns +WHERE table_name = 'table_name' +ORDER BY ordinal_position; +``` + +### 6.3 查看表的索引 + +```sql +-- 查看表的所有索引 +\d table_name + +-- 或者使用 SQL +SELECT + indexname AS "索引名", + indexdef AS "索引定义" +FROM pg_indexes +WHERE tablename = 'table_name'; +``` + +### 6.4 查看表的约束 + +```sql +-- 查看表的所有约束 +\d table_name + +-- 或者使用 SQL +SELECT + conname AS "约束名", + contype AS "约束类型", + pg_get_constraintdef(oid) AS "约束定义" +FROM pg_constraint +WHERE conrelid = 'table_name'::regclass; +``` + +--- + +## 七、创建表 + +### 7.1 基本语法 + +```sql +CREATE TABLE table_name ( + column1_name data_type [constraints], + column2_name data_type [constraints], + ... +); +``` + +### 7.2 创建表示例 + +```sql +-- 示例1:创建用户表 +CREATE TABLE users ( + user_id BIGSERIAL PRIMARY KEY, + username VARCHAR(100) NOT NULL UNIQUE, + 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 +); + +-- 示例2:创建订单表(带外键) +CREATE TABLE orders ( + order_id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + total_amount DECIMAL(10, 2) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +); + +-- 示例3:创建带检查约束的表 +CREATE TABLE products ( + product_id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + price DECIMAL(10, 2) NOT NULL CHECK (price > 0), + stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +### 7.3 常用数据类型 + +| 数据类型 | 说明 | 示例 | +|---------|------|------| +| `BIGSERIAL` | 自增整数(64位) | `user_id BIGSERIAL PRIMARY KEY` | +| `BIGINT` | 大整数(64位) | `user_id BIGINT` | +| `INTEGER` | 整数(32位) | `age INTEGER` | +| `VARCHAR(n)` | 可变长度字符串 | `username VARCHAR(100)` | +| `TEXT` | 长文本 | `description TEXT` | +| `DECIMAL(p,s)` | 精确小数 | `price DECIMAL(10, 2)` | +| `BOOLEAN` | 布尔值 | `is_active BOOLEAN` | +| `DATE` | 日期 | `birthday DATE` | +| `TIMESTAMP` | 时间戳 | `created_at TIMESTAMP` | +| `JSONB` | JSON 二进制 | `metadata JSONB` | + +### 7.4 常用约束 + +```sql +-- PRIMARY KEY:主键 +user_id BIGSERIAL PRIMARY KEY + +-- NOT NULL:非空 +username VARCHAR(100) NOT NULL + +-- UNIQUE:唯一 +email VARCHAR(100) UNIQUE + +-- DEFAULT:默认值 +created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + +-- CHECK:检查约束 +price DECIMAL(10, 2) CHECK (price > 0) + +-- FOREIGN KEY:外键 +user_id BIGINT REFERENCES users(user_id) + +-- 组合约束 +CONSTRAINT pk_user PRIMARY KEY (user_id), +CONSTRAINT uk_email UNIQUE (email), +CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES users(user_id) +``` + +### 7.5 验证表是否创建成功 + +```sql +-- 查看所有表 +\dt + +-- 查看表结构 +\d table_name + +-- 查看表是否存在 +SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'table_name' +); +``` + +--- + +## 八、常用操作命令 + +### 8.1 数据库操作 + +```sql +-- 创建数据库 +CREATE DATABASE database_name; + +-- 创建数据库(指定编码) +CREATE DATABASE database_name + WITH ENCODING 'UTF8' + LC_COLLATE='en_US.UTF-8' + LC_CTYPE='en_US.UTF-8'; + +-- 删除数据库 +DROP DATABASE database_name; + +-- 重命名数据库 +ALTER DATABASE old_name RENAME TO new_name; +``` + +### 8.2 表操作 + +```sql +-- 删除表 +DROP TABLE table_name; + +-- 删除表(如果存在) +DROP TABLE IF EXISTS table_name; + +-- 清空表数据(保留表结构) +TRUNCATE TABLE table_name; + +-- 重命名表 +ALTER TABLE old_name RENAME TO new_name; + +-- 添加列 +ALTER TABLE table_name ADD COLUMN column_name data_type; + +-- 删除列 +ALTER TABLE table_name DROP COLUMN column_name; + +-- 修改列类型 +ALTER TABLE table_name ALTER COLUMN column_name TYPE new_type; + +-- 添加约束 +ALTER TABLE table_name ADD CONSTRAINT constraint_name CHECK (condition); +``` + +### 8.3 数据操作(CRUD) + +```sql +-- 插入数据 +INSERT INTO users (username, email, password_hash) +VALUES ('john', 'john@example.com', 'hashed_password'); + +-- 查询数据 +SELECT * FROM users; +SELECT username, email FROM users WHERE user_id = 1; + +-- 更新数据 +UPDATE users SET email = 'newemail@example.com' WHERE user_id = 1; + +-- 删除数据 +DELETE FROM users WHERE user_id = 1; +``` + +### 8.4 psql 常用命令 + +```sql +-- 帮助命令 +\? -- 查看所有命令 +\h -- SQL 命令帮助 +\h CREATE TABLE -- 查看特定 SQL 命令帮助 + +-- 数据库相关 +\l -- 列出所有数据库 +\c dbname -- 连接到数据库 +\conninfo -- 显示当前连接信息 + +-- 表相关 +\dt -- 列出所有表 +\d table -- 查看表结构 +\d+ table -- 查看表详细信息 + +-- 其他 +\q -- 退出 +\du -- 列出所有用户 +\dn -- 列出所有模式(schema) +\df -- 列出所有函数 +\timing -- 开启/关闭执行时间显示 +\x -- 开启/关闭扩展显示(用于宽表) +``` + +### 8.5 查看执行计划 + +```sql +-- 查看查询执行计划 +EXPLAIN SELECT * FROM users WHERE user_id = 1; + +-- 查看详细执行计划(包含实际执行时间) +EXPLAIN ANALYZE SELECT * FROM users WHERE user_id = 1; +``` + +--- + +## 九、实用技巧 + +### 9.1 设置用户名和密码 + +#### 9.1.1 为现有用户设置密码 + +```sql +-- 为 postgres 用户设置密码 +ALTER USER postgres WITH PASSWORD 'your_password'; + +-- 为当前用户设置密码 +ALTER USER current_user WITH PASSWORD 'your_password'; + +-- 查看当前用户 +SELECT current_user; +``` + +#### 9.1.2 创建新用户并设置密码 + +```sql +-- 创建新用户并设置密码 +CREATE USER username WITH PASSWORD 'your_password'; + +-- 创建用户并指定更多选项 +CREATE USER username + WITH PASSWORD 'your_password' + CREATEDB -- 允许创建数据库 + CREATEROLE; -- 允许创建角色 + +-- 查看所有用户 +\du +``` + +#### 9.1.3 为用户授予数据库权限 + +```sql +-- 授予数据库的所有权限 +GRANT ALL PRIVILEGES ON DATABASE database_name TO username; + +-- 授予特定权限 +GRANT CONNECT ON DATABASE database_name TO username; +GRANT CREATE ON DATABASE database_name TO username; + +-- 授予表的权限(需要先连接到数据库) +\c database_name +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO username; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO username; + +-- 授予未来创建的表和序列的权限 +ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT ALL ON TABLES TO username; +ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT ALL ON SEQUENCES TO username; +``` + +#### 9.1.4 完整示例:为新数据库设置用户和密码 + +```sql +-- 1. 连接到 PostgreSQL(使用超级用户) +psql -d postgres + +-- 2. 创建新用户 +CREATE USER vest_mind_user WITH PASSWORD 'your_secure_password'; + +-- 3. 授予数据库权限 +GRANT ALL PRIVILEGES ON DATABASE vest_mind TO vest_mind_user; + +-- 4. 切换到目标数据库 +\c vest_mind + +-- 5. 授予 schema 权限 +GRANT ALL ON SCHEMA public TO vest_mind_user; + +-- 6. 授予现有表的权限 +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO vest_mind_user; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO vest_mind_user; + +-- 7. 授予未来创建的表和序列的权限 +ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT ALL ON TABLES TO vest_mind_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT ALL ON SEQUENCES TO vest_mind_user; + +-- 8. 验证权限 +\du vest_mind_user +``` + +#### 9.1.5 使用新用户连接数据库 + +```bash +# 方式1:使用密码连接 +psql -U vest_mind_user -d vest_mind + +# 方式2:使用环境变量(用于脚本) +PGPASSWORD=your_secure_password psql -U vest_mind_user -d vest_mind + +# 方式3:在连接字符串中指定 +psql "postgresql://vest_mind_user:your_secure_password@localhost:5432/vest_mind" +``` + +#### 9.1.6 修改用户密码 + +```sql +-- 修改用户密码 +ALTER USER username WITH PASSWORD 'new_password'; + +-- 强制用户下次登录时修改密码 +ALTER USER username WITH PASSWORD 'new_password' VALID UNTIL '2025-12-31'; +``` + +#### 9.1.7 删除用户 + +```sql +-- 删除用户(如果用户拥有对象,需要先转移所有权) +DROP USER username; + +-- 或者先转移所有权再删除 +REASSIGN OWNED BY username TO postgres; +DROP OWNED BY username; +DROP USER username; +``` + +#### 9.1.8 配置 PostgreSQL 认证方式(可选) + +如果需要强制使用密码认证,可以编辑 PostgreSQL 配置文件: + +```bash +# 找到配置文件位置(Homebrew 安装) +# 通常在:/opt/homebrew/var/postgresql@18/pg_hba.conf +# 或:/usr/local/var/postgresql@18/pg_hba.conf + +# 编辑配置文件 +nano /opt/homebrew/var/postgresql@18/pg_hba.conf +``` + +在 `pg_hba.conf` 文件中,找到类似这样的行: + +``` +# TYPE DATABASE USER ADDRESS METHOD +local all all trust +host all all 127.0.0.1/32 trust +``` + +修改为需要密码认证: + +``` +# TYPE DATABASE USER ADDRESS METHOD +local all all md5 +host all all 127.0.0.1/32 md5 +``` + +然后重启 PostgreSQL 服务: + +```bash +brew services restart postgresql@18 +``` + +**认证方式说明:** +- `trust`:无需密码(当前默认) +- `md5`:使用 MD5 加密的密码 +- `password`:使用明文密码(不推荐) +- `scram-sha-256`:使用 SCRAM-SHA-256 加密(推荐,PostgreSQL 10+) + +### 9.2 导出和导入数据 + +```bash +# 导出数据库 +pg_dump -U postgres -d database_name > backup.sql + +# 导入数据库 +psql -U postgres -d database_name < backup.sql + +# 导出表 +pg_dump -U postgres -d database_name -t table_name > table_backup.sql + +# 导出为 CSV +psql -U postgres -d database_name -c "COPY table_name TO STDOUT WITH CSV HEADER" > data.csv +``` + +### 9.3 查看数据库大小 + +```sql +-- 查看当前数据库大小 +SELECT pg_size_pretty(pg_database_size(current_database())); + +-- 查看所有数据库大小 +SELECT + datname AS "数据库名", + pg_size_pretty(pg_database_size(datname)) AS "大小" +FROM pg_database +ORDER BY pg_database_size(datname) DESC; + +-- 查看表大小 +SELECT + schemaname AS "模式", + tablename AS "表名", + pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS "大小" +FROM pg_tables +WHERE schemaname = 'public' +ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC; +``` + +### 9.4 查看连接信息 + +```sql +-- 查看当前连接 +\conninfo + +-- 查看所有活动连接 +SELECT + pid, + usename, + datname, + client_addr, + state, + query +FROM pg_stat_activity +WHERE datname IS NOT NULL; +``` + +### 9.5 环境变量配置(可选) + +在 `~/.zshrc` 或 `~/.bash_profile` 中添加: + +```bash +# PostgreSQL 路径配置 +export PATH="/opt/homebrew/opt/postgresql@18/bin:$PATH" +export LDFLAGS="-L/opt/homebrew/opt/postgresql@18/lib" +export CPPFLAGS="-I/opt/homebrew/opt/postgresql@18/include" + +# PostgreSQL 连接别名(可选) +alias pgstart='brew services start postgresql@18' +alias pgstop='brew services stop postgresql@18' +alias pgrestart='brew services restart postgresql@18' +alias pgstatus='brew services list | grep postgresql' +``` + +--- + +## 十、快速参考 + +### 10.1 常用连接方式 + +```bash +# 最简单的方式 +psql + +# 指定数据库 +psql -d mydb + +# 指定用户和数据库 +psql -U postgres -d mydb + +# 带密码连接(用于脚本) +PGPASSWORD=password psql -U postgres -d mydb +``` + +### 10.2 常用 SQL 命令速查 + +```sql +-- 数据库 +CREATE DATABASE dbname; +DROP DATABASE dbname; +\l + +-- 表 +CREATE TABLE tname (...); +DROP TABLE tname; +\dt +\d tname + +-- 数据 +INSERT INTO tname VALUES (...); +SELECT * FROM tname; +UPDATE tname SET ... WHERE ...; +DELETE FROM tname WHERE ...; + +-- 用户 +CREATE USER username WITH PASSWORD 'password'; +\du +``` + +### 10.3 故障排查 + +```bash +# 检查 PostgreSQL 是否运行 +pg_isready + +# 查看日志(Homebrew 安装) +tail -f /opt/homebrew/var/log/postgresql@18.log + +# 或者 +tail -f /usr/local/var/log/postgresql@18.log + +# 重启服务 +brew services restart postgresql@18 +``` + +--- + +## 十一、示例:完整工作流程 + +```bash +# 1. 启动 PostgreSQL +brew services start postgresql@18 + +# 2. 连接到数据库 +psql -d postgres + +# 3. 创建数据库 +CREATE DATABASE myapp; + +# 4. 切换到新数据库 +\c myapp + +# 5. 创建表 +CREATE TABLE users ( + user_id BIGSERIAL PRIMARY KEY, + username VARCHAR(100) NOT NULL UNIQUE, + email VARCHAR(100) UNIQUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +# 6. 查看表 +\dt +\d users + +# 7. 插入数据 +INSERT INTO users (username, email) +VALUES ('john', 'john@example.com'); + +# 8. 查询数据 +SELECT * FROM users; + +# 9. 退出 +\q +``` + +--- + +## 十二、图形化工具推荐 + +### 12.1 pgAdmin +- 官网:https://www.pgadmin.org/ +- 功能强大的 PostgreSQL 管理工具 + +### 12.2 DBeaver +- 官网:https://dbeaver.io/ +- 免费、跨平台的数据库管理工具 + +### 12.3 TablePlus +- 官网:https://tableplus.com/ +- Mac 上优秀的数据库管理工具(付费) + +### 12.4 Postico +- 官网:https://eggerapps.at/postico/ +- Mac 专用的 PostgreSQL 客户端(付费) + +--- + +## 十三、常见问题 + +### Q1: 连接时提示 "password authentication failed" +**A:** 检查用户名和密码是否正确,或者重置 postgres 用户密码: +```sql +ALTER USER postgres WITH PASSWORD 'new_password'; +``` + +### Q2: 找不到 psql 命令 +**A:** 需要将 PostgreSQL 的 bin 目录添加到 PATH: +```bash +export PATH="/opt/homebrew/opt/postgresql@18/bin:$PATH" +``` + +### Q3: 端口 5432 已被占用 +**A:** 检查端口占用情况: +```bash +lsof -i :5432 +``` + +### Q4: 如何查看 PostgreSQL 版本 +**A:** +```bash +psql --version +# 或者在 psql 中 +SELECT version(); +``` + +--- + +**文档版本:** v1.0 +**最后更新:** 2024年 + diff --git a/packages/design-document/机生文档/PostgreSQL使用文档.md b/packages/design-document/机生文档/PostgreSQL使用文档.md new file mode 100644 index 0000000..76e63ec --- /dev/null +++ b/packages/design-document/机生文档/PostgreSQL使用文档.md @@ -0,0 +1,1527 @@ +# PostgreSQL 使用文档 + +## 目录 + +1. [安装 PostgreSQL](#一安装-postgresql) +2. [基本操作](#二基本操作) +3. [数据库操作](#三数据库操作) +4. [表操作](#四表操作) +5. [数据操作(CRUD)](#五数据操作crud) +6. [索引操作](#六索引操作) +7. [高级查询](#七高级查询) +8. [事务管理](#八事务管理) +9. [常用函数](#九常用函数) +10. [实用技巧](#十实用技巧) + +--- + +## 一、安装 PostgreSQL + +### 1.1 macOS 安装 + +#### 方法一:使用 Homebrew(推荐) + +```bash +# 安装 Homebrew(如果还没有) +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + +# 安装 PostgreSQL +brew install postgresql@15 + +# 启动 PostgreSQL 服务 +brew services start postgresql@15 + +# 或者手动启动 +pg_ctl -D /usr/local/var/postgresql@15 start +``` + +#### 方法二:使用 Postgres.app(图形化工具) + +1. 访问 https://postgresapp.com/ +2. 下载并安装 Postgres.app +3. 打开应用,点击 "Initialize" 初始化数据库 + +#### 方法三:使用官方安装包 + +1. 访问 https://www.postgresql.org/download/macosx/ +2. 下载官方安装包 +3. 按照安装向导完成安装 + +### 1.2 Linux 安装 + +#### Ubuntu/Debian + +```bash +# 更新包列表 +sudo apt update + +# 安装 PostgreSQL +sudo apt install postgresql postgresql-contrib + +# 启动服务 +sudo systemctl start postgresql + +# 设置开机自启 +sudo systemctl enable postgresql + +# 查看服务状态 +sudo systemctl status postgresql +``` + +#### CentOS/RHEL/Fedora + +```bash +# CentOS/RHEL 7+ +sudo yum install postgresql-server postgresql-contrib + +# 或者使用 dnf (Fedora/CentOS 8+) +sudo dnf install postgresql-server postgresql-contrib + +# 初始化数据库 +sudo postgresql-setup --initdb + +# 启动服务 +sudo systemctl start postgresql +sudo systemctl enable postgresql +``` + +#### Arch Linux + +```bash +# 安装 +sudo pacman -S postgresql + +# 初始化数据库 +sudo -u postgres initdb -D /var/lib/postgres/data + +# 启动服务 +sudo systemctl start postgresql +sudo systemctl enable postgresql +``` + +### 1.3 验证安装 + +```bash +# 检查 PostgreSQL 版本 +psql --version + +# 或者 +postgres --version + +# 检查服务状态(Linux) +sudo systemctl status postgresql +``` + +### 1.4 初始配置 + +#### 设置 postgres 用户密码 + +```bash +# 切换到 postgres 用户(Linux) +sudo -u postgres psql + +# 或者直接连接(macOS) +psql -U postgres + +# 在 psql 中设置密码 +ALTER USER postgres PASSWORD 'your_password'; +``` + +#### 创建新用户和数据库 + +```bash +# 使用 postgres 用户登录 +psql -U postgres + +# 创建新用户 +CREATE USER myuser WITH PASSWORD 'mypassword'; + +# 创建数据库 +CREATE DATABASE mydb OWNER myuser; + +# 授予权限 +GRANT ALL PRIVILEGES ON DATABASE mydb TO myuser; + +# 退出 +\q +``` + +--- + +## 二、基本操作 + +### 2.1 连接数据库 + +```bash +# 使用默认用户连接默认数据库 +psql + +# 指定用户和数据库 +psql -U username -d database_name + +# 指定主机和端口 +psql -h localhost -p 5432 -U username -d database_name + +# 使用连接字符串 +psql postgresql://username:password@localhost:5432/database_name +``` + +### 2.2 psql 常用命令 + +```sql +-- 查看所有数据库 +\l +-- 或者 +\list + +-- 连接到指定数据库 +\c database_name +-- 或者 +\connect database_name + +-- 查看当前数据库 +SELECT current_database(); + +-- 查看所有表 +\dt + +-- 查看表结构 +\d table_name + +-- 查看表详细信息(包括索引、约束等) +\d+ table_name + +-- 查看所有用户 +\du + +-- 查看当前用户 +SELECT current_user; + +-- 查看帮助 +\? +-- 查看 SQL 命令帮助 +\h +-- 查看特定命令帮助 +\h SELECT + +-- 退出 psql +\q +-- 或者 +exit + +-- 清屏 +\! clear -- Linux +\! cls -- Windows + +-- 执行外部 SQL 文件 +\i /path/to/file.sql + +-- 将查询结果导出到文件 +\o /path/to/output.txt +SELECT * FROM users; +\o + +-- 显示执行时间 +\timing + +-- 显示查询结果(表格格式) +\x -- 切换显示模式(横向/纵向) + +-- 查看命令历史 +\s + +-- 保存命令历史到文件 +\s /path/to/history.sql +``` + +--- + +## 三、数据库操作 + +### 3.1 创建数据库 + +```sql +-- 基本语法 +CREATE DATABASE database_name; + +-- 指定所有者 +CREATE DATABASE database_name OWNER username; + +-- 指定编码 +CREATE DATABASE database_name + WITH ENCODING 'UTF8'; + +-- 完整示例 +CREATE DATABASE vestmind + OWNER myuser + ENCODING 'UTF8' + LC_COLLATE 'zh_CN.UTF-8' + LC_CTYPE 'zh_CN.UTF-8' + TEMPLATE template0; +``` + +### 3.2 查看数据库 + +```sql +-- 查看所有数据库 +SELECT datname FROM pg_database; + +-- 查看数据库详细信息 +\l +-- 或者 +\list + +-- 查看当前数据库 +SELECT current_database(); +``` + +### 3.3 删除数据库 + +```sql +-- 删除数据库(需要先断开所有连接) +DROP DATABASE database_name; + +-- 强制删除(终止所有连接) +-- 首先终止所有连接 +SELECT pg_terminate_backend(pid) +FROM pg_stat_activity +WHERE datname = 'database_name'; + +-- 然后删除 +DROP DATABASE database_name; +``` + +### 3.4 修改数据库 + +```sql +-- 重命名数据库 +ALTER DATABASE old_name RENAME TO new_name; + +-- 修改所有者 +ALTER DATABASE database_name OWNER TO new_owner; + +-- 修改连接限制 +ALTER DATABASE database_name WITH CONNECTION LIMIT 10; +``` + +### 3.5 备份和恢复 + +```bash +# 备份数据库 +pg_dump -U username -d database_name > backup.sql + +# 备份为自定义格式(压缩) +pg_dump -U username -d database_name -F c -f backup.dump + +# 备份所有数据库 +pg_dumpall -U username > all_databases.sql + +# 恢复数据库 +psql -U username -d database_name < backup.sql + +# 恢复自定义格式备份 +pg_restore -U username -d database_name backup.dump + +# 只恢复表结构 +pg_dump -U username -d database_name -s > schema_only.sql + +# 只恢复数据 +pg_dump -U username -d database_name -a > data_only.sql +``` + +--- + +## 四、表操作 + +### 4.1 创建表 + +#### 基本语法 + +```sql +CREATE TABLE table_name ( + column1 datatype constraint, + column2 datatype constraint, + ... +); +``` + +#### 数据类型 + +**数值类型:** +```sql +SMALLINT -- 2字节,-32768 到 32767 +INTEGER -- 4字节,-2147483648 到 2147483647 +BIGINT -- 8字节,大整数 +DECIMAL(p,s) -- 精确数值,p=精度,s=小数位数 +NUMERIC(p,s) -- 同 DECIMAL +REAL -- 4字节,单精度浮点数 +DOUBLE PRECISION -- 8字节,双精度浮点数 +SERIAL -- 自增整数(1 到 2147483647) +BIGSERIAL -- 大自增整数 +``` + +**字符串类型:** +```sql +VARCHAR(n) -- 可变长度字符串,最大n字符 +CHAR(n) -- 固定长度字符串,n字符 +TEXT -- 无限长度字符串 +``` + +**日期时间类型:** +```sql +DATE -- 日期(年-月-日) +TIME -- 时间(时:分:秒) +TIMESTAMP -- 日期和时间 +TIMESTAMPTZ -- 带时区的时间戳 +INTERVAL -- 时间间隔 +``` + +**布尔类型:** +```sql +BOOLEAN -- true/false +``` + +**JSON 类型:** +```sql +JSON -- JSON 数据 +JSONB -- 二进制 JSON(推荐,性能更好) +``` + +#### 约束 + +```sql +-- 主键约束 +id INTEGER PRIMARY KEY +-- 或者 +id INTEGER, +PRIMARY KEY (id) + +-- 外键约束 +user_id INTEGER REFERENCES users(id) +-- 或者 +user_id INTEGER, +FOREIGN KEY (user_id) REFERENCES users(id) + +-- 唯一约束 +email VARCHAR(100) UNIQUE +-- 或者 +email VARCHAR(100), +UNIQUE (email) + +-- 非空约束 +name VARCHAR(50) NOT NULL + +-- 检查约束 +age INTEGER CHECK (age > 0 AND age < 150) + +-- 默认值 +created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + +-- 自增(使用 SERIAL) +id SERIAL PRIMARY KEY +``` + +#### 完整示例 + +```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), + status VARCHAR(20) DEFAULT 'active', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_login_at TIMESTAMP, + CHECK (status IN ('active', 'inactive', 'deleted')) +); + +-- 创建账户表 +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, + currency VARCHAR(10) NOT NULL DEFAULT 'CNY', + description TEXT, + status VARCHAR(20) DEFAULT 'active', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CHECK (type IN ('stock', 'fund', 'cash', 'mixed')), + CHECK (status IN ('active', 'archived', 'deleted')) +); + +-- 创建持仓表 +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, + 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', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(account_id, symbol, market), + CHECK (shares >= 0), + CHECK (status IN ('active', 'suspended', 'delisted')) +); +``` + +### 4.2 查看表 + +```sql +-- 查看所有表 +\dt + +-- 查看表结构 +\d table_name + +-- 查看表详细信息 +\d+ table_name + +-- 查看表的所有列 +SELECT column_name, data_type, is_nullable, column_default +FROM information_schema.columns +WHERE table_name = 'users'; + +-- 查看表的约束 +SELECT constraint_name, constraint_type +FROM information_schema.table_constraints +WHERE table_name = 'users'; +``` + +### 4.3 修改表 + +```sql +-- 添加列 +ALTER TABLE table_name +ADD COLUMN column_name datatype; + +-- 示例 +ALTER TABLE users +ADD COLUMN age INTEGER; + +-- 删除列 +ALTER TABLE table_name +DROP COLUMN column_name; + +-- 修改列类型 +ALTER TABLE table_name +ALTER COLUMN column_name TYPE new_datatype; + +-- 示例 +ALTER TABLE users +ALTER COLUMN age TYPE SMALLINT; + +-- 重命名列 +ALTER TABLE table_name +RENAME COLUMN old_name TO new_name; + +-- 添加约束 +ALTER TABLE table_name +ADD CONSTRAINT constraint_name constraint_definition; + +-- 示例:添加外键 +ALTER TABLE accounts +ADD CONSTRAINT fk_user +FOREIGN KEY (user_id) REFERENCES users(id); + +-- 删除约束 +ALTER TABLE table_name +DROP CONSTRAINT constraint_name; + +-- 重命名表 +ALTER TABLE old_table_name +RENAME TO new_table_name; + +-- 设置列的默认值 +ALTER TABLE table_name +ALTER COLUMN column_name SET DEFAULT default_value; + +-- 删除列的默认值 +ALTER TABLE table_name +ALTER COLUMN column_name DROP DEFAULT; + +-- 设置列非空 +ALTER TABLE table_name +ALTER COLUMN column_name SET NOT NULL; + +-- 取消列非空 +ALTER TABLE table_name +ALTER COLUMN column_name DROP NOT NULL; +``` + +### 4.4 删除表 + +```sql +-- 删除表 +DROP TABLE table_name; + +-- 删除表(如果存在) +DROP TABLE IF EXISTS table_name; + +-- 级联删除(删除表及其依赖对象) +DROP TABLE table_name CASCADE; + +-- 清空表数据(保留表结构) +TRUNCATE TABLE table_name; + +-- 清空表数据并重置自增序列 +TRUNCATE TABLE table_name RESTART IDENTITY; +``` + +--- + +## 五、数据操作(CRUD) + +### 5.1 插入数据(INSERT) + +#### 基本语法 + +```sql +-- 插入单行数据 +INSERT INTO table_name (column1, column2, ...) +VALUES (value1, value2, ...); + +-- 插入多行数据 +INSERT INTO table_name (column1, column2, ...) +VALUES + (value1, value2, ...), + (value3, value4, ...), + (value5, value6, ...); + +-- 插入所有列(按表定义顺序) +INSERT INTO table_name +VALUES (value1, value2, ...); + +-- 从查询结果插入 +INSERT INTO table_name (column1, column2, ...) +SELECT column1, column2, ... +FROM other_table +WHERE condition; +``` + +#### 示例 + +```sql +-- 插入用户 +INSERT INTO users (username, email, password_hash, nickname) +VALUES ('john_doe', 'john@example.com', 'hashed_password', 'John'); + +-- 插入多用户 +INSERT INTO users (username, email, password_hash, nickname) +VALUES + ('alice', 'alice@example.com', 'hash1', 'Alice'), + ('bob', 'bob@example.com', 'hash2', 'Bob'), + ('charlie', 'charlie@example.com', 'hash3', 'Charlie'); + +-- 使用 RETURNING 返回插入的数据 +INSERT INTO users (username, email, password_hash) +VALUES ('david', 'david@example.com', 'hash4') +RETURNING id, username, created_at; + +-- 插入时处理冲突(ON CONFLICT) +INSERT INTO users (username, email, password_hash) +VALUES ('john_doe', 'john@example.com', 'new_hash') +ON CONFLICT (username) +DO UPDATE SET + email = EXCLUDED.email, + password_hash = EXCLUDED.password_hash, + updated_at = CURRENT_TIMESTAMP; +``` + +### 5.2 查询数据(SELECT) + +#### 基本语法 + +```sql +-- 查询所有列 +SELECT * FROM table_name; + +-- 查询指定列 +SELECT column1, column2, ... FROM table_name; + +-- 条件查询 +SELECT * FROM table_name WHERE condition; + +-- 排序 +SELECT * FROM table_name ORDER BY column_name ASC/DESC; + +-- 限制结果数量 +SELECT * FROM table_name LIMIT number; + +-- 跳过结果 +SELECT * FROM table_name OFFSET number; + +-- 组合使用 +SELECT * FROM table_name +WHERE condition +ORDER BY column_name DESC +LIMIT 10 OFFSET 20; +``` + +#### 示例 + +```sql +-- 查询所有用户 +SELECT * FROM users; + +-- 查询指定列 +SELECT id, username, email, created_at FROM users; + +-- 条件查询 +SELECT * FROM users WHERE status = 'active'; + +-- 多条件查询 +SELECT * FROM users +WHERE status = 'active' AND created_at > '2024-01-01'; + +-- 模糊查询 +SELECT * FROM users WHERE username LIKE 'john%'; + +-- IN 查询 +SELECT * FROM users WHERE id IN (1, 2, 3, 4, 5); + +-- BETWEEN 查询 +SELECT * FROM users +WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31'; + +-- 排序 +SELECT * FROM users ORDER BY created_at DESC; + +-- 多列排序 +SELECT * FROM users +ORDER BY status ASC, created_at DESC; + +-- 限制数量 +SELECT * FROM users LIMIT 10; + +-- 分页查询 +SELECT * FROM users +ORDER BY id +LIMIT 10 OFFSET 0; -- 第1页 +SELECT * FROM users +ORDER BY id +LIMIT 10 OFFSET 10; -- 第2页 + +-- 去重 +SELECT DISTINCT status FROM users; + +-- 聚合函数 +SELECT + COUNT(*) as total_users, + COUNT(DISTINCT status) as status_count, + MAX(created_at) as latest_user, + MIN(created_at) as earliest_user +FROM users; + +-- 分组查询 +SELECT status, COUNT(*) as count +FROM users +GROUP BY status; + +-- HAVING 子句 +SELECT status, COUNT(*) as count +FROM users +GROUP BY status +HAVING COUNT(*) > 10; +``` + +### 5.3 更新数据(UPDATE) + +#### 基本语法 + +```sql +UPDATE table_name +SET column1 = value1, column2 = value2, ... +WHERE condition; +``` + +#### 示例 + +```sql +-- 更新单列 +UPDATE users +SET nickname = 'Johnny' +WHERE id = 1; + +-- 更新多列 +UPDATE users +SET + nickname = 'Johnny', + email = 'newemail@example.com', + updated_at = CURRENT_TIMESTAMP +WHERE id = 1; + +-- 使用表达式更新 +UPDATE positions +SET current_price = current_price * 1.1 +WHERE symbol = '600519'; + +-- 基于子查询更新 +UPDATE accounts +SET status = 'archived' +WHERE user_id IN ( + SELECT id FROM users WHERE status = 'deleted' +); + +-- 更新时返回结果 +UPDATE users +SET nickname = 'NewName' +WHERE id = 1 +RETURNING id, username, nickname; +``` + +### 5.4 删除数据(DELETE) + +#### 基本语法 + +```sql +DELETE FROM table_name WHERE condition; +``` + +#### 示例 + +```sql +-- 删除指定记录 +DELETE FROM users WHERE id = 1; + +-- 删除多条记录 +DELETE FROM users WHERE status = 'deleted'; + +-- 删除所有记录(危险!) +DELETE FROM table_name; + +-- 使用子查询删除 +DELETE FROM accounts +WHERE user_id IN ( + SELECT id FROM users WHERE status = 'deleted' +); + +-- 删除时返回结果 +DELETE FROM users +WHERE id = 1 +RETURNING id, username; +``` + +### 5.5 连接查询(JOIN) + +```sql +-- 内连接 +SELECT u.username, a.name as account_name +FROM users u +INNER JOIN accounts a ON u.id = a.user_id; + +-- 左连接 +SELECT u.username, a.name as account_name +FROM users u +LEFT JOIN accounts a ON u.id = a.user_id; + +-- 右连接 +SELECT u.username, a.name as account_name +FROM users u +RIGHT JOIN accounts a ON u.id = a.user_id; + +-- 全外连接 +SELECT u.username, a.name as account_name +FROM users u +FULL OUTER JOIN accounts a ON u.id = a.user_id; + +-- 多表连接 +SELECT + u.username, + a.name as account_name, + p.symbol, + p.shares +FROM users u +INNER JOIN accounts a ON u.id = a.user_id +INNER JOIN positions p ON a.id = p.account_id +WHERE u.status = 'active'; +``` + +### 5.6 子查询 + +```sql +-- 标量子查询 +SELECT + username, + (SELECT COUNT(*) FROM accounts WHERE user_id = users.id) as account_count +FROM users; + +-- EXISTS 子查询 +SELECT * FROM users u +WHERE EXISTS ( + SELECT 1 FROM accounts a WHERE a.user_id = u.id +); + +-- IN 子查询 +SELECT * FROM users +WHERE id IN ( + SELECT DISTINCT user_id FROM accounts +); + +-- 相关子查询 +SELECT + u.*, + (SELECT COUNT(*) FROM accounts WHERE user_id = u.id) as account_count +FROM users u; +``` + +--- + +## 六、索引操作 + +### 6.1 索引的作用 + +- 提高查询速度 +- 加速排序和分组 +- 强制唯一性约束 +- 加速表连接 + +### 6.2 创建索引 + +#### 基本语法 + +```sql +-- 创建单列索引 +CREATE INDEX index_name ON table_name (column_name); + +-- 创建多列索引 +CREATE INDEX index_name ON table_name (column1, column2, ...); + +-- 创建唯一索引 +CREATE UNIQUE INDEX index_name ON table_name (column_name); + +-- 创建部分索引(带条件) +CREATE INDEX index_name ON table_name (column_name) +WHERE condition; + +-- 创建表达式索引 +CREATE INDEX index_name ON table_name (expression); +``` + +#### 示例 + +```sql +-- 基本索引 +CREATE INDEX idx_users_email ON users (email); + +-- 唯一索引 +CREATE UNIQUE INDEX idx_users_username ON users (username); + +-- 多列索引 +CREATE INDEX idx_transactions_user_date ON transactions (user_id, date); + +-- 降序索引 +CREATE INDEX idx_users_created_desc ON users (created_at DESC); + +-- 部分索引(只索引活跃用户) +CREATE INDEX idx_users_active_email ON users (email) +WHERE status = 'active'; + +-- 表达式索引 +CREATE INDEX idx_users_lower_email ON users (LOWER(email)); + +-- 文本搜索索引(GIN) +CREATE INDEX idx_transaction_thoughts_content +ON transaction_thoughts +USING gin(to_tsvector('jiebacfg', content)); +``` + +### 6.3 查看索引 + +```sql +-- 查看表的所有索引 +\d table_name + +-- 或者 +\di + +-- 查看索引详细信息 +SELECT + indexname, + indexdef +FROM pg_indexes +WHERE tablename = 'users'; + +-- 查看索引使用情况 +SELECT + schemaname, + tablename, + indexname, + idx_scan as index_scans, + idx_tup_read as tuples_read, + idx_tup_fetch as tuples_fetched +FROM pg_stat_user_indexes +WHERE tablename = 'users'; +``` + +### 6.4 删除索引 + +```sql +-- 删除索引 +DROP INDEX index_name; + +-- 删除索引(如果存在) +DROP INDEX IF EXISTS index_name; + +-- 级联删除 +DROP INDEX index_name CASCADE; +``` + +### 6.5 重建索引 + +```sql +-- 重建索引(释放空间,提高性能) +REINDEX INDEX index_name; + +-- 重建表的所有索引 +REINDEX TABLE table_name; + +-- 重建数据库的所有索引 +REINDEX DATABASE database_name; +``` + +### 6.6 索引类型 + +```sql +-- B-tree 索引(默认,最常用) +CREATE INDEX idx_name ON table_name (column_name); + +-- Hash 索引(只支持等值查询) +CREATE INDEX idx_name ON table_name USING HASH (column_name); + +-- GIN 索引(全文搜索、数组、JSONB) +CREATE INDEX idx_name ON table_name USING GIN (jsonb_column); + +-- GiST 索引(几何数据、全文搜索) +CREATE INDEX idx_name ON table_name USING GIST (geometry_column); + +-- BRIN 索引(大表,有序数据) +CREATE INDEX idx_name ON table_name USING BRIN (column_name); +``` + +### 6.7 索引维护 + +```sql +-- 分析表(更新统计信息,帮助查询优化器) +ANALYZE table_name; + +-- 分析所有表 +ANALYZE; + +-- 查看索引大小 +SELECT + pg_size_pretty(pg_relation_size('index_name')) as index_size; + +-- 查看表和索引总大小 +SELECT + pg_size_pretty(pg_total_relation_size('table_name')) as total_size; +``` + +--- + +## 七、高级查询 + +### 7.1 窗口函数 + +```sql +-- ROW_NUMBER:行号 +SELECT + id, + username, + ROW_NUMBER() OVER (ORDER BY created_at) as row_num +FROM users; + +-- RANK:排名(相同值相同排名,跳过后续排名) +SELECT + id, + username, + RANK() OVER (ORDER BY created_at DESC) as rank +FROM users; + +-- DENSE_RANK:密集排名(相同值相同排名,不跳过) +SELECT + id, + username, + DENSE_RANK() OVER (ORDER BY created_at DESC) as dense_rank +FROM users; + +-- 分组窗口函数 +SELECT + user_id, + date, + amount, + SUM(amount) OVER (PARTITION BY user_id ORDER BY date) as running_total +FROM transactions; +``` + +### 7.2 公共表表达式(CTE) + +```sql +-- 基本 CTE +WITH active_users AS ( + SELECT * FROM users WHERE status = 'active' +) +SELECT * FROM active_users; + +-- 递归 CTE +WITH RECURSIVE category_tree AS ( + -- 基础查询 + SELECT id, name, parent_id, 1 as level + FROM categories + WHERE parent_id IS NULL + + UNION ALL + + -- 递归查询 + SELECT c.id, c.name, c.parent_id, ct.level + 1 + FROM categories c + INNER JOIN category_tree ct ON c.parent_id = ct.id +) +SELECT * FROM category_tree; +``` + +### 7.3 集合操作 + +```sql +-- UNION:合并结果(去重) +SELECT column1 FROM table1 +UNION +SELECT column1 FROM table2; + +-- UNION ALL:合并结果(不去重) +SELECT column1 FROM table1 +UNION ALL +SELECT column1 FROM table2; + +-- INTERSECT:交集 +SELECT column1 FROM table1 +INTERSECT +SELECT column1 FROM table2; + +-- EXCEPT:差集 +SELECT column1 FROM table1 +EXCEPT +SELECT column1 FROM table2; +``` + +### 7.4 条件表达式 + +```sql +-- CASE 表达式 +SELECT + username, + CASE + WHEN status = 'active' THEN '活跃' + WHEN status = 'inactive' THEN '非活跃' + ELSE '未知' + END as status_text +FROM users; + +-- COALESCE:返回第一个非空值 +SELECT + username, + COALESCE(nickname, username) as display_name +FROM users; + +-- NULLIF:如果两个值相等返回 NULL +SELECT NULLIF(column1, column2) FROM table_name; + +-- GREATEST/LEAST:返回最大/最小值 +SELECT + GREATEST(price1, price2, price3) as max_price, + LEAST(price1, price2, price3) as min_price +FROM products; +``` + +--- + +## 八、事务管理 + +### 8.1 事务基本概念 + +事务的 ACID 特性: +- **原子性(Atomicity)**:事务中的所有操作要么全部成功,要么全部失败 +- **一致性(Consistency)**:事务执行前后数据库保持一致状态 +- **隔离性(Isolation)**:并发事务之间相互隔离 +- **持久性(Durability)**:事务提交后,数据永久保存 + +### 8.2 事务操作 + +```sql +-- 开始事务 +BEGIN; +-- 或者 +START TRANSACTION; + +-- 提交事务 +COMMIT; + +-- 回滚事务 +ROLLBACK; + +-- 保存点 +SAVEPOINT savepoint_name; + +-- 回滚到保存点 +ROLLBACK TO SAVEPOINT savepoint_name; + +-- 释放保存点 +RELEASE SAVEPOINT savepoint_name; +``` + +### 8.3 事务示例 + +```sql +-- 基本事务 +BEGIN; +INSERT INTO users (username, email, password_hash) +VALUES ('user1', 'user1@example.com', 'hash1'); +INSERT INTO accounts (user_id, name, type) +VALUES (currval('users_id_seq'), '主账户', 'mixed'); +COMMIT; + +-- 带错误处理的事务 +BEGIN; +INSERT INTO users (username, email, password_hash) +VALUES ('user2', 'user2@example.com', 'hash2'); +-- 如果出错,自动回滚 +SAVEPOINT before_account; +INSERT INTO accounts (user_id, name, type) +VALUES (currval('users_id_seq'), '主账户', 'mixed'); +-- 如果账户创建失败,回滚到保存点 +ROLLBACK TO SAVEPOINT before_account; +COMMIT; +``` + +### 8.4 事务隔离级别 + +```sql +-- 设置事务隔离级别 +SET TRANSACTION ISOLATION LEVEL READ COMMITTED; + +-- 查看当前隔离级别 +SHOW transaction_isolation; + +-- 隔离级别: +-- READ UNCOMMITTED(PostgreSQL 不支持) +-- READ COMMITTED(默认) +-- REPEATABLE READ +-- SERIALIZABLE +``` + +--- + +## 九、常用函数 + +### 9.1 字符串函数 + +```sql +-- 连接字符串 +SELECT CONCAT('Hello', ' ', 'World'); +SELECT 'Hello' || ' ' || 'World'; + +-- 长度 +SELECT LENGTH('Hello'); +SELECT CHAR_LENGTH('Hello'); + +-- 大小写转换 +SELECT UPPER('hello'); +SELECT LOWER('HELLO'); + +-- 截取 +SELECT SUBSTRING('Hello World', 1, 5); +SELECT LEFT('Hello World', 5); +SELECT RIGHT('Hello World', 5); + +-- 替换 +SELECT REPLACE('Hello World', 'World', 'PostgreSQL'); + +-- 去除空格 +SELECT TRIM(' Hello '); +SELECT LTRIM(' Hello'); +SELECT RTRIM('Hello '); + +-- 分割字符串 +SELECT SPLIT_PART('a,b,c', ',', 2); -- 返回 'b' +``` + +### 9.2 数值函数 + +```sql +-- 绝对值 +SELECT ABS(-10); + +-- 四舍五入 +SELECT ROUND(3.14159, 2); -- 3.14 + +-- 向上取整 +SELECT CEIL(3.14); -- 4 + +-- 向下取整 +SELECT FLOOR(3.14); -- 3 + +-- 幂运算 +SELECT POWER(2, 3); -- 8 + +-- 平方根 +SELECT SQRT(16); -- 4 + +-- 随机数 +SELECT RANDOM(); +SELECT FLOOR(RANDOM() * 100) + 1; -- 1-100 随机整数 +``` + +### 9.3 日期时间函数 + +```sql +-- 当前日期时间 +SELECT CURRENT_DATE; +SELECT CURRENT_TIME; +SELECT CURRENT_TIMESTAMP; +SELECT NOW(); + +-- 提取日期部分 +SELECT EXTRACT(YEAR FROM CURRENT_TIMESTAMP); +SELECT EXTRACT(MONTH FROM CURRENT_TIMESTAMP); +SELECT EXTRACT(DAY FROM CURRENT_TIMESTAMP); + +-- 日期加减 +SELECT CURRENT_DATE + INTERVAL '1 day'; +SELECT CURRENT_DATE + INTERVAL '1 month'; +SELECT CURRENT_DATE - INTERVAL '1 year'; + +-- 日期格式化 +SELECT TO_CHAR(CURRENT_TIMESTAMP, 'YYYY-MM-DD HH24:MI:SS'); +SELECT TO_CHAR(CURRENT_DATE, 'YYYY年MM月DD日'); + +-- 日期差 +SELECT CURRENT_DATE - '2024-01-01'::DATE; +SELECT AGE('2024-12-31'::DATE, '2024-01-01'::DATE); +``` + +### 9.4 聚合函数 + +```sql +-- 计数 +SELECT COUNT(*) FROM users; +SELECT COUNT(DISTINCT status) FROM users; + +-- 求和 +SELECT SUM(amount) FROM transactions; + +-- 平均值 +SELECT AVG(price) FROM positions; + +-- 最大值/最小值 +SELECT MAX(created_at) FROM users; +SELECT MIN(created_at) FROM users; + +-- 分组聚合 +SELECT + status, + COUNT(*) as count, + AVG(EXTRACT(YEAR FROM AGE(CURRENT_DATE, created_at))) as avg_age +FROM users +GROUP BY status; +``` + +### 9.5 JSON 函数 + +```sql +-- 创建 JSON +SELECT '{"name": "John", "age": 30}'::JSON; +SELECT JSON_BUILD_OBJECT('name', 'John', 'age', 30); + +-- 提取 JSON 值 +SELECT '{"name": "John"}'::JSON->>'name'; +SELECT '{"user": {"name": "John"}}'::JSON->'user'->>'name'; + +-- JSON 数组 +SELECT JSON_ARRAY_LENGTH('[1,2,3]'::JSON); + +-- JSONB 操作符 +SELECT '{"name": "John"}'::JSONB @> '{"name": "John"}'::JSONB; -- 包含 +SELECT '{"name": "John"}'::JSONB ? 'name'; -- 键存在 +``` + +--- + +## 十、实用技巧 + +### 10.1 性能优化 + +```sql +-- 使用 EXPLAIN 分析查询计划 +EXPLAIN SELECT * FROM users WHERE email = 'test@example.com'; + +-- 详细分析(包含实际执行时间) +EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com'; + +-- 查看慢查询 +SELECT + query, + calls, + total_time, + mean_time +FROM pg_stat_statements +ORDER BY mean_time DESC +LIMIT 10; +``` + +### 10.2 数据导入导出 + +```sql +-- 导出为 CSV +COPY users TO '/tmp/users.csv' WITH CSV HEADER; + +-- 从 CSV 导入 +COPY users FROM '/tmp/users.csv' WITH CSV HEADER; + +-- 导出为文本 +COPY users TO '/tmp/users.txt'; + +-- 从文本导入 +COPY users FROM '/tmp/users.txt'; +``` + +### 10.3 查看数据库信息 + +```sql +-- 查看数据库大小 +SELECT pg_size_pretty(pg_database_size('database_name')); + +-- 查看表大小 +SELECT pg_size_pretty(pg_total_relation_size('table_name')); + +-- 查看所有表大小 +SELECT + schemaname, + tablename, + pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size +FROM pg_tables +WHERE schemaname = 'public' +ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC; + +-- 查看连接数 +SELECT count(*) FROM pg_stat_activity; + +-- 查看当前连接 +SELECT * FROM pg_stat_activity; +``` + +### 10.4 权限管理 + +```sql +-- 授予权限 +GRANT SELECT, INSERT, UPDATE ON table_name TO username; +GRANT ALL PRIVILEGES ON DATABASE database_name TO username; + +-- 撤销权限 +REVOKE SELECT ON table_name FROM username; + +-- 查看权限 +\dp table_name +``` + +### 10.5 序列操作 + +```sql +-- 查看当前序列值 +SELECT currval('users_id_seq'); + +-- 查看下一个序列值 +SELECT nextval('users_id_seq'); + +-- 设置序列值 +SELECT setval('users_id_seq', 100); + +-- 重置序列 +ALTER SEQUENCE users_id_seq RESTART WITH 1; +``` + +### 10.6 常用配置 + +```sql +-- 查看配置 +SHOW ALL; +SHOW shared_buffers; +SHOW max_connections; + +-- 设置配置(会话级别) +SET work_mem = '256MB'; + +-- 查看时区 +SHOW timezone; +SET timezone = 'Asia/Shanghai'; +``` + +--- + +## 十一、常见问题 + +### 11.1 连接问题 + +```bash +# 问题:无法连接到数据库 +# 解决:检查 PostgreSQL 服务是否运行 +sudo systemctl status postgresql # Linux +brew services list # macOS + +# 问题:认证失败 +# 解决:检查 pg_hba.conf 配置文件 +sudo nano /etc/postgresql/15/main/pg_hba.conf +``` + +### 11.2 性能问题 + +```sql +-- 问题:查询慢 +-- 解决:创建索引 +CREATE INDEX idx_column ON table_name (column_name); + +-- 问题:表膨胀 +-- 解决:VACUUM +VACUUM ANALYZE table_name; + +-- 问题:统计信息过期 +-- 解决:更新统计信息 +ANALYZE table_name; +``` + +### 11.3 数据备份恢复 + +```bash +# 定期备份脚本 +#!/bin/bash +DATE=$(date +%Y%m%d_%H%M%S) +pg_dump -U postgres database_name > /backup/db_$DATE.sql + +# 自动清理旧备份(保留30天) +find /backup -name "db_*.sql" -mtime +30 -delete +``` + +--- + +## 十二、参考资源 + +- **官方文档**:https://www.postgresql.org/docs/ +- **中文文档**:https://www.postgresql.org/docs/current/index.html +- **psql 命令参考**:https://www.postgresql.org/docs/current/app-psql.html +- **SQL 语法参考**:https://www.postgresql.org/docs/current/sql.html + +--- + +**文档版本**:v1.0 +**创建日期**:2024年 +**适用版本**:PostgreSQL 12+ + diff --git a/packages/design-document/机生文档/产品需求文档-去除空格.md b/packages/design-document/机生文档/产品需求文档-去除空格.md new file mode 100644 index 0000000..efb6b05 --- /dev/null +++ b/packages/design-document/机生文档/产品需求文档-去除空格.md @@ -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月* + +*文档状态:待评审* \ No newline at end of file diff --git a/packages/design-document/机生文档/产品需求文档.md b/packages/design-document/机生文档/产品需求文档.md new file mode 100644 index 0000000..efb6b05 --- /dev/null +++ b/packages/design-document/机生文档/产品需求文档.md @@ -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月* + +*文档状态:待评审* \ No newline at end of file diff --git a/packages/design-document/机生文档/命名问题.md b/packages/design-document/机生文档/命名问题.md new file mode 100644 index 0000000..627b169 --- /dev/null +++ b/packages/design-document/机生文档/命名问题.md @@ -0,0 +1,11 @@ +# 应用如何命名 +## 参考命名 +- 投资复盘笔记 + + +## 审核是描述 +这只是一个个人投资交易的记录与复盘工具 + +考虑更稳妥的命名:如果希望最大化降低审核风险,可以考虑对名称进行微调,使其更偏向“个人工具”属性,同时保留核心含义。例如: +- 增加个人化前缀/后缀:比如“我的复盘笔记”、“知行投资笔记”。 +- 使用更中性的词汇:例如“资产轨迹日记”、“收益账本与思考”。 \ No newline at end of file diff --git a/packages/design-document/机生文档/投资收益记录系统设计.md b/packages/design-document/机生文档/投资收益记录系统设计.md new file mode 100644 index 0000000..678a963 --- /dev/null +++ b/packages/design-document/机生文档/投资收益记录系统设计.md @@ -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送5,bonusRatio = 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年 + diff --git a/packages/design-document/机生文档/持仓计价方案设计文档.md b/packages/design-document/机生文档/持仓计价方案设计文档.md new file mode 100644 index 0000000..6b6e20a --- /dev/null +++ b/packages/design-document/机生文档/持仓计价方案设计文档.md @@ -0,0 +1,1471 @@ +# 持仓计价方案设计文档 + +## 一、背景与需求 + +### 1.1 产品定位 +投资记账软件,用于记录用户分散在不同市场、不同券商下的所有资产,统计总市值、投资收益率、年化收益率等。 + +### 1.2 新建持仓设计决策 + +#### 1.2.1 设计方案选择 + +**问题:** 新建持仓时,应该先输入并联想,还是先选择资产类型再搜索? + +**决策:** 采用**方案A:搜索优先 + 智能切换**的设计。 + +**方案对比:** + +| 方案 | 优点 | 缺点 | 适用场景 | +|------|------|------|----------| +| **方案A:搜索优先 + 智能切换** | ✅ 灵活,支持多种创建方式
✅ 容错性强,搜索失败可切换
✅ 用户体验流畅 | ⚠️ 实现复杂度稍高 | **推荐采用** | +| 方案B:资产类型优先 + 条件搜索 | ✅ 逻辑清晰
✅ 搜索范围精准 | ⚠️ 不够灵活
⚠️ 必须先选择类型 | 备选方案 | + +#### 1.2.2 方案A:搜索优先 + 智能切换(最终采用) + +**核心设计思路:** + +1. **搜索框优先,但不强制** + - 搜索框始终显示在顶部 + - 用户可以选择搜索或直接手动输入 + - 搜索失败时,可以无缝切换到手动输入 + +2. **智能联动** + - 选择资产类型后,搜索框自动过滤该类型 + - 搜索时,优先显示匹配的资产类型 + - 字段根据选择动态显示/隐藏 + +3. **渐进式披露** + - 第一步:搜索框 + 资产类型选择 + - 第二步:根据选择显示对应字段 + - 第三步:填写价格和数量 + +**弹窗布局(从上到下):** +``` +┌─────────────────────────────────────────┐ +│ 搜索框(始终显示,支持全局搜索) │ +│ [🔍 输入代码/名称搜索...] │ +│ ↓ 联想结果列表(最多10条) │ +├─────────────────────────────────────────┤ +│ 资产类型选择(Tab切换,可选) │ +│ [股票] [基金] [债券] [现金] [其他] │ +├─────────────────────────────────────────┤ +│ 根据资产类型显示字段: │ +│ - 股票/基金/债券:市场、券商、代码、名称 │ +│ - 现金:券商 │ +├─────────────────────────────────────────┤ +│ 价格和数量: │ +│ - 成本价(人民币)、数量、最新价(可选) │ +├─────────────────────────────────────────┤ +│ 其他选项: │ +│ - 货币类型、是否自动更新价格 │ +└─────────────────────────────────────────┘ +``` + +**三种创建方式:** + +**方式1:快速创建(通过搜索)** +- 用户在搜索框输入资产代码或名称 +- 系统联想出结果,用户选择 +- 系统自动填充:资产类型、市场、资产代码、资产名称 +- 用户只需填写:券商、成本价、数量、最新价 + +**方式2:智能创建(先选类型再搜索)** +- 用户先选择资产类型(如:股票) +- 搜索框自动过滤,只搜索该类型 +- 用户输入代码或名称搜索 +- 选择结果后自动填充相关信息 + +**方式3:手动创建(不通过搜索)** +- 用户选择资产类型 +- 跳过搜索,直接手动填写所有字段 +- 或搜索无结果时,自动切换到手动输入模式 + +**设计原则:** +- ✅ **灵活性**:三种方式默认都支持,用户可以根据习惯选择 +- ✅ **智能联动**:搜索框根据资产类型自动过滤 +- ✅ **容错性**:搜索失败可以无缝切换到手动输入 +- ✅ **渐进式披露**:根据用户选择逐步显示字段,不会一次性显示所有字段 +- ✅ **自动填充**:通过搜索选择后,相关字段自动填充且禁用,防止误修改 +- ✅ **可重置**:用户可以点击"重新选择"清空搜索结果,恢复手动填写 + +**关键交互细节:** + +1. **搜索框智能过滤** + - 用户选择"股票" → 搜索框自动过滤,只搜索股票 + - 用户选择"基金" → 搜索框自动过滤,只搜索基金 + - 用户未选择 → 搜索框全局搜索所有类型(股票、基金、债券) + +2. **搜索失败处理** + - 搜索无结果 → 显示"未找到匹配的资产,点击手动输入" + - 点击后 → 隐藏搜索框(可选),显示完整表单 + - 用户可以继续手动填写所有信息 + +3. **字段联动** + - 通过搜索选择 → 自动填充:资产类型、市场、资产代码、资产名称(禁用状态) + - 手动输入 → 所有字段可编辑 + - 切换资产类型 → 清空相关字段,重新显示对应字段 + +4. **搜索框状态** + - 未选择:显示占位符"输入代码/名称搜索..." + - 已选择:显示选中的资产名称,右侧显示"重新选择"按钮 + - 点击"重新选择" → 清空选择,恢复搜索状态 + +### 1.2 核心需求 +- 用户记录持仓数据(成本价、最新价、持股数量) +- 每日收盘后自动更新持仓股票的最新价格 +- 计算当日资产总值、净值 +- 呈现所有持仓的总市值变化情况、各持仓占比 +- 形成图表,方便用户统计资产总市值、投资收益率、年化收益率 +- **最终总资产、总成本和各持仓市值占比需要使用人民币进行计价** + +### 1.3 用户操作场景 +- 用户每次变更持仓时,只需要更新:**成本价、最新价、持股数量** 三个数值 +- 用户可能在不同日期多次买入/卖出同一只股票 +- 用户可能持有A股、港股、美股等不同市场的资产 + +### 1.4 核心问题 +如何统一处理不同市场的货币计价问题,确保最终展示和计算统一为人民币? + +--- + +## 二、方案对比 + +### 2.1 方案A:统一人民币存储方案(推荐采用) + +#### 2.1.1 核心思路 +- **用户输入**:所有价格统一使用人民币计价 +- **数据存储**:数据库中所有价格字段统一存储为人民币 +- **系统处理**:所有计算和展示统一为人民币 +- **无需汇率**:完全避免汇率问题 + +#### 2.1.2 数据存储设计 + +**数据库字段:** +```sql +-- positions 表 +cost_price DECIMAL(18, 4) -- 成本价(人民币) +current_price DECIMAL(18, 4) -- 最新价(人民币) +previous_price DECIMAL(18, 4) -- 上一次价格(人民币,用于涨跌显示) +shares DECIMAL(18, 4) -- 持股数量 +currency VARCHAR(10) -- 货币类型(仅用于标识市场,不影响计算) +``` + +**字段说明:** +- `cost_price`:成本价,统一为人民币 +- `current_price`:最新价,统一为人民币 +- `currency`:仅用于标识市场(CNY/HKD/USD),不影响计算和展示 + +#### 2.1.3 用户操作流程 + +**创建持仓:** +1. 用户输入成本价(人民币) +2. 用户输入最新价(人民币) +3. 用户输入持股数量 +4. 用户选择市场(CNY/HKD/USD,仅标识) + +**更新持仓:** +1. 用户修改成本价(人民币) +2. 用户修改最新价(人民币) +3. 用户修改持股数量 + +**价格更新:** +- **手动更新**:用户直接在界面上更新人民币价格 +- **自动更新**:系统获取原始货币价格 → 转换为人民币 → 更新到数据库 + +#### 2.1.4 系统处理逻辑 + +**价格自动更新流程:** +``` +每日收盘后: +1. 获取原始货币价格(如港币160) +2. 获取当前汇率(如0.9) +3. 转换为人民币(160 × 0.9 = ¥144) +4. 更新到数据库的 current_price 字段 +``` + +**计算逻辑:** +```typescript +// 所有计算统一为人民币 +const costValue = shares * costPrice; // 人民币成本 +const marketValue = shares * currentPrice; // 人民币市值 +const profit = marketValue - costValue; // 人民币盈亏 +const profitPercent = (profit / costValue) * 100; +const assetPercent = (marketValue / totalAsset) * 100; +``` + +#### 2.1.5 优点 +- ✅ **极简**:用户只需输入人民币价格,操作简单 +- ✅ **统一**:所有数据统一为人民币,计算和展示一致 +- ✅ **无汇率问题**:完全避免汇率存储、更新、同步等问题 +- ✅ **符合记账场景**:用户记录的是自己的资产情况,通常知道人民币成本 +- ✅ **实现简单**:无需复杂的汇率处理逻辑 + +#### 2.1.6 缺点 +- ⚠️ **用户需要转换**:持有港股/美股的用户需要自己将港币/美元价格转换为人民币 +- ⚠️ **无法直接核对**:无法直接与市场报价(原始货币)对比 + +#### 2.1.7 适用场景 +- ✅ 纯手动记账 +- ✅ 用户主要关心人民币价值 +- ✅ 记账软件定位(非实时交易软件) + +--- + +### 2.2 方案B:混合计价方案(对比参考) + +#### 2.2.1 核心思路 +- **数据存储**:同时存储人民币价格和原始币种价格 +- **用户输入**:可以选择输入人民币价格或原始币种价格 +- **系统处理**:自动转换和同步两种价格 +- **展示策略**:主要显示人民币,辅助显示原始币种 + +#### 2.2.2 数据存储设计 + +**数据库字段:** +```sql +-- positions 表 +cost_price DECIMAL(18, 4) -- 成本价(人民币,主要) +current_price DECIMAL(18, 4) -- 最新价(人民币,主要) +cost_price_original DECIMAL(18, 4) -- 成本价(原始币种,可选) +current_price_original DECIMAL(18, 4) -- 最新价(原始币种,可选) +cost_exchange_rate DECIMAL(10, 6) -- 成本汇率(加权平均) +current_exchange_rate DECIMAL(10, 6) -- 当前汇率 +exchange_rate_updated_at TIMESTAMP -- 汇率更新时间 +shares DECIMAL(18, 4) +currency VARCHAR(10) +``` + +#### 2.2.3 用户操作流程 + +**创建持仓(三种方式):** + +**方式1:直接输入人民币价格(推荐)** +``` +成本价:¥135(人民币) +最新价:¥144(人民币) +数量:100股 +``` + +**方式2:输入原始币种 + 人民币总成本** +``` +成本价:HK$150(原始币种) +数量:100股 +人民币总成本:¥13,500(必须输入) +→ 系统自动计算加权平均汇率:13,500 / (100 × 150) = 0.9 +→ 系统自动计算人民币成本价:150 × 0.9 = ¥135 +``` + +**方式3:输入原始币种 + 加权平均汇率** +``` +成本价:HK$150(原始币种) +数量:100股 +加权平均汇率:0.9(用户自己计算) +→ 系统自动计算人民币成本价:150 × 0.9 = ¥135 +``` + +#### 2.2.4 系统处理逻辑 + +**汇率计算:** +- 成本汇率:从人民币总成本反推加权平均汇率 +- 当前汇率:每日自动更新,用于价格转换 + +**价格更新:** +``` +每日收盘后: +1. 获取原始货币价格(如港币160) +2. 获取当前汇率(如0.9) +3. 转换为人民币(160 × 0.9 = ¥144) +4. 同时更新 current_price(人民币)和 current_price_original(原始币种) +``` + +#### 2.2.5 优点 +- ✅ 支持自动更新价格(可以获取市场原始数据) +- ✅ 可以核对市场报价(显示原始币种价格) +- ✅ 用户可以选择输入方式 + +#### 2.2.6 缺点 +- ⚠️ **复杂**:需要存储和管理汇率 +- ⚠️ **多次买入问题**:需要处理加权平均汇率 +- ⚠️ **汇率同步**:需要定期更新汇率 +- ⚠️ **实现复杂**:需要处理汇率转换逻辑 + +#### 2.2.7 适用场景 +- ✅ 需要自动更新价格 +- ✅ 用户需要核对市场报价 +- ✅ 需要显示原始币种价格 + +--- + +## 三、方案选择 + +### 3.1 最终选择:方案A(统一人民币存储方案) + +**选择理由:** +1. **符合记账软件定位**:用户记录的是自己的资产情况,通常知道人民币成本 +2. **简化用户操作**:用户只需输入3个数字(成本价、最新价、数量),无需考虑汇率 +3. **简化系统实现**:无需存储和管理汇率,计算逻辑简单 +4. **满足最终需求**:总资产、总成本、持仓占比都统一为人民币 + +### 3.2 关于价格更新的处理 + +**方式1:系统自动更新(推荐)** +- 系统获取原始货币价格 +- 自动转换为人民币 +- 更新到数据库 + +**方式2:用户手动更新** +- 用户直接在界面上更新人民币价格 + +--- + +## 四、产品需求文档 + +### 4.1 新建持仓 + +#### 4.1.1 功能描述 +用户创建新的持仓记录,支持两种创建方式: +1. **快速创建**:通过搜索框搜索资产代码或名称,选择后自动填充基本信息 +2. **手动创建**:手动选择资产类型,然后填写所有信息 + +两种方式默认都支持,用户可以根据习惯选择。 + +#### 4.1.2 用户角色 +所有注册用户 + +#### 4.1.3 功能入口 +- 持仓列表页面 → "新增持仓" 按钮 +- 顶部导航栏 → "资产" → "新增持仓" + +#### 4.1.4 弹窗设计 + +**弹窗布局:** +- **PC端**:居中弹窗(Modal),宽度:600px +- **移动端**:底部弹窗(Drawer),高度:80% + +**弹窗结构(从上到下):** + +1. **搜索框**(顶部,始终显示) + - 位置:弹窗最顶部 + - 功能:支持全局搜索资产代码或名称 + - 交互: + - 用户输入代码(如:600519、00700、AAPL)或名称(如:贵州茅台、腾讯控股) + - 系统实时联想,显示匹配结果 + - 联想结果包含:资产代码、资产名称、市场、资产类型 + - 用户选择后,自动填充:资产类型、市场、资产代码、资产名称 + - 填充后,搜索框显示选中的资产名称(可点击重新搜索) + +2. **资产类型选择**(搜索框下方) + - 如果通过搜索选择了资产,此字段自动填充且禁用 + - 如果未通过搜索选择,用户手动选择:股票/基金/债券/现金/其他 + +3. **市场和券商**(根据资产类型显示) + - **股票/基金/债券**:显示"市场"和"券商"字段 + - **现金**:只显示"券商"字段,不显示"市场"字段 + - 如果通过搜索选择了资产,"市场"字段自动填充 + +4. **资产代码和资产名称**(根据资产类型显示) + - **股票/基金/债券**:显示"资产代码"和"资产名称"字段 + - **现金**:不显示这两个字段 + - 如果通过搜索选择了资产,这两个字段自动填充且禁用 + +5. **价格和数量信息** + - **成本价(人民币)**:数字输入,单位:元(必填) + - **持股数量**:数字输入(必填) + - **最新价(人民币)**:数字输入,单位:元(可选) + +6. **其他选项** + - **货币类型**:下拉选择(人民币/港币/美元),默认:人民币 + - **是否自动更新价格**:开关,默认:关闭 + +#### 4.1.5 输入字段详细说明 + +**搜索框:** +- **功能**:全局搜索资产代码或名称 +- **搜索范围**:股票、基金、债券(不包含现金) +- **联想结果格式**: + ``` + 600519 - 贵州茅台 (A股-上海) + 00700 - 腾讯控股 (港股) + AAPL - 苹果公司 (美股) + ``` +- **选择后行为**: + - 自动填充:资产类型、市场、资产代码、资产名称 + - 搜索框显示选中的资产名称(可点击重新搜索) + - 相关字段变为禁用状态(可点击"重新选择"按钮恢复) + +**资产类型:** +- **选项**:股票、基金、债券、现金、其他 +- **默认值**:无(必须选择) +- **联动规则**: + - 选择"股票/基金/债券":显示市场、券商、资产代码、资产名称字段 + - 选择"现金":只显示券商字段,隐藏市场、资产代码、资产名称字段 + +**市场:** +- **选项**:A股-上海、A股-深圳、港股、美股等 +- **显示条件**:资产类型为股票/基金/债券时显示 +- **自动填充**:如果通过搜索选择了资产,自动填充对应市场 + +**券商:** +- **选项**:从用户已添加的券商中选择 +- **显示条件**:所有资产类型都显示 +- **必填**:是 + +**资产代码:** +- **输入方式**:文本输入 +- **显示条件**:资产类型为股票/基金/债券时显示 +- **自动填充**:如果通过搜索选择了资产,自动填充且禁用 +- **示例**:600519、00700、AAPL + +**资产名称:** +- **输入方式**:文本输入 +- **显示条件**:资产类型为股票/基金/债券时显示 +- **自动填充**:如果通过搜索选择了资产,自动填充且禁用 +- **示例**:贵州茅台、腾讯控股、苹果公司 + +**成本价(人民币):** +- **输入方式**:数字输入,支持小数 +- **单位**:元 +- **必填**:是 +- **说明**:用户从券商系统看到的成本价,如果是港股/美股,需要转换为人民币后输入 +- **示例**:1600.00 + +**持股数量:** +- **输入方式**:数字输入,支持小数 +- **必填**:是 +- **说明**:当前持仓的股数/份数 +- **示例**:100 + +**最新价(人民币):** +- **输入方式**:数字输入,支持小数 +- **单位**:元 +- **必填**:否 +- **说明**:当前市场价格,如果是港股/美股,需要转换为人民币后输入 +- **示例**:1850.00 + +**货币类型:** +- **选项**:人民币、港币、美元 +- **默认值**:人民币 +- **说明**:仅用于标识市场,不影响计算和展示 + +**是否自动更新价格:** +- **类型**:开关 +- **默认值**:关闭 +- **说明**:开启后,系统每日收盘后自动更新最新价 + +#### 4.1.5 业务规则 + +1. **搜索规则**: + - 搜索框支持全局搜索股票、基金、债券(不包含现金、其他) + - 搜索范围:资产代码、资产名称 + - 搜索方式:模糊匹配,不区分大小写 + - 联想结果最多显示10条 + - 联想结果按匹配度排序(完全匹配 > 前缀匹配 > 包含匹配) + - **智能过滤**:如果用户选择了资产类型,搜索框自动过滤,只搜索该类型 + - **防抖处理**:输入后300ms才触发搜索,避免频繁请求 + +2. **资产类型联动规则**: + - 用户选择资产类型后,搜索框自动过滤该类型 + - 搜索框占位符动态变化:"搜索股票..."、"搜索基金..."等 + - 如果用户清空资产类型选择,搜索框恢复全局搜索 + - 如果通过搜索选择了资产,资产类型自动填充且禁用 + +3. **自动填充规则**: + - 通过搜索选择资产后,自动填充:资产类型、市场、资产代码、资产名称、货币类型 + - 自动填充的字段变为禁用状态,显示锁定图标,防止误修改 + - 用户可以通过"重新选择"按钮清空并重新搜索 + - 如果用户手动修改了资产类型,清空自动填充的字段(资产类型、市场、代码、名称) + +4. **字段显示规则**: + - **资产类型为股票/基金/债券**: + - 显示:市场、券商、资产代码、资产名称、成本价、持股数量、最新价 + - **资产类型为现金**: + - 显示:券商、成本价、持股数量、最新价 + - 隐藏:市场、资产代码、资产名称 + - **资产类型为其他**: + - 显示:券商、资产代码、资产名称、成本价、持股数量、最新价 + - 隐藏:市场(可选) + +5. **搜索失败处理规则**: + - 搜索无结果时,显示提示:"未找到匹配的资产,点击手动输入" + - 用户点击后,可以: + - 选择隐藏搜索框,显示完整表单 + - 或保留搜索框,但允许用户手动填写所有字段 + - 用户可以继续完成创建流程 + +6. **唯一性约束**: + - 同一用户、同一券商、同一资产代码、同一市场、同一资产类型只能有一条持仓 + - 如果已存在,提示用户"该持仓已存在,请直接编辑现有持仓" + - 现金类型:同一用户、同一券商只能有一条现金持仓 + +7. **价格输入规则**: + - 成本价和最新价必须使用人民币计价 + - 如果持有港股/美股,用户需要自己转换为人民币后输入 + - 系统不提供汇率转换工具(简化设计) + - 价格支持小数,精度:2位小数 + +8. **数量规则**: + - 持股数量必须大于0 + - 支持小数(如基金份额) + - 精度:4位小数 + +9. **价格更新规则**: + - 如果开启自动更新,系统每日收盘后自动更新最新价 + - 更新时:获取原始货币价格 → 转换为人民币 → 更新 + - 现金类型不支持自动更新价格 + +10. **表单验证规则**: + - 搜索选择资产后,资产类型、市场、资产代码、资产名称自动填充,无需验证 + - 手动创建时,资产类型、券商、成本价、持股数量为必填 + - 股票/基金/债券类型,市场、资产代码、资产名称为必填 + - 现金类型,不需要市场、资产代码、资产名称 + - 其他类型,资产代码、资产名称可选 + +#### 4.1.6 用户提示 + +**表单提示信息:** +``` +💡 提示: +• 成本价和最新价请使用人民币计价 +• 如果持有港股/美股,请将港币/美元价格转换为人民币后输入 +• 系统会自动更新价格(如果开启自动更新) +• 成本价是您从券商系统看到的加权平均成本价 +``` + +#### 4.1.7 交互流程 + +**方式1:快速创建(通过搜索)** + +1. 用户点击"新增持仓"按钮 +2. 弹出表单对话框(PC端居中,移动端底部) +3. 用户在顶部搜索框输入资产代码或名称(如:600519 或 贵州茅台) +4. 系统实时联想,显示匹配结果列表(最多10条) +5. 用户从联想结果中选择一个资产 +6. 系统自动填充: + - 资产类型(自动填充且禁用) + - 市场(自动填充) + - 资产代码(自动填充且禁用) + - 资产名称(自动填充且禁用) + - 搜索框显示选中的资产名称,右侧显示"重新选择"按钮 +7. 用户选择券商 +8. 用户输入成本价(人民币) +9. 用户输入持股数量 +10. 用户输入最新价(可选) +11. 用户设置其他选项(货币类型、自动更新价格) +12. 用户点击"保存"按钮 +13. 系统验证数据 +14. 系统保存持仓 +15. 刷新持仓列表 +16. 显示成功提示 + +**方式2:智能创建(先选类型再搜索)** + +1. 用户点击"新增持仓"按钮 +2. 弹出表单对话框(PC端居中,移动端底部) +3. 用户先选择资产类型(如:股票) +4. 搜索框自动过滤,只搜索该类型的资产 +5. 用户在搜索框输入资产代码或名称 +6. 系统实时联想,显示匹配结果列表(仅显示该类型) +7. 用户从联想结果中选择一个资产 +8. 系统自动填充相关信息(同方式1) +9. 用户填写剩余字段(券商、成本价、数量、最新价等) +10. 用户点击"保存"按钮 +11. 系统验证数据并保存 + +**方式3:手动创建(不通过搜索)** + +1. 用户点击"新增持仓"按钮 +2. 弹出表单对话框(PC端居中,移动端底部) +3. 用户选择资产类型(股票/基金/债券/现金/其他) +4. 根据资产类型显示对应字段: + - **股票/基金/债券**:显示市场、券商、资产代码、资产名称 + - **现金**:只显示券商 +5. 用户填写必填字段: + - 选择市场(股票/基金/债券必填) + - 选择券商(所有类型必填) + - 输入资产代码(股票/基金/债券必填) + - 输入资产名称(股票/基金/债券必填) + - 输入成本价(所有类型必填) + - 输入持股数量(所有类型必填) +6. 用户填写可选字段: + - 输入最新价(可选) + - 选择货币类型(默认人民币) + - 设置是否自动更新价格(默认关闭) +7. 用户点击"保存"按钮 +8. 系统验证数据 +9. 系统保存持仓 +10. 刷新持仓列表 +11. 显示成功提示 + +**搜索失败处理流程:** + +1. 用户输入搜索关键词 +2. 系统搜索无结果 +3. 显示提示:"未找到匹配的资产,点击手动输入" +4. 用户点击"手动输入" +5. 搜索框变为可选(可隐藏或保留) +6. 显示完整表单,用户可以手动填写所有字段 +7. 用户可以继续完成创建流程 + +**搜索框交互细节:** + +- **输入时**: + - 用户输入字符,系统实时搜索(防抖300ms) + - 显示联想结果列表(最多10条) + - 结果包含:资产代码、资产名称、市场、资产类型 + - 高亮匹配的字符 + - 如果选择了资产类型,只显示该类型的结果 + +- **选择后**: + - 搜索框显示选中的资产名称(可点击重新搜索) + - 自动填充相关字段(资产类型、市场、代码、名称) + - 填充的字段变为禁用状态,显示锁定图标 + - 搜索框右侧显示"重新选择"按钮 + +- **重新选择**: + - 点击"重新选择"按钮或点击搜索框 + - 清空已填充的字段 + - 恢复字段为可编辑状态 + - 搜索框恢复为空,可重新输入 + - 如果之前选择了资产类型,搜索框恢复为该类型的过滤状态 + +- **资产类型联动**: + - 用户选择资产类型(如:股票) + - 搜索框自动过滤,只搜索股票类型 + - 搜索框占位符变为"搜索股票..." + - 如果用户清空资产类型选择,搜索框恢复全局搜索 + +- **搜索框状态**: + - **未选择**:显示占位符"输入代码/名称搜索..."或"搜索[资产类型]..." + - **已选择**:显示选中的资产名称,右侧显示"重新选择"按钮 + - **搜索中**:显示加载动画 + - **无结果**:显示"未找到匹配的资产,点击手动输入" + +#### 4.1.8 异常处理 + +**搜索相关:** +- **搜索无结果**: + - 显示"未找到匹配的资产" + - 提供"点击手动输入"按钮 + - 点击后可以隐藏搜索框(可选),显示完整表单 + - 用户可以继续手动填写所有字段完成创建 +- **搜索网络错误**: + - 显示"搜索失败,请稍后重试" + - 允许用户继续手动填写 + - 提供"重试"按钮 +- **搜索超时**: + - 显示"搜索超时,请稍后重试" + - 允许用户继续手动填写 + - 提供"重试"按钮 +- **资产类型过滤后无结果**: + - 提示"当前类型下未找到匹配的资产" + - 提供"清除类型过滤"或"切换为手动输入"选项 + +**表单验证:** +- **持仓已存在**:提示"该持仓已存在,请直接编辑现有持仓" +- **必填字段为空**:表单验证提示,高亮显示未填写的必填字段 +- **价格格式错误**:提示"请输入有效的价格(支持小数)" +- **数量小于等于0**:提示"持股数量必须大于0" +- **资产类型未选择**:提示"请选择资产类型或通过搜索选择资产" + +**数据一致性:** +- **搜索选择的资产与手动选择的资产类型不一致**: + - 如果用户通过搜索选择了股票,但手动将资产类型改为基金 + - 清空自动填充的字段,提示"资产类型已更改,请重新填写相关信息" +- **搜索选择的资产市场与手动选择的市场不一致**: + - 提示"搜索选择的资产市场与手动选择的市场不一致,请确认" + - 允许用户选择使用哪个市场 + +--- + +### 4.2 编辑持仓 + +#### 4.2.1 功能描述 +用户修改现有持仓的信息,主要是更新成本价、最新价、持股数量。 + +#### 4.2.2 用户角色 +所有注册用户(只能编辑自己的持仓) + +#### 4.2.3 功能入口 +- 持仓列表页面 → 点击持仓卡片 → "编辑" 按钮 +- 持仓详情页面 → "编辑" 按钮 + +#### 4.2.4 可编辑字段 + +**可编辑字段:** +- **券商**:下拉选择(可修改) +- **成本价(人民币)**:数字输入,单位:元 + - 说明:修改加权平均成本价 +- **最新价(人民币)**:数字输入,单位:元 + - 说明:修改当前市场价格 +- **持股数量**:数字输入 + - 说明:修改持仓数量 +- **货币类型**:下拉选择(可修改) +- **是否自动更新价格**:开关(可修改) +- **状态**:下拉选择(活跃/暂停/退市) + +**不可编辑字段:** +- 资产类型 +- 资产代码 +- 资产名称 +- 市场(部分系统可能允许修改) + +#### 4.2.5 业务规则 + +1. **价格更新规则**: + - 更新最新价时,系统自动将旧价格保存到 `previous_price` 字段 + - 用于显示涨跌颜色(红色/绿色) + +2. **数量更新规则**: + - 数量可以增加(加仓)或减少(减仓) + - 数量不能小于0 + +3. **成本价更新规则**: + - 如果用户修改了成本价,说明可能是加仓或减仓 + - 系统记录变更历史(可选功能) + +#### 4.2.6 用户提示 + +**表单提示信息:** +``` +💡 提示: +• 修改成本价、最新价、数量后,系统会自动重新计算盈亏和占比 +• 成本价和最新价请使用人民币计价 +• 如果修改了成本价或数量,建议检查成本价是否正确(加权平均) +``` + +#### 4.2.7 交互流程 + +1. 用户点击"编辑"按钮 +2. 弹出编辑表单对话框(预填充现有数据) +3. 用户修改字段 +4. 用户点击"保存"按钮 +5. 系统验证数据 +6. 系统更新持仓 +7. 刷新持仓列表 +8. 显示成功提示 + +#### 4.2.8 异常处理 + +- **持仓不存在**:提示"持仓不存在" +- **无权限**:提示"您没有权限编辑此持仓" +- **数据验证失败**:表单验证提示 + +--- + +### 4.3 价格自动更新 + +#### 4.3.1 功能描述 +系统每日收盘后自动更新开启了"自动更新价格"的持仓的最新价。 + +#### 4.3.2 触发时机 +- 每个工作日下午4点(A股收盘后) +- 可配置定时任务 + +#### 4.3.3 更新流程 + +1. **查询需要更新的持仓**: + ```sql + SELECT * FROM positions + WHERE auto_price_update = true + AND status = 'active' + AND asset_type IN ('stock', 'fund') + ``` + +2. **获取市场价格**: + - 调用市场数据API获取原始货币价格 + - 根据 `currency` 字段判断币种 + +3. **转换为人民币**: + - 如果 `currency = 'CNY'`:直接使用价格 + - 如果 `currency != 'CNY'`: + - 获取当前汇率 + - 转换为人民币:`人民币价格 = 原始货币价格 × 汇率` + +4. **更新数据库**: + - 将旧价格保存到 `previous_price` + - 更新 `current_price` 为人民币价格 + - 更新 `updated_at` 时间戳 + +#### 4.3.4 异常处理 + +- **市场数据获取失败**:记录日志,跳过该持仓 +- **汇率获取失败**:使用上次汇率或跳过更新 +- **价格异常**:记录日志,不更新 + +--- + +## 五、技术实现细节 + +### 5.1 数据库设计 + +#### 5.1.1 表结构 + +```sql +CREATE TABLE positions ( + position_id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, + broker_id BIGINT NOT NULL REFERENCES brokers(broker_id), + asset_type VARCHAR(20) NOT NULL CHECK (asset_type IN ('stock', 'fund', 'cash', 'bond', 'other')), + symbol VARCHAR(50) NOT NULL, + name VARCHAR(100) NOT NULL, + market VARCHAR(20), + shares DECIMAL(18, 4) NOT NULL DEFAULT 0, + cost_price DECIMAL(18, 4) NOT NULL, -- 成本价(人民币) + current_price DECIMAL(18, 4), -- 最新价(人民币) + previous_price DECIMAL(18, 4), -- 上一次价格(人民币) + currency VARCHAR(10) NOT NULL DEFAULT 'CNY', -- 货币类型(仅标识) + auto_price_update BOOLEAN NOT NULL DEFAULT false, + status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'delisted')), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, broker_id, symbol, market, asset_type) +); +``` + +#### 5.1.2 索引设计 + +```sql +CREATE INDEX idx_positions_user_id ON positions(user_id); +CREATE INDEX idx_positions_broker_id ON positions(broker_id); +CREATE INDEX idx_positions_status ON positions(status); +CREATE INDEX idx_positions_auto_update ON positions(auto_price_update) WHERE auto_price_update = true; +``` + +### 5.2 后端实现 + +#### 5.2.1 搜索资产接口 + +```typescript +GET /api/assets/search?keyword=600519&assetType=stock&limit=10 + +Request Query: +- keyword: string (必填) - 搜索关键词(资产代码或名称) +- assetType: string (可选) - 资产类型过滤(stock/fund/bond) + - 如果提供,只搜索该类型的资产 + - 如果不提供,搜索所有类型(股票、基金、债券) +- limit: number (可选) - 返回结果数量,默认10 + +Response: +{ + code: 0, + data: [ + { + symbol: string; // 资产代码 + name: string; // 资产名称 + market: string; // 市场(sh/sz/hk/us等) + assetType: string; // 资产类型(stock/fund/bond) + currency: string; // 货币类型(CNY/HKD/USD) + } + ] +} +``` + +**搜索逻辑:** +- 搜索范围:股票、基金、债券(不包含现金、其他) +- 搜索字段:资产代码、资产名称 +- 匹配方式:模糊匹配,不区分大小写 +- 排序规则:完全匹配 > 前缀匹配 > 包含匹配 +- **智能过滤**: + - 如果提供了 `assetType` 参数,只搜索该类型的资产 + - 如果没有提供,搜索所有类型(股票、基金、债券) + - 搜索结果按匹配度排序,同类型优先 + +**后端实现示例:** +```typescript +@Get('search') +async searchAssets( + @Query('keyword') keyword: string, + @Query('assetType') assetType?: string, + @Query('limit') limit: number = 10, +) { + const query = this.assetRepository + .createQueryBuilder('asset') + .where('asset.symbol LIKE :keyword OR asset.name LIKE :keyword', { + keyword: `%${keyword}%`, + }) + .andWhere('asset.assetType IN (:...types)', { + types: assetType + ? [assetType] + : ['stock', 'fund', 'bond'], + }) + .orderBy('CASE WHEN asset.symbol = :exact THEN 1 WHEN asset.symbol LIKE :prefix THEN 2 ELSE 3 END', 'ASC') + .addOrderBy('asset.name', 'ASC') + .limit(limit); + + return query.getMany(); +} +``` + +#### 5.2.2 创建持仓接口 + +```typescript +POST /api/positions + +Request Body: +{ + brokerId: number; + assetType: string; + symbol: string; // 资产代码(股票/基金/债券必填,现金不需要) + name: string; // 资产名称(股票/基金/债券必填,现金不需要) + market?: string; // 市场(股票/基金/债券可选,现金不需要) + shares: number; + costPrice: number; // 人民币成本价 + currentPrice?: number; // 人民币最新价 + currency?: string; // 仅标识,默认CNY + autoPriceUpdate?: boolean; + status?: string; +} + +Response: +{ + code: 0, + data: { + positionId: number; + // ... 其他字段 + } +} +``` + +**字段验证规则:** +- 如果 `assetType` 为 `stock/fund/bond`: + - `symbol`、`name` 必填 + - `market` 可选(但建议填写) +- 如果 `assetType` 为 `cash`: + - `symbol`、`name`、`market` 不需要 + +#### 5.2.2 更新持仓接口 + +```typescript +PUT /api/positions/:id + +Request Body: +{ + brokerId?: number; + costPrice?: number; // 人民币成本价 + currentPrice?: number; // 人民币最新价 + shares?: number; + currency?: string; + autoPriceUpdate?: boolean; + status?: string; +} + +Response: +{ + code: 0, + data: { + positionId: number; + // ... 更新后的字段 + } +} +``` + +#### 5.2.3 查询持仓列表接口 + +```typescript +GET /api/positions + +Response: +{ + code: 0, + data: [ + { + positionId: number; + costPrice: number; // 人民币成本价 + currentPrice: number; // 人民币最新价 + shares: number; + costValue: number; // 人民币成本 = shares * costPrice + marketValue: number; // 人民币市值 = shares * currentPrice + profit: number; // 人民币盈亏 = marketValue - costValue + profitPercent: number; // 盈亏比例 + assetPercent: number; // 持仓占比 + // ... 其他字段 + } + ] +} +``` + +#### 5.2.4 价格自动更新服务 + +```typescript +@Injectable() +export class PriceUpdateService { + /** + * 每日收盘后更新价格 + */ + @Cron('0 16 * * 1-5') + async updateDailyPrices() { + const positions = await this.getPositionsToUpdate(); + + for (const position of positions) { + try { + // 1. 获取原始货币价格 + const originalPrice = await this.getMarketPrice( + position.symbol, + position.market + ); + + // 2. 转换为人民币 + let cnyPrice = originalPrice; + if (position.currency !== 'CNY') { + const exchangeRate = await this.getCurrentExchangeRate( + position.currency, + 'CNY' + ); + cnyPrice = originalPrice * exchangeRate; + } + + // 3. 更新持仓 + await this.updatePositionPrice( + position.positionId, + cnyPrice + ); + } catch (error) { + this.logger.error(`更新价格失败: ${position.symbol}`, error); + } + } + } +} +``` + +### 5.3 前端实现 + +#### 5.3.1 新建持仓表单 + +```typescript +const CreatePositionForm = () => { + const [searchKeyword, setSearchKeyword] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [selectedAsset, setSelectedAsset] = useState(null); + const [assetType, setAssetType] = useState(''); + const [isSearching, setIsSearching] = useState(false); + const [showManualInput, setShowManualInput] = useState(false); + const form = Form.useForm(); + + // 防抖搜索 + const debouncedSearch = useMemo( + () => debounce(async (keyword: string, type?: string) => { + if (!keyword || keyword.length < 1) { + setSearchResults([]); + return; + } + + setIsSearching(true); + try { + const params: any = { keyword, limit: 10 }; + // 如果选择了资产类型,只搜索该类型 + if (type && type !== 'cash' && type !== 'other') { + params.assetType = type; + } + + const response = await api.get('/api/assets/search', { params }); + setSearchResults(response.data || []); + } catch (error) { + console.error('搜索失败', error); + setSearchResults([]); + } finally { + setIsSearching(false); + } + }, 300), + [] + ); + + // 搜索资产 + const handleSearch = (keyword: string) => { + setSearchKeyword(keyword); + debouncedSearch(keyword, assetType); + }; + + // 选择资产 + const handleSelectAsset = (asset: any) => { + setSelectedAsset(asset); + setSearchKeyword(asset.name); + setSearchResults([]); + setShowManualInput(false); + + // 自动填充表单 + form.setFieldsValue({ + assetType: asset.assetType, + market: asset.market, + symbol: asset.symbol, + name: asset.name, + currency: asset.currency, + }); + + setAssetType(asset.assetType); + }; + + // 重新选择 + const handleResetSearch = () => { + setSelectedAsset(null); + setSearchKeyword(''); + setSearchResults([]); + form.setFieldsValue({ + assetType: assetType || undefined, + market: undefined, + symbol: undefined, + name: undefined, + }); + }; + + // 资产类型变化 + const handleAssetTypeChange = (value: string) => { + setAssetType(value); + form.setFieldsValue({ assetType: value }); + + // 如果手动修改了资产类型,清空搜索选择 + if (selectedAsset && selectedAsset.assetType !== value) { + handleResetSearch(); + } + + // 如果选择了现金或其他,隐藏搜索框 + if (value === 'cash' || value === 'other') { + setShowManualInput(true); + } else { + setShowManualInput(false); + } + + // 如果搜索框有内容,重新搜索(过滤类型) + if (searchKeyword) { + handleSearch(searchKeyword); + } + }; + + // 获取搜索框占位符 + const getSearchPlaceholder = () => { + if (assetType === 'stock') return '搜索股票...'; + if (assetType === 'fund') return '搜索基金...'; + if (assetType === 'bond') return '搜索债券...'; + return '输入代码/名称搜索...'; + }; + + return ( +
+ {/* 搜索框(现金和其他类型时隐藏) */} + {!showManualInput && ( + + ({ + value: `${asset.symbol} - ${asset.name}`, + label: ( +
+
+ {asset.symbol} - {asset.name} +
+
+ {getMarketName(asset.market)} - {getAssetTypeName(asset.assetType)} +
+
+ ), + asset: asset, + }))} + onSearch={handleSearch} + onSelect={(value, option) => handleSelectAsset(option.asset)} + placeholder={getSearchPlaceholder()} + allowClear + disabled={!!selectedAsset} + notFoundContent={ + searchKeyword && !isSearching ? ( +
+
未找到匹配的资产
+ +
+ ) : null + } + /> + {selectedAsset && ( +
+ {selectedAsset.name} + +
+ )} +
+ )} + + {/* 资产类型 */} + + + + + {/* 市场和券商(股票/基金/债券显示) */} + {(assetType === 'stock' || assetType === 'fund' || assetType === 'bond') && ( + <> + + + + + + + + + + + + + + + + + )} + + {/* 券商(现金显示) */} + {assetType === 'cash' && ( + + + + )} + + {/* 价格和数量 */} + + + + + + + + + + + + + {/* 其他选项 */} + + + + + + + + + +

• 可以通过搜索框快速选择资产,系统会自动填充相关信息

+

• 成本价和最新价请使用人民币计价

+

• 如果持有港股/美股,请将港币/美元价格转换为人民币后输入

+

• 系统会自动更新价格(如果开启自动更新)

+ + } + type="info" + showIcon + /> + + ); +}; +``` + +#### 5.3.2 持仓列表展示 + +```typescript +const PositionList = () => { + const { positions, totalAssetCNY } = usePositions(); + + return ( +
+ {/* 总资产 */} +
+

总资产

+
+ ¥{totalAssetCNY.toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + })} +
+
+ + {/* 持仓列表 */} + {positions.map(position => ( + +
+
{position.name}
+
+ {position.symbol} + {position.market && {getMarketName(position.market)}} +
+
+ 持股:{position.shares.toLocaleString()} 股 +
+ +
+
+ 市值:¥{position.marketValue.toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + })} +
+
+ 成本:¥{position.costValue.toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + })} +
+
= 0 ? 'positive' : 'negative'}`}> + 盈亏:¥{position.profit.toLocaleString('zh-CN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + })} ({position.profitPercent >= 0 ? '+' : ''}{position.profitPercent.toFixed(2)}%) +
+
+ 占比:{position.assetPercent.toFixed(2)}% +
+
+
+
+ ))} +
+ ); +}; +``` + +--- + +## 六、总结 + +### 6.1 最终方案 +采用**统一人民币存储方案**,所有价格统一使用人民币计价,简化用户操作和系统实现。 + +### 6.2 核心要点 +1. **用户输入**:所有价格统一使用人民币 +2. **数据存储**:数据库中所有价格字段统一为人民币 +3. **系统处理**:所有计算和展示统一为人民币 +4. **价格更新**:系统自动获取原始货币价格并转换为人民币 + +### 6.3 优势 +- ✅ 极简:用户操作简单,只需输入3个数字 +- ✅ 统一:所有数据统一为人民币,计算和展示一致 +- ✅ 无汇率问题:完全避免汇率存储、更新、同步等问题 +- ✅ 符合记账场景:用户记录的是自己的资产情况 + +### 6.4 后续优化方向 +1. 提供简单的汇率转换工具(可选) +2. 支持批量导入持仓(从Excel等) +3. 支持持仓变更历史记录 +4. 支持持仓占比图表展示 + +--- + +--- + +## 七、方案A实施要点总结 + +### 7.1 核心优势 + +1. **灵活性**:支持三种创建方式,用户可以根据习惯选择 + - 快速创建:直接搜索,自动填充 + - 智能创建:先选类型再搜索,精准过滤 + - 手动创建:完全手动输入,适合特殊资产 + +2. **容错性**:搜索失败可以无缝切换到手动输入 + - 搜索无结果时,提供手动输入选项 + - 网络错误时,允许用户继续操作 + - 不会因为搜索功能影响基本功能 + +3. **智能联动**:搜索框和资产类型相互联动 + - 选择资产类型后,搜索框自动过滤 + - 搜索选择后,资产类型自动填充 + - 字段根据选择动态显示/隐藏 + +4. **用户体验**:渐进式披露,不会一次性显示所有字段 + - 先显示搜索框和资产类型 + - 根据选择逐步显示对应字段 + - 减少用户认知负担 + +### 7.2 实现要点 + +1. **搜索功能**: + - 实现防抖搜索(300ms) + - 支持资产类型过滤 + - 搜索结果按匹配度排序 + - 处理搜索失败情况 + +2. **字段联动**: + - 搜索选择后自动填充字段 + - 字段禁用状态管理 + - 资产类型变化时清空相关字段 + +3. **表单验证**: + - 根据资产类型动态验证 + - 搜索选择后跳过相关字段验证 + - 手动输入时完整验证 + +4. **用户体验优化**: + - 搜索框占位符动态变化 + - 搜索状态提示(加载中、无结果等) + - 提供"重新选择"和"手动输入"选项 + +### 7.3 技术实现建议 + +1. **前端**: + - 使用 `AutoComplete` 组件实现搜索 + - 使用 `Form` 组件管理表单状态 + - 使用 `useMemo` 和 `debounce` 优化搜索性能 + - 使用状态管理控制字段显示/隐藏 + +2. **后端**: + - 实现搜索接口,支持关键词和类型过滤 + - 使用数据库索引优化搜索性能 + - 实现搜索结果缓存(可选) + +3. **数据准备**: + - 准备资产数据库(股票、基金、债券) + - 包含:代码、名称、市场、类型等信息 + - 定期更新资产数据 + +--- + +**文档版本**:v1.1 +**最后更新**:2024年 +**作者**:AI Assistant +**更新说明**:采用方案A(搜索优先 + 智能切换)设计 diff --git a/packages/design-document/机生文档/数据库设计文档.md b/packages/design-document/机生文档/数据库设计文档.md new file mode 100644 index 0000000..706effe --- /dev/null +++ b/packages/design-document/机生文档/数据库设计文档.md @@ -0,0 +1,832 @@ +# 思投录数据库设计文档 + +## 一、数据库选型分析 + +### 1.1 业务特点分析 + +根据业务需求,系统需要处理以下类型的数据: + +1. **结构化数据** + - 用户账户、持仓、交易记录 + - 交易计划、计划步骤 + - 资产净值、收益率等数值数据 + +2. **时间序列数据** + - 每日单位净值记录 + - 资产快照(每日) + - 价格历史数据 + +3. **文本内容数据** + - 交易思考(交易记录时填写) + - 复盘内容(定期复盘时填写) + - 计划思考(创建计划时填写) + +4. **关联关系** + - 交易记录与持仓的关联 + - 交易计划与交易记录的关联 + - 复盘与交易记录的关联 + - 计划步骤与计划的关联 + +5. **查询需求** + - 复杂的时间范围查询 + - 多表关联查询 + - 聚合统计查询 + - 时间线查询(按时间排序) + +### 1.2 数据库选型推荐 + +#### 推荐方案:PostgreSQL + +**推荐理由:** + +1. **关系型数据库优势** + - ✅ 数据结构清晰,关系明确 + - ✅ 支持复杂查询和统计(JOIN、聚合函数) + - ✅ 支持事务,保证数据一致性 + - ✅ ACID特性,数据可靠性高 + +2. **PostgreSQL特有优势** + - ✅ 支持JSON/JSONB类型,可灵活存储思考内容等文本 + - ✅ 支持数组类型,适合存储计划步骤等 + - ✅ 强大的时间序列查询能力 + - ✅ 支持全文搜索(可用于搜索思考内容) + - ✅ 性能优秀,适合中小型应用 + - ✅ 开源免费,社区活跃 + +3. **其他考虑** + - 如果团队更熟悉MySQL,MySQL 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+ + diff --git a/packages/design-document/机生文档/设计优化评估-简化交易记录.md b/packages/design-document/机生文档/设计优化评估-简化交易记录.md new file mode 100644 index 0000000..b7182fc --- /dev/null +++ b/packages/design-document/机生文档/设计优化评估-简化交易记录.md @@ -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年 +**评估结论**:✅ 推荐采用新设计,但需要补充资金流和快照功能 + diff --git a/packages/design-ui/PRD.md b/packages/design-ui/PRD.md new file mode 100644 index 0000000..5126bae --- /dev/null +++ b/packages/design-ui/PRD.md @@ -0,0 +1,33 @@ +## 核心思想 +名称选择:思投录 +愿景:让每笔投资都经得起思考 +副标题:**投资决策与复盘工具**。 +### 核心功能 +#### 持仓 +记录自己的股票持仓,统计各个股票持仓占比,按基金收益法统计的持仓收益。可以基于用户设置的单只股票仓位上限进行预警。 +#### 交易计划 +- “计划你的交易,交易你的计划”,让用户可以创建交易计划,选择股票、市场、目标价格、截止时间、投资金额或股份数(两者选一个即可)等。 +- 之后到目标价后可以提醒用户。 +- 用户可以为计划设置步骤,默认分三步进行买入等,可以分步设置买入价格。 +#### 交易记录/复盘 +- 引导用户记录每一笔交易,可以是从计划中点完成计划等操作跳转到记录页中,也可以是用户主动记录。用户可以记录每一笔交易的企业名称、买卖份数、买卖单价,最重要的是要引导用户写下买卖思考。 +- 定期弹出页面应到用户写下复盘和思考。 +- 用户可以参看每一笔交易的附带思考和复盘记录的时间线,让用户可以在交易中学习和成长,类似 QQ 空间的说说功能。 +- 用户可以分享自己的交易时间线(不确定是否需要)。 +#### 我的 +不确定是否应该把 我的 作为一个 Tabbar。 +- 展示一些我的信息。 +- 最主要的是一些工具入口,例如 投资检查清单、复利计算器、估值工具(可以分老唐估值法和两段式现金流折现估值法)、自由目标等。 +- 投资检查清单:可以分为买入和卖出两份检查清单,在每次设置交易计划时最后弹出,让用户每项检查。 +- 复利计算器:侧重于投资者角度,可以填入初始金额,每年可投入的金额,调整预计的年复合增长率,然后查看未来的总收益。 +- 自由目标:“让自己有的选”,自由目标可以和复利计算器和持仓结合起来,让用户自己设定一个总资产达成目标。把这个目标和财务自由、赎回自己的时间关联起来,让用户有“奔头”。 + +## App名称 +中文名:思投录 +英文名:VestMind +“Vest”(投资记录与管理)和“Mind”(思考与决策清单)。用户能直观感受到这是一个与“投资”和“思考”相关的工具。 +Mind”强调了投资不应是冲动行为,而应是经过深思熟虑的、理性的“思维活动”,这与你“让每笔投资都经得起思考”的愿景高度契合。 +这个词组合起来听起来专业、简洁,且带有一种“智慧投资”的质感,能吸引那些希望提升自己投资决策质量的用户。 + +## 主题色 +主题色需要偏紫色调,注重让人安静思考的色调。 diff --git a/packages/design-ui/README.md b/packages/design-ui/README.md new file mode 100644 index 0000000..ec7a796 --- /dev/null +++ b/packages/design-ui/README.md @@ -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文档要求,实现了思投录应用的核心功能界面。设计风格现代简洁,交互流畅自然,完全适配移动端和小程序使用场景。紫色主题营造了安静思考的投资氛围,符合"让每笔投资都经得起思考"的产品愿景。 diff --git a/packages/design-ui/index.html b/packages/design-ui/index.html new file mode 100644 index 0000000..7d1fcd3 --- /dev/null +++ b/packages/design-ui/index.html @@ -0,0 +1,229 @@ + + + + + + + 思投录 - 投资决策与复盘工具 + + + + + + + + +
+ +
+
+

思投录

+

让每笔投资都经得起思考

+
+
+ + +
+ +
+
+

持仓概览

+ ¥128,450.00 +
+
+
+ 今日收益 + +¥1,250.00 +
+
+ 总收益率 + +12.5% +
+
+
+ + + +
+ + +
+ + +
+
+
+
贵州茅台
+
600519
+
+
+
100股
+
¥1,850.00
+
+¥2,500.00 (+15.6%)
+
+
+ +
+
+
腾讯控股
+
00700
+
+
+
200股
+
¥320.00
+
-¥800.00 (-1.2%)
+
+
+
+
+ + + + + + + + + +
+ + + + + + + + \ No newline at end of file diff --git a/packages/design-ui/script.js b/packages/design-ui/script.js new file mode 100644 index 0000000..996f19f --- /dev/null +++ b/packages/design-ui/script.js @@ -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 => ` +
+
+
${holding.name}
+
${holding.code}
+
+
+
${holding.shares}股
+
¥${holding.currentPrice.toFixed(2)}
+
+ ${holding.profit >= 0 ? '+' : ''}¥${holding.profit.toFixed(2)} (${holding.profitRate >= 0 ? '+' : ''}${holding.profitRate}%) +
+
+
+ `).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 => ` +
+
+
+
${plan.name}
+
${plan.code}
+
+
进行中
+
+
+
+ 目标价格: + ¥${plan.targetPrice.toFixed(2)} +
+
+ 计划金额: + ¥${plan.amount.toLocaleString()} +
+
+ 截止时间: + ${plan.deadline} +
+
+
+
+
+
+ 已完成 ${plan.progress}% +
+
+ `).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 => ` +
+
${record.date}
+
+
+
+
+
${record.name}
+
${record.code}
+
+
${record.type === 'buy' ? '买入' : '卖出'}
+
+
+
+ 数量: + ${record.shares}股 +
+
+ 价格: + ¥${record.price.toFixed(2)} +
+
+
+

交易思考:

+

${record.thoughts}

+
+
+
+
+ `).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 = ` +
+
+

买入检查清单

+
+ + + + + +
+
+
+

卖出检查清单

+
+ + + + +
+
+
+ `; + this.showModal(); + } + + showCalculatorModal() { + this.modalTitle.textContent = '复利计算器'; + this.modalBody.innerHTML = ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ `; + 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 = ` +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+
+ `; + 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 = ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ `; + 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 = ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ `; + this.showModal(); + } + + showAddPlanModal() { + this.modalTitle.textContent = '新建交易计划'; + this.modalBody.innerHTML = ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ `; + this.showModal(); + } + + showAddRecordModal() { + this.modalTitle.textContent = '记录交易'; + this.modalBody.innerHTML = ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ `; + 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 = ` + +`; + +document.head.insertAdjacentHTML('beforeend', additionalStyles); diff --git a/packages/design-ui/src/investment-record-v2.html b/packages/design-ui/src/investment-record-v2.html new file mode 100644 index 0000000..fcebf3e --- /dev/null +++ b/packages/design-ui/src/investment-record-v2.html @@ -0,0 +1,1630 @@ + + + + + + + 投资记录 - 买股票就是买公司 + + + + + + + + +
+ + +
+
+
+ +
+ 头像 +
+ + +
+
+
+
+
+ + +
+
+
累计收益率
+
+28.5%
+
+
+
年化收益率
+
+15.7%
+
+
+
+ + +
+ +
+
+

总资产趋势

+
+
+
+ + + + +
+
+
+
+ + +
+
+

累计收益率

+
+
+
+ + + + +
+
+
+
+
+ + +
+
+

持仓分布

+
+
+
+
+
+ + +
+
+

我的持仓

+ +
+
+ +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/packages/design-ui/src/investment-record.html b/packages/design-ui/src/investment-record.html new file mode 100644 index 0000000..f4a2e3d --- /dev/null +++ b/packages/design-ui/src/investment-record.html @@ -0,0 +1,1308 @@ + + + + + + + 投资记录 - 买股票就是买公司 + + + + + +
+ + + + +
+
+
张三
+
记账: 300天
+
+
+
¥128,500.00
+
+ 上一个交易日收益: + +¥1,200.00 +
+
+ 累计收益: + +¥28,500.00 +
+
+ 累计收益率: + +28.52% +
+
+ 年化收益率: + +15.68% +
+
+
+ + +
+ +
+
+
总资产趋势
+
+ + + + +
+
+
+
+ + +
+
+
累计收益率
+
+ + + + +
+
+
+
+
+ + +
+
+
持仓分布
+
+
+
+ + +
+
+

我的持仓

+ +
+
+ +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/packages/design-ui/src/pc-web-ui.html b/packages/design-ui/src/pc-web-ui.html new file mode 100644 index 0000000..cb51d69 --- /dev/null +++ b/packages/design-ui/src/pc-web-ui.html @@ -0,0 +1,1289 @@ + + + + + + + 思投录 (VestMind) - PC Web端 + + + + + +
+ + + + +
+ +
+
+
+
资产账户
+
买股票就是买公司
+
+
+
+ +
+
+ + +
+ +
+ +
+
+
总资产
+
¥1,234,567
+
+2.5% 今日
+
+
+
累计收益
+
¥234,567
+
+23.4%
+
+
+
累计收益率
+
23.4%
+
+2.3%
+
+
+
记账时长
+
300天
+
+
+ + +
+
+
资产趋势
+
+
+
+ + +
+
+
我的持仓
+ +
+
+
+
贵州茅台
+
600519 · 上海 · 华泰证券
+
+
+
¥456,789
+
+¥56,789 (+14.2%)
+
+
+
+
+
腾讯控股
+
00700 · 香港 · 富途证券
+
+
+
¥345,678
+
+¥45,678 (+15.2%)
+
+
+
+
+
苹果公司
+
AAPL · 美股 · 盈透证券
+
+
+
¥432,100
+
-¥12,100 (-2.7%)
+
+
+
+
+ + +
+
+
+
交易计划
+ +
+
+
+
买入贵州茅台
+ 执行中 +
+
+
+
目标价格
+
¥1,800
+
+
+
计划金额
+
¥100,000
+
+
+
截止时间
+
2024-12-31
+
+
+
完成进度
+
60%
+
+
+
+
+
+
买入腾讯控股
+ 待执行 +
+
+
+
目标价格
+
HK$380
+
+
+
计划金额
+
¥50,000
+
+
+
截止时间
+
2024-11-30
+
+
+
完成进度
+
0%
+
+
+
+
+
+
卖出苹果公司
+ 已完成 +
+
+
+
目标价格
+
$180
+
+
+
计划金额
+
¥200,000
+
+
+
完成时间
+
2024-10-15
+
+
+
完成进度
+
100%
+
+
+
+
+
+ + +
+
+
+
投资复盘
+ +
+
+
+
+
+
2024-10-20
+
+ 买入贵州茅台
+ 今天以¥1,750的价格买入了100股贵州茅台。主要考虑是:1)茅台作为白酒龙头,品牌价值难以复制;2)当前估值相对合理;3)长期看好消费升级趋势。计划持有3-5年。 +
+
+
+
+
+
+
2024-09-15
+
+ 卖出部分腾讯控股
+ 以HK$350的价格卖出了200股腾讯控股,主要是为了锁定部分利润。虽然长期看好,但考虑到当前市场环境,决定降低仓位。剩余持仓继续持有。 +
+
+
+
+
+
+
2024-08-01
+
+ 月度复盘
+ 本月整体收益+5.2%,主要贡献来自腾讯和茅台的上涨。需要反思的是:1)对市场波动反应过度,频繁交易;2)应该更专注于长期价值投资;3)需要建立更系统的投资检查清单。 +
+
+
+
+
+
+ + +
+
+
+
SEO配置
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
12,345
+
总用户数
+
+
+
8,901
+
日活跃用户 (DAU)
+
+
+
56,789
+
月活跃用户 (MAU)
+
+
+
234,567
+
总访问量 (PV)
+
+
+ +
+
+
访问趋势
+
+
+
+ +
+
+
用户行为统计
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
日期UVPV新用户活跃用户
2024-10-201,2345,678123890
2024-10-191,1895,43298856
2024-10-181,1565,23487823
2024-10-171,0984,98776789
2024-10-161,0454,75665756
+
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/packages/design-ui/styles.css b/packages/design-ui/styles.css new file mode 100644 index 0000000..827443f --- /dev/null +++ b/packages/design-ui/styles.css @@ -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; +}