fix: 修复 CMPP 协议字段容量与版本兼容性
CSS quality / css-quality (push) Has been cancelled

This commit is contained in:
hectorzhao
2026-09-20 15:49:37 +08:00
parent b24cd7c08d
commit 001d5f2cbd
37 changed files with 1933 additions and 295 deletions
@@ -0,0 +1,10 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { map } from 'rxjs/operators';
import { protocolFieldsToJson } from './protocol-uint32';
@Injectable()
export class ProtocolFieldsInterceptor implements NestInterceptor {
intercept(_context: ExecutionContext, next: CallHandler) {
return next.handle().pipe(map(protocolFieldsToJson));
}
}
+40
View File
@@ -0,0 +1,40 @@
import {
protocolFieldsToJson,
protocolUint32,
protocolUint32FromDb,
protocolUint32ToDb,
parseProtocolSequence,
} from './protocol-uint32';
describe('CMPP unsigned protocol fields', () => {
it.each([0, 2147483647, 2147483648, 4294967295])(
'round trips %s without changing the JSON number contract',
(value) => {
expect(protocolUint32FromDb(protocolUint32ToDb(value))).toBe(value);
expect(
JSON.parse(
JSON.stringify(protocolFieldsToJson({ rows: [{ sequenceId: BigInt(value), ackResult: BigInt(value) }] })),
),
).toEqual({ rows: [{ sequenceId: value, ackResult: value }] });
},
);
it.each([-1, 4294967296, 1.5, NaN, Infinity, '', '0', ' ', {}, true])('rejects invalid wire value %s', (value) => {
expect(() => protocolUint32(value)).toThrow();
expect(() => protocolUint32ToDb(value)).toThrow();
});
it('preserves optional historical nulls and unrelated serializers', () => {
expect(protocolUint32ToDb(null)).toBeUndefined();
expect(protocolUint32FromDb(null)).toBeUndefined();
const date = new Date();
expect(
protocolFieldsToJson({ date, money: 10000n, sequenceId: null, gatewayMessageId: '18446744073709551615' }),
).toEqual({ date, money: 10000n, sequenceId: null, gatewayMessageId: '18446744073709551615' });
expect(() => protocolUint32FromDb(4294967296n)).toThrow();
});
it('distinguishes text zero from missing or malformed historical sequences', () => {
for (const value of [null, undefined, '', ' ', '-1', '1.5', '1e2', '4294967296'])
expect(parseProtocolSequence(value)).toBeUndefined();
expect(parseProtocolSequence('0')).toBe(0);
expect(parseProtocolSequence('4294967295')).toBe(4294967295);
});
});
+39
View File
@@ -0,0 +1,39 @@
import { BadRequestException } from '@nestjs/common';
/** Protocol integers are exact JS numbers on the wire and bigint in PostgreSQL. */
export function protocolUint32(value: unknown, field = 'sequenceId'): number {
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > 0xffffffff) {
throw new BadRequestException(`${field} must be an unsigned 32-bit integer`);
}
return value;
}
export function protocolUint32ToDb(value: unknown, field = 'sequenceId'): bigint | undefined {
return value == null ? undefined : BigInt(protocolUint32(value, field));
}
export function protocolUint32FromDb(value: bigint | number | null | undefined): number | undefined {
if (value == null) return undefined;
return protocolUint32(typeof value === 'bigint' ? Number(value) : value);
}
/** Historical Submit sequence columns are text; blanks must never become zero. */
export function parseProtocolSequence(value: string | null | undefined): number | undefined {
if (value == null || !/^\d+$/.test(value)) return undefined;
const number = Number(value);
return Number.isInteger(number) && number <= 0xffffffff ? number : undefined;
}
/** Only protocol fields are converted, leaving money and dates to their existing serializers. */
export function protocolFieldsToJson(value: unknown): unknown {
if (Array.isArray(value)) return value.map(protocolFieldsToJson);
if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype) return value;
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [
key,
(key === 'sequenceId' || key === 'ackResult') && typeof item === 'bigint'
? protocolUint32FromDb(item)
: protocolFieldsToJson(item),
]),
);
}