- 📛 证券名称增强:修复 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,防止浮点精度丢失
173 lines
5.3 KiB
TypeScript
173 lines
5.3 KiB
TypeScript
import { Prisma } from '@prisma/client'
|
|
|
|
const mockPositionFindMany = jest.fn()
|
|
const mockSecurityFindMany = jest.fn()
|
|
const mockExchangeRateFindMany = jest.fn()
|
|
|
|
global.fetch = jest.fn()
|
|
|
|
jest.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
position: {
|
|
findMany: (...args: any[]) => mockPositionFindMany(...args),
|
|
},
|
|
security: {
|
|
findMany: (...args: any[]) => mockSecurityFindMany(...args),
|
|
},
|
|
exchangeRate: {
|
|
findMany: (...args: any[]) => mockExchangeRateFindMany(...args),
|
|
},
|
|
},
|
|
}))
|
|
|
|
import { GET } from '../src/app/api/dashboard/analytics/route'
|
|
|
|
describe('Dashboard Analytics API - GET', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
;(global.fetch as jest.Mock).mockReset()
|
|
})
|
|
|
|
it('无持仓返回空prices和quotes', async () => {
|
|
mockPositionFindMany.mockResolvedValue([])
|
|
mockExchangeRateFindMany.mockResolvedValue([])
|
|
|
|
const req = new Request('http://localhost/api/dashboard/analytics')
|
|
const res = await GET(req)
|
|
|
|
expect(res.status).toBe(200)
|
|
const data = await res.json()
|
|
expect(data.prices).toEqual({})
|
|
expect(data.quotes).toEqual({})
|
|
})
|
|
|
|
it('计算持仓盈亏 - 盈利场景', async () => {
|
|
const mockPositions = [
|
|
{
|
|
id: 'pos-1',
|
|
accountId: 'acc-1',
|
|
symbol: 'AAPL',
|
|
quantity: new Prisma.Decimal('10'),
|
|
averageCost: new Prisma.Decimal('150'),
|
|
currency: 'USD',
|
|
account: { name: '美股', marketType: 'US' as const, baseCurrency: 'USD' },
|
|
},
|
|
]
|
|
|
|
const mockSecurities = [
|
|
{ symbol: 'AAPL', name: 'Apple Inc', isCrypto: false },
|
|
]
|
|
|
|
mockPositionFindMany.mockResolvedValue(mockPositions)
|
|
mockSecurityFindMany.mockResolvedValue(mockSecurities)
|
|
mockExchangeRateFindMany.mockResolvedValue([])
|
|
;(global.fetch as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
text: () => Promise.resolve('v_xxx="100~Apple Inc~AAPL~180.00~150.00~..."'),
|
|
})
|
|
|
|
const req = new Request('http://localhost/api/dashboard/analytics')
|
|
const res = await GET(req)
|
|
|
|
expect(res.status).toBe(200)
|
|
const data = await res.json()
|
|
|
|
expect(data.positions).toHaveLength(1)
|
|
expect(data.positions[0].pnl).toBe(300)
|
|
expect(data.positions[0].pnlPercent).toBe(20)
|
|
expect(data.positions[0].marketValue).toBe(1800)
|
|
expect(data.positions[0].costBasis).toBe(1500)
|
|
})
|
|
|
|
it('计算持仓盈亏 - 亏损场景', async () => {
|
|
const mockPositions = [
|
|
{
|
|
id: 'pos-1',
|
|
accountId: 'acc-1',
|
|
symbol: 'AAPL',
|
|
quantity: new Prisma.Decimal('10'),
|
|
averageCost: new Prisma.Decimal('200'),
|
|
currency: 'USD',
|
|
account: { name: '美股', marketType: 'US' as const, baseCurrency: 'USD' },
|
|
},
|
|
]
|
|
|
|
const mockSecurities = [{ symbol: 'AAPL', name: 'Apple Inc', isCrypto: false }]
|
|
|
|
mockPositionFindMany.mockResolvedValue(mockPositions)
|
|
mockSecurityFindMany.mockResolvedValue(mockSecurities)
|
|
mockExchangeRateFindMany.mockResolvedValue([])
|
|
;(global.fetch as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
text: () => Promise.resolve('v_xxx="100~Apple Inc~AAPL~150.00~200.00~..."'),
|
|
})
|
|
|
|
const req = new Request('http://localhost/api/dashboard/analytics')
|
|
const res = await GET(req)
|
|
|
|
expect(res.status).toBe(200)
|
|
const data = await res.json()
|
|
|
|
expect(data.positions[0].pnl).toBe(-500)
|
|
expect(data.positions[0].pnlPercent).toBe(-25)
|
|
})
|
|
|
|
it('除以零防护 - 零成本基准', async () => {
|
|
const mockPositions = [
|
|
{
|
|
id: 'pos-1',
|
|
accountId: 'acc-1',
|
|
symbol: 'AAPL',
|
|
quantity: new Prisma.Decimal('0'),
|
|
averageCost: new Prisma.Decimal('0'),
|
|
currency: 'USD',
|
|
account: { name: '美股', marketType: 'US' as const, baseCurrency: 'USD' },
|
|
},
|
|
]
|
|
|
|
const mockSecurities = [{ symbol: 'AAPL', name: 'Apple Inc', isCrypto: false }]
|
|
|
|
mockPositionFindMany.mockResolvedValue(mockPositions)
|
|
mockSecurityFindMany.mockResolvedValue(mockSecurities)
|
|
mockExchangeRateFindMany.mockResolvedValue([])
|
|
;(global.fetch as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
text: () => Promise.resolve('v_xxx="100~Apple Inc~AAPL~150.00~0.00~..."'),
|
|
})
|
|
|
|
const req = new Request('http://localhost/api/dashboard/analytics')
|
|
const res = await GET(req)
|
|
|
|
expect(res.status).toBe(200)
|
|
const data = await res.json()
|
|
|
|
expect(data.positions[0].pnlPercent).toBe(0)
|
|
expect(data.summary.totalPnLPercent).toBe(0)
|
|
})
|
|
|
|
it('腾讯行情API失败时降级到成本价', async () => {
|
|
const mockPositions = [
|
|
{
|
|
id: 'pos-1',
|
|
accountId: 'acc-1',
|
|
symbol: 'AAPL',
|
|
quantity: new Prisma.Decimal('10'),
|
|
averageCost: new Prisma.Decimal('150'),
|
|
currency: 'USD',
|
|
account: { name: '美股', marketType: 'US' as const, baseCurrency: 'USD' },
|
|
},
|
|
]
|
|
|
|
mockPositionFindMany.mockResolvedValue(mockPositions)
|
|
mockSecurityFindMany.mockResolvedValue([{ symbol: 'AAPL', name: 'Apple Inc', isCrypto: false }])
|
|
mockExchangeRateFindMany.mockResolvedValue([])
|
|
;(global.fetch as jest.Mock).mockRejectedValue(new Error('Network error'))
|
|
|
|
const req = new Request('http://localhost/api/dashboard/analytics')
|
|
const res = await GET(req)
|
|
|
|
expect(res.status).toBe(200)
|
|
const data = await res.json()
|
|
expect(data.prices).toEqual({})
|
|
})
|
|
}) |