fix: close sms scheduling and billing gaps
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
@@ -168,6 +168,12 @@ export class RiskReviewService {
|
||||
|
||||
async evaluateTask(data: EvaluateSmsTaskDto) {
|
||||
await this.ensureDefaultRules();
|
||||
if (data.createdById) {
|
||||
const creator = await this.prisma.user.findUnique({ where: { id: data.createdById }, select: { id: true } });
|
||||
if (!creator) {
|
||||
throw new BadRequestException('createdById does not reference an existing user');
|
||||
}
|
||||
}
|
||||
const phones = data.phones ?? [];
|
||||
const uniquePhones = [...new Set(phones)];
|
||||
const phoneTotal = phones.length;
|
||||
@@ -177,15 +183,17 @@ export class RiskReviewService {
|
||||
const illegalRatio = ratio(illegalCount, phoneTotal);
|
||||
const blacklistHitCount = await this.countBlacklistHits(data.tenantId, uniquePhones);
|
||||
const blacklistHitRatio = ratio(blacklistHitCount, phoneTotal);
|
||||
const [application, template, rules, recentTaskCount] = await Promise.all([
|
||||
const [application, template, rules, recentTaskCount, sensitiveWords] = await Promise.all([
|
||||
data.applicationId ? this.prisma.smsApplication.findUnique({ where: { id: data.applicationId } }) : null,
|
||||
data.templateId
|
||||
? this.prisma.smsTemplate.findUnique({ where: { id: data.templateId }, include: { variables: true } })
|
||||
: null,
|
||||
this.effectiveRules(data.tenantId),
|
||||
this.countRecentTasks(data.tenantId),
|
||||
this.prisma.sensitiveWord.findMany({ where: { status: 'active' }, select: { word: true, level: true } }),
|
||||
]);
|
||||
const variableIssues = evaluateTemplateVariables(template?.variables ?? [], data.content, data.variables ?? {});
|
||||
const contentIssues = evaluateContent(data.content, sensitiveWords);
|
||||
const requestedAt = data.requestedAt ? new Date(data.requestedAt) : new Date();
|
||||
const nonWorkingMarketingPhones =
|
||||
isMarketing(data.category ?? template?.category) && isNonWorkingTime(requestedAt) ? phoneTotal : 0;
|
||||
@@ -199,6 +207,7 @@ export class RiskReviewService {
|
||||
recentTaskCount,
|
||||
variableIssueCount: variableIssues.length,
|
||||
});
|
||||
hits.push(...contentIssues.map(contentIssueToHit));
|
||||
const decision = decideRiskAction(hits);
|
||||
const reason = hits.length > 0 ? hits.map((hit) => hit.reason).join('; ') : null;
|
||||
const task = await this.prisma.smsSendTask.create({
|
||||
@@ -214,7 +223,7 @@ export class RiskReviewService {
|
||||
duplicateRatio,
|
||||
illegalRatio,
|
||||
blacklistHitRatio,
|
||||
variableIssues: variableIssues as Prisma.InputJsonValue,
|
||||
variableIssues: { variables: variableIssues, content: contentIssues } as unknown as Prisma.InputJsonValue,
|
||||
status: decision.status,
|
||||
riskDecision: decision.riskDecision,
|
||||
reviewReason: decision.status === 'pending_review' ? reason : null,
|
||||
@@ -418,6 +427,39 @@ function evaluateTemplateVariables(
|
||||
];
|
||||
}
|
||||
|
||||
function evaluateContent(content: string, sensitiveWords: Array<{ word: string; level: string }>) {
|
||||
const issues: RuleEvaluation[] = [];
|
||||
const controlMatches = [...content].filter((char) => /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/u.test(char));
|
||||
if (controlMatches.length > 0) {
|
||||
issues.push({
|
||||
ruleCode: 'CONTENT_CONTROL_CHAR',
|
||||
ruleName: '短信内容非法控制字符',
|
||||
thresholdValue: 0,
|
||||
actualValue: controlMatches.length,
|
||||
action: 'block',
|
||||
reason: `短信内容包含 ${controlMatches.length} 个非法控制字符,处理动作 直接拒绝`,
|
||||
});
|
||||
}
|
||||
const matchedWords = sensitiveWords
|
||||
.filter((item) => item.word && content.includes(item.word))
|
||||
.map((item) => item.word);
|
||||
if (matchedWords.length > 0) {
|
||||
issues.push({
|
||||
ruleCode: 'SENSITIVE_WORD',
|
||||
ruleName: '敏感词命中',
|
||||
thresholdValue: 0,
|
||||
actualValue: matchedWords.length,
|
||||
action: 'block',
|
||||
reason: `短信内容命中敏感词:${matchedWords.join('、')},处理动作 直接拒绝`,
|
||||
});
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
function contentIssueToHit(issue: RuleEvaluation) {
|
||||
return issue;
|
||||
}
|
||||
|
||||
function inferVariables(content: string) {
|
||||
const matches = content.match(/\$\{[a-zA-Z0-9_]+\}/g) ?? [];
|
||||
return [...new Set(matches)].map((match) => ({ name: match.slice(2, -1), required: true }));
|
||||
@@ -450,4 +492,3 @@ function formatAction(action: string) {
|
||||
}
|
||||
return '放行';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user