feat: polish reporting templates and shared controls
This commit is contained in:
@@ -43,11 +43,7 @@ export class ReportsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
async listReconciliation(query: ReportListQuery) {
|
||||
const { page, pageSize, skip } = pagination(query);
|
||||
const where: Prisma.DailyReconciliationReportWhereInput = {
|
||||
reportDate: dateFilter(query.dateFrom, query.dateTo),
|
||||
tenantId: query.tenantId || undefined,
|
||||
applicationId: query.applicationId || undefined,
|
||||
};
|
||||
const where = reconciliationWhere(query);
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.dailyReconciliationReport.findMany({ where, orderBy: [{ reportDate: 'desc' }, { tenantName: 'asc' }, { applicationName: 'asc' }], skip, take: pageSize }),
|
||||
this.prisma.dailyReconciliationReport.count({ where }),
|
||||
@@ -57,14 +53,7 @@ export class ReportsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
async listProfit(query: ReportListQuery) {
|
||||
const { page, pageSize, skip } = pagination(query);
|
||||
const dimensionType = query.dimensionType === 'channel' ? 'channel' : 'application';
|
||||
const where: Prisma.DailyProfitReportWhereInput = {
|
||||
dimensionType,
|
||||
reportDate: dateFilter(query.dateFrom, query.dateTo),
|
||||
tenantId: dimensionType === 'application' ? query.tenantId || undefined : undefined,
|
||||
applicationId: dimensionType === 'application' ? query.applicationId || undefined : undefined,
|
||||
channelId: dimensionType === 'channel' ? query.channelId || undefined : undefined,
|
||||
};
|
||||
const { dimensionType, where } = profitWhere(query);
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.dailyProfitReport.findMany({ where, orderBy: [{ reportDate: 'desc' }, { dimensionName: 'asc' }], skip, take: pageSize }),
|
||||
this.prisma.dailyProfitReport.count({ where }),
|
||||
@@ -74,15 +63,7 @@ export class ReportsService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
async listQuality(query: ReportListQuery) {
|
||||
const { page, pageSize, skip } = pagination(query);
|
||||
const allowedDimensions = new Set(['application', 'channel', 'signature', 'drainage']);
|
||||
const dimensionType = allowedDimensions.has(String(query.dimensionType)) ? String(query.dimensionType) : 'application';
|
||||
const where: Prisma.DailyQualityReportWhereInput = {
|
||||
dimensionType,
|
||||
reportDate: dateFilter(query.dateFrom, query.dateTo),
|
||||
tenantId: query.tenantId || undefined,
|
||||
applicationId: query.applicationId || undefined,
|
||||
channelId: query.channelId || undefined,
|
||||
};
|
||||
const { dimensionType, where } = qualityWhere(query);
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.dailyQualityReport.findMany({ where, orderBy: [{ sentUnits: 'desc' }, { reportDate: 'desc' }, { dimensionName: 'asc' }], skip, take: pageSize }),
|
||||
this.prisma.dailyQualityReport.count({ where }),
|
||||
@@ -90,6 +71,23 @@ export class ReportsService implements OnModuleInit, OnModuleDestroy {
|
||||
return { items, total, page, pageSize, dimensionType };
|
||||
}
|
||||
|
||||
async exportReconciliation(query: ReportListQuery) {
|
||||
const items = await this.prisma.dailyReconciliationReport.findMany({ where: reconciliationWhere(query), orderBy: [{ reportDate: 'desc' }, { tenantName: 'asc' }, { applicationName: 'asc' }] });
|
||||
return csvExport('对账单', ['发送日期', '企业', '企业应用', '日发送条数', '成功条数', '生成时间'], items.map((item) => [dateKey(item.reportDate), item.tenantName, item.applicationName, item.sentUnits, item.successUnits, formatCsvDate(item.generatedAt)]));
|
||||
}
|
||||
|
||||
async exportProfit(query: ReportListQuery) {
|
||||
const { dimensionType, where } = profitWhere(query);
|
||||
const items = await this.prisma.dailyProfitReport.findMany({ where, orderBy: [{ reportDate: 'desc' }, { dimensionName: 'asc' }] });
|
||||
return csvExport(`利润报表-${dimensionType === 'channel' ? '通道' : '企业应用'}`, ['发送日期', '统计维度', '企业', '发送条数', '成功条数', '消费金额(分)', '成本金额(分)', '利润(分)', '利润率(%)', '生成时间'], items.map((item) => [dateKey(item.reportDate), item.dimensionName, item.tenantName ?? '', item.sentUnits, item.successUnits, item.revenueCents, item.costCents, item.profitCents, (item.profitRateBps / 100).toFixed(2), formatCsvDate(item.generatedAt)]));
|
||||
}
|
||||
|
||||
async exportQuality(query: ReportListQuery) {
|
||||
const { dimensionType, where } = qualityWhere(query);
|
||||
const items = await this.prisma.dailyQualityReport.findMany({ where, orderBy: [{ sentUnits: 'desc' }, { reportDate: 'desc' }, { dimensionName: 'asc' }] });
|
||||
return csvExport(`发送质量报表-${dimensionType}`, ['发送日期', '统计对象', '企业', '发送条数', '成功条数', '成功率(%)', '平均到达时长(毫秒)', '生成时间'], items.map((item) => [dateKey(item.reportDate), item.dimensionName, item.tenantName ?? '', item.sentUnits, item.successUnits, (item.successRateBps / 100).toFixed(2), item.avgArrivalMs ?? '', formatCsvDate(item.generatedAt)]));
|
||||
}
|
||||
|
||||
async refreshRollingWindow(now = new Date()) {
|
||||
const days = completedBusinessDays(now, 4);
|
||||
for (const day of days) await this.refreshBusinessDay(day);
|
||||
@@ -403,6 +401,53 @@ function pagination(query: ReportListQuery) {
|
||||
return { page, pageSize, skip: (page - 1) * pageSize };
|
||||
}
|
||||
|
||||
function reconciliationWhere(query: ReportListQuery): Prisma.DailyReconciliationReportWhereInput {
|
||||
return { reportDate: dateFilter(query.dateFrom, query.dateTo), tenantId: query.tenantId || undefined, applicationId: query.applicationId || undefined };
|
||||
}
|
||||
|
||||
function profitWhere(query: ReportListQuery) {
|
||||
const dimensionType = query.dimensionType === 'channel' ? 'channel' : 'application';
|
||||
const where: Prisma.DailyProfitReportWhereInput = {
|
||||
dimensionType,
|
||||
reportDate: dateFilter(query.dateFrom, query.dateTo),
|
||||
tenantId: dimensionType === 'application' ? query.tenantId || undefined : undefined,
|
||||
applicationId: dimensionType === 'application' ? query.applicationId || undefined : undefined,
|
||||
channelId: dimensionType === 'channel' ? query.channelId || undefined : undefined,
|
||||
};
|
||||
return { dimensionType, where };
|
||||
}
|
||||
|
||||
function qualityWhere(query: ReportListQuery) {
|
||||
const allowedDimensions = new Set(['application', 'channel', 'signature', 'drainage']);
|
||||
const dimensionType = allowedDimensions.has(String(query.dimensionType)) ? String(query.dimensionType) : 'application';
|
||||
const where: Prisma.DailyQualityReportWhereInput = {
|
||||
dimensionType,
|
||||
reportDate: dateFilter(query.dateFrom, query.dateTo),
|
||||
tenantId: query.tenantId || undefined,
|
||||
applicationId: query.applicationId || undefined,
|
||||
channelId: query.channelId || undefined,
|
||||
};
|
||||
return { dimensionType, where };
|
||||
}
|
||||
|
||||
function csvExport(name: string, headers: string[], rows: Array<Array<string | number>>) {
|
||||
const content = [headers, ...rows].map((row) => row.map(csvCell).join(',')).join('\n');
|
||||
return { fileName: `${name}-${shanghaiDateKey(new Date())}.csv`, content };
|
||||
}
|
||||
|
||||
function csvCell(value: string | number) {
|
||||
const text = String(value);
|
||||
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
||||
}
|
||||
|
||||
function dateKey(value: Date | string) {
|
||||
return (value instanceof Date ? value.toISOString() : String(value)).slice(0, 10);
|
||||
}
|
||||
|
||||
function formatCsvDate(value: Date | string) {
|
||||
return value instanceof Date ? value.toISOString() : String(value);
|
||||
}
|
||||
|
||||
function positiveInteger(value: string | undefined, fallback: number) {
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||
|
||||
Reference in New Issue
Block a user