fix(GOV-009): 完成脚手架审查整改
This commit is contained in:
@@ -3,20 +3,26 @@ import { describe, expect, it } from 'vitest';
|
||||
import { createMessageEnvelope } from './message-envelope';
|
||||
|
||||
describe('RocketMQ 消息信封', () => {
|
||||
it('规范化并冻结版本 1 消息', () => {
|
||||
it('规范化并冻结带完整追溯字段的版本 1 消息', () => {
|
||||
const envelope = createMessageEnvelope({
|
||||
version: 1,
|
||||
eventId: 'event-20260813-001',
|
||||
eventType: 'market.snapshot.updated',
|
||||
schemaVersion: 1,
|
||||
idempotencyKey: 'snapshot:2026-08-13T09:30:00+08:00',
|
||||
occurredAt: '2026-08-13T01:30:00.000Z',
|
||||
tradeDate: '2026-08-13',
|
||||
traceId: 'trace-20260813-001',
|
||||
payload: { market: 'A_SHARE' },
|
||||
});
|
||||
|
||||
expect(envelope).toEqual({
|
||||
version: 1,
|
||||
eventId: 'event-20260813-001',
|
||||
eventType: 'market.snapshot.updated',
|
||||
schemaVersion: 1,
|
||||
idempotencyKey: 'snapshot:2026-08-13T09:30:00+08:00',
|
||||
occurredAt: '2026-08-13T01:30:00.000Z',
|
||||
tradeDate: '2026-08-13',
|
||||
traceId: 'trace-20260813-001',
|
||||
payload: { market: 'A_SHARE' },
|
||||
});
|
||||
expect(Object.isFrozen(envelope)).toBe(true);
|
||||
@@ -24,17 +30,23 @@ describe('RocketMQ 消息信封', () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ version: 2 }, /version/],
|
||||
[{ eventId: '' }, /eventId/],
|
||||
[{ eventType: ' ' }, /eventType/],
|
||||
[{ schemaVersion: 2 }, /schemaVersion/],
|
||||
[{ idempotencyKey: '' }, /idempotencyKey/],
|
||||
[{ occurredAt: 'not-a-date' }, /occurredAt/],
|
||||
[{ tradeDate: '2026-02-30' }, /tradeDate/],
|
||||
[{ traceId: ' ' }, /traceId/],
|
||||
])('拒绝非法信封字段 %#', (override, expectedError) => {
|
||||
expect(() =>
|
||||
createMessageEnvelope({
|
||||
version: 1,
|
||||
eventId: 'event-1',
|
||||
eventType: 'market.snapshot.updated',
|
||||
schemaVersion: 1,
|
||||
idempotencyKey: 'snapshot:1',
|
||||
occurredAt: '2026-08-13T01:30:00.000Z',
|
||||
tradeDate: '2026-08-13',
|
||||
traceId: 'trace-1',
|
||||
payload: { market: 'A_SHARE' },
|
||||
...override,
|
||||
}),
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
/** Server 内部消息统一信封;字段同时满足幂等、版本演进和跨进程链路追踪。 */
|
||||
export interface MessageEnvelope<TPayload> {
|
||||
version: 1;
|
||||
eventId: string;
|
||||
eventType: string;
|
||||
schemaVersion: 1;
|
||||
idempotencyKey: string;
|
||||
occurredAt: string;
|
||||
tradeDate: string;
|
||||
traceId: string;
|
||||
payload: TPayload;
|
||||
}
|
||||
|
||||
type MessageEnvelopeInput<TPayload> = Omit<MessageEnvelope<TPayload>, 'version'> & {
|
||||
version: number;
|
||||
type MessageEnvelopeInput<TPayload> = Omit<MessageEnvelope<TPayload>, 'schemaVersion'> & {
|
||||
schemaVersion: number;
|
||||
};
|
||||
|
||||
function deepFreeze<TValue>(value: TValue): Readonly<TValue> {
|
||||
@@ -18,22 +22,36 @@ function deepFreeze<TValue>(value: TValue): Readonly<TValue> {
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 校验、规范化并深冻结消息信封,防止投递后字段被异步处理链路改写。 */
|
||||
export function createMessageEnvelope<TPayload>(
|
||||
input: MessageEnvelopeInput<TPayload>,
|
||||
): Readonly<MessageEnvelope<TPayload>> {
|
||||
if (input.version !== 1) throw new Error('version 只支持 1');
|
||||
if (!input.eventId.trim()) throw new Error('eventId 不能为空');
|
||||
if (!input.eventType.trim()) throw new Error('eventType 不能为空');
|
||||
if (input.schemaVersion !== 1) throw new Error('schemaVersion 只支持 1');
|
||||
if (!input.idempotencyKey.trim()) throw new Error('idempotencyKey 不能为空');
|
||||
if (!input.occurredAt.trim() || Number.isNaN(Date.parse(input.occurredAt))) {
|
||||
throw new Error('occurredAt 必须为合法 ISO 时间');
|
||||
}
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(input.tradeDate)) {
|
||||
throw new Error('tradeDate 必须为合法 YYYY-MM-DD 日期');
|
||||
}
|
||||
const [year, month, day] = input.tradeDate.split('-').map(Number);
|
||||
const parsedTradeDate = new Date(Date.UTC(year, month - 1, day));
|
||||
if (parsedTradeDate.toISOString().slice(0, 10) !== input.tradeDate) {
|
||||
throw new Error('tradeDate 必须为合法 YYYY-MM-DD 日期');
|
||||
}
|
||||
if (!input.traceId.trim()) throw new Error('traceId 不能为空');
|
||||
if (input.payload === undefined) throw new Error('payload 不能为空');
|
||||
|
||||
return deepFreeze({
|
||||
version: 1,
|
||||
eventId: input.eventId.trim(),
|
||||
eventType: input.eventType.trim(),
|
||||
schemaVersion: 1,
|
||||
idempotencyKey: input.idempotencyKey.trim(),
|
||||
occurredAt: new Date(input.occurredAt).toISOString(),
|
||||
tradeDate: input.tradeDate,
|
||||
traceId: input.traceId.trim(),
|
||||
payload: input.payload,
|
||||
}) as Readonly<MessageEnvelope<TPayload>>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user