feat(GOV-009): 建立后端契约与健康检查

This commit is contained in:
2026-08-13 17:26:51 +08:00
parent 3fc703de0b
commit 420aa22342
32 changed files with 1625 additions and 8 deletions
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import { createMessageEnvelope } from './message-envelope';
describe('RocketMQ 消息信封', () => {
it('规范化并冻结版本 1 消息', () => {
const envelope = createMessageEnvelope({
version: 1,
eventType: 'market.snapshot.updated',
idempotencyKey: 'snapshot:2026-08-13T09:30:00+08:00',
occurredAt: '2026-08-13T01:30:00.000Z',
payload: { market: 'A_SHARE' },
});
expect(envelope).toEqual({
version: 1,
eventType: 'market.snapshot.updated',
idempotencyKey: 'snapshot:2026-08-13T09:30:00+08:00',
occurredAt: '2026-08-13T01:30:00.000Z',
payload: { market: 'A_SHARE' },
});
expect(Object.isFrozen(envelope)).toBe(true);
expect(Object.isFrozen(envelope.payload)).toBe(true);
});
it.each([
[{ version: 2 }, /version/],
[{ eventType: ' ' }, /eventType/],
[{ idempotencyKey: '' }, /idempotencyKey/],
[{ occurredAt: 'not-a-date' }, /occurredAt/],
])('拒绝非法信封字段 %#', (override, expectedError) => {
expect(() =>
createMessageEnvelope({
version: 1,
eventType: 'market.snapshot.updated',
idempotencyKey: 'snapshot:1',
occurredAt: '2026-08-13T01:30:00.000Z',
payload: { market: 'A_SHARE' },
...override,
}),
).toThrow(expectedError);
});
});
@@ -0,0 +1,39 @@
export interface MessageEnvelope<TPayload> {
version: 1;
eventType: string;
idempotencyKey: string;
occurredAt: string;
payload: TPayload;
}
type MessageEnvelopeInput<TPayload> = Omit<MessageEnvelope<TPayload>, 'version'> & {
version: number;
};
function deepFreeze<TValue>(value: TValue): Readonly<TValue> {
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child);
Object.freeze(value);
}
return value;
}
export function createMessageEnvelope<TPayload>(
input: MessageEnvelopeInput<TPayload>,
): Readonly<MessageEnvelope<TPayload>> {
if (input.version !== 1) throw new Error('version 只支持 1');
if (!input.eventType.trim()) throw new Error('eventType 不能为空');
if (!input.idempotencyKey.trim()) throw new Error('idempotencyKey 不能为空');
if (!input.occurredAt.trim() || Number.isNaN(Date.parse(input.occurredAt))) {
throw new Error('occurredAt 必须为合法 ISO 时间');
}
if (input.payload === undefined) throw new Error('payload 不能为空');
return deepFreeze({
version: 1,
eventType: input.eventType.trim(),
idempotencyKey: input.idempotencyKey.trim(),
occurredAt: new Date(input.occurredAt).toISOString(),
payload: input.payload,
}) as Readonly<MessageEnvelope<TPayload>>;
}