This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { matchRules, type MonitorScope, type Rule } from './monitor-metrics';
|
||||
|
||||
type Query = Record<string, string | undefined>;
|
||||
const names = `jsonb_build_object('tenantName',t.name,'applicationName',a.name,'signatureName',s.name,
|
||||
'tenantStatus',t.status,'applicationStatus',a.status,'signatureStatus',s."auditStatus")`;
|
||||
const joins = `LEFT JOIN "Tenant" t ON t.id=r.scope->>'tenantId'
|
||||
LEFT JOIN "SmsApplication" a ON a.id=r.scope->>'applicationId'
|
||||
LEFT JOIN "SmsSignature" s ON s.id=r.scope->>'signatureId'`;
|
||||
const pageOf = (value = '1') => {
|
||||
const page = Number(value);
|
||||
if (!Number.isSafeInteger(page) || page < 1 || page > 100000) throw new BadRequestException('分页参数无效');
|
||||
return page;
|
||||
};
|
||||
export function managementScope(q: Query): MonitorScope {
|
||||
const scope = Object.fromEntries(
|
||||
['tenantId', 'applicationId', 'signatureId'].filter((key) => q[key]).map((key) => [key, q[key]]),
|
||||
);
|
||||
if (Object.values(scope).some((v) => typeof v !== 'string' || !v.trim() || v.length > 200))
|
||||
throw new BadRequestException('规则范围无效');
|
||||
if (Object.keys(scope).length && (!scope.tenantId || (!scope.applicationId && !scope.signatureId)))
|
||||
throw new BadRequestException('请选择企业应用或签名');
|
||||
return scope;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MonitorRuleManagementService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(q: Query) {
|
||||
const page = pageOf(q.page),
|
||||
keyword = q.keyword?.trim() ?? '',
|
||||
kind = q.kind ?? '';
|
||||
if (!['', 'application', 'signature', 'combined'].includes(kind)) throw new BadRequestException('覆盖类型无效');
|
||||
const where = `r.type='overall' AND r.scope<>'{}'::jsonb
|
||||
AND (COALESCE((r.config->>'deleted')::boolean,false)=false OR r."effectiveAt">(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'))
|
||||
AND ($1='' OR concat_ws(' ',t.name,a.name,s.name) ILIKE '%'||$1||'%')
|
||||
AND ($2='' OR CASE WHEN r.scope ? 'applicationId' AND r.scope ? 'signatureId' THEN 'combined'
|
||||
WHEN r.scope ? 'signatureId' THEN 'signature' ELSE 'application' END=$2)`;
|
||||
return this.prisma.$transaction(
|
||||
async (tx) => {
|
||||
const items = await tx.$queryRawUnsafe(
|
||||
`SELECT r.*,${names} AS names,
|
||||
(SELECT to_jsonb(v) FROM "SendingMonitorRuleVersion" v WHERE v."ruleId"=r.id
|
||||
AND v."effectiveAt"<=(CURRENT_TIMESTAMP AT TIME ZONE 'UTC') ORDER BY version DESC LIMIT 1) AS active
|
||||
FROM "SendingMonitorRule" r ${joins} WHERE ${where} ORDER BY r."updatedAt" DESC,r.id LIMIT 20 OFFSET $3`,
|
||||
keyword,
|
||||
kind,
|
||||
(page - 1) * 20,
|
||||
);
|
||||
const count = await tx.$queryRawUnsafe<Array<{ total: number }>>(
|
||||
`SELECT count(*)::int total FROM "SendingMonitorRule" r ${joins} WHERE ${where}`,
|
||||
keyword,
|
||||
kind,
|
||||
);
|
||||
return { items, total: count[0].total, page, pageSize: 20, serverTime: new Date().toISOString() };
|
||||
},
|
||||
{ isolationLevel: 'RepeatableRead' },
|
||||
);
|
||||
}
|
||||
|
||||
async options(q: Query) {
|
||||
const page = pageOf(q.page),
|
||||
keyword = q.keyword?.trim() ?? '';
|
||||
let from: string, fields: string, restriction: string;
|
||||
if (q.kind === 'tenant') {
|
||||
from = '"Tenant" t';
|
||||
fields = `t.id,t.name,t.id AS "tenantId"`;
|
||||
restriction = `t.status<>'deleted' AND ($1='' OR t.name ILIKE '%'||$1||'%') AND $2::text IS NOT NULL AND $3::text IS NOT NULL`;
|
||||
} else if (q.kind === 'application') {
|
||||
from = '"SmsApplication" a JOIN "Tenant" t ON t.id=a."tenantId"';
|
||||
fields = `a.id,concat_ws(' · ',t.name,a.name) AS name,t.id AS "tenantId"`;
|
||||
restriction = `a.status<>'deleted' AND t.status<>'deleted' AND ($1='' OR concat_ws(' ',t.name,a.name) ILIKE '%'||$1||'%') AND ($2='' OR t.id=$2) AND $3::text IS NOT NULL`;
|
||||
} else if (q.kind === 'signature' && q.tenantId) {
|
||||
from =
|
||||
'"SmsSignature" s JOIN "Tenant" t ON t.id=s."tenantId" LEFT JOIN "SmsApplication" a ON a.id=s."applicationId"';
|
||||
fields = `s.id,concat_ws(' · ',s.name,a.name) AS name,t.id AS "tenantId"`;
|
||||
restriction = `s."auditStatus"<>'deleted' AND t.status<>'deleted' AND (a.id IS NULL OR a.status<>'deleted') AND ($1='' OR s.name ILIKE '%'||$1||'%') AND t.id=$2 AND ($3='' OR s."applicationId"=$3)`;
|
||||
} else throw new BadRequestException('选项类型无效或尚未选择企业');
|
||||
const items = await this.prisma.$queryRawUnsafe<Array<{ id: string; name: string; tenantId: string }>>(
|
||||
`SELECT ${fields} FROM ${from} WHERE ${restriction} ORDER BY name,id LIMIT 21 OFFSET $4`,
|
||||
keyword,
|
||||
q.tenantId ?? '',
|
||||
q.applicationId ?? '',
|
||||
(page - 1) * 20,
|
||||
);
|
||||
return { items: items.slice(0, 20), page, hasMore: items.length > 20 };
|
||||
}
|
||||
|
||||
async editor(q: Query) {
|
||||
const scope = managementScope(q),
|
||||
scopeKey = JSON.stringify(Object.fromEntries(Object.entries(scope).sort()));
|
||||
return this.prisma.$transaction(
|
||||
async (tx) => {
|
||||
const current = await tx.$queryRawUnsafe<Array<Rule & { id: string }>>(
|
||||
`SELECT r.*,${names} AS names FROM "SendingMonitorRule" r ${joins} WHERE r.type='overall' AND r."scopeKey"=$1`,
|
||||
scopeKey,
|
||||
);
|
||||
const versions = await tx.$queryRawUnsafe<Rule[]>(`SELECT r.*,${names} AS names FROM
|
||||
(SELECT DISTINCT ON ("ruleId") * FROM "SendingMonitorRuleVersion" WHERE type='overall'
|
||||
AND "effectiveAt"<=(CURRENT_TIMESTAMP AT TIME ZONE 'UTC') ORDER BY "ruleId",version DESC) r ${joins}`);
|
||||
const matched = matchRules(versions, 'overall', scope);
|
||||
return {
|
||||
current: current[0] ?? null,
|
||||
matched,
|
||||
inherited: matched.filter((r) => r.ruleId !== current[0]?.id),
|
||||
serverTime: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
{ isolationLevel: 'RepeatableRead' },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { randomUUID } from 'node:crypto';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import type { SessionRequest } from '../auth/session-validation.middleware';
|
||||
import { MonitorRuleManagementService } from './monitor-rule-management.service';
|
||||
import {
|
||||
horizons,
|
||||
matchRules,
|
||||
@@ -217,19 +218,31 @@ export class SendingMonitorService {
|
||||
.sort(),
|
||||
) as MonitorScope;
|
||||
if (
|
||||
!body.config.deleted &&
|
||||
scope.tenantId &&
|
||||
!(await this.prisma.tenant.findFirst({
|
||||
where: { id: scope.tenantId, status: { not: 'deleted' } },
|
||||
select: { id: true },
|
||||
}))
|
||||
)
|
||||
throw new BadRequestException('企业不存在或已删除');
|
||||
if (
|
||||
!body.config.deleted &&
|
||||
scope.applicationId &&
|
||||
!(await this.prisma.smsApplication.findFirst({
|
||||
where: { id: scope.applicationId, tenantId: scope.tenantId },
|
||||
where: { id: scope.applicationId, tenantId: scope.tenantId, status: { not: 'deleted' } },
|
||||
select: { id: true },
|
||||
}))
|
||||
)
|
||||
throw new BadRequestException('应用不属于该企业');
|
||||
if (
|
||||
!body.config.deleted &&
|
||||
scope.signatureId &&
|
||||
!(await this.prisma.smsSignature.findFirst({
|
||||
where: {
|
||||
id: scope.signatureId,
|
||||
tenantId: scope.tenantId,
|
||||
auditStatus: { not: 'deleted' },
|
||||
...(scope.applicationId ? { applicationId: scope.applicationId } : {}),
|
||||
},
|
||||
select: { id: true },
|
||||
@@ -245,6 +258,7 @@ export class SendingMonitorService {
|
||||
type,
|
||||
scopeKey,
|
||||
);
|
||||
if (body.config.deleted && !old[0]) throw new NotFoundException('要恢复继承的规则不存在');
|
||||
if ((old[0]?.version ?? 0) !== body.version) throw new ConflictException('规则已被修改,请刷新后重试');
|
||||
const id = old[0]?.id ?? randomUUID(),
|
||||
period = type === 'overall' ? 600 : 300;
|
||||
@@ -285,6 +299,14 @@ export class SendingMonitorService {
|
||||
return result;
|
||||
});
|
||||
}
|
||||
async restoreRule(id: string, version: number, actor: string) {
|
||||
const rows = await this.prisma.$queryRawUnsafe<Array<{ type: string; scope: MonitorScope; config: RuleConfig }>>(
|
||||
`SELECT type,scope,config FROM "SendingMonitorRule" WHERE id=$1 AND type='overall' AND scope<>'{}'::jsonb`,
|
||||
id,
|
||||
);
|
||||
if (!rows[0]) throw new NotFoundException('个性化规则不存在');
|
||||
return this.saveRule({ ...rows[0], config: { ...rows[0].config, deleted: true }, version }, actor);
|
||||
}
|
||||
async alerts(query: QueryParams, user: string) {
|
||||
const page = pageNumber(query.page, 1),
|
||||
size = pageNumber(query.pageSize, 20, 100);
|
||||
@@ -338,7 +360,29 @@ export class SendingMonitorService {
|
||||
|
||||
@Controller('admin/sending-monitor')
|
||||
class SendingMonitorController {
|
||||
constructor(private readonly service: SendingMonitorService) {}
|
||||
constructor(
|
||||
private readonly service: SendingMonitorService,
|
||||
private readonly management: MonitorRuleManagementService,
|
||||
) {}
|
||||
@Get('rule-management') async managementList(@Req() req: SessionRequest, @Query() q: QueryParams) {
|
||||
await this.service.authorize(req);
|
||||
return this.management.list(q);
|
||||
}
|
||||
@Get('rule-options') async ruleOptions(@Req() req: SessionRequest, @Query() q: QueryParams) {
|
||||
await this.service.authorize(req);
|
||||
return this.management.options(q);
|
||||
}
|
||||
@Get('rule-editor') async ruleEditor(@Req() req: SessionRequest, @Query() q: QueryParams) {
|
||||
await this.service.authorize(req);
|
||||
return this.management.editor(q);
|
||||
}
|
||||
@Post('rules/:id/restore') async restore(
|
||||
@Req() req: SessionRequest,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { version?: number },
|
||||
) {
|
||||
return this.service.restoreRule(id, body?.version as number, await this.service.authorize(req, 'rules'));
|
||||
}
|
||||
@Get('rows') async rows(@Req() req: SessionRequest, @Query() q: QueryParams) {
|
||||
await this.service.authorize(req);
|
||||
return this.service.rows(q);
|
||||
@@ -393,5 +437,9 @@ class SendingMonitorController {
|
||||
return this.service.read(id, await this.service.authorize(req));
|
||||
}
|
||||
}
|
||||
@Module({ imports: [PrismaModule], controllers: [SendingMonitorController], providers: [SendingMonitorService] })
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [SendingMonitorController],
|
||||
providers: [SendingMonitorService, MonitorRuleManagementService],
|
||||
})
|
||||
export class SendingMonitorModule {}
|
||||
|
||||
Reference in New Issue
Block a user