feat: add operations acceptance phase

This commit is contained in:
hectorzhao
2026-07-01 13:46:54 +08:00
parent 27bb2d798a
commit 924457a48e
11 changed files with 692 additions and 2 deletions
+241
View File
@@ -0,0 +1,241 @@
import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
export interface MessageQuery {
tenantId?: string;
applicationId?: string;
channelId?: string;
taskId?: string;
phoneNumber?: string;
status?: string;
}
export interface TraceQuery extends MessageQuery {
messageId?: string;
}
@Injectable()
export class OperationsService {
constructor(private readonly prisma: PrismaService) {}
listBatchTasks(query: { tenantId?: string; status?: string }) {
return this.prisma.smsBatchTask.findMany({
where: { tenantId: query.tenantId, status: query.status },
include: { apiRequests: true },
orderBy: { createdAt: 'desc' },
take: 200,
});
}
listMessages(query: MessageQuery) {
return this.prisma.smsMessageRecord.findMany({
where: messageWhere(query),
include: { submitRecords: true, receiptRecords: true },
orderBy: { queuedAt: 'desc' },
take: 500,
});
}
listUplinkMessages(query: { tenantId?: string; channelId?: string }) {
return this.prisma.smsUplinkMessage.findMany({
where: { tenantId: query.tenantId, channelId: query.channelId },
orderBy: { receivedAt: 'desc' },
take: 500,
});
}
async monitor(query: { tenantId?: string; channelId?: string }) {
const where = messageWhere(query);
const [byStatus, recentMessages, recentReceipts, recentUplinks] = await Promise.all([
this.prisma.smsMessageRecord.groupBy({ by: ['status'], where, _count: { _all: true } }),
this.prisma.smsMessageRecord.findMany({
where,
include: { submitRecords: true, receiptRecords: true },
orderBy: { queuedAt: 'desc' },
take: 20,
}),
this.prisma.smsReceiptRecord.findMany({
where: { tenantId: query.tenantId, channelId: query.channelId },
orderBy: { createdAt: 'desc' },
take: 20,
}),
this.listUplinkMessages({ tenantId: query.tenantId, channelId: query.channelId }),
]);
return {
byStatus,
recentMessages,
recentReceipts,
recentUplinks: recentUplinks.slice(0, 20),
};
}
async dashboard(query: { tenantId?: string }) {
const messageWhereClause = messageWhere({ tenantId: query.tenantId });
const [taskCount, messageGroups, uplinkCount, billingAggregate, transactionAggregate] = await Promise.all([
this.prisma.smsBatchTask.count({ where: { tenantId: query.tenantId } }),
this.prisma.smsMessageRecord.groupBy({
by: ['status'],
where: messageWhereClause,
_count: { _all: true },
_sum: { amountCents: true, billingUnits: true },
}),
this.prisma.smsUplinkMessage.count({ where: { tenantId: query.tenantId } }),
this.prisma.smsBillingRecord.aggregate({
where: { tenantId: query.tenantId },
_sum: { amountCents: true, billingUnits: true },
_count: { _all: true },
}),
this.prisma.accountTransaction.aggregate({
where: { tenantId: query.tenantId },
_sum: { amountCents: true, smsUnits: true },
_count: { _all: true },
}),
]);
return {
taskCount,
messageStatus: messageGroups,
uplinkCount,
billing: billingAggregate,
transactions: transactionAggregate,
};
}
async statistics(query: { tenantId?: string; groupBy?: string }) {
const groupBy = normalizeGroupBy(query.groupBy);
if (groupBy === 'tenantId') {
return this.prisma.smsMessageRecord.groupBy({
by: ['tenantId'],
where: messageWhere({ tenantId: query.tenantId }),
_count: { _all: true },
_sum: { amountCents: true, billingUnits: true },
});
}
if (groupBy === 'applicationId') {
return this.prisma.smsMessageRecord.groupBy({
by: ['applicationId'],
where: messageWhere({ tenantId: query.tenantId }),
_count: { _all: true },
_sum: { amountCents: true, billingUnits: true },
});
}
return this.prisma.smsMessageRecord.groupBy({
by: ['channelId'],
where: messageWhere({ tenantId: query.tenantId }),
_count: { _all: true },
_sum: { amountCents: true, billingUnits: true },
});
}
auditLogs(query: { tenantId?: string; userId?: string }) {
return this.prisma.operationLog.findMany({
where: { tenantId: query.tenantId, userId: query.userId },
orderBy: { createdAt: 'desc' },
take: 500,
});
}
auditSummary(query: { tenantId?: string }) {
return this.prisma.operationLog.groupBy({
by: ['action', 'resource'],
where: { tenantId: query.tenantId },
_count: { _all: true },
orderBy: { _count: { action: 'desc' } },
take: 100,
});
}
async trace(query: TraceQuery) {
const messages = await this.prisma.smsMessageRecord.findMany({
where: {
...messageWhere(query),
messageId: query.messageId,
},
include: {
batchTask: { include: { apiRequests: true } },
submitRecords: { include: { session: true } },
receiptRecords: true,
},
orderBy: { queuedAt: 'desc' },
take: 100,
});
const messageIds = messages.map((message) => message.messageId);
const [billingRecords, uplinks] = await Promise.all([
this.prisma.smsBillingRecord.findMany({
where: {
tenantId: query.tenantId,
taskId: query.taskId,
messageId: messageIds.length > 0 ? { in: messageIds } : undefined,
},
orderBy: { createdAt: 'desc' },
}),
this.prisma.smsUplinkMessage.findMany({
where: {
tenantId: query.tenantId,
messageId: messageIds.length > 0 ? { in: messageIds } : undefined,
},
orderBy: { receivedAt: 'desc' },
}),
]);
return { messages, billingRecords, uplinks };
}
async reconciliation(query: { tenantId?: string; taskId?: string }) {
const [messages, billing, transactions] = await Promise.all([
this.prisma.smsMessageRecord.aggregate({
where: messageWhere({ tenantId: query.tenantId, taskId: query.taskId }),
_count: { _all: true },
_sum: { amountCents: true, billingUnits: true },
}),
this.prisma.smsBillingRecord.aggregate({
where: { tenantId: query.tenantId, taskId: query.taskId },
_count: { _all: true },
_sum: { amountCents: true, billingUnits: true },
}),
this.prisma.accountTransaction.aggregate({
where: {
tenantId: query.tenantId,
relatedType: query.taskId ? { in: ['sms_batch_task', 'sms_message_record'] } : undefined,
relatedId: query.taskId,
},
_count: { _all: true },
_sum: { amountCents: true, smsUnits: true },
}),
]);
const messageAmount = messages._sum.amountCents ?? 0;
const billingAmount = billing._sum.amountCents ?? 0;
const transactionAmount = transactions._sum.amountCents ?? 0;
return {
messages,
billing,
transactions,
diff: {
messageVsBillingAmountCents: messageAmount - billingAmount,
billingVsTransactionAmountCents: billingAmount + transactionAmount,
messageVsBillingUnits: (messages._sum.billingUnits ?? 0) - (billing._sum.billingUnits ?? 0),
},
};
}
}
function messageWhere(query: MessageQuery): Prisma.SmsMessageRecordWhereInput {
return {
tenantId: query.tenantId,
applicationId: query.applicationId,
channelId: query.channelId,
batchTaskId: query.taskId,
phoneNumber: query.phoneNumber,
status: query.status,
};
}
function normalizeGroupBy(groupBy?: string) {
if (groupBy === 'tenant' || groupBy === 'tenantId') {
return 'tenantId';
}
if (groupBy === 'application' || groupBy === 'applicationId') {
return 'applicationId';
}
return 'channelId';
}