feat(GOV-009): 建立前端 OpenAPI 客户端边界

This commit is contained in:
2026-08-13 17:36:55 +08:00
parent 420aa22342
commit 4665336a63
11 changed files with 691 additions and 27 deletions
@@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest';
import type { components } from '../generated/schema';
import { toHealthViewModel } from './health.adapter';
type HealthResponseDto = components['schemas']['HealthResponseDto'];
const healthyDto: HealthResponseDto = {
status: 'healthy',
process: 'api',
checkedAt: '2026-08-13T09:30:00.000Z',
dependencies: {
mysql: { status: 'healthy', detail: 'MySQL 5.7 ready' },
redis: { status: 'healthy', detail: 'PONG' },
rocketMq: { status: 'healthy', detail: 'RocketMQ ready' },
},
};
describe('健康响应页面适配器', () => {
it('把全部可用的传输状态映射为健康页面模型', () => {
expect(toHealthViewModel(healthyDto)).toEqual({
state: 'healthy',
stateLabel: '运行正常',
checkedAt: new Date('2026-08-13T09:30:00.000Z'),
services: [
{ key: 'redis', name: 'Redis', available: true, note: 'PONG' },
{ key: 'mysql', name: 'MySQL', available: true, note: 'MySQL 5.7 ready' },
{ key: 'rocketMq', name: 'RocketMQ', available: true, note: 'RocketMQ ready' },
],
});
});
it('把可选能力停用映射为降级而不是后端枚举', () => {
const dto: HealthResponseDto = {
...healthyDto,
dependencies: {
...healthyDto.dependencies,
rocketMq: { status: 'disabled', detail: '当前版本未启用' },
},
};
const viewModel = toHealthViewModel(dto);
expect(viewModel.state).toBe('degraded');
expect(viewModel.stateLabel).toBe('部分能力停用');
expect(viewModel.services[2]).toEqual({
key: 'rocketMq',
name: 'RocketMQ',
available: false,
note: '当前版本未启用',
});
});
it('把任一依赖不可用映射为不可用', () => {
const dto: HealthResponseDto = {
...healthyDto,
status: 'unavailable',
dependencies: {
...healthyDto.dependencies,
redis: { status: 'unavailable', detail: '检查失败' },
},
};
expect(toHealthViewModel(dto).state).toBe('unavailable');
expect(toHealthViewModel(dto).stateLabel).toBe('服务异常');
});
it('拒绝运行时出现的未知依赖状态', () => {
const malformedDto = structuredClone(healthyDto) as unknown as {
dependencies: { redis: { status: string } };
};
malformedDto.dependencies.redis.status = 'unknown';
expect(() => toHealthViewModel(malformedDto as never)).toThrow(/未知依赖状态/);
});
});
+72
View File
@@ -0,0 +1,72 @@
import type { components } from '../generated/schema';
type HealthResponseDto = components['schemas']['HealthResponseDto'];
type DependencyHealthDto = components['schemas']['DependencyHealthDto'];
export type HealthViewState = 'healthy' | 'degraded' | 'unavailable';
export interface HealthServiceViewModel {
key: 'redis' | 'mysql' | 'rocketMq';
name: 'Redis' | 'MySQL' | 'RocketMQ';
available: boolean;
note: string;
}
export interface HealthViewModel {
state: HealthViewState;
stateLabel: '运行正常' | '部分能力停用' | '服务异常';
checkedAt: Date;
services: HealthServiceViewModel[];
}
function assertDependencyStatus(status: string): asserts status is DependencyHealthDto['status'] {
if (status !== 'healthy' && status !== 'unavailable' && status !== 'disabled') {
throw new Error(`未知依赖状态:${status}`);
}
}
function toServiceViewModel(
key: HealthServiceViewModel['key'],
name: HealthServiceViewModel['name'],
dependency: DependencyHealthDto,
): HealthServiceViewModel {
assertDependencyStatus(dependency.status);
return {
key,
name,
available: dependency.status === 'healthy',
note: dependency.detail,
};
}
export function toHealthViewModel(dto: HealthResponseDto): HealthViewModel {
const dependencies = Object.values(dto.dependencies);
for (const dependency of dependencies) assertDependencyStatus(dependency.status);
const hasUnavailable = dependencies.some((dependency) => dependency.status === 'unavailable');
const hasDisabled = dependencies.some((dependency) => dependency.status === 'disabled');
const state: HealthViewState =
dto.status === 'unavailable' || hasUnavailable
? 'unavailable'
: hasDisabled
? 'degraded'
: 'healthy';
const stateLabel = {
healthy: '运行正常',
degraded: '部分能力停用',
unavailable: '服务异常',
} as const;
// 页面只消费稳定的展示模型,后端枚举和依赖对象结构不会直接扩散到 Vue 组件。
return {
state,
stateLabel: stateLabel[state],
checkedAt: new Date(dto.checkedAt),
services: [
toServiceViewModel('redis', 'Redis', dto.dependencies.redis),
toServiceViewModel('mysql', 'MySQL', dto.dependencies.mysql),
toServiceViewModel('rocketMq', 'RocketMQ', dto.dependencies.rocketMq),
],
};
}