feat: integrate analytics and fragment receipt improvements

This commit is contained in:
hectorzhao
2026-08-03 15:34:41 +08:00
parent 3357ace7e1
commit 530a65de80
89 changed files with 1964 additions and 324 deletions
@@ -2,6 +2,11 @@ import { BadRequestException, ConflictException, Injectable, Optional } from '@n
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { PhoneRoutingLookupService } from './phone-routing-lookup.service';
import {
detectDrainageContentWithRules,
invalidateDrainageDetectionRuleCache,
validateDrainageDetectionPattern,
} from '../send-chain/drainage-content-detection';
export interface CreatePhoneSegmentDto {
prefix: string;
@@ -58,6 +63,23 @@ export interface CreateDrainageFieldDto {
description?: string;
}
export interface UpsertDrainageDetectionRuleDto {
code: string;
name: string;
category: 'url' | 'mobile' | 'landline';
pattern: string;
flags?: string;
priority?: number;
status?: string;
description?: string;
operatorId?: string;
}
export interface TestDrainageDetectionDto {
content: string;
rule?: UpsertDrainageDetectionRuleDto;
}
export interface CreateCommonReportFieldDto {
drainageFieldId: string;
reportType: 'signature' | 'drainage';
@@ -374,6 +396,97 @@ export class DictionariesService {
});
}
listDrainageDetectionRules(query: { keyword?: string; status?: string } = {}) {
const keyword = query.keyword?.trim();
return this.prisma.drainageDetectionRule.findMany({
where: {
status: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
OR: keyword ? [
{ code: { contains: keyword, mode: 'insensitive' } },
{ name: { contains: keyword, mode: 'insensitive' } },
{ description: { contains: keyword, mode: 'insensitive' } },
] : undefined,
},
orderBy: [{ priority: 'asc' }, { createdAt: 'asc' }],
});
}
async createDrainageDetectionRule(data: UpsertDrainageDetectionRuleDto) {
this.validateDrainageDetectionRule(data);
const created = await this.prisma.drainageDetectionRule.create({
data: {
code: data.code.trim().toUpperCase(),
name: data.name.trim(),
category: data.category,
pattern: data.pattern,
flags: data.flags ?? 'giu',
priority: data.priority ?? 100,
status: data.status ?? 'active',
description: data.description?.trim() || null,
},
});
invalidateDrainageDetectionRuleCache();
await this.writeOperationLog(data.operatorId, 'drainage_detection_rule.create', 'drainage_detection_rule', created.id, { code: created.code });
return created;
}
async updateDrainageDetectionRule(id: string, data: UpsertDrainageDetectionRuleDto) {
this.validateDrainageDetectionRule(data);
const updated = await this.prisma.drainageDetectionRule.update({
where: { id },
data: {
code: data.code.trim().toUpperCase(),
name: data.name.trim(),
category: data.category,
pattern: data.pattern,
flags: data.flags ?? 'giu',
priority: data.priority ?? 100,
status: data.status ?? 'active',
description: data.description?.trim() || null,
version: { increment: 1 },
},
});
invalidateDrainageDetectionRuleCache();
await this.writeOperationLog(data.operatorId, 'drainage_detection_rule.update', 'drainage_detection_rule', id, { code: updated.code, version: updated.version });
return updated;
}
async changeDrainageDetectionRuleStatus(id: string, data: DictionaryStatusDto) {
const status = data.status === 'inactive' ? 'inactive' : 'active';
const updated = await this.prisma.drainageDetectionRule.update({
where: { id },
data: { status, version: { increment: 1 } },
});
invalidateDrainageDetectionRuleCache();
await this.writeOperationLog(data.operatorId, `drainage_detection_rule.${status}`, 'drainage_detection_rule', id, { reason: data.reason });
return updated;
}
async testDrainageDetection(data: TestDrainageDetectionDto) {
if (!data.content?.trim()) throw new BadRequestException('测试短信内容不能为空');
const rules = data.rule
? [{
id: 'preview',
code: data.rule.code?.trim().toUpperCase() || 'PREVIEW',
name: data.rule.name?.trim() || '预览规则',
category: data.rule.category,
pattern: data.rule.pattern,
flags: data.rule.flags ?? 'giu',
priority: data.rule.priority ?? 100,
version: 1,
}]
: await this.prisma.drainageDetectionRule.findMany({ where: { status: 'active' }, orderBy: { priority: 'asc' } });
if (data.rule) this.validateDrainageDetectionRule(data.rule);
return detectDrainageContentWithRules(data.content, rules);
}
private validateDrainageDetectionRule(data: UpsertDrainageDetectionRuleDto) {
if (!data.code?.trim() || !data.name?.trim()) throw new BadRequestException('规则编码和名称不能为空');
if (!['url', 'mobile', 'landline'].includes(data.category)) throw new BadRequestException('规则类型仅支持 URL、手机号或固话');
if (data.status && !['active', 'inactive'].includes(data.status)) throw new BadRequestException('规则状态不正确');
validateDrainageDetectionPattern(data.pattern, data.flags ?? 'giu');
}
listCommonReportFields() {
return this.prisma.commonReportField.findMany({
include: { drainageField: true },