feat: 新增财务交易流水后端模块

This commit is contained in:
2026-03-04 11:33:29 +08:00
parent 2970134200
commit d437b146d1
24 changed files with 2602 additions and 0 deletions

View File

@@ -0,0 +1,256 @@
namespace TakeoutSaaS.Application.App.Finance.Transactions.Dto;
/// <summary>
/// 交易流水列表行。
/// </summary>
public sealed class FinanceTransactionListItemDto
{
/// <summary>
/// 交易标识。
/// </summary>
public string TransactionId { get; set; } = string.Empty;
/// <summary>
/// 流水号。
/// </summary>
public string TransactionNo { get; set; } = string.Empty;
/// <summary>
/// 关联订单号。
/// </summary>
public string? OrderNo { get; set; }
/// <summary>
/// 交易类型编码。
/// </summary>
public string TransactionType { get; set; } = string.Empty;
/// <summary>
/// 交易类型文案。
/// </summary>
public string TransactionTypeText { get; set; } = string.Empty;
/// <summary>
/// 渠道文案。
/// </summary>
public string ChannelText { get; set; } = string.Empty;
/// <summary>
/// 支付方式文案。
/// </summary>
public string PaymentMethodText { get; set; } = string.Empty;
/// <summary>
/// 交易金额(带符号)。
/// </summary>
public decimal AmountSigned { get; set; }
/// <summary>
/// 交易时间。
/// </summary>
public DateTime OccurredAt { get; set; }
/// <summary>
/// 备注。
/// </summary>
public string Remark { get; set; } = string.Empty;
/// <summary>
/// 是否收入。
/// </summary>
public bool IsIncome { get; set; }
}
/// <summary>
/// 交易流水列表结果。
/// </summary>
public sealed class FinanceTransactionListResultDto
{
/// <summary>
/// 分页数据。
/// </summary>
public List<FinanceTransactionListItemDto> 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>
/// 本页收入合计。
/// </summary>
public decimal PageIncomeAmount { get; set; }
/// <summary>
/// 本页退款合计。
/// </summary>
public decimal PageRefundAmount { get; set; }
}
/// <summary>
/// 交易流水统计。
/// </summary>
public sealed class FinanceTransactionStatsDto
{
/// <summary>
/// 总收入。
/// </summary>
public decimal TotalIncome { get; set; }
/// <summary>
/// 总退款。
/// </summary>
public decimal TotalRefund { get; set; }
/// <summary>
/// 交易笔数。
/// </summary>
public int TotalCount { get; set; }
}
/// <summary>
/// 交易流水详情。
/// </summary>
public sealed class FinanceTransactionDetailDto
{
/// <summary>
/// 交易标识。
/// </summary>
public string TransactionId { get; set; } = string.Empty;
/// <summary>
/// 流水号。
/// </summary>
public string TransactionNo { get; set; } = string.Empty;
/// <summary>
/// 交易类型编码。
/// </summary>
public string TransactionType { get; set; } = string.Empty;
/// <summary>
/// 交易类型文案。
/// </summary>
public string TransactionTypeText { get; set; } = string.Empty;
/// <summary>
/// 门店标识。
/// </summary>
public long StoreId { get; set; }
/// <summary>
/// 关联订单号。
/// </summary>
public string? OrderNo { get; set; }
/// <summary>
/// 渠道文案。
/// </summary>
public string ChannelText { get; set; } = string.Empty;
/// <summary>
/// 支付方式文案。
/// </summary>
public string PaymentMethodText { get; set; } = string.Empty;
/// <summary>
/// 交易金额(带符号)。
/// </summary>
public decimal AmountSigned { get; set; }
/// <summary>
/// 交易时间。
/// </summary>
public DateTime OccurredAt { get; set; }
/// <summary>
/// 备注。
/// </summary>
public string Remark { get; set; } = string.Empty;
/// <summary>
/// 顾客姓名。
/// </summary>
public string CustomerName { get; set; } = string.Empty;
/// <summary>
/// 顾客手机号。
/// </summary>
public string CustomerPhone { get; set; } = string.Empty;
/// <summary>
/// 退款单号。
/// </summary>
public string? RefundNo { get; set; }
/// <summary>
/// 退款原因。
/// </summary>
public string? RefundReason { get; set; }
/// <summary>
/// 会员名称。
/// </summary>
public string? MemberName { get; set; }
/// <summary>
/// 会员手机号脱敏值。
/// </summary>
public string? MemberMobileMasked { get; set; }
/// <summary>
/// 充值金额。
/// </summary>
public decimal? RechargeAmount { get; set; }
/// <summary>
/// 赠送金额。
/// </summary>
public decimal? GiftAmount { get; set; }
/// <summary>
/// 到账金额。
/// </summary>
public decimal? ArrivedAmount { get; set; }
/// <summary>
/// 积分变动值。
/// </summary>
public int? PointChangeAmount { get; set; }
/// <summary>
/// 积分变动后余额。
/// </summary>
public int? PointBalanceAfterChange { get; set; }
}
/// <summary>
/// 交易流水导出结果。
/// </summary>
public sealed class FinanceTransactionExportDto
{
/// <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,81 @@
using System.Globalization;
using System.Text;
using MediatR;
using TakeoutSaaS.Application.App.Finance.Transactions.Dto;
using TakeoutSaaS.Application.App.Finance.Transactions.Queries;
using TakeoutSaaS.Domain.Finance.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Handlers;
/// <summary>
/// 交易流水 CSV 导出查询处理器。
/// </summary>
public sealed class ExportFinanceTransactionCsvQueryHandler(
IFinanceTransactionRepository financeTransactionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<ExportFinanceTransactionCsvQuery, FinanceTransactionExportDto>
{
/// <inheritdoc />
public async Task<FinanceTransactionExportDto> Handle(ExportFinanceTransactionCsvQuery request, CancellationToken cancellationToken)
{
// 1. 按筛选读取导出数据。
var tenantId = tenantProvider.GetCurrentTenantId();
var records = await financeTransactionRepository.ListForExportAsync(
tenantId,
request.StoreId,
request.StartAt,
request.EndAt,
request.TransactionType,
request.DeliveryType,
request.PaymentMethod,
request.Keyword,
cancellationToken);
// 2. 组装 CSV 并输出 Base64。
var csv = BuildCsv(records.Select(FinanceTransactionMapping.ToListItem).ToList());
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(csv)).ToArray();
return new FinanceTransactionExportDto
{
FileName = $"交易流水_{DateTime.UtcNow:yyyyMMddHHmmss}.csv",
FileContentBase64 = Convert.ToBase64String(bytes),
TotalCount = records.Count
};
}
private static string BuildCsv(IReadOnlyList<FinanceTransactionListItemDto> rows)
{
var builder = new StringBuilder();
builder.AppendLine("流水号,关联订单,类型,渠道,支付方式,金额,交易时间,备注");
foreach (var row in rows)
{
var cells = new[]
{
Escape(row.TransactionNo),
Escape(string.IsNullOrWhiteSpace(row.OrderNo) ? "—" : row.OrderNo!),
Escape(row.TransactionTypeText),
Escape(row.ChannelText),
Escape(row.PaymentMethodText),
Escape(FinanceTransactionMapping.FormatAmount(row.AmountSigned)),
Escape(row.OccurredAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)),
Escape(string.IsNullOrWhiteSpace(row.Remark) ? "—" : row.Remark)
};
builder.AppendLine(string.Join(',', cells));
}
return builder.ToString();
}
private static string Escape(string value)
{
if (!value.Contains('"') && !value.Contains(',') && !value.Contains('\n') && !value.Contains('\r'))
{
return value;
}
return $"\"{value.Replace("\"", "\"\"")}\"";
}
}

View File

@@ -0,0 +1,155 @@
using System.Globalization;
using TakeoutSaaS.Application.App.Finance.Transactions.Dto;
using TakeoutSaaS.Domain.Finance.Enums;
using TakeoutSaaS.Domain.Finance.Models;
using TakeoutSaaS.Domain.Orders.Enums;
using TakeoutSaaS.Domain.Payments.Enums;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Handlers;
/// <summary>
/// 交易流水映射与文案转换。
/// </summary>
internal static class FinanceTransactionMapping
{
/// <summary>
/// 生成交易复合标识。
/// </summary>
public static string BuildTransactionId(FinanceTransactionSourceType sourceType, long sourceId)
{
var sourceCode = sourceType switch
{
FinanceTransactionSourceType.PaymentRecord => "payment",
FinanceTransactionSourceType.PaymentRefundRecord => "payment_refund",
FinanceTransactionSourceType.RefundRequest => "refund_request",
FinanceTransactionSourceType.StoredCardRechargeRecord => "stored_card_recharge",
FinanceTransactionSourceType.MemberPointLedger => "member_point",
_ => "unknown"
};
return $"{sourceCode}:{sourceId}";
}
/// <summary>
/// 解析交易类型编码。
/// </summary>
public static string ToTransactionTypeCode(FinanceTransactionType transactionType)
{
return transactionType switch
{
FinanceTransactionType.Income => "income",
FinanceTransactionType.Refund => "refund",
FinanceTransactionType.StoredCardRecharge => "stored_card_recharge",
FinanceTransactionType.PointRedeem => "point_redeem",
_ => "unknown"
};
}
/// <summary>
/// 解析交易类型文案。
/// </summary>
public static string ToTransactionTypeText(FinanceTransactionType transactionType)
{
return transactionType switch
{
FinanceTransactionType.Income => "收入",
FinanceTransactionType.Refund => "退款",
FinanceTransactionType.StoredCardRecharge => "储值充值",
FinanceTransactionType.PointRedeem => "积分抵扣",
_ => "未知"
};
}
/// <summary>
/// 解析渠道文案。
/// </summary>
public static string ToChannelText(DeliveryType? deliveryType)
{
return deliveryType switch
{
DeliveryType.Delivery => "外卖",
DeliveryType.Pickup => "自提",
DeliveryType.DineIn => "堂食",
_ => "—"
};
}
/// <summary>
/// 解析支付方式文案。
/// </summary>
public static string ToPaymentMethodText(PaymentMethod? paymentMethod)
{
return paymentMethod switch
{
PaymentMethod.WeChatPay => "微信",
PaymentMethod.Alipay => "支付宝",
PaymentMethod.Cash => "现金",
PaymentMethod.Card => "刷卡",
PaymentMethod.Balance => "储值余额",
_ => "—"
};
}
/// <summary>
/// 映射列表行。
/// </summary>
public static FinanceTransactionListItemDto ToListItem(FinanceTransactionRecord source)
{
return new FinanceTransactionListItemDto
{
TransactionId = BuildTransactionId(source.SourceType, source.SourceId),
TransactionNo = source.TransactionNo ?? string.Empty,
OrderNo = source.OrderNo,
TransactionType = ToTransactionTypeCode(source.TransactionType),
TransactionTypeText = ToTransactionTypeText(source.TransactionType),
ChannelText = ToChannelText(source.DeliveryType),
PaymentMethodText = ToPaymentMethodText(source.PaymentMethod),
AmountSigned = decimal.Round(source.AmountSigned, 2, MidpointRounding.AwayFromZero),
OccurredAt = source.OccurredAt,
Remark = string.IsNullOrWhiteSpace(source.Remark) ? "—" : source.Remark.Trim(),
IsIncome = source.AmountSigned > 0
};
}
/// <summary>
/// 映射详情。
/// </summary>
public static FinanceTransactionDetailDto ToDetail(FinanceTransactionRecord source)
{
return new FinanceTransactionDetailDto
{
TransactionId = BuildTransactionId(source.SourceType, source.SourceId),
TransactionNo = source.TransactionNo ?? string.Empty,
TransactionType = ToTransactionTypeCode(source.TransactionType),
TransactionTypeText = ToTransactionTypeText(source.TransactionType),
StoreId = source.StoreId,
OrderNo = source.OrderNo,
ChannelText = ToChannelText(source.DeliveryType),
PaymentMethodText = ToPaymentMethodText(source.PaymentMethod),
AmountSigned = decimal.Round(source.AmountSigned, 2, MidpointRounding.AwayFromZero),
OccurredAt = source.OccurredAt,
Remark = string.IsNullOrWhiteSpace(source.Remark) ? "—" : source.Remark.Trim(),
CustomerName = string.IsNullOrWhiteSpace(source.CustomerName) ? "—" : source.CustomerName.Trim(),
CustomerPhone = string.IsNullOrWhiteSpace(source.CustomerPhone) ? "—" : source.CustomerPhone.Trim(),
RefundNo = source.RefundNo,
RefundReason = source.RefundReason,
MemberName = source.MemberName,
MemberMobileMasked = source.MemberMobileMasked,
RechargeAmount = source.RechargeAmount,
GiftAmount = source.GiftAmount,
ArrivedAmount = source.ArrivedAmount,
PointChangeAmount = source.PointChangeAmount,
PointBalanceAfterChange = source.PointBalanceAfterChange
};
}
/// <summary>
/// 导出金额文本。
/// </summary>
public static string FormatAmount(decimal amountSigned)
{
var rounded = decimal.Round(amountSigned, 2, MidpointRounding.AwayFromZero);
var sign = rounded >= 0 ? "+" : string.Empty;
return $"{sign}{rounded.ToString("0.00", CultureInfo.InvariantCulture)}";
}
}

View File

@@ -0,0 +1,32 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Transactions.Dto;
using TakeoutSaaS.Application.App.Finance.Transactions.Queries;
using TakeoutSaaS.Domain.Finance.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Handlers;
/// <summary>
/// 交易流水详情查询处理器。
/// </summary>
public sealed class GetFinanceTransactionDetailQueryHandler(
IFinanceTransactionRepository financeTransactionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<GetFinanceTransactionDetailQuery, FinanceTransactionDetailDto?>
{
/// <inheritdoc />
public async Task<FinanceTransactionDetailDto?> Handle(GetFinanceTransactionDetailQuery request, CancellationToken cancellationToken)
{
// 1. 读取租户上下文并查询详情。
var tenantId = tenantProvider.GetCurrentTenantId();
var record = await financeTransactionRepository.GetDetailAsync(
tenantId,
request.StoreId,
request.SourceType,
request.SourceId,
cancellationToken);
// 2. 映射详情输出。
return record is null ? null : FinanceTransactionMapping.ToDetail(record);
}
}

View File

@@ -0,0 +1,41 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Transactions.Dto;
using TakeoutSaaS.Application.App.Finance.Transactions.Queries;
using TakeoutSaaS.Domain.Finance.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Handlers;
/// <summary>
/// 交易流水统计查询处理器。
/// </summary>
public sealed class GetFinanceTransactionStatsQueryHandler(
IFinanceTransactionRepository financeTransactionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<GetFinanceTransactionStatsQuery, FinanceTransactionStatsDto>
{
/// <inheritdoc />
public async Task<FinanceTransactionStatsDto> Handle(GetFinanceTransactionStatsQuery request, CancellationToken cancellationToken)
{
// 1. 读取租户上下文并执行统计查询。
var tenantId = tenantProvider.GetCurrentTenantId();
var snapshot = await financeTransactionRepository.GetStatsAsync(
tenantId,
request.StoreId,
request.StartAt,
request.EndAt,
request.TransactionType,
request.DeliveryType,
request.PaymentMethod,
request.Keyword,
cancellationToken);
// 2. 映射统计结果。
return new FinanceTransactionStatsDto
{
TotalIncome = snapshot.TotalIncome,
TotalRefund = snapshot.TotalRefund,
TotalCount = snapshot.TotalCount
};
}
}

View File

@@ -0,0 +1,49 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Transactions.Dto;
using TakeoutSaaS.Application.App.Finance.Transactions.Queries;
using TakeoutSaaS.Domain.Finance.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Handlers;
/// <summary>
/// 交易流水列表查询处理器。
/// </summary>
public sealed class SearchFinanceTransactionListQueryHandler(
IFinanceTransactionRepository financeTransactionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<SearchFinanceTransactionListQuery, FinanceTransactionListResultDto>
{
/// <inheritdoc />
public async Task<FinanceTransactionListResultDto> Handle(SearchFinanceTransactionListQuery request, CancellationToken cancellationToken)
{
// 1. 读取租户上下文并执行分页查询。
var tenantId = tenantProvider.GetCurrentTenantId();
var page = Math.Max(1, request.Page);
var pageSize = Math.Clamp(request.PageSize, 1, 200);
var snapshot = await financeTransactionRepository.SearchPageAsync(
tenantId,
request.StoreId,
request.StartAt,
request.EndAt,
request.TransactionType,
request.DeliveryType,
request.PaymentMethod,
request.Keyword,
page,
pageSize,
cancellationToken);
// 2. 映射结果并返回。
return new FinanceTransactionListResultDto
{
Items = snapshot.Items.Select(FinanceTransactionMapping.ToListItem).ToList(),
Total = snapshot.TotalCount,
Page = page,
PageSize = pageSize,
PageIncomeAmount = snapshot.PageIncomeAmount,
PageRefundAmount = snapshot.PageRefundAmount
};
}
}

View File

@@ -0,0 +1,11 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Transactions.Dto;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Queries;
/// <summary>
/// 交易流水 CSV 导出查询。
/// </summary>
public sealed class ExportFinanceTransactionCsvQuery : FinanceTransactionFilterQueryBase, IRequest<FinanceTransactionExportDto>
{
}

View File

@@ -0,0 +1,46 @@
using TakeoutSaaS.Domain.Finance.Enums;
using TakeoutSaaS.Domain.Orders.Enums;
using TakeoutSaaS.Domain.Payments.Enums;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Queries;
/// <summary>
/// 交易流水筛选查询基类。
/// </summary>
public abstract class FinanceTransactionFilterQueryBase
{
/// <summary>
/// 门店 ID。
/// </summary>
public long StoreId { get; init; }
/// <summary>
/// 开始时间(含)。
/// </summary>
public DateTime? StartAt { get; init; }
/// <summary>
/// 结束时间(不含)。
/// </summary>
public DateTime? EndAt { get; init; }
/// <summary>
/// 交易类型。
/// </summary>
public FinanceTransactionType? TransactionType { get; init; }
/// <summary>
/// 渠道。
/// </summary>
public DeliveryType? DeliveryType { get; init; }
/// <summary>
/// 支付方式。
/// </summary>
public PaymentMethod? PaymentMethod { get; init; }
/// <summary>
/// 关键词。
/// </summary>
public string? Keyword { get; init; }
}

View File

@@ -0,0 +1,26 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Transactions.Dto;
using TakeoutSaaS.Domain.Finance.Enums;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Queries;
/// <summary>
/// 交易流水详情查询。
/// </summary>
public sealed class GetFinanceTransactionDetailQuery : IRequest<FinanceTransactionDetailDto?>
{
/// <summary>
/// 门店 ID。
/// </summary>
public long StoreId { get; init; }
/// <summary>
/// 来源类型。
/// </summary>
public FinanceTransactionSourceType SourceType { get; init; }
/// <summary>
/// 来源标识。
/// </summary>
public long SourceId { get; init; }
}

View File

@@ -0,0 +1,11 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Transactions.Dto;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Queries;
/// <summary>
/// 交易流水统计查询。
/// </summary>
public sealed class GetFinanceTransactionStatsQuery : FinanceTransactionFilterQueryBase, IRequest<FinanceTransactionStatsDto>
{
}

View File

@@ -0,0 +1,20 @@
using MediatR;
using TakeoutSaaS.Application.App.Finance.Transactions.Dto;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Queries;
/// <summary>
/// 交易流水列表查询。
/// </summary>
public sealed class SearchFinanceTransactionListQuery : FinanceTransactionFilterQueryBase, IRequest<FinanceTransactionListResultDto>
{
/// <summary>
/// 页码。
/// </summary>
public int Page { get; init; } = 1;
/// <summary>
/// 每页条数。
/// </summary>
public int PageSize { get; init; } = 20;
}

View File

@@ -0,0 +1,22 @@
using FluentValidation;
using TakeoutSaaS.Application.App.Finance.Transactions.Queries;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Validators;
/// <summary>
/// 交易流水导出查询验证器。
/// </summary>
public sealed class ExportFinanceTransactionCsvQueryValidator : AbstractValidator<ExportFinanceTransactionCsvQuery>
{
/// <summary>
/// 初始化验证规则。
/// </summary>
public ExportFinanceTransactionCsvQueryValidator()
{
RuleFor(x => x.StoreId).GreaterThan(0);
RuleFor(x => x.Keyword).MaximumLength(64);
RuleFor(x => x)
.Must(x => !x.StartAt.HasValue || !x.EndAt.HasValue || x.StartAt < x.EndAt)
.WithMessage("开始时间必须早于结束时间");
}
}

View File

@@ -0,0 +1,27 @@
using FluentValidation;
using TakeoutSaaS.Application.App.Finance.Transactions.Queries;
using TakeoutSaaS.Domain.Finance.Enums;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Validators;
/// <summary>
/// 交易流水详情查询验证器。
/// </summary>
public sealed class GetFinanceTransactionDetailQueryValidator : AbstractValidator<GetFinanceTransactionDetailQuery>
{
/// <summary>
/// 初始化验证规则。
/// </summary>
public GetFinanceTransactionDetailQueryValidator()
{
RuleFor(x => x.StoreId).GreaterThan(0);
RuleFor(x => x.SourceId).GreaterThan(0);
RuleFor(x => x.SourceType)
.Must(x => x is FinanceTransactionSourceType.PaymentRecord
or FinanceTransactionSourceType.PaymentRefundRecord
or FinanceTransactionSourceType.RefundRequest
or FinanceTransactionSourceType.StoredCardRechargeRecord
or FinanceTransactionSourceType.MemberPointLedger)
.WithMessage("sourceType 非法");
}
}

View File

@@ -0,0 +1,22 @@
using FluentValidation;
using TakeoutSaaS.Application.App.Finance.Transactions.Queries;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Validators;
/// <summary>
/// 交易流水统计查询验证器。
/// </summary>
public sealed class GetFinanceTransactionStatsQueryValidator : AbstractValidator<GetFinanceTransactionStatsQuery>
{
/// <summary>
/// 初始化验证规则。
/// </summary>
public GetFinanceTransactionStatsQueryValidator()
{
RuleFor(x => x.StoreId).GreaterThan(0);
RuleFor(x => x.Keyword).MaximumLength(64);
RuleFor(x => x)
.Must(x => !x.StartAt.HasValue || !x.EndAt.HasValue || x.StartAt < x.EndAt)
.WithMessage("开始时间必须早于结束时间");
}
}

View File

@@ -0,0 +1,24 @@
using FluentValidation;
using TakeoutSaaS.Application.App.Finance.Transactions.Queries;
namespace TakeoutSaaS.Application.App.Finance.Transactions.Validators;
/// <summary>
/// 交易流水列表查询验证器。
/// </summary>
public sealed class SearchFinanceTransactionListQueryValidator : AbstractValidator<SearchFinanceTransactionListQuery>
{
/// <summary>
/// 初始化验证规则。
/// </summary>
public SearchFinanceTransactionListQueryValidator()
{
RuleFor(x => x.StoreId).GreaterThan(0);
RuleFor(x => x.Page).GreaterThan(0);
RuleFor(x => x.PageSize).InclusiveBetween(1, 200);
RuleFor(x => x.Keyword).MaximumLength(64);
RuleFor(x => x)
.Must(x => !x.StartAt.HasValue || !x.EndAt.HasValue || x.StartAt < x.EndAt)
.WithMessage("开始时间必须早于结束时间");
}
}