feat: 优化签名热力图与金额展示
This commit is contained in:
@@ -56,18 +56,19 @@ export class ReportsService implements OnModuleInit, OnModuleDestroy {
|
||||
async listProfit(query: ReportListQuery) {
|
||||
const { page, pageSize, skip } = pagination(query);
|
||||
const { dimensionType, where } = profitWhere(query);
|
||||
const [items, total, aggregate] = await Promise.all([
|
||||
const [storedItems, total, aggregate] = await Promise.all([
|
||||
this.prisma.dailyProfitReport.findMany({ where, orderBy: [{ reportDate: 'desc' }, { dimensionName: 'asc' }], skip, take: pageSize }),
|
||||
this.prisma.dailyProfitReport.count({ where }),
|
||||
this.prisma.dailyProfitReport.aggregate({
|
||||
where,
|
||||
_sum: { ...reportVolumeSumSelection, revenueCents: true, refundCents: true, costCents: true, profitCents: true },
|
||||
_sum: { ...reportVolumeSumSelection, revenueCents: true, costCents: true, profitCents: true },
|
||||
}),
|
||||
]);
|
||||
// refundCents 暂留在持久化模型中兼容既有数据和回滚,但利润报表接口不再暴露返还口径。
|
||||
const items = storedItems.map(({ refundCents: _refundCents, ...item }) => item);
|
||||
const summary = {
|
||||
...volumeSummary(aggregate._sum),
|
||||
revenueCents: Number(aggregate._sum.revenueCents ?? 0),
|
||||
refundCents: Number(aggregate._sum.refundCents ?? 0),
|
||||
costCents: Number(aggregate._sum.costCents ?? 0),
|
||||
profitCents: Number(aggregate._sum.profitCents ?? 0),
|
||||
// 利润率必须用全量筛选结果的合计利润/合计收入重新计算,不能对每日百分比求和或简单平均。
|
||||
@@ -100,7 +101,7 @@ export class ReportsService implements OnModuleInit, OnModuleDestroy {
|
||||
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.submittedUnits, item.sentUnits, item.unknownUnits, item.successUnits, item.failedUnits, moneyUnitsToFixedYuan(item.revenueCents), moneyUnitsToFixedYuan(item.refundCents), moneyUnitsToFixedYuan(item.costCents), moneyUnitsToFixedYuan(item.profitCents), (item.profitRateBps / 100).toFixed(2), formatCsvDate(item.generatedAt)]));
|
||||
return csvExport(`利润报表-${dimensionType === 'channel' ? '通道' : '企业应用'}`, ['发送日期', '统计维度', '企业', '提交条数', '发送条数', '未知条数', '成功条数', '失败条数', '收入金额(元)', '成本金额(元)', '利润(元)', '利润率(%)', '生成时间'], items.map((item) => [dateKey(item.reportDate), item.dimensionName, item.tenantName ?? '', item.submittedUnits, item.sentUnits, item.unknownUnits, item.successUnits, item.failedUnits, moneyUnitsToFixedYuan(item.revenueCents), moneyUnitsToFixedYuan(item.costCents), moneyUnitsToFixedYuan(item.profitCents), (item.profitRateBps / 100).toFixed(2), formatCsvDate(item.generatedAt)]));
|
||||
}
|
||||
|
||||
async exportQuality(query: ReportListQuery) {
|
||||
@@ -169,13 +170,7 @@ export class ReportsService implements OnModuleInit, OnModuleDestroy {
|
||||
`);
|
||||
|
||||
await tx.$executeRaw(Prisma.sql`
|
||||
WITH billing AS (
|
||||
SELECT "messageId",
|
||||
SUM(CASE WHEN "billingStatus" = 'charged' THEN "amountCents" ELSE 0 END)::bigint AS revenue,
|
||||
SUM(CASE WHEN "billingStatus" = 'refunded' THEN "amountCents" ELSE 0 END)::bigint AS refund
|
||||
FROM "SmsBillingRecord"
|
||||
GROUP BY "messageId"
|
||||
), costs AS (
|
||||
WITH costs AS (
|
||||
SELECT
|
||||
submit."messageRecordId",
|
||||
SUM(submit."costUnitPrice" * CASE
|
||||
@@ -230,18 +225,24 @@ export class ReportsService implements OnModuleInit, OnModuleDestroy {
|
||||
COALESCE(SUM(CASE WHEN NOT (COALESCE(message.status = 'delivered', false) OR COALESCE(message."receiptStatus" = 'delivered', false))
|
||||
AND (COALESCE(message.status IN ('submit_failed', 'failed', 'timeout'), false) OR COALESCE(message."receiptStatus" = 'undelivered', false))
|
||||
THEN message."billingUnits" ELSE 0 END), 0)::integer,
|
||||
COALESCE(SUM(billing.revenue), 0)::bigint,
|
||||
COALESCE(SUM(billing.refund), 0)::bigint,
|
||||
-- 收入按每条最终成功短信的计费条数和发送时客户价快照计算,不能依赖随后可能变为 refunded 的账单状态。
|
||||
COALESCE(SUM(CASE WHEN message.status = 'delivered' OR message."receiptStatus" = 'delivered'
|
||||
THEN message."billingUnits" * message."unitPrice" ELSE 0 END), 0)::bigint,
|
||||
0::bigint,
|
||||
COALESCE(SUM(costs.cost), 0)::bigint,
|
||||
(COALESCE(SUM(billing.revenue), 0) - COALESCE(SUM(costs.cost), 0))::bigint,
|
||||
CASE WHEN COALESCE(SUM(billing.revenue), 0) = 0 THEN 0
|
||||
ELSE ROUND((COALESCE(SUM(billing.revenue), 0) - COALESCE(SUM(costs.cost), 0)) * 10000.0 / SUM(billing.revenue))::integer END,
|
||||
(COALESCE(SUM(CASE WHEN message.status = 'delivered' OR message."receiptStatus" = 'delivered'
|
||||
THEN message."billingUnits" * message."unitPrice" ELSE 0 END), 0) - COALESCE(SUM(costs.cost), 0))::bigint,
|
||||
CASE WHEN COALESCE(SUM(CASE WHEN message.status = 'delivered' OR message."receiptStatus" = 'delivered'
|
||||
THEN message."billingUnits" * message."unitPrice" ELSE 0 END), 0) = 0 THEN 0
|
||||
ELSE ROUND((COALESCE(SUM(CASE WHEN message.status = 'delivered' OR message."receiptStatus" = 'delivered'
|
||||
THEN message."billingUnits" * message."unitPrice" ELSE 0 END), 0) - COALESCE(SUM(costs.cost), 0)) * 10000.0 /
|
||||
SUM(CASE WHEN message.status = 'delivered' OR message."receiptStatus" = 'delivered'
|
||||
THEN message."billingUnits" * message."unitPrice" ELSE 0 END))::integer END,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM "SmsMessageRecord" message
|
||||
JOIN "Tenant" tenant ON tenant.id = message."tenantId"
|
||||
JOIN "SmsApplication" application ON application.id = message."applicationId"
|
||||
LEFT JOIN billing ON billing."messageId" = message."messageId"
|
||||
LEFT JOIN costs ON costs."messageRecordId" = message.id
|
||||
WHERE message."queuedAt" >= ${day.startAt}
|
||||
AND message."queuedAt" < ${day.endAt}
|
||||
@@ -249,13 +250,6 @@ export class ReportsService implements OnModuleInit, OnModuleDestroy {
|
||||
`);
|
||||
|
||||
await tx.$executeRaw(Prisma.sql`
|
||||
WITH billing AS (
|
||||
SELECT "messageId",
|
||||
SUM(CASE WHEN "billingStatus" = 'charged' THEN "amountCents" ELSE 0 END)::bigint AS revenue,
|
||||
SUM(CASE WHEN "billingStatus" = 'refunded' THEN "amountCents" ELSE 0 END)::bigint AS refund
|
||||
FROM "SmsBillingRecord"
|
||||
GROUP BY "messageId"
|
||||
)
|
||||
INSERT INTO "DailyProfitReport" (
|
||||
"id", "reportDate", "dimensionType", "dimensionId", "dimensionName",
|
||||
"tenantId", "tenantName", "applicationId", "channelId",
|
||||
@@ -297,31 +291,41 @@ export class ReportsService implements OnModuleInit, OnModuleDestroy {
|
||||
AND receipt."channelId" = submit."channelId"
|
||||
AND receipt."receiptStatus" = 'undelivered'
|
||||
) OR submit."submitStatus" IN ('rejected', 'timeout')) THEN message."billingUnits" ELSE 0 END), 0)::integer,
|
||||
COALESCE(SUM(CASE WHEN message."submitId" = submit."submitId" THEN billing.revenue ELSE 0 END), 0)::bigint,
|
||||
COALESCE(SUM(CASE WHEN message."submitId" = submit."submitId" THEN billing.refund ELSE 0 END), 0)::bigint,
|
||||
-- 补发可能产生多次提交,收入只归属最终成功提交,避免同一短信在多个通道重复计收。
|
||||
COALESCE(SUM(CASE WHEN message."submitId" = submit."submitId"
|
||||
AND (message.status = 'delivered' OR message."receiptStatus" = 'delivered')
|
||||
THEN message."billingUnits" * message."unitPrice" ELSE 0 END), 0)::bigint,
|
||||
0::bigint,
|
||||
COALESCE(SUM(submit."costUnitPrice" * CASE
|
||||
WHEN segment_receipts.audit_count > 0 THEN segment_receipts.delivered_count
|
||||
WHEN legacy_receipt.delivered THEN message."billingUnits"
|
||||
ELSE 0
|
||||
END), 0)::bigint,
|
||||
(COALESCE(SUM(CASE WHEN message."submitId" = submit."submitId" THEN billing.revenue ELSE 0 END), 0) - COALESCE(SUM(submit."costUnitPrice" * CASE
|
||||
(COALESCE(SUM(CASE WHEN message."submitId" = submit."submitId"
|
||||
AND (message.status = 'delivered' OR message."receiptStatus" = 'delivered')
|
||||
THEN message."billingUnits" * message."unitPrice" ELSE 0 END), 0) - COALESCE(SUM(submit."costUnitPrice" * CASE
|
||||
WHEN segment_receipts.audit_count > 0 THEN segment_receipts.delivered_count
|
||||
WHEN legacy_receipt.delivered THEN message."billingUnits"
|
||||
ELSE 0
|
||||
END), 0))::bigint,
|
||||
CASE WHEN COALESCE(SUM(CASE WHEN message."submitId" = submit."submitId" THEN billing.revenue ELSE 0 END), 0) = 0 THEN 0
|
||||
ELSE ROUND((COALESCE(SUM(CASE WHEN message."submitId" = submit."submitId" THEN billing.revenue ELSE 0 END), 0) - COALESCE(SUM(submit."costUnitPrice" * CASE
|
||||
CASE WHEN COALESCE(SUM(CASE WHEN message."submitId" = submit."submitId"
|
||||
AND (message.status = 'delivered' OR message."receiptStatus" = 'delivered')
|
||||
THEN message."billingUnits" * message."unitPrice" ELSE 0 END), 0) = 0 THEN 0
|
||||
ELSE ROUND((COALESCE(SUM(CASE WHEN message."submitId" = submit."submitId"
|
||||
AND (message.status = 'delivered' OR message."receiptStatus" = 'delivered')
|
||||
THEN message."billingUnits" * message."unitPrice" ELSE 0 END), 0) - COALESCE(SUM(submit."costUnitPrice" * CASE
|
||||
WHEN segment_receipts.audit_count > 0 THEN segment_receipts.delivered_count
|
||||
WHEN legacy_receipt.delivered THEN message."billingUnits"
|
||||
ELSE 0
|
||||
END), 0)) * 10000.0 /
|
||||
SUM(CASE WHEN message."submitId" = submit."submitId" THEN billing.revenue ELSE 0 END))::integer END,
|
||||
SUM(CASE WHEN message."submitId" = submit."submitId"
|
||||
AND (message.status = 'delivered' OR message."receiptStatus" = 'delivered')
|
||||
THEN message."billingUnits" * message."unitPrice" ELSE 0 END))::integer END,
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
FROM "SmsSubmitRecord" submit
|
||||
JOIN "SmsMessageRecord" message ON message.id = submit."messageRecordId"
|
||||
JOIN "SmsChannel" channel ON channel.id = submit."channelId"
|
||||
LEFT JOIN billing ON billing."messageId" = message."messageId"
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*)::integer AS audit_count,
|
||||
|
||||
Reference in New Issue
Block a user