feat(FEAT-MO-001): 完成TDX市场概览首屏

This commit is contained in:
2026-08-18 15:28:35 +08:00
parent 27bc3c198f
commit 82619489a9
38 changed files with 1230 additions and 54 deletions
@@ -26,9 +26,16 @@ describe('市场概览服务', () => {
const response = await service.getIndices();
expect(response).toMatchObject({ source: 'tdx', status: 'fresh', data: { items: expect.any(Array) } });
expect(response).toMatchObject({
source: 'tdx',
status: 'fresh',
data: { items: expect.any(Array) },
});
expect(response.snapshotId).toMatch(/^20260818-/);
expect(client.request).toHaveBeenCalledWith('get_market_data', expect.objectContaining({ count: 5 }));
expect(client.request).toHaveBeenCalledWith(
'get_market_data',
expect.objectContaining({ count: 5 }),
);
});
it('对未验证的市场宽度能力返回不可用而不是伪造数据', async () => {
@@ -23,7 +23,10 @@ export type MarketOverviewResponse<TData> = {
warnings: string[];
};
function createSnapshotMeta(): Pick<MarketOverviewResponse<never>, 'snapshotId' | 'asOf' | 'source'> {
function createSnapshotMeta(): Pick<
MarketOverviewResponse<never>,
'snapshotId' | 'asOf' | 'source'
> {
const asOf = new Date().toISOString();
const snapshotId = `${asOf.slice(0, 10).replaceAll('-', '')}-${asOf.slice(11, 19).replaceAll(':', '')}`;
return { snapshotId, asOf, source: 'tdx' };
@@ -76,9 +79,15 @@ export class MarketOverviewService {
const averageChange = changes.length
? Math.round((changes.reduce((sum, value) => sum + value, 0) / changes.length) * 100) / 100
: null;
const marketStatus = averageChange === null ? '指数数据不完整' : averageChange >= 0 ? '指数偏强' : '指数偏弱';
const marketStatus =
averageChange === null ? '指数数据不完整' : averageChange >= 0 ? '指数偏强' : '指数偏弱';
const summary: SummaryDataDto = {
verdict: averageChange === null ? '市场状态待确认' : averageChange >= 0 ? '市场维持偏强震荡' : '市场维持偏弱震荡',
verdict:
averageChange === null
? '市场状态待确认'
: averageChange >= 0
? '市场维持偏强震荡'
: '市场维持偏弱震荡',
marketStatus,
volumeStatus: '成交量口径待接入',
sentimentStatus: '市场情绪口径待接入',
@@ -12,19 +12,21 @@ function response(body: unknown, init?: ResponseInit): Response {
describe('TDX 客户端', () => {
it('发送固定 JSON 请求并返回业务结果', async () => {
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
response({ id: 1, result: { ErrorId: '0', Value: {} } }),
);
const fetchImpl = vi
.fn<typeof fetch>()
.mockResolvedValue(response({ id: 1, result: { ErrorId: '0', Value: {} } }));
const client = createTdxClient({
baseUrl: 'http://127.0.0.1:17710',
timeoutMs: 5_000,
fetchImpl,
});
await expect(client.request('get_market_data', { stock_list: ['600519.SH'] })).resolves.toEqual({
ErrorId: '0',
Value: {},
});
await expect(client.request('get_market_data', { stock_list: ['600519.SH'] })).resolves.toEqual(
{
ErrorId: '0',
Value: {},
},
);
expect(fetchImpl).toHaveBeenCalledWith(
'http://127.0.0.1:17710/',
@@ -42,9 +44,11 @@ describe('TDX 客户端', () => {
});
it('将 TDX 错误响应转换为脱敏协议错误', async () => {
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(
response({ id: 1, error: { code: 'bad_request', message: '内部连接信息' } }),
);
const fetchImpl = vi
.fn<typeof fetch>()
.mockResolvedValue(
response({ id: 1, error: { code: 'bad_request', message: '内部连接信息' } }),
);
const client = createTdxClient({
baseUrl: 'http://127.0.0.1:17710',
timeoutMs: 5_000,
@@ -60,7 +64,9 @@ describe('TDX 客户端', () => {
});
it('将 AbortError 转换为超时错误', async () => {
const fetchImpl = vi.fn<typeof fetch>().mockRejectedValue(new DOMException('aborted', 'AbortError'));
const fetchImpl = vi
.fn<typeof fetch>()
.mockRejectedValue(new DOMException('aborted', 'AbortError'));
const client = createTdxClient({
baseUrl: 'http://127.0.0.1:17710',
timeoutMs: 50,
@@ -4,7 +4,10 @@ import request from 'supertest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { MarketOverviewController } from '../src/market-overview/market-overview.controller';
import { MarketOverviewService, TDX_CLIENT_PORT } from '../src/market-overview/market-overview.service';
import {
MarketOverviewService,
TDX_CLIENT_PORT,
} from '../src/market-overview/market-overview.service';
describe('市场概览 HTTP API', () => {
let app: INestApplication | undefined;
@@ -57,9 +60,15 @@ describe('市场概览 HTTP API', () => {
app.setGlobalPrefix('api/v1');
await app.init();
const response = await request(app.getHttpServer()).get('/api/v1/market-overview/indices').expect(200);
const response = await request(app.getHttpServer())
.get('/api/v1/market-overview/indices')
.expect(200);
expect(response.body).toMatchObject({ source: 'tdx', status: 'fresh', data: { items: expect.any(Array) } });
expect(response.body).toMatchObject({
source: 'tdx',
status: 'fresh',
data: { items: expect.any(Array) },
});
expect(JSON.stringify(response.body)).not.toContain('127.0.0.1');
});
@@ -75,7 +84,9 @@ describe('市场概览 HTTP API', () => {
app.setGlobalPrefix('api/v1');
await app.init();
const response = await request(app.getHttpServer()).get('/api/v1/market-overview/sectors').expect(200);
const response = await request(app.getHttpServer())
.get('/api/v1/market-overview/sectors')
.expect(200);
expect(response.body).toMatchObject({ source: 'tdx', status: 'unavailable', data: null });
expect(response.body.warnings[0]).toContain('板块排行');
+4 -1
View File
@@ -14,7 +14,10 @@ import {
MYSQL_HEALTH_PORT,
REDIS_HEALTH_PORT,
} from '../apps/api/src/health.service';
import { MarketOverviewService, TDX_CLIENT_PORT } from '../apps/api/src/market-overview/market-overview.service';
import {
MarketOverviewService,
TDX_CLIENT_PORT,
} from '../apps/api/src/market-overview/market-overview.service';
import { DisabledHealthClient } from '../libs/infrastructure/src/disabled-health.client';
@Module({
+3
View File
@@ -46,6 +46,9 @@ export async function runTestApi() {
LOG_LEVEL: 'error',
API_HOST: '127.0.0.1',
API_PORT: '3000',
// API 配置要求始终声明本机 TDX 边界;E2E 不依赖 TDX 可达性,页面会验证不可用状态。
TDX_BASE_URL: 'http://127.0.0.1:17710',
TDX_TIMEOUT_MS: '5000',
REDIS_HOST: '127.0.0.1',
REDIS_PORT: '6379',
REDIS_USERNAME: '',