merge: bring finance report api changes into dev
Some checks failed
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Failing after 26s
Some checks failed
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Failing after 26s
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表列表行 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceBusinessReportListItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 报表 ID。
|
||||
/// </summary>
|
||||
public string ReportId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 日期文案。
|
||||
/// </summary>
|
||||
public string DateText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 营业额。
|
||||
/// </summary>
|
||||
public decimal RevenueAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单数。
|
||||
/// </summary>
|
||||
public int OrderCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客单价。
|
||||
/// </summary>
|
||||
public decimal AverageOrderValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 退款率(百分数)。
|
||||
/// </summary>
|
||||
public decimal RefundRatePercent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 成本总额。
|
||||
/// </summary>
|
||||
public decimal CostTotalAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 净利润。
|
||||
/// </summary>
|
||||
public decimal NetProfitAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 利润率(百分数)。
|
||||
/// </summary>
|
||||
public decimal ProfitRatePercent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态编码。
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态文案。
|
||||
/// </summary>
|
||||
public string StatusText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否可下载。
|
||||
/// </summary>
|
||||
public bool CanDownload { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表列表结果 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceBusinessReportListResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 列表项。
|
||||
/// </summary>
|
||||
public List<FinanceBusinessReportListItemDto> Items { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 总数。
|
||||
/// </summary>
|
||||
public int Total { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表 KPI DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceBusinessReportKpiDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 指标键。
|
||||
/// </summary>
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 指标名称。
|
||||
/// </summary>
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 指标值文本。
|
||||
/// </summary>
|
||||
public string ValueText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 同比变化率(百分数)。
|
||||
/// </summary>
|
||||
public decimal YoyChangeRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 环比变化率(百分数)。
|
||||
/// </summary>
|
||||
public decimal MomChangeRate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表明细行 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceBusinessReportBreakdownItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 明细键。
|
||||
/// </summary>
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 明细名称。
|
||||
/// </summary>
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 占比(百分数)。
|
||||
/// </summary>
|
||||
public decimal RatioPercent { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表详情 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceBusinessReportDetailDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 报表 ID。
|
||||
/// </summary>
|
||||
public string ReportId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 标题。
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 周期类型编码。
|
||||
/// </summary>
|
||||
public string PeriodType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态编码。
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态文案。
|
||||
/// </summary>
|
||||
public string StatusText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 关键指标。
|
||||
/// </summary>
|
||||
public List<FinanceBusinessReportKpiDto> Kpis { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 收入明细(按渠道)。
|
||||
/// </summary>
|
||||
public List<FinanceBusinessReportBreakdownItemDto> IncomeBreakdowns { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 成本明细(按类别)。
|
||||
/// </summary>
|
||||
public List<FinanceBusinessReportBreakdownItemDto> CostBreakdowns { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表导出 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceBusinessReportExportDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件名。
|
||||
/// </summary>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Base64 文件内容。
|
||||
/// </summary>
|
||||
public string FileContentBase64 { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 总记录数。
|
||||
/// </summary>
|
||||
public int TotalCount { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Globalization;
|
||||
using System.IO.Compression;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
using TakeoutSaaS.Domain.Finance.Repositories;
|
||||
using TakeoutSaaS.Domain.Finance.Services;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表批量导出处理器(ZIP:PDF + Excel)。
|
||||
/// </summary>
|
||||
public sealed class ExportFinanceBusinessReportBatchQueryHandler(
|
||||
IFinanceBusinessReportRepository financeBusinessReportRepository,
|
||||
IFinanceBusinessReportExportService financeBusinessReportExportService,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ExportFinanceBusinessReportBatchQuery, FinanceBusinessReportExportDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceBusinessReportExportDto> Handle(
|
||||
ExportFinanceBusinessReportBatchQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取租户上下文并归一化分页参数。
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var normalizedPage = Math.Max(1, request.Page);
|
||||
var normalizedPageSize = Math.Clamp(request.PageSize, 1, 200);
|
||||
|
||||
// 2. 确保成本配置并补齐快照。
|
||||
await financeBusinessReportRepository.EnsureDefaultCostProfilesAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
cancellationToken);
|
||||
await financeBusinessReportRepository.QueueSnapshotsForPageAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.PeriodType,
|
||||
normalizedPage,
|
||||
normalizedPageSize,
|
||||
cancellationToken);
|
||||
|
||||
// 3. 查询导出明细集合(允许实时补算)。
|
||||
var details = await financeBusinessReportRepository.ListBatchDetailsAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.PeriodType,
|
||||
normalizedPage,
|
||||
normalizedPageSize,
|
||||
allowRealtimeBuild: true,
|
||||
cancellationToken);
|
||||
|
||||
// 4. 生成批量 PDF/Excel 并打包 ZIP。
|
||||
var periodCode = FinanceBusinessReportMapping.ToPeriodTypeCode(request.PeriodType);
|
||||
var zipBytes = await CreateZipAsync(
|
||||
details,
|
||||
periodCode,
|
||||
financeBusinessReportExportService,
|
||||
cancellationToken);
|
||||
|
||||
return new FinanceBusinessReportExportDto
|
||||
{
|
||||
FileName = string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"business-report-batch-{request.StoreId}-{periodCode}-{DateTime.UtcNow:yyyyMMddHHmmss}.zip"),
|
||||
FileContentBase64 = Convert.ToBase64String(zipBytes),
|
||||
TotalCount = details.Count
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<byte[]> CreateZipAsync(
|
||||
IReadOnlyList<Domain.Finance.Models.FinanceBusinessReportDetailSnapshot> details,
|
||||
string periodCode,
|
||||
IFinanceBusinessReportExportService exportService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (details.Count == 0)
|
||||
{
|
||||
using var emptyStream = new MemoryStream();
|
||||
using (var emptyArchive = new ZipArchive(emptyStream, ZipArchiveMode.Create, true))
|
||||
{
|
||||
var entry = emptyArchive.CreateEntry("README.txt");
|
||||
await using var writer = new StreamWriter(entry.Open());
|
||||
await writer.WriteAsync("No business report data in current selection.");
|
||||
}
|
||||
|
||||
return emptyStream.ToArray();
|
||||
}
|
||||
|
||||
var pdfBytes = await exportService.ExportBatchPdfAsync(details, cancellationToken);
|
||||
var excelBytes = await exportService.ExportBatchExcelAsync(details, cancellationToken);
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, true))
|
||||
{
|
||||
var pdfEntry = archive.CreateEntry($"business-report-{periodCode}.pdf");
|
||||
await using (var pdfEntryStream = pdfEntry.Open())
|
||||
{
|
||||
await pdfEntryStream.WriteAsync(pdfBytes, cancellationToken);
|
||||
}
|
||||
|
||||
var excelEntry = archive.CreateEntry($"business-report-{periodCode}.xlsx");
|
||||
await using (var excelEntryStream = excelEntry.Open())
|
||||
{
|
||||
await excelEntryStream.WriteAsync(excelBytes, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Globalization;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
using TakeoutSaaS.Domain.Finance.Repositories;
|
||||
using TakeoutSaaS.Domain.Finance.Services;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表 Excel 导出处理器。
|
||||
/// </summary>
|
||||
public sealed class ExportFinanceBusinessReportExcelQueryHandler(
|
||||
IFinanceBusinessReportRepository financeBusinessReportRepository,
|
||||
IFinanceBusinessReportExportService financeBusinessReportExportService,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ExportFinanceBusinessReportExcelQuery, FinanceBusinessReportExportDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceBusinessReportExportDto> Handle(
|
||||
ExportFinanceBusinessReportExcelQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取租户上下文并确保成本配置存在。
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
await financeBusinessReportRepository.EnsureDefaultCostProfilesAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
cancellationToken);
|
||||
|
||||
// 2. 查询报表详情(允许实时补算)。
|
||||
var detail = await financeBusinessReportRepository.GetDetailAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.ReportId,
|
||||
allowRealtimeBuild: true,
|
||||
cancellationToken);
|
||||
|
||||
if (detail is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "经营报表不存在");
|
||||
}
|
||||
|
||||
// 3. 导出 Excel 并返回 Base64。
|
||||
var fileBytes = await financeBusinessReportExportService.ExportSingleExcelAsync(detail, cancellationToken);
|
||||
var periodCode = FinanceBusinessReportMapping.ToPeriodTypeCode(detail.PeriodType);
|
||||
return new FinanceBusinessReportExportDto
|
||||
{
|
||||
FileName = string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"business-report-{request.StoreId}-{periodCode}-{request.ReportId}.xlsx"),
|
||||
FileContentBase64 = Convert.ToBase64String(fileBytes),
|
||||
TotalCount = 1
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Globalization;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
using TakeoutSaaS.Domain.Finance.Repositories;
|
||||
using TakeoutSaaS.Domain.Finance.Services;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表 PDF 导出处理器。
|
||||
/// </summary>
|
||||
public sealed class ExportFinanceBusinessReportPdfQueryHandler(
|
||||
IFinanceBusinessReportRepository financeBusinessReportRepository,
|
||||
IFinanceBusinessReportExportService financeBusinessReportExportService,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ExportFinanceBusinessReportPdfQuery, FinanceBusinessReportExportDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceBusinessReportExportDto> Handle(
|
||||
ExportFinanceBusinessReportPdfQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取租户上下文并确保成本配置存在。
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
await financeBusinessReportRepository.EnsureDefaultCostProfilesAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
cancellationToken);
|
||||
|
||||
// 2. 查询报表详情(允许实时补算)。
|
||||
var detail = await financeBusinessReportRepository.GetDetailAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.ReportId,
|
||||
allowRealtimeBuild: true,
|
||||
cancellationToken);
|
||||
|
||||
if (detail is null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "经营报表不存在");
|
||||
}
|
||||
|
||||
// 3. 导出 PDF 并返回 Base64。
|
||||
var fileBytes = await financeBusinessReportExportService.ExportSinglePdfAsync(detail, cancellationToken);
|
||||
var periodCode = FinanceBusinessReportMapping.ToPeriodTypeCode(detail.PeriodType);
|
||||
return new FinanceBusinessReportExportDto
|
||||
{
|
||||
FileName = string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"business-report-{request.StoreId}-{periodCode}-{request.ReportId}.pdf"),
|
||||
FileContentBase64 = Convert.ToBase64String(fileBytes),
|
||||
TotalCount = 1
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
using System.Globalization;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Dto;
|
||||
using TakeoutSaaS.Domain.Finance.Enums;
|
||||
using TakeoutSaaS.Domain.Finance.Models;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表映射工具。
|
||||
/// </summary>
|
||||
internal static class FinanceBusinessReportMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// 映射列表行 DTO。
|
||||
/// </summary>
|
||||
public static FinanceBusinessReportListItemDto ToListItem(FinanceBusinessReportListItemSnapshot source)
|
||||
{
|
||||
return new FinanceBusinessReportListItemDto
|
||||
{
|
||||
ReportId = source.ReportId.ToString(CultureInfo.InvariantCulture),
|
||||
DateText = FormatPeriodText(source.PeriodType, source.PeriodStartAt, source.PeriodEndAt),
|
||||
RevenueAmount = RoundMoney(source.RevenueAmount),
|
||||
OrderCount = Math.Max(0, source.OrderCount),
|
||||
AverageOrderValue = RoundMoney(source.AverageOrderValue),
|
||||
RefundRatePercent = RoundPercent(source.RefundRate),
|
||||
CostTotalAmount = RoundMoney(source.CostTotalAmount),
|
||||
NetProfitAmount = RoundMoney(source.NetProfitAmount),
|
||||
ProfitRatePercent = RoundPercent(source.ProfitRate),
|
||||
Status = ToStatusCode(source.Status),
|
||||
StatusText = ToStatusText(source.Status),
|
||||
CanDownload = source.Status == FinanceBusinessReportStatus.Succeeded
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 映射详情 DTO。
|
||||
/// </summary>
|
||||
public static FinanceBusinessReportDetailDto ToDetail(FinanceBusinessReportDetailSnapshot source)
|
||||
{
|
||||
return new FinanceBusinessReportDetailDto
|
||||
{
|
||||
ReportId = source.ReportId.ToString(CultureInfo.InvariantCulture),
|
||||
Title = BuildTitle(source.PeriodType, source.PeriodStartAt, source.PeriodEndAt),
|
||||
PeriodType = ToPeriodTypeCode(source.PeriodType),
|
||||
Status = ToStatusCode(source.Status),
|
||||
StatusText = ToStatusText(source.Status),
|
||||
Kpis = source.Kpis.Select(ToKpi).ToList(),
|
||||
IncomeBreakdowns = source.IncomeBreakdowns.Select(ToBreakdown).ToList(),
|
||||
CostBreakdowns = source.CostBreakdowns.Select(ToBreakdown).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 周期类型编码。
|
||||
/// </summary>
|
||||
public static string ToPeriodTypeCode(FinanceBusinessReportPeriodType value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
FinanceBusinessReportPeriodType.Daily => "daily",
|
||||
FinanceBusinessReportPeriodType.Weekly => "weekly",
|
||||
FinanceBusinessReportPeriodType.Monthly => "monthly",
|
||||
_ => "daily"
|
||||
};
|
||||
}
|
||||
|
||||
private static FinanceBusinessReportKpiDto ToKpi(FinanceBusinessReportKpiSnapshot source)
|
||||
{
|
||||
return new FinanceBusinessReportKpiDto
|
||||
{
|
||||
Key = source.Key,
|
||||
Label = source.Label,
|
||||
ValueText = FormatKpiValue(source.Key, source.Value),
|
||||
YoyChangeRate = RoundRate(source.YoyChangeRate),
|
||||
MomChangeRate = RoundRate(source.MomChangeRate)
|
||||
};
|
||||
}
|
||||
|
||||
private static FinanceBusinessReportBreakdownItemDto ToBreakdown(FinanceBusinessReportBreakdownSnapshot source)
|
||||
{
|
||||
return new FinanceBusinessReportBreakdownItemDto
|
||||
{
|
||||
Key = source.Key,
|
||||
Label = source.Label,
|
||||
Amount = RoundMoney(source.Amount),
|
||||
RatioPercent = RoundPercent(source.Ratio)
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatPeriodText(
|
||||
FinanceBusinessReportPeriodType periodType,
|
||||
DateTime startAt,
|
||||
DateTime endAt)
|
||||
{
|
||||
return periodType switch
|
||||
{
|
||||
FinanceBusinessReportPeriodType.Daily => startAt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
FinanceBusinessReportPeriodType.Weekly =>
|
||||
$"{startAt:MM-dd} ~ {endAt.AddDays(-1):MM-dd}",
|
||||
FinanceBusinessReportPeriodType.Monthly => startAt.ToString("yyyy年M月", CultureInfo.InvariantCulture),
|
||||
_ => startAt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildTitle(
|
||||
FinanceBusinessReportPeriodType periodType,
|
||||
DateTime startAt,
|
||||
DateTime endAt)
|
||||
{
|
||||
return periodType switch
|
||||
{
|
||||
FinanceBusinessReportPeriodType.Daily => $"{startAt:yyyy年M月d日} 经营日报",
|
||||
FinanceBusinessReportPeriodType.Weekly => $"{startAt:yyyy年M月d日}~{endAt.AddDays(-1):M月d日} 经营周报",
|
||||
FinanceBusinessReportPeriodType.Monthly => $"{startAt:yyyy年M月} 经营月报",
|
||||
_ => "经营报表"
|
||||
};
|
||||
}
|
||||
|
||||
private static string ToStatusCode(FinanceBusinessReportStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
FinanceBusinessReportStatus.Queued => "queued",
|
||||
FinanceBusinessReportStatus.Running => "running",
|
||||
FinanceBusinessReportStatus.Succeeded => "succeeded",
|
||||
FinanceBusinessReportStatus.Failed => "failed",
|
||||
_ => "queued"
|
||||
};
|
||||
}
|
||||
|
||||
private static string ToStatusText(FinanceBusinessReportStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
FinanceBusinessReportStatus.Queued => "排队中",
|
||||
FinanceBusinessReportStatus.Running => "生成中",
|
||||
FinanceBusinessReportStatus.Succeeded => "已生成",
|
||||
FinanceBusinessReportStatus.Failed => "生成失败",
|
||||
_ => "排队中"
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatKpiValue(string key, decimal value)
|
||||
{
|
||||
if (key is "order_count")
|
||||
{
|
||||
return Math.Round(value, 0, MidpointRounding.AwayFromZero).ToString("0", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (key is "refund_rate" or "profit_rate")
|
||||
{
|
||||
return $"{RoundPercent(value):0.##}%";
|
||||
}
|
||||
|
||||
if (key is "average_order_value")
|
||||
{
|
||||
return $"¥{RoundMoney(value):0.##}";
|
||||
}
|
||||
|
||||
return $"¥{RoundMoney(value):0.##}";
|
||||
}
|
||||
|
||||
private static decimal RoundMoney(decimal value)
|
||||
{
|
||||
return decimal.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static decimal RoundPercent(decimal value)
|
||||
{
|
||||
return decimal.Round(value * 100m, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static decimal RoundRate(decimal value)
|
||||
{
|
||||
return decimal.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
using TakeoutSaaS.Domain.Finance.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表详情查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceBusinessReportDetailQueryHandler(
|
||||
IFinanceBusinessReportRepository financeBusinessReportRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetFinanceBusinessReportDetailQuery, FinanceBusinessReportDetailDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceBusinessReportDetailDto?> Handle(
|
||||
GetFinanceBusinessReportDetailQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取租户上下文并确保成本配置存在。
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
await financeBusinessReportRepository.EnsureDefaultCostProfilesAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
cancellationToken);
|
||||
|
||||
// 2. 查询详情(允许实时补算)并映射输出。
|
||||
var detail = await financeBusinessReportRepository.GetDetailAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.ReportId,
|
||||
allowRealtimeBuild: true,
|
||||
cancellationToken);
|
||||
|
||||
return detail is null ? null : FinanceBusinessReportMapping.ToDetail(detail);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
using TakeoutSaaS.Domain.Finance.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表分页查询处理器。
|
||||
/// </summary>
|
||||
public sealed class SearchFinanceBusinessReportListQueryHandler(
|
||||
IFinanceBusinessReportRepository financeBusinessReportRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<SearchFinanceBusinessReportListQuery, FinanceBusinessReportListResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceBusinessReportListResultDto> Handle(
|
||||
SearchFinanceBusinessReportListQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 读取租户上下文并归一化分页参数。
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var normalizedPage = Math.Max(1, request.Page);
|
||||
var normalizedPageSize = Math.Clamp(request.PageSize, 1, 200);
|
||||
|
||||
// 2. 确保成本配置并补齐分页周期快照。
|
||||
await financeBusinessReportRepository.EnsureDefaultCostProfilesAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
cancellationToken);
|
||||
await financeBusinessReportRepository.QueueSnapshotsForPageAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.PeriodType,
|
||||
normalizedPage,
|
||||
normalizedPageSize,
|
||||
cancellationToken);
|
||||
|
||||
// 3. 查询分页快照并映射输出。
|
||||
var pageSnapshot = await financeBusinessReportRepository.SearchPageAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.PeriodType,
|
||||
normalizedPage,
|
||||
normalizedPageSize,
|
||||
cancellationToken);
|
||||
|
||||
return new FinanceBusinessReportListResultDto
|
||||
{
|
||||
Items = pageSnapshot.Items.Select(FinanceBusinessReportMapping.ToListItem).ToList(),
|
||||
Total = pageSnapshot.TotalCount,
|
||||
Page = normalizedPage,
|
||||
PageSize = normalizedPageSize
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Dto;
|
||||
using TakeoutSaaS.Domain.Finance.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 批量导出经营报表(ZIP:PDF + Excel)。
|
||||
/// </summary>
|
||||
public sealed class ExportFinanceBusinessReportBatchQuery : IRequest<FinanceBusinessReportExportDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 周期类型。
|
||||
/// </summary>
|
||||
public FinanceBusinessReportPeriodType PeriodType { get; init; } = FinanceBusinessReportPeriodType.Daily;
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 导出经营报表 Excel。
|
||||
/// </summary>
|
||||
public sealed class ExportFinanceBusinessReportExcelQuery : IRequest<FinanceBusinessReportExportDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 报表 ID。
|
||||
/// </summary>
|
||||
public long ReportId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 导出经营报表 PDF。
|
||||
/// </summary>
|
||||
public sealed class ExportFinanceBusinessReportPdfQuery : IRequest<FinanceBusinessReportExportDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 报表 ID。
|
||||
/// </summary>
|
||||
public long ReportId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询经营报表详情。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceBusinessReportDetailQuery : IRequest<FinanceBusinessReportDetailDto?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 报表 ID。
|
||||
/// </summary>
|
||||
public long ReportId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Dto;
|
||||
using TakeoutSaaS.Domain.Finance.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询经营报表分页列表。
|
||||
/// </summary>
|
||||
public sealed class SearchFinanceBusinessReportListQuery : IRequest<FinanceBusinessReportListResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 周期类型。
|
||||
/// </summary>
|
||||
public FinanceBusinessReportPeriodType PeriodType { get; init; } = FinanceBusinessReportPeriodType.Daily;
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 20;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表批量导出查询验证器。
|
||||
/// </summary>
|
||||
public sealed class ExportFinanceBusinessReportBatchQueryValidator : AbstractValidator<ExportFinanceBusinessReportBatchQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public ExportFinanceBusinessReportBatchQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.Page).GreaterThan(0);
|
||||
RuleFor(x => x.PageSize).InclusiveBetween(1, 200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表 Excel 导出查询验证器。
|
||||
/// </summary>
|
||||
public sealed class ExportFinanceBusinessReportExcelQueryValidator : AbstractValidator<ExportFinanceBusinessReportExcelQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public ExportFinanceBusinessReportExcelQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.ReportId).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表 PDF 导出查询验证器。
|
||||
/// </summary>
|
||||
public sealed class ExportFinanceBusinessReportPdfQueryValidator : AbstractValidator<ExportFinanceBusinessReportPdfQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public ExportFinanceBusinessReportPdfQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.ReportId).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表详情查询验证器。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceBusinessReportDetailQueryValidator : AbstractValidator<GetFinanceBusinessReportDetailQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public GetFinanceBusinessReportDetailQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.ReportId).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Finance.Reports.Queries;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Reports.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 经营报表分页查询验证器。
|
||||
/// </summary>
|
||||
public sealed class SearchFinanceBusinessReportListQueryValidator : AbstractValidator<SearchFinanceBusinessReportListQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public SearchFinanceBusinessReportListQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.Page).GreaterThan(0);
|
||||
RuleFor(x => x.PageSize).InclusiveBetween(1, 200);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user