perf(cmpp): add durable inbound fast path
This commit is contained in:
@@ -80,6 +80,7 @@ export class PhoneFrequencyService {
|
||||
phones: string[],
|
||||
sourceType?: string,
|
||||
requestedAt = new Date(),
|
||||
reservationKey?: string,
|
||||
) {
|
||||
if (!applicationId) return new Map<string, PhoneFrequencyRejection>();
|
||||
const normalizedPhones = [...new Set(phones.map((phone) => phone.trim()).filter(Boolean))].sort();
|
||||
@@ -87,14 +88,35 @@ export class PhoneFrequencyService {
|
||||
|
||||
await this.riskReview.ensureDefaultRules();
|
||||
const rules = await this.effectiveRules(applicationId);
|
||||
if (rules.length === 0) return new Map<string, PhoneFrequencyRejection>();
|
||||
const normalizedReservationKey = reservationKey?.trim();
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
if (normalizedReservationKey) {
|
||||
// Frequency counters and the reservation result commit together. Retrying a reclaimed
|
||||
// Inbox item therefore returns the original decision without incrementing either window.
|
||||
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${'phone-frequency:' + normalizedReservationKey}, 0))`;
|
||||
const existingReservation = await tx.phoneFrequencyReservation.findUnique({
|
||||
where: { reservationKey: normalizedReservationKey },
|
||||
});
|
||||
if (existingReservation) {
|
||||
if (existingReservation.tenantId !== tenantId || existingReservation.applicationId !== applicationId) {
|
||||
throw new BadRequestException('号码频控幂等键已用于另一笔预留');
|
||||
}
|
||||
return frequencyRejectionsFromJson(existingReservation.result);
|
||||
}
|
||||
}
|
||||
const rejected = new Map<string, PhoneFrequencyRejection>();
|
||||
// 平台级白名单只截断号码频控链路;调用 reserve 之前已执行的格式、黑名单等校验不受影响。
|
||||
const whitelistedPhones = await this.findActiveWhitelistedPhones(tx, normalizedPhones);
|
||||
const controlledPhones = normalizedPhones.filter((phone) => !whitelistedPhones.has(phone));
|
||||
if (controlledPhones.length === 0) return rejected;
|
||||
if (controlledPhones.length === 0 || rules.length === 0) {
|
||||
if (normalizedReservationKey) {
|
||||
await tx.phoneFrequencyReservation.create({
|
||||
data: { reservationKey: normalizedReservationKey, tenantId, applicationId, result: [] },
|
||||
});
|
||||
}
|
||||
return rejected;
|
||||
}
|
||||
for (const rule of rules) {
|
||||
const window = fixedShanghaiWindow(requestedAt, readPeriodSeconds(rule));
|
||||
// 分块限制 SQL 参数数量,但两条规则的全部分块仍在同一事务中提交或回滚。
|
||||
@@ -144,6 +166,16 @@ export class PhoneFrequencyService {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (normalizedReservationKey) {
|
||||
await tx.phoneFrequencyReservation.create({
|
||||
data: {
|
||||
reservationKey: normalizedReservationKey,
|
||||
tenantId,
|
||||
applicationId,
|
||||
result: [...rejected.entries()].map(([phoneNumber, rejection]) => ({ phoneNumber, ...rejection })),
|
||||
},
|
||||
});
|
||||
}
|
||||
return rejected;
|
||||
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted });
|
||||
}
|
||||
@@ -566,6 +598,18 @@ export class PhoneFrequencyService {
|
||||
}
|
||||
}
|
||||
|
||||
function frequencyRejectionsFromJson(value: Prisma.JsonValue) {
|
||||
const result = new Map<string, PhoneFrequencyRejection>();
|
||||
if (!Array.isArray(value)) return result;
|
||||
for (const item of value) {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) continue;
|
||||
const phoneNumber = typeof item.phoneNumber === 'string' ? item.phoneNumber : '';
|
||||
const reason = typeof item.reason === 'string' ? item.reason : '';
|
||||
if (phoneNumber && reason) result.set(phoneNumber, { code: 'PHONE_FREQUENCY_LIMIT', reason });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const whitelistUserInclude = {
|
||||
createdBy: { select: { id: true, username: true, displayName: true } },
|
||||
updatedBy: { select: { id: true, username: true, displayName: true } },
|
||||
|
||||
Reference in New Issue
Block a user