feat: aggregate cmpp template mismatch reviews
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
export interface CreateRiskRuleDto {
|
||||
@@ -33,6 +33,15 @@ export interface ReviewSmsTaskDto {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface AggregateTemplateMismatchDto {
|
||||
tenantId: string;
|
||||
applicationId: string;
|
||||
account: string;
|
||||
messageRecordId: string;
|
||||
signatureId: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface RuleEvaluation {
|
||||
ruleId?: string;
|
||||
ruleCode: string;
|
||||
@@ -153,8 +162,14 @@ export class RiskReviewService {
|
||||
where: {
|
||||
tenantId,
|
||||
status,
|
||||
...(status === 'pending_review' ? {
|
||||
OR: [
|
||||
{ sourceType: { not: 'cmpp_template_mismatch' } },
|
||||
{ windowEndsAt: { lte: new Date() } },
|
||||
],
|
||||
} : {}),
|
||||
},
|
||||
include: { riskHits: true },
|
||||
include: { riskHits: true, _count: { select: { messageRecords: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
@@ -163,6 +178,61 @@ export class RiskReviewService {
|
||||
return this.listTasks(undefined, 'pending_review');
|
||||
}
|
||||
|
||||
async aggregateTemplateMismatch(data: AggregateTemplateMismatchDto) {
|
||||
const normalizedContent = data.content.replace(/\r\n/g, '\n').trim();
|
||||
const contentHash = createHash('sha256').update(normalizedContent, 'utf8').digest('hex');
|
||||
const windowMs = Math.max(1_000, Number(process.env.CMPP_TEMPLATE_REVIEW_WINDOW_MS ?? 10_000));
|
||||
const now = new Date();
|
||||
const windowStartedAt = new Date(Math.floor(now.getTime() / windowMs) * windowMs);
|
||||
const windowEndsAt = new Date(windowStartedAt.getTime() + windowMs);
|
||||
const aggregationKey = createHash('sha256')
|
||||
.update(`${data.applicationId}|${data.account}|${contentHash}|${windowStartedAt.toISOString()}`, 'utf8')
|
||||
.digest('hex');
|
||||
const task = await this.prisma.$transaction(async (tx) => {
|
||||
const aggregated = await tx.smsSendTask.upsert({
|
||||
where: { aggregationKey },
|
||||
update: {
|
||||
phoneTotal: { increment: 1 },
|
||||
uniquePhoneTotal: { increment: 1 },
|
||||
},
|
||||
create: {
|
||||
tenantId: data.tenantId,
|
||||
applicationId: data.applicationId,
|
||||
taskNo: `CMPP-REVIEW-${windowStartedAt.getTime()}-${aggregationKey.slice(0, 8)}`,
|
||||
sourceType: 'cmpp_template_mismatch',
|
||||
aggregationKey,
|
||||
contentHash,
|
||||
windowStartedAt,
|
||||
windowEndsAt,
|
||||
content: normalizedContent,
|
||||
phoneTotal: 1,
|
||||
uniquePhoneTotal: 1,
|
||||
status: 'pending_review',
|
||||
riskDecision: 'manual_review',
|
||||
reviewReason: '企业应用已配置模板不匹配进入人工审核',
|
||||
variableIssues: {
|
||||
sourceType: 'cmpp',
|
||||
account: data.account,
|
||||
aggregationWindowMs: windowMs,
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
await tx.smsMessageRecord.update({
|
||||
where: { id: data.messageRecordId },
|
||||
data: {
|
||||
reviewTaskId: aggregated.id,
|
||||
signatureId: data.signatureId,
|
||||
status: 'pending_review',
|
||||
},
|
||||
});
|
||||
return aggregated;
|
||||
});
|
||||
return this.prisma.smsSendTask.findUnique({
|
||||
where: { id: task.id },
|
||||
include: { riskHits: true, _count: { select: { messageRecords: true } } },
|
||||
});
|
||||
}
|
||||
|
||||
async evaluateTask(data: EvaluateSmsTaskDto) {
|
||||
await this.ensureDefaultRules();
|
||||
if (data.createdById) {
|
||||
@@ -261,6 +331,7 @@ export class RiskReviewService {
|
||||
if (!task) {
|
||||
throw new NotFoundException('SMS send task not found');
|
||||
}
|
||||
this.assertAggregationWindowClosed(task);
|
||||
return this.prisma.smsSendTask.update({
|
||||
where: { id: taskId },
|
||||
data: {
|
||||
@@ -280,6 +351,7 @@ export class RiskReviewService {
|
||||
if (!task) {
|
||||
throw new NotFoundException('SMS send task not found');
|
||||
}
|
||||
this.assertAggregationWindowClosed(task);
|
||||
const reason = data.reason ?? task.reviewReason ?? '审核拒绝';
|
||||
return this.prisma.smsSendTask.update({
|
||||
where: { id: taskId },
|
||||
@@ -306,6 +378,12 @@ export class RiskReviewService {
|
||||
}
|
||||
}
|
||||
|
||||
private assertAggregationWindowClosed(task: { sourceType?: string | null; windowEndsAt?: Date | null }) {
|
||||
if (task.sourceType === 'cmpp_template_mismatch' && task.windowEndsAt && task.windowEndsAt.getTime() > Date.now()) {
|
||||
throw new BadRequestException('聚合窗口尚未关闭,请在窗口结束后审核');
|
||||
}
|
||||
}
|
||||
|
||||
private async effectiveRules(tenantId: string) {
|
||||
const rules = await this.prisma.riskRule.findMany({
|
||||
where: {
|
||||
|
||||
Reference in New Issue
Block a user