This commit is contained in:
@@ -46,8 +46,10 @@ export class AdminOperationsController {
|
||||
@Query('hasDrainage') hasDrainage?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('monitorSnapshotId') monitorSnapshotId?: string,
|
||||
) {
|
||||
return this.operations.listMessagesPage({
|
||||
monitorSnapshotId,
|
||||
tenantId,
|
||||
applicationId,
|
||||
channelId,
|
||||
@@ -80,8 +82,10 @@ export class AdminOperationsController {
|
||||
@Query('status') status: string | undefined,
|
||||
@Query('hasDrainage') hasDrainage: string | undefined,
|
||||
@Res() response: DownloadResponse,
|
||||
@Query('monitorSnapshotId') monitorSnapshotId?: string,
|
||||
) {
|
||||
const exported = await this.operations.exportMessages({
|
||||
monitorSnapshotId,
|
||||
tenantId,
|
||||
applicationId,
|
||||
channelId,
|
||||
@@ -105,25 +109,37 @@ export class AdminOperationsController {
|
||||
}
|
||||
|
||||
@Get('message-segment-audits')
|
||||
messageSegmentAudits(
|
||||
@Query('messageId') messageId?: string,
|
||||
@Query('messageRecordId') messageRecordId?: string,
|
||||
) {
|
||||
messageSegmentAudits(@Query('messageId') messageId?: string, @Query('messageRecordId') messageRecordId?: string) {
|
||||
return this.operations.listMessageSegmentAudits({ messageId, messageRecordId });
|
||||
}
|
||||
|
||||
@Get('uplink-messages')
|
||||
listUplinkMessages(@Query('tenantId') tenantId?: string, @Query('channelId') channelId?: string, @Query('phoneNumber') phoneNumber?: string, @Query('keyword') keyword?: string, @Query('startTime') startTime?: string, @Query('endTime') endTime?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) {
|
||||
listUplinkMessages(
|
||||
@Query('tenantId') tenantId?: string,
|
||||
@Query('channelId') channelId?: string,
|
||||
@Query('phoneNumber') phoneNumber?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('startTime') startTime?: string,
|
||||
@Query('endTime') endTime?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
return page || pageSize
|
||||
? this.operations.listUplinkMessagesPage({ tenantId, channelId, phoneNumber, keyword, startTime, endTime, page: Number(page), pageSize: Number(pageSize) })
|
||||
? this.operations.listUplinkMessagesPage({
|
||||
tenantId,
|
||||
channelId,
|
||||
phoneNumber,
|
||||
keyword,
|
||||
startTime,
|
||||
endTime,
|
||||
page: Number(page),
|
||||
pageSize: Number(pageSize),
|
||||
})
|
||||
: this.operations.listUplinkMessages({ tenantId, channelId, phoneNumber, keyword, startTime, endTime });
|
||||
}
|
||||
|
||||
@Post('uplink-messages/:id/claim')
|
||||
claimUplinkMatchCandidate(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { candidateId?: string; operatorId?: string },
|
||||
) {
|
||||
claimUplinkMatchCandidate(@Param('id') id: string, @Body() body: { candidateId?: string; operatorId?: string }) {
|
||||
return this.sendChain.claimUplinkMatchCandidate(id, String(body.candidateId ?? ''), body.operatorId);
|
||||
}
|
||||
|
||||
@@ -231,10 +247,7 @@ export class AdminOperationsController {
|
||||
}
|
||||
|
||||
@Post('gateway-submit-dead-letters/:id/resolve')
|
||||
resolveGatewaySubmitDeadLetter(
|
||||
@Param('id') id: string,
|
||||
@CurrentSessionUserId() operatorId?: string,
|
||||
) {
|
||||
resolveGatewaySubmitDeadLetter(@Param('id') id: string, @CurrentSessionUserId() operatorId?: string) {
|
||||
return this.sendChain.resolveGatewaySubmitDeadLetter(id, operatorId);
|
||||
}
|
||||
|
||||
@@ -381,16 +394,23 @@ export class AdminOperationsController {
|
||||
@Body() body: { previewToken?: string; reason?: string; ratePerSecond?: number; consecutiveFailureLimit?: number },
|
||||
@CurrentSessionUserId() operatorId?: string,
|
||||
) {
|
||||
return this.sendChain.createDownstreamRequeueTask({
|
||||
previewToken: body.previewToken ?? '',
|
||||
reason: body.reason ?? '',
|
||||
ratePerSecond: body.ratePerSecond,
|
||||
consecutiveFailureLimit: body.consecutiveFailureLimit,
|
||||
}, operatorId);
|
||||
return this.sendChain.createDownstreamRequeueTask(
|
||||
{
|
||||
previewToken: body.previewToken ?? '',
|
||||
reason: body.reason ?? '',
|
||||
ratePerSecond: body.ratePerSecond,
|
||||
consecutiveFailureLimit: body.consecutiveFailureLimit,
|
||||
},
|
||||
operatorId,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('downstream-requeue-tasks')
|
||||
listDownstreamRequeueTasks(@Query('status') status?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) {
|
||||
listDownstreamRequeueTasks(
|
||||
@Query('status') status?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
return this.sendChain.listDownstreamRequeueTasks({ status, page: Number(page), pageSize: Number(pageSize) });
|
||||
}
|
||||
|
||||
@@ -407,7 +427,12 @@ export class AdminOperationsController {
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
return this.sendChain.listDownstreamRequeueTaskItems(id, { status, keyword, page: Number(page), pageSize: Number(pageSize) });
|
||||
return this.sendChain.listDownstreamRequeueTaskItems(id, {
|
||||
status,
|
||||
keyword,
|
||||
page: Number(page),
|
||||
pageSize: Number(pageSize),
|
||||
});
|
||||
}
|
||||
|
||||
@Post('downstream-requeue-tasks/:id/:action')
|
||||
@@ -441,7 +466,18 @@ export class AdminSystemLogsController {
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
return this.protocolLogs.list({ protocol, direction, eventType, status, keyword, range, createdAtFrom, createdAtTo, page: Number(page), pageSize: Number(pageSize) });
|
||||
return this.protocolLogs.list({
|
||||
protocol,
|
||||
direction,
|
||||
eventType,
|
||||
status,
|
||||
keyword,
|
||||
range,
|
||||
createdAtFrom,
|
||||
createdAtTo,
|
||||
page: Number(page),
|
||||
pageSize: Number(pageSize),
|
||||
});
|
||||
}
|
||||
|
||||
@Get()
|
||||
@@ -457,11 +493,34 @@ export class AdminSystemLogsController {
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
) {
|
||||
return this.operations.systemLogs({ tenantId, userId, keyword, level, module, range, createdAtFrom, createdAtTo, page: Number(page), pageSize: Number(pageSize) });
|
||||
return this.operations.systemLogs({
|
||||
tenantId,
|
||||
userId,
|
||||
keyword,
|
||||
level,
|
||||
module,
|
||||
range,
|
||||
createdAtFrom,
|
||||
createdAtTo,
|
||||
page: Number(page),
|
||||
pageSize: Number(pageSize),
|
||||
});
|
||||
}
|
||||
|
||||
@Post('exports')
|
||||
export(@Body() body: { tenantId?: string; userId?: string; keyword?: string; level?: string; module?: string; range?: string; createdAtFrom?: string; createdAtTo?: string }) {
|
||||
export(
|
||||
@Body()
|
||||
body: {
|
||||
tenantId?: string;
|
||||
userId?: string;
|
||||
keyword?: string;
|
||||
level?: string;
|
||||
module?: string;
|
||||
range?: string;
|
||||
createdAtFrom?: string;
|
||||
createdAtTo?: string;
|
||||
},
|
||||
) {
|
||||
return this.operations.exportSystemLogs(body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Stable controller/query contracts extracted in R2.
|
||||
|
||||
export interface MessageQuery {
|
||||
monitorSnapshotId?: string;
|
||||
tenantId?: string;
|
||||
applicationId?: string;
|
||||
channelId?: string;
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { moneyToNumber } from '../../common/money';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import type { MessageQuery, TraceQuery, OperationLogQuery, GatewaySubmitDeadLetterQuery, DownstreamDeliveryQuery, DownstreamDeliveryDashboardQuery, DownstreamRecoveryStatusQuery, MessageSegmentAuditQuery, SignatureQualityQuery } from '../operations.contracts';
|
||||
import { messageWhere, recognizedCarrierValues, carrierWhere, startOfShanghaiDay, endOfShanghaiDay, qualityBusinessDay, shanghaiDateKey, normalizeGroupBy, returnedTransactionWhere, createdAtRange, downstreamAlertPendingMinutes, downstreamAlertRecentFailedHours, downstreamAlertWindows, downstreamAlertWhere, stalledPendingWhere, downstreamDeliveryScopedWhere, parseDateBoundary, downstreamRecoveryStatusWhere, escapeCsvCell, formatCsvDate, formatExportTimestamp, clientApplicationView, clientReceiptView, clientMessageView, clientBatchTaskView, clientUplinkView, clientAccountView, clientRechargeView, summarizeMessageGroups, groupDownstreamByType, groupDownstreamByApplication, positiveInteger, operationLogLevelWhere, normalizeOperationLog, sanitizeGatewaySubmitException, redactGatewayCommandValue } from '../operations.helpers';
|
||||
import type { MessageQuery } from '../operations.contracts';
|
||||
import {
|
||||
messageWhere,
|
||||
escapeCsvCell,
|
||||
formatExportTimestamp,
|
||||
clientMessageView,
|
||||
clientBatchTaskView,
|
||||
} from '../operations.helpers';
|
||||
|
||||
// R2 messages query domain. Method bodies are preserved byte-for-byte from the facade baseline.
|
||||
// Message queries share the same bounded monitor sample filter for lists and exports.
|
||||
export class OperationsMessageQueries {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
listBatchTasks(query: { tenantId?: string; status?: string }) {
|
||||
listBatchTasks(query: { tenantId?: string; status?: string }) {
|
||||
return this.prisma.smsBatchTask.findMany({
|
||||
where: { tenantId: query.tenantId, status: query.status, sourceType: 'client' },
|
||||
include: { apiRequests: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
async listClientBatchTasks(query: { tenantId?: string; status?: string }) {
|
||||
async listClientBatchTasks(query: { tenantId?: string; status?: string }) {
|
||||
const items = await this.listBatchTasks(query);
|
||||
return items.map(clientBatchTaskView);
|
||||
}
|
||||
listMessages(query: MessageQuery) {
|
||||
listMessages(query: MessageQuery) {
|
||||
return this.prisma.smsMessageRecord.findMany({
|
||||
where: messageWhere(query),
|
||||
include: {
|
||||
@@ -38,10 +43,34 @@ listMessages(query: MessageQuery) {
|
||||
orderBy: { queuedAt: 'desc' },
|
||||
});
|
||||
}
|
||||
async listMessagesPage(query: MessageQuery) {
|
||||
private async messageFilter(query: MessageQuery) {
|
||||
const where = messageWhere(query);
|
||||
if (query.monitorSnapshotId) {
|
||||
const snapshot = await this.prisma.sendingMonitorSnapshot.findUnique({ where: { id: query.monitorSnapshotId } });
|
||||
if (
|
||||
!snapshot ||
|
||||
(query.tenantId &&
|
||||
(snapshot.dimensions as { tenantId?: string }).tenantId &&
|
||||
(snapshot.dimensions as { tenantId?: string }).tenantId !== query.tenantId)
|
||||
)
|
||||
throw new NotFoundException('监控快照不存在');
|
||||
if (snapshot.windowFrom.getTime() < Date.now() - 72 * 3600000)
|
||||
throw new BadRequestException('该窗口已超过72小时样本保留期,历史快照仍可查询');
|
||||
where.monitorFacts = {
|
||||
some: {
|
||||
kind: snapshot.type === 'industry' ? 'attempt' : 'business',
|
||||
dimensionKey: snapshot.dimensionKey,
|
||||
submittedAt: { gte: snapshot.windowFrom, lt: snapshot.evaluationAt },
|
||||
...(snapshot.type === 'verification' ? { verification: true } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
return where;
|
||||
}
|
||||
async listMessagesPage(query: MessageQuery) {
|
||||
const page = Math.max(1, Math.floor(Number(query.page) || 1));
|
||||
const pageSize = Math.min(100, Math.max(1, Math.floor(Number(query.pageSize) || 25)));
|
||||
const where = messageWhere(query);
|
||||
const where = await this.messageFilter(query);
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.smsMessageRecord.findMany({
|
||||
where,
|
||||
@@ -75,7 +104,7 @@ async listMessagesPage(query: MessageQuery) {
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
|
||||
async getMessage(id: string) {
|
||||
async getMessage(id: string) {
|
||||
const item = await this.prisma.smsMessageRecord.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
@@ -121,9 +150,10 @@ async getMessage(id: string) {
|
||||
if (!item) throw new NotFoundException('Message record not found');
|
||||
return item;
|
||||
}
|
||||
async exportMessages(query: MessageQuery) {
|
||||
async exportMessages(query: MessageQuery) {
|
||||
const where = await this.messageFilter(query);
|
||||
const items = await this.prisma.smsMessageRecord.findMany({
|
||||
where: messageWhere(query),
|
||||
where,
|
||||
select: {
|
||||
messageId: true,
|
||||
queuedAt: true,
|
||||
@@ -144,7 +174,22 @@ async exportMessages(query: MessageQuery) {
|
||||
orderBy: [{ queuedAt: 'desc' }, { id: 'desc' }],
|
||||
});
|
||||
const rows = [
|
||||
['消息编号', '企业', '应用', '提交时间', '手机号', '地区', '运营商', '计费条数', '金额', '通道', '状态', '是否含引流', '回执时间', '短信内容'],
|
||||
[
|
||||
'消息编号',
|
||||
'企业',
|
||||
'应用',
|
||||
'提交时间',
|
||||
'手机号',
|
||||
'地区',
|
||||
'运营商',
|
||||
'计费条数',
|
||||
'金额',
|
||||
'通道',
|
||||
'状态',
|
||||
'是否含引流',
|
||||
'回执时间',
|
||||
'短信内容',
|
||||
],
|
||||
...items.map((item) => [
|
||||
item.messageId,
|
||||
item.tenant?.name ?? '',
|
||||
@@ -156,7 +201,9 @@ async exportMessages(query: MessageQuery) {
|
||||
String(item.billingUnits),
|
||||
String(moneyToNumber(item.amountCents)),
|
||||
item.channel?.name ?? '',
|
||||
item.status === 'submit_failed' || ['rejected', 'timeout'].includes(item.submitStatus ?? '') ? 'submit_failed' : item.status,
|
||||
item.status === 'submit_failed' || ['rejected', 'timeout'].includes(item.submitStatus ?? '')
|
||||
? 'submit_failed'
|
||||
: item.status,
|
||||
item.hasDrainageContent === true ? '是' : item.hasDrainageContent === false ? '否' : '未检测',
|
||||
item.deliveredAt?.toISOString() ?? '',
|
||||
item.content,
|
||||
@@ -167,11 +214,11 @@ async exportMessages(query: MessageQuery) {
|
||||
content: rows.map((row) => row.map((cell) => escapeCsvCell(String(cell))).join(',')).join('\n'),
|
||||
};
|
||||
}
|
||||
async listClientMessages(query: MessageQuery) {
|
||||
async listClientMessages(query: MessageQuery) {
|
||||
const items = await this.listMessages(query);
|
||||
return items.map(clientMessageView);
|
||||
}
|
||||
async listClientMessagesPage(query: MessageQuery) {
|
||||
async listClientMessagesPage(query: MessageQuery) {
|
||||
const result = await this.listMessagesPage(query);
|
||||
return { ...result, items: result.items.map(clientMessageView) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user