feat(finance): add tenant settlement query backend

This commit is contained in:
2026-03-04 15:48:37 +08:00
parent 39e28c1a62
commit b0bb87d97c
25 changed files with 11599 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
namespace TakeoutSaaS.Application.App.Finance.Settlement.Dto;
/// <summary>
/// 到账查询汇总行 DTO。
/// </summary>
public sealed class FinanceSettlementListItemDto
{
/// <summary>
/// 到账日期UTC 日期)。
/// </summary>
public DateTime ArrivedDate { get; set; }
/// <summary>
/// 渠道编码wechat/alipay
/// </summary>
public string Channel { get; set; } = string.Empty;
/// <summary>
/// 渠道文案。
/// </summary>
public string ChannelText { get; set; } = string.Empty;
/// <summary>
/// 交易笔数。
/// </summary>
public int TransactionCount { get; set; }
/// <summary>
/// 到账金额。
/// </summary>
public decimal ArrivedAmount { get; set; }
}
/// <summary>
/// 到账查询分页结果 DTO。
/// </summary>
public sealed class FinanceSettlementListResultDto
{
/// <summary>
/// 列表项。
/// </summary>
public List<FinanceSettlementListItemDto> 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>
/// 到账明细行 DTO。
/// </summary>
public sealed class FinanceSettlementDetailItemDto
{
/// <summary>
/// 订单号。
/// </summary>
public string OrderNo { get; set; } = string.Empty;
/// <summary>
/// 金额。
/// </summary>
public decimal Amount { get; set; }
/// <summary>
/// 支付时间UTC
/// </summary>
public DateTime PaidAt { get; set; }
}
/// <summary>
/// 到账明细结果 DTO。
/// </summary>
public sealed class FinanceSettlementDetailResultDto
{
/// <summary>
/// 明细列表。
/// </summary>
public List<FinanceSettlementDetailItemDto> Items { get; set; } = [];
}
/// <summary>
/// 到账统计 DTO。
/// </summary>
public sealed class FinanceSettlementStatsDto
{
/// <summary>
/// 今日到账金额。
/// </summary>
public decimal TodayArrivedAmount { get; set; }
/// <summary>
/// 昨日到账金额。
/// </summary>
public decimal YesterdayArrivedAmount { get; set; }
/// <summary>
/// 本月到账金额。
/// </summary>
public decimal CurrentMonthArrivedAmount { get; set; }
/// <summary>
/// 本月交易笔数。
/// </summary>
public int CurrentMonthTransactionCount { get; set; }
}
/// <summary>
/// 到账账户信息 DTO。
/// </summary>
public sealed class FinanceSettlementAccountDto
{
/// <summary>
/// 银行名称。
/// </summary>
public string BankName { get; set; } = string.Empty;
/// <summary>
/// 开户名。
/// </summary>
public string BankAccountName { get; set; } = string.Empty;
/// <summary>
/// 脱敏银行账号。
/// </summary>
public string BankAccountNoMasked { get; set; } = string.Empty;
/// <summary>
/// 脱敏微信商户号。
/// </summary>
public string WechatMerchantNoMasked { get; set; } = string.Empty;
/// <summary>
/// 脱敏支付宝 PID。
/// </summary>
public string AlipayPidMasked { get; set; } = string.Empty;
/// <summary>
/// 结算周期文案。
/// </summary>
public string SettlementPeriodText { get; set; } = string.Empty;
}
/// <summary>
/// 到账导出 DTO。
/// </summary>
public sealed class FinanceSettlementExportDto
{
/// <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; }
}

View File

@@ -0,0 +1,71 @@
using System.Globalization;
using System.Text;
using MediatR;
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
using TakeoutSaaS.Domain.Finance.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
/// <summary>
/// 到账汇总导出处理器。
/// </summary>
public sealed class ExportFinanceSettlementCsvQueryHandler(
IFinanceTransactionRepository financeTransactionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<ExportFinanceSettlementCsvQuery, FinanceSettlementExportDto>
{
/// <inheritdoc />
public async Task<FinanceSettlementExportDto> Handle(
ExportFinanceSettlementCsvQuery request,
CancellationToken cancellationToken)
{
var tenantId = tenantProvider.GetCurrentTenantId();
var rows = await financeTransactionRepository.ListSettlementForExportAsync(
tenantId,
request.StoreId,
request.StartAt,
request.EndAt,
request.PaymentMethod,
cancellationToken);
var list = rows.Select(FinanceSettlementMapping.ToListItem).ToList();
var csv = BuildCsv(list);
return new FinanceSettlementExportDto
{
FileName = $"settlement-{request.StoreId}-{DateTime.UtcNow:yyyyMMddHHmmss}.csv",
FileContentBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(csv)),
TotalCount = list.Count
};
}
private static string BuildCsv(IReadOnlyList<FinanceSettlementListItemDto> rows)
{
var sb = new StringBuilder();
sb.Append('\uFEFF');
sb.AppendLine("到账日期,支付渠道,交易笔数,到账金额");
foreach (var row in rows)
{
sb.AppendLine(string.Join(',',
Escape(row.ArrivedDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)),
Escape(row.ChannelText),
Escape(row.TransactionCount.ToString(CultureInfo.InvariantCulture)),
Escape(FinanceSettlementMapping.FormatAmount(row.ArrivedAmount))));
}
return sb.ToString();
}
private static string Escape(string? value)
{
var normalized = value ?? string.Empty;
if (normalized.Contains(',') || normalized.Contains('"') || normalized.Contains('\n'))
{
return $"\"{normalized.Replace("\"", "\"\"", StringComparison.Ordinal)}\"";
}
return normalized;
}
}

View File

@@ -0,0 +1,75 @@
using System.Globalization;
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
using TakeoutSaaS.Domain.Finance.Models;
using TakeoutSaaS.Domain.Payments.Enums;
namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
/// <summary>
/// 到账查询映射辅助。
/// </summary>
internal static class FinanceSettlementMapping
{
/// <summary>
/// 支付方式转渠道编码。
/// </summary>
public static string ToChannelCode(PaymentMethod paymentMethod)
{
return paymentMethod switch
{
PaymentMethod.WeChatPay => "wechat",
PaymentMethod.Alipay => "alipay",
_ => "unknown"
};
}
/// <summary>
/// 支付方式转渠道文案。
/// </summary>
public static string ToChannelText(PaymentMethod paymentMethod)
{
return paymentMethod switch
{
PaymentMethod.WeChatPay => "微信支付",
PaymentMethod.Alipay => "支付宝",
_ => "未知渠道"
};
}
/// <summary>
/// 映射到账汇总行。
/// </summary>
public static FinanceSettlementListItemDto ToListItem(FinanceSettlementListItemSnapshot source)
{
return new FinanceSettlementListItemDto
{
ArrivedDate = source.ArrivedDate,
Channel = ToChannelCode(source.PaymentMethod),
ChannelText = ToChannelText(source.PaymentMethod),
TransactionCount = source.TransactionCount,
ArrivedAmount = decimal.Round(source.ArrivedAmount, 2, MidpointRounding.AwayFromZero)
};
}
/// <summary>
/// 映射到账明细行。
/// </summary>
public static FinanceSettlementDetailItemDto ToDetailItem(FinanceSettlementDetailItemSnapshot source)
{
return new FinanceSettlementDetailItemDto
{
OrderNo = source.OrderNo,
Amount = decimal.Round(source.Amount, 2, MidpointRounding.AwayFromZero),
PaidAt = source.PaidAt
};
}
/// <summary>
/// 格式化金额(导出场景)。
/// </summary>
public static string FormatAmount(decimal value)
{
return decimal.Round(value, 2, MidpointRounding.AwayFromZero)
.ToString("0.00", CultureInfo.InvariantCulture);
}
}

View File

@@ -0,0 +1,42 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
using TakeoutSaaS.Domain.Finance.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
/// <summary>
/// 到账账户信息查询处理器。
/// </summary>
public sealed class GetFinanceSettlementAccountQueryHandler(
IFinanceTransactionRepository financeTransactionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<GetFinanceSettlementAccountQuery, FinanceSettlementAccountDto?>
{
/// <inheritdoc />
public async Task<FinanceSettlementAccountDto?> Handle(
GetFinanceSettlementAccountQuery request,
CancellationToken cancellationToken)
{
var tenantId = tenantProvider.GetCurrentTenantId();
var snapshot = await financeTransactionRepository.GetSettlementAccountAsync(
tenantId,
cancellationToken);
if (snapshot is null)
{
return null;
}
return new FinanceSettlementAccountDto
{
BankName = snapshot.BankName,
BankAccountName = snapshot.BankAccountName,
BankAccountNoMasked = snapshot.BankAccountNoMasked,
WechatMerchantNoMasked = snapshot.WechatMerchantNoMasked,
AlipayPidMasked = snapshot.AlipayPidMasked,
SettlementPeriodText = snapshot.SettlementPeriodText
};
}
}

View File

@@ -0,0 +1,36 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
using TakeoutSaaS.Domain.Finance.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
/// <summary>
/// 到账明细查询处理器。
/// </summary>
public sealed class GetFinanceSettlementDetailQueryHandler(
IFinanceTransactionRepository financeTransactionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<GetFinanceSettlementDetailQuery, FinanceSettlementDetailResultDto>
{
/// <inheritdoc />
public async Task<FinanceSettlementDetailResultDto> Handle(
GetFinanceSettlementDetailQuery request,
CancellationToken cancellationToken)
{
var tenantId = tenantProvider.GetCurrentTenantId();
var rows = await financeTransactionRepository.GetSettlementDetailsAsync(
tenantId,
request.StoreId,
request.ArrivedDate,
request.PaymentMethod,
request.Take,
cancellationToken);
return new FinanceSettlementDetailResultDto
{
Items = rows.Select(FinanceSettlementMapping.ToDetailItem).ToList()
};
}
}

View File

@@ -0,0 +1,37 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
using TakeoutSaaS.Domain.Finance.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
/// <summary>
/// 到账统计查询处理器。
/// </summary>
public sealed class GetFinanceSettlementStatsQueryHandler(
IFinanceTransactionRepository financeTransactionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<GetFinanceSettlementStatsQuery, FinanceSettlementStatsDto>
{
/// <inheritdoc />
public async Task<FinanceSettlementStatsDto> Handle(
GetFinanceSettlementStatsQuery request,
CancellationToken cancellationToken)
{
var tenantId = tenantProvider.GetCurrentTenantId();
var snapshot = await financeTransactionRepository.GetSettlementStatsAsync(
tenantId,
request.StoreId,
DateTime.UtcNow,
cancellationToken);
return new FinanceSettlementStatsDto
{
TodayArrivedAmount = snapshot.TodayArrivedAmount,
YesterdayArrivedAmount = snapshot.YesterdayArrivedAmount,
CurrentMonthArrivedAmount = snapshot.CurrentMonthArrivedAmount,
CurrentMonthTransactionCount = snapshot.CurrentMonthTransactionCount
};
}
}

View File

@@ -0,0 +1,44 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
using TakeoutSaaS.Domain.Finance.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
/// <summary>
/// 到账汇总分页查询处理器。
/// </summary>
public sealed class SearchFinanceSettlementListQueryHandler(
IFinanceTransactionRepository financeTransactionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<SearchFinanceSettlementListQuery, FinanceSettlementListResultDto>
{
/// <inheritdoc />
public async Task<FinanceSettlementListResultDto> Handle(
SearchFinanceSettlementListQuery request,
CancellationToken cancellationToken)
{
var tenantId = tenantProvider.GetCurrentTenantId();
var normalizedPage = Math.Max(1, request.Page);
var normalizedPageSize = Math.Clamp(request.PageSize, 1, 200);
var snapshot = await financeTransactionRepository.SearchSettlementPageAsync(
tenantId,
request.StoreId,
request.StartAt,
request.EndAt,
request.PaymentMethod,
normalizedPage,
normalizedPageSize,
cancellationToken);
return new FinanceSettlementListResultDto
{
Items = snapshot.Items.Select(FinanceSettlementMapping.ToListItem).ToList(),
Total = snapshot.TotalCount,
Page = normalizedPage,
PageSize = normalizedPageSize
};
}
}

View File

@@ -0,0 +1,31 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
using TakeoutSaaS.Domain.Payments.Enums;
namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
/// <summary>
/// 导出到账汇总 CSV。
/// </summary>
public sealed class ExportFinanceSettlementCsvQuery : IRequest<FinanceSettlementExportDto>
{
/// <summary>
/// 门店标识。
/// </summary>
public long StoreId { get; init; }
/// <summary>
/// 开始时间UTC闭区间
/// </summary>
public DateTime? StartAt { get; init; }
/// <summary>
/// 结束时间UTC开区间
/// </summary>
public DateTime? EndAt { get; init; }
/// <summary>
/// 支付方式筛选。
/// </summary>
public PaymentMethod? PaymentMethod { get; init; }
}

View File

@@ -0,0 +1,11 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
/// <summary>
/// 查询到账账户信息。
/// </summary>
public sealed class GetFinanceSettlementAccountQuery : IRequest<FinanceSettlementAccountDto?>
{
}

View File

@@ -0,0 +1,31 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
using TakeoutSaaS.Domain.Payments.Enums;
namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
/// <summary>
/// 查询到账明细。
/// </summary>
public sealed class GetFinanceSettlementDetailQuery : IRequest<FinanceSettlementDetailResultDto>
{
/// <summary>
/// 门店标识。
/// </summary>
public long StoreId { get; init; }
/// <summary>
/// 到账日期UTC 日期)。
/// </summary>
public DateTime ArrivedDate { get; init; }
/// <summary>
/// 渠道(微信/支付宝)。
/// </summary>
public PaymentMethod PaymentMethod { get; init; }
/// <summary>
/// 限制条数。
/// </summary>
public int Take { get; init; } = 20;
}

View File

@@ -0,0 +1,15 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
/// <summary>
/// 查询到账统计。
/// </summary>
public sealed class GetFinanceSettlementStatsQuery : IRequest<FinanceSettlementStatsDto>
{
/// <summary>
/// 门店标识。
/// </summary>
public long StoreId { get; init; }
}

View File

@@ -0,0 +1,41 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
using TakeoutSaaS.Domain.Payments.Enums;
namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
/// <summary>
/// 查询到账汇总分页。
/// </summary>
public sealed class SearchFinanceSettlementListQuery : IRequest<FinanceSettlementListResultDto>
{
/// <summary>
/// 门店标识。
/// </summary>
public long StoreId { get; init; }
/// <summary>
/// 开始时间UTC闭区间
/// </summary>
public DateTime? StartAt { get; init; }
/// <summary>
/// 结束时间UTC开区间
/// </summary>
public DateTime? EndAt { get; init; }
/// <summary>
/// 支付方式筛选。
/// </summary>
public PaymentMethod? PaymentMethod { get; init; }
/// <summary>
/// 页码。
/// </summary>
public int Page { get; init; } = 1;
/// <summary>
/// 每页条数。
/// </summary>
public int PageSize { get; init; } = 20;
}