- 📛 证券名称增强:修复 INTC 等证券名称显示为空的问题 (BUG-101) - 🔒 SELL超量校验:修复卖出数量超过持仓导致持仓变负的Bug (BUG-002) - 🔄 撤销BUY成本还原:修复撤销买入时平均成本计算公式错误 (BUG-003) - 💹 Decimal精度计算:持仓分析改用Prisma.Decimal进行金融计算,防止浮点精度丢失 (BUG-004) - 📛 证券名称显示:在持仓分析卡片、资产分布、盈亏排行等位置同时显示股票代码和名称 - 📋 证券数据库扩展:新增 Intel Corp. (INTC) 证券记录 - 🔍 回退逻辑增强:确保证券名称为空时显示代码而非空白 - 📈 腾讯行情解析升级:精准解析股票名称(索引1),支持港/A/美股及 ETF 名称自动获取 - 🔀 多市场涨跌解析修复:重构腾讯行情多市场适配层,美股(索引4/5)与港A股(索引31/32)使用差异化索引解析涨跌数据 - 🇨🇳 GBK中文解码修复:改用 `arrayBuffer() + TextDecoder('gbk')` 替代 `text()`,彻底解决A股/港股中文股票名称乱码问题 - 💱 JisuAPI实时汇率:接入 JisuAPI 获取实时汇率,缓存1小时,支持 CNY/HKD/USD 转换 - 📊 资产配置动态图:环形图改由后端实时聚合持仓数据驱动,支持 Tooltip 和百分比显示 - 🎨 资产配置图表优化:精美毛玻璃 Tooltip、颜色图标、去除生硬描边、useMemo 性能优化 - 💱 全局货币联动:资产配置图表数值随 CNY/USD/HKD 切换实时转换 - 📝 交易流水增强:新增证券名称列,显示"名称+代码"双行格式 - 💹 全局汇率展示:在导航栏实时显示 USD/CNY/HKD 汇率信息 - 🔧 BUG-202 修复:修正 `convertCurrency` 汇率换算逻辑(原逻辑除法/乘法颠倒,导致 USD→CNY 换算失效) - 🔧 BUG-201 修复:腾讯行情 API 获取失败时,`priceAvailable` 标记配合前端显示 "N/A" 替代虚假 0% - 🔧 BUG-203 增强:持仓分析 `name` 字段确保回退到 `pos.symbol`,名称永不空 - 💹 Decimal 精度保障:所有盈亏/汇率计算均使用 Prisma.Decimal,防止浮点精度丢失
21 KiB
个人投资持仓管理系统 - 需求规格说明书
文档版本:v1.0.0
日期:2026-04-12
状态:已完成
1. 产品概述
1.1 产品名称
个人投资持仓管理系统(Stock Portfolio Manager)
1.2 产品简介
现代化、全面化的个人投资组合管理平台,支持多市场(美股、A股、港股、加密货币)统一管理,实时获取行情数据,自动计算持仓成本与盈亏。
1.3 目标用户
- 拥有多个市场投资账户的个人投资者
- 需要统一管理分散在不同平台的投资组合
- 希望实时了解总体资产配置和盈亏状况
2. 技术架构
2.1 技术栈
| 层级 | 技术 | 版本 |
|---|---|---|
| 前端框架 | Next.js | 16 |
| UI 框架 | React | 19 |
| 类型系统 | TypeScript | 5 |
| 样式方案 | Tailwind CSS | 4 |
| 组件库 | shadcn/ui | - |
| 图表库 | Recharts | - |
| 后端框架 | Next.js API Routes | - |
| ORM | Prisma | - |
| 数据库 | PostgreSQL | 16 |
2.2 系统架构
┌─────────────────────────────────────────────────────┐
│ 前端 (Next.js) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Dashboard │ │ 持仓明细 │ │ 交易流水 │ │ 分析 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
└───────┼────────────┼────────────┼────────────┼───────┘
│ │ │ │
└────────────┴─────┬──────┴────────────┘
│ REST API
┌──────────────────┼──────────────────┐
│ │ │
┌────▼────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Accounts │ │ Transactions │ │ Positions │
│ API │ │ API │ │ API │
└────┬────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌────▼──────────────────▼──────────────────▼────┐
│ Prisma ORM │
└────────────────────┬─────────────────────────┘
│
┌────────────────────▼─────────────────────────┐
│ PostgreSQL Database │
│ User │ Account │ Security │ Transaction │ │
│ Position │ ExchangeRate │
└──────────────────────────────────────────────┘
2.3 目录结构
stock-portfolio/
├── prisma/
│ ├── schema.prisma # 数据模型定义
│ └── seed.ts # 初始化数据
├── src/
│ ├── app/
│ │ ├── page.tsx # 主页面(Dashboard)
│ │ └── api/ # API 路由
│ │ ├── accounts/
│ │ ├── transactions/
│ │ ├── positions/
│ │ ├── securities/
│ │ ├── exchange-rates/
│ │ ├── dashboard/
│ │ │ ├── stats/
│ │ │ └── analytics/
│ │ └── import/
│ ├── components/ # UI 组件
│ ├── lib/
│ │ ├── api.ts # API 调用封装
│ │ ├── prisma.ts # Prisma 客户端
│ │ └── import-export.ts
│ └── types/ # TypeScript 类型定义
├── .env # 环境变量
└── README.md
3. 功能规格
3.1 市场与账户
3.1.1 支持的市场
| 市场代码 | 市场名称 | 结算货币 | 示例证券 |
|---|---|---|---|
| US | 美股 | USD | AAPL, GOOGL, MSFT |
| CN | A股 | CNY | 600690, 159235 |
| HK | 港股 | HKD | 00700, 09868 |
| CRYPTO | 加密货币 | USDT | BTC, ETH |
3.1.2 账户管理
- 每个市场对应一个默认账户
- 账户信息包含:名称、市场类型、基准货币、余额
- 创建账户时自动设置对应市场的基准货币
3.2 证券管理
3.2.1 证券数据结构
| 字段 | 类型 | 说明 |
|---|---|---|
| symbol | String | 证券代码(唯一) |
| name | String | 证券名称 |
| market | MarketType | 市场类型 |
| currency | String | 结算货币 |
| lotSize | Int | 每手股数(港股=100,A股=100,美股=1) |
| priceDecimals | Int | 价格精度 |
| qtyDecimals | Int | 数量精度(股票=0,数字货币=8) |
| isCrypto | Boolean | 是否为加密货币 |
3.2.2 预置证券
| 代码 | 名称 | 市场 | 货币 |
|---|---|---|---|
| 00700 | 腾讯控股 | HK | HKD |
| 09868 | 小鹏汽车 | HK | HKD |
| 09988 | 阿里巴巴 | HK | HKD |
| AAPL | Apple Inc. | US | USD |
| MSFT | Microsoft Corp. | US | USD |
| NVDA | NVIDIA Corp. | US | USD |
| GOOGL | Alphabet Inc. | US | USD |
| INTC | Intel Corp. | US | USD |
| 600690 | 海尔智家 | CN | CNY |
| 159235 | 中证现金流ETF | CN | CNY |
| BTC | Bitcoin | CRYPTO | USDT |
| ETH | Ethereum | CRYPTO | USDT |
3.3 交易类型
| 类型代码 | 中文名称 | 说明 | 账户影响 | 持仓影响 |
|---|---|---|---|---|
| BUY | 买入 | 购买证券 | 扣减余额 | 增加持仓 |
| SELL | 卖出 | 卖出证券 | 增加余额 | 减少持仓 |
| DEPOSIT | 入金 | 资金转入 | 增加余额 | 无 |
| WITHDRAW | 出金 | 资金转出 | 扣减余额 | 无 |
| DIVIDEND | 分红 | 现金分红 | 增加余额 | 无 |
| INTEREST | 利息 | 利息收入 | 增加余额 | 无 |
| FEE | 费用 | 手续费支出 | 扣减余额 | 无 |
3.4 行情数据
3.4.1 数据源
使用腾讯行情接口获取实时价格:
- 接口地址:
https://qt.gtimg.cn/q= - 支持批量查询,用逗号分隔
3.4.2 代码格式转换
用户输入证券代码时,系统自动转换为腾讯接口格式:
| 市场 | 用户输入 | 腾讯接口格式 |
|---|---|---|
| 港股 | 09868 | r_hk09868 |
| A股上海 | sh600690 或 600690 | sh600690 |
| A股深圳 | sz159235 或 159235 | sz159235 |
| 美股 | GOOGL | s_usGOOGL |
3.4.3 解析字段(多市场差异化索引)
腾讯行情各市场返回字段索引不同,必须根据市场前缀分别解析:
| 市场 | 前缀 | 名称索引 | 当前价索引 | 涨跌额索引 | 涨跌幅索引 |
|---|---|---|---|---|---|
| 美股 | s_us |
1 | 3 | 4 | 5 |
| 港股 | r_hk |
1 | 3 | 31 | 32 |
| A股(上海) | sh |
1 | 3 | 31 | 32 |
| A股(深圳) | sz |
1 | 3 | 31 | 32 |
返回数据示例:
# 美股 (s_us) - 涨跌额在索引4,涨跌幅在索引5
v_s_usGOOG="200~Alphabet-C~GOOG.OQ~315.72~-0.65~-0.21~..."
# 港股 (r_hk) - 涨跌额在索引31,涨跌幅在索引32
v_r_hk09868="100~小鹏集团-W~09868~67.600~...~2026/04/13 16:08:56~0.600~0.90~..."
# A股 (sh) - 涨跌额在索引31,涨跌幅在索引32
v_sh600690="1~海尔智家~600690~...~20260413161426~-0.14~-0.67~..."
异常处理策略:
- 当 Security 表中存在证券记录时,优先使用 Security 表中的名称
- 当 Security 表中无记录时,使用腾讯行情返回的名称(索引1)
- 当前价获取失败时,降级使用持仓的平均成本价(avgCost)
- 涨跌数据获取失败时,降级通过
当前价 - 昨收价计算 - GBK 中文解码:腾讯 API 返回 GBK 编码数据,必须使用
arrayBuffer() + TextDecoder('gbk')解码,禁止直接使用text()
3.5 持仓计算
3.5.1 成本计算 - 平均成本法
平均成本 = (Σ买入金额) / 总数量
= (Q1×P1 + Q2×P2 + ...) / (Q1 + Q2 + ...)
3.5.2 市值计算
市值 = 持仓数量 × 当前价格
3.5.3 盈亏计算
浮动盈亏 = 市值 - 成本基数
= (数量 × 当前价) - (数量 × 平均成本)
浮动盈亏率 = (浮动盈亏 / 成本基数) × 100%
3.5.4 货币转换
系统接入 JisuAPI (https://api.jisuapi.com/exchange/convert) 获取实时汇率。
汇率获取策略:
- 优先使用 JisuAPI 实时汇率
- 缓存周期:1 小时(防止 API 调用频率限制)
- 降级机制:JisuAPI 失败时使用默认固定汇率
默认固定汇率(降级用):
| 从货币 | 到 USD 汇率 |
|---|---|
| USD | 1 |
| CNY | 0.137 |
| HKD | 0.129 |
| USDT | 1 |
汇率转换公式:
目标货币金额 = 源货币金额 × 汇率
示例:1000 CNY × 0.137 = 137 USD
3.6 货币显示
3.6.1 显示货币选择
顶部导航栏提供显示货币切换器:
- CNY(人民币)
- USD(美元)
- HKD(港币)
3.6.2 持仓显示规则
持仓明细和分析页面根据市场显示对应货币:
- 港股持仓 → HKD
- A股持仓 → CNY
- 美股持仓 → USD
- 加密货币持仓 → USDT
3.6.3 总资产显示
总资产、总盈亏等汇总数据根据用户选择的显示货币进行转换:
显示金额 = USD金额 × 显示货币汇率
资产配置环形图联动:
- 环形图的市值数值、Tooltip 提示金额、图例金额均随全局显示货币(CNY/USD/HKD)实时联动转换
- 使用
useMemo缓存计算结果,避免重复渲染
3.7 数据导入导出
3.7.1 CSV 导出
支持导出:
- 交易记录(transactions)
- 持仓明细(positions)
导出字段:
- 交易记录:时间、类型、证券代码、数量、价格、金额、手续费、备注
- 持仓明细:证券代码、名称、市场、数量、成本价、当前价、市值、盈亏
3.7.2 CSV 导入
导入交易记录:
- 支持批量导入
- 自动验证数据格式
- 按账户批量创建交易
导入模板包含字段:
- type, symbol, quantity, price, amount, fee, currency, notes, executedAt
4. 页面与界面
4.1 顶部导航栏
┌─────────────────────────────────────────────────────────────────┐
│ [💰] 投资持仓管理 [CNY▼] [账户▼] [导入] [导出] [+记录交易] │
└─────────────────────────────────────────────────────────────────┘
功能:
- Logo 和标题
- 显示货币选择(下拉)
- 账户选择(下拉)
- 导入按钮
- 导出按钮
- 记录交易按钮
4.2 资产概览卡片
四个卡片并排显示:
- 总资产 - 渐变蓝背景,显示总市值和成本
- 浮动盈亏 - 渐变绿背景,显示盈亏金额和百分比
- 持仓市值 - 渐变紫背景,显示持仓数量和总市值
- 账户数量 - 渐变橙背景,显示市场数量
4.3 持仓分析卡片
展示前4个持仓的概览:
- 证券代码、图标和证券名称
- 市值(使用对应市场货币)
- 盈亏金额和百分比
4.3.1 证券名称显示
在以下位置均显示证券代码和对应名称:
| 位置 | 显示内容 |
|---|---|
| 持仓分析卡片 | 代码 + 名称(truncate) |
| 资产分布条形图 | 代码 + 名称(truncate) |
| 盈亏排行榜 | 代码 + 名称(truncate,最大宽度120px) |
证券名称从数据库 Security 表中获取,与腾讯行情接口解析的实时价格配合使用。
4.4 标签页
4.4.1 持仓明细标签页
表格列:证券 | 市场 | 数量 | 成本价 | 当前价 | 市值 | 盈亏 | 操作
功能:
- 按市场货币显示价格和市值
- 显示涨跌百分比
- 支持删除持仓(卖出全部)
4.4.2 交易流水标签页
表格列:时间 | 类型 | 证券(名称+代码) | 数量 | 价格 | 金额 | 账户 | 操作
证券列显示:
- 上行:证券名称(加粗)
- 下行:证券代码(灰色小字)
功能:
- 支持编辑交易记录
- 支持删除交易记录
- 支持导出 CSV
- 后端通过关联查询附加证券名称
4.4.3 分析标签页
包含两个卡片:
- 资产配置环形图 - 由后端实时聚合持仓市值数据,按市场(美股/港股/A股/加密)分组显示占比
- 盈亏排行 - 列表显示按盈亏排序的持仓
资产配置数据来源:
- 后端 API
/api/dashboard/analytics返回marketDistribution字段 - 数据结构:
{ name: string, value: number, percent: number } - 使用 Recharts
<PieChart>渲染环形图
4.5 对话框
4.5.1 记录交易对话框
字段:
- 账户选择
- 交易类型(买入/卖出/入金/出金/分红)
- 证券代码搜索(自动补全)
- 数量(买入/卖出时显示)
- 价格(买入/卖出时显示)
- 成交总额(自动计算)
- 手续费
- 交易时间
- 备注
- 确认按钮
4.5.2 交易确认对话框
显示交易摘要:
- 账户信息
- 交易类型和证券
- 成交总额
- 手续费
- 交易时间
4.5.3 编辑交易对话框
与记录交易类似,但预填充现有数据。
4.5.4 删除确认对话框
显示待删除项目详情,确认后执行删除。
5. API 规格
5.1 账户 API
GET /api/accounts
获取所有账户列表
Response:
{
"id": "string",
"name": "string",
"marketType": "US|CN|HK|CRYPTO",
"baseCurrency": "string",
"balance": "number"
}
5.2 持仓 API
GET /api/positions
获取持仓列表
Query: ?accountId=xxx
Response:
{
"symbol": "string",
"quantity": "string",
"averageCost": "string",
"currency": "string",
"accountName": "string",
"marketType": "string"
}
5.3 证券 API
GET /api/securities
获取证券列表
Query: ?market=US&search=AAPL
POST /api/securities
创建证券
Body:
{
"symbol": "string",
"name": "string",
"market": "US|CN|HK|CRYPTO",
"currency": "string",
"lotSize": 100,
"priceDecimals": 2,
"qtyDecimals": 0,
"isCrypto": false
}
5.4 交易 API
GET /api/transactions
获取交易流水
Query: ?accountId=xxx&page=1&limit=20
Response:
{
"data": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5
}
}
POST /api/transactions
创建交易
Body:
{
"accountId": "string",
"type": "BUY|SELL|DEPOSIT|WITHDRAW|DIVIDEND",
"symbol": "string|null",
"quantity": "number|null",
"price": "number|null",
"amount": "number",
"fee": "number",
"currency": "string",
"notes": "string|null",
"executedAt": "ISO8601"
}
PATCH /api/transactions
更新交易
Body:
{
"id": "string",
"type": "string",
"symbol": "string|null",
"quantity": "number|null",
"price": "number|null",
"amount": "number|null",
"fee": "number|null",
"currency": "string|null",
"notes": "string|null",
"executedAt": "string|null"
}
DELETE /api/transactions?id=xxx
删除交易
5.5 行情 API
GET /api/dashboard/analytics
获取持仓分析和汇总
Response:
{
"prices": {
"AAPL": { "price": 150.00, "change": 2.50, "changePercent": 1.69 }
},
"positions": [
{
"symbol": "AAPL",
"name": "Apple Inc.",
"marketType": "US",
"quantity": 10,
"avgCost": 145.00,
"currentPrice": 150.00,
"change": 2.50,
"changePercent": 1.69,
"costBasis": 1450.00,
"costBasisUSD": 1450.00,
"marketValue": 1500.00,
"marketValueUSD": 1500.00,
"pnl": 50.00,
"pnlPercent": 3.45,
"pnlUSD": 50.00,
"currency": "USD"
}
],
"summary": {
"totalCostBasis": 10000.00,
"totalMarketValue": 10500.00,
"totalPnL": 500.00,
"totalPnLPercent": 5.00,
"positionCount": 3
},
"byMarket": {
"US": { "totalCost": 5000, "totalValue": 5500, "totalPnL": 500 },
"CN": { "totalCost": 3000, "totalValue": 3000, "totalPnL": 0 }
}
}
6. 数据库模型
6.1 User(用户)
model User {
id String @id @default(cuid())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
}
6.2 Account(账户)
model Account {
id String @id @default(cuid())
userId String
user User @relation(...)
name String
marketType MarketType
baseCurrency String
balance Decimal @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
transactions Transaction[]
positions Position[]
}
enum MarketType {
US // 美股
CN // A股
HK // 港股
CRYPTO // 加密货币
}
6.3 Security(证券)
model Security {
id String @id @default(cuid())
symbol String @unique
name String
market MarketType
currency String
lotSize Int @default(1)
priceDecimals Int @default(2)
qtyDecimals Int @default(0)
isCrypto Boolean @default(false)
}
6.4 Transaction(交易流水)
model Transaction {
id String @id @default(cuid())
accountId String
account Account @relation(...)
type TransactionType
symbol String?
quantity Decimal?
price Decimal?
amount Decimal
fee Decimal @default(0)
networkFee Decimal?
currency String
exchangeRate Decimal?
notes String?
executedAt DateTime
}
enum TransactionType {
DEPOSIT // 入金
WITHDRAW // 出金
BUY // 买入
SELL // 卖出
DIVIDEND // 分红
INTEREST // 利息
FEE // 费用
}
6.5 Position(持仓)
model Position {
id String @id @default(cuid())
accountId String
account Account @relation(...)
symbol String
quantity Decimal
averageCost Decimal
currency String
updatedAt DateTime @updatedAt
@@unique([accountId, symbol])
}
6.6 ExchangeRate(汇率)
model ExchangeRate {
id String @id @default(cuid())
fromCurrency String
toCurrency String
rate Decimal
effectiveDate DateTime
@@unique([fromCurrency, toCurrency, effectiveDate])
}
7. 汇率配置
7.1 初始汇率(以 USD 为基准)
| 从货币 | 到 USD 汇率 |
|---|---|
| USD | 1.000 |
| CNY | 0.137 |
| HKD | 0.129 |
| USDT | 1.000 |
7.2 货币转换公式
amountInTargetCurrency = amountInUSD × targetCurrencyRate
示例:
100 USD → CNY = 100 × 7.24 = 724 CNY
100 HKD → USD = 100 × 0.129 = 12.9 USD
8. 非功能性需求
8.1 性能
- 页面加载时间 < 2秒
- API 响应时间 < 500ms
- 股价缓存时间 60 秒
8.2 兼容性
- 支持 Chrome、Firefox、Safari、Edge 最新版本
- 响应式设计,支持桌面和移动端
8.3 数据安全
- 所有敏感配置通过环境变量管理
- 数据库连接使用密码认证
9. 版本历史
| 版本 | 日期 | 说明 |
|---|---|---|
| v1.0.0 | 2026-04-12 | 初始版本,完成核心功能 |
10. 附录
10.1 环境变量
DATABASE_URL="postgresql://user:password@host:5432/database"
ALPHA_VANTAGE_API_KEY="your_api_key"
10.2 腾讯行情接口示例
港股(小鹏汽车):
GET https://qt.gtimg.cn/q=r_hk09868
Response: v_r_hk09868="100~小鹏汽车-W~09868~67.000~66.950~..."
A股(海尔智家):
GET https://qt.gtimg.cn/q=sh600690
Response: v_sh600690="1~海尔智家~600690~20.88~20.75~20.79~..."
美股(Google):
GET https://qt.gtimg.cn/q=s_usGOOG
Response: v_s_usGOOG="200~Alphabet-C~GOOG.OQ~315.72~-0.65~-0.21~..."
10.3 批量查询示例
GET https://qt.gtimg.cn/q=r_hk09868,sh600690,sz159235,s_usGOOG