feat(finance): add overview dashboard and platform fee rate
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
namespace TakeoutSaaS.Application.App.Finance.Overview.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 财务概览指标卡 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceOverviewKpiCardDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 指标值。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 对比基准值。
|
||||
/// </summary>
|
||||
public decimal CompareAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 变化率(%)。
|
||||
/// </summary>
|
||||
public decimal ChangeRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 趋势(up/down/flat)。
|
||||
/// </summary>
|
||||
public string Trend { get; set; } = "flat";
|
||||
|
||||
/// <summary>
|
||||
/// 对比文案(较昨日/较上周)。
|
||||
/// </summary>
|
||||
public string CompareLabel { get; set; } = "较昨日";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收入趋势点 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceOverviewIncomeTrendPointDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 日期(yyyy-MM-dd)。
|
||||
/// </summary>
|
||||
public string Date { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 轴标签(MM/dd)。
|
||||
/// </summary>
|
||||
public string DateLabel { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 实收金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收入趋势 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceOverviewIncomeTrendDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 近 7 天。
|
||||
/// </summary>
|
||||
public List<FinanceOverviewIncomeTrendPointDto> Last7Days { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 近 30 天。
|
||||
/// </summary>
|
||||
public List<FinanceOverviewIncomeTrendPointDto> Last30Days { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 利润趋势点 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceOverviewProfitTrendPointDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 日期(yyyy-MM-dd)。
|
||||
/// </summary>
|
||||
public string Date { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 轴标签(MM/dd)。
|
||||
/// </summary>
|
||||
public string DateLabel { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 营收。
|
||||
/// </summary>
|
||||
public decimal RevenueAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 成本。
|
||||
/// </summary>
|
||||
public decimal CostAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 净利润。
|
||||
/// </summary>
|
||||
public decimal NetProfitAmount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 利润趋势 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceOverviewProfitTrendDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 近 7 天。
|
||||
/// </summary>
|
||||
public List<FinanceOverviewProfitTrendPointDto> Last7Days { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 近 30 天。
|
||||
/// </summary>
|
||||
public List<FinanceOverviewProfitTrendPointDto> Last30Days { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收入构成项 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceOverviewIncomeCompositionItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 渠道编码(delivery/pickup/dine_in)。
|
||||
/// </summary>
|
||||
public string Channel { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 渠道文案。
|
||||
/// </summary>
|
||||
public string ChannelText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 占比(%)。
|
||||
/// </summary>
|
||||
public decimal Percentage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收入构成 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceOverviewIncomeCompositionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 总实收。
|
||||
/// </summary>
|
||||
public decimal TotalAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构成项。
|
||||
/// </summary>
|
||||
public List<FinanceOverviewIncomeCompositionItemDto> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 成本构成项 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceOverviewCostCompositionItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 分类编码(food/labor/fixed/packaging/platform)。
|
||||
/// </summary>
|
||||
public string Category { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 分类文案。
|
||||
/// </summary>
|
||||
public string CategoryText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 占比(%)。
|
||||
/// </summary>
|
||||
public decimal Percentage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 成本构成 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceOverviewCostCompositionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 总成本。
|
||||
/// </summary>
|
||||
public decimal TotalAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构成项。
|
||||
/// </summary>
|
||||
public List<FinanceOverviewCostCompositionItemDto> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TOP 商品 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceOverviewTopProductItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 排名。
|
||||
/// </summary>
|
||||
public int Rank { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品名称。
|
||||
/// </summary>
|
||||
public string ProductName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 销量。
|
||||
/// </summary>
|
||||
public int SalesQuantity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 营收金额。
|
||||
/// </summary>
|
||||
public decimal RevenueAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 营收占比(%)。
|
||||
/// </summary>
|
||||
public decimal Percentage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TOP 商品区块 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceOverviewTopProductDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 统计周期(天)。
|
||||
/// </summary>
|
||||
public int PeriodDays { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// 排行数据。
|
||||
/// </summary>
|
||||
public List<FinanceOverviewTopProductItemDto> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 财务概览页面 DTO。
|
||||
/// </summary>
|
||||
public sealed class FinanceOverviewDashboardDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 维度编码(tenant/store)。
|
||||
/// </summary>
|
||||
public string Dimension { get; set; } = "tenant";
|
||||
|
||||
/// <summary>
|
||||
/// 门店标识(门店维度时有值)。
|
||||
/// </summary>
|
||||
public string? StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 今日营业额卡片。
|
||||
/// </summary>
|
||||
public FinanceOverviewKpiCardDto TodayRevenue { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 实收卡片。
|
||||
/// </summary>
|
||||
public FinanceOverviewKpiCardDto ActualReceived { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 退款卡片。
|
||||
/// </summary>
|
||||
public FinanceOverviewKpiCardDto RefundAmount { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 净收入卡片。
|
||||
/// </summary>
|
||||
public FinanceOverviewKpiCardDto NetIncome { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 可提现余额卡片。
|
||||
/// </summary>
|
||||
public FinanceOverviewKpiCardDto WithdrawableBalance { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 收入趋势。
|
||||
/// </summary>
|
||||
public FinanceOverviewIncomeTrendDto IncomeTrend { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 利润趋势。
|
||||
/// </summary>
|
||||
public FinanceOverviewProfitTrendDto ProfitTrend { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 收入构成。
|
||||
/// </summary>
|
||||
public FinanceOverviewIncomeCompositionDto IncomeComposition { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 成本构成。
|
||||
/// </summary>
|
||||
public FinanceOverviewCostCompositionDto CostComposition { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// TOP 商品排行。
|
||||
/// </summary>
|
||||
public FinanceOverviewTopProductDto TopProducts { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
using System.Globalization;
|
||||
using TakeoutSaaS.Application.App.Finance.Overview.Dto;
|
||||
using TakeoutSaaS.Domain.Finance.Enums;
|
||||
using TakeoutSaaS.Domain.Finance.Models;
|
||||
using TakeoutSaaS.Domain.Orders.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Overview.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 财务概览映射与格式化。
|
||||
/// </summary>
|
||||
internal static class FinanceOverviewMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// 构建财务概览页面 DTO。
|
||||
/// </summary>
|
||||
public static FinanceOverviewDashboardDto ToDashboardDto(FinanceOverviewDashboardSnapshot snapshot)
|
||||
{
|
||||
// 1. 指标卡映射。
|
||||
var todayNetIncome = snapshot.Summary.TodayNetReceived - snapshot.Summary.TodayTotalCost;
|
||||
var yesterdayNetIncome = snapshot.Summary.YesterdayNetReceived - snapshot.Summary.YesterdayTotalCost;
|
||||
|
||||
// 2. 近 30/7 天趋势映射。
|
||||
var incomeLast30 = snapshot.IncomeTrend
|
||||
.OrderBy(item => item.BusinessDate)
|
||||
.Select(item => new FinanceOverviewIncomeTrendPointDto
|
||||
{
|
||||
Date = item.BusinessDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
DateLabel = item.BusinessDate.ToString("MM/dd", CultureInfo.InvariantCulture),
|
||||
Amount = RoundAmount(item.NetReceivedAmount)
|
||||
})
|
||||
.ToList();
|
||||
var incomeLast7 = incomeLast30.Skip(Math.Max(0, incomeLast30.Count - 7)).ToList();
|
||||
|
||||
var profitLast30 = snapshot.ProfitTrend
|
||||
.OrderBy(item => item.BusinessDate)
|
||||
.Select(item => new FinanceOverviewProfitTrendPointDto
|
||||
{
|
||||
Date = item.BusinessDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
DateLabel = item.BusinessDate.ToString("MM/dd", CultureInfo.InvariantCulture),
|
||||
RevenueAmount = RoundAmount(item.RevenueAmount),
|
||||
CostAmount = RoundAmount(item.CostAmount),
|
||||
NetProfitAmount = RoundAmount(item.NetProfitAmount)
|
||||
})
|
||||
.ToList();
|
||||
var profitLast7 = profitLast30.Skip(Math.Max(0, profitLast30.Count - 7)).ToList();
|
||||
|
||||
// 3. 收入构成映射。
|
||||
var incomeTotal = RoundAmount(snapshot.IncomeComposition.Sum(item => item.Amount));
|
||||
var incomeItems = snapshot.IncomeComposition
|
||||
.OrderBy(item => GetChannelSort(item.Channel))
|
||||
.Select(item => new FinanceOverviewIncomeCompositionItemDto
|
||||
{
|
||||
Channel = ToChannelCode(item.Channel),
|
||||
ChannelText = ToChannelText(item.Channel),
|
||||
Amount = RoundAmount(item.Amount),
|
||||
Percentage = incomeTotal > 0
|
||||
? RoundAmount(item.Amount / incomeTotal * 100m)
|
||||
: 0m
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// 4. 成本构成映射。
|
||||
var costTotal = RoundAmount(snapshot.CostComposition.Sum(item => item.Amount));
|
||||
var costItems = snapshot.CostComposition
|
||||
.OrderBy(item => GetCostCategorySort(item.CategoryCode))
|
||||
.Select(item => new FinanceOverviewCostCompositionItemDto
|
||||
{
|
||||
Category = item.CategoryCode,
|
||||
CategoryText = ToCostCategoryText(item.CategoryCode),
|
||||
Amount = RoundAmount(item.Amount),
|
||||
Percentage = costTotal > 0
|
||||
? RoundAmount(item.Amount / costTotal * 100m)
|
||||
: 0m
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// 5. TOP10 映射。
|
||||
var topTotalRevenue = snapshot.TopProductTotalRevenue > 0
|
||||
? snapshot.TopProductTotalRevenue
|
||||
: snapshot.TopProducts.Sum(item => item.RevenueAmount);
|
||||
var topItems = snapshot.TopProducts
|
||||
.OrderByDescending(item => item.RevenueAmount)
|
||||
.ThenByDescending(item => item.SalesQuantity)
|
||||
.Select((item, index) => new FinanceOverviewTopProductItemDto
|
||||
{
|
||||
Rank = index + 1,
|
||||
ProductName = item.ProductName,
|
||||
SalesQuantity = item.SalesQuantity,
|
||||
RevenueAmount = RoundAmount(item.RevenueAmount),
|
||||
Percentage = topTotalRevenue > 0
|
||||
? RoundAmount(item.RevenueAmount / topTotalRevenue * 100m)
|
||||
: 0m
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new FinanceOverviewDashboardDto
|
||||
{
|
||||
Dimension = ToDimensionCode(snapshot.Dimension),
|
||||
StoreId = snapshot.StoreId?.ToString(CultureInfo.InvariantCulture),
|
||||
TodayRevenue = BuildKpi(snapshot.Summary.TodayGrossRevenue, snapshot.Summary.YesterdayGrossRevenue, "较昨日"),
|
||||
ActualReceived = BuildKpi(snapshot.Summary.TodayNetReceived, snapshot.Summary.YesterdayNetReceived, "较昨日"),
|
||||
RefundAmount = BuildKpi(snapshot.Summary.TodayRefundAmount, snapshot.Summary.YesterdayRefundAmount, "较昨日"),
|
||||
NetIncome = BuildKpi(todayNetIncome, yesterdayNetIncome, "较昨日"),
|
||||
WithdrawableBalance = BuildKpi(
|
||||
snapshot.Summary.WithdrawableBalance,
|
||||
snapshot.Summary.WithdrawableBalanceLastWeek,
|
||||
"较上周"),
|
||||
IncomeTrend = new FinanceOverviewIncomeTrendDto
|
||||
{
|
||||
Last7Days = incomeLast7,
|
||||
Last30Days = incomeLast30
|
||||
},
|
||||
ProfitTrend = new FinanceOverviewProfitTrendDto
|
||||
{
|
||||
Last7Days = profitLast7,
|
||||
Last30Days = profitLast30
|
||||
},
|
||||
IncomeComposition = new FinanceOverviewIncomeCompositionDto
|
||||
{
|
||||
TotalAmount = incomeTotal,
|
||||
Items = incomeItems
|
||||
},
|
||||
CostComposition = new FinanceOverviewCostCompositionDto
|
||||
{
|
||||
TotalAmount = costTotal,
|
||||
Items = costItems
|
||||
},
|
||||
TopProducts = new FinanceOverviewTopProductDto
|
||||
{
|
||||
PeriodDays = 30,
|
||||
Items = topItems
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static FinanceOverviewKpiCardDto BuildKpi(decimal current, decimal previous, string compareLabel)
|
||||
{
|
||||
var normalizedCurrent = RoundAmount(current);
|
||||
var normalizedPrevious = RoundAmount(previous);
|
||||
return new FinanceOverviewKpiCardDto
|
||||
{
|
||||
Amount = normalizedCurrent,
|
||||
CompareAmount = normalizedPrevious,
|
||||
ChangeRate = CalculateChangeRate(normalizedCurrent, normalizedPrevious),
|
||||
Trend = ResolveTrend(normalizedCurrent, normalizedPrevious),
|
||||
CompareLabel = compareLabel
|
||||
};
|
||||
}
|
||||
|
||||
private static decimal CalculateChangeRate(decimal current, decimal previous)
|
||||
{
|
||||
if (previous == 0m)
|
||||
{
|
||||
return current == 0m ? 0m : 100m;
|
||||
}
|
||||
|
||||
var rate = (current - previous) / previous * 100m;
|
||||
return RoundAmount(rate);
|
||||
}
|
||||
|
||||
private static string ResolveTrend(decimal current, decimal previous)
|
||||
{
|
||||
if (current > previous)
|
||||
{
|
||||
return "up";
|
||||
}
|
||||
|
||||
if (current < previous)
|
||||
{
|
||||
return "down";
|
||||
}
|
||||
|
||||
return "flat";
|
||||
}
|
||||
|
||||
private static string ToDimensionCode(FinanceCostDimension value)
|
||||
{
|
||||
return value == FinanceCostDimension.Store ? "store" : "tenant";
|
||||
}
|
||||
|
||||
private static string ToChannelCode(DeliveryType value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
DeliveryType.Delivery => "delivery",
|
||||
DeliveryType.Pickup => "pickup",
|
||||
DeliveryType.DineIn => "dine_in",
|
||||
_ => "delivery"
|
||||
};
|
||||
}
|
||||
|
||||
private static string ToChannelText(DeliveryType value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
DeliveryType.Delivery => "外卖",
|
||||
DeliveryType.Pickup => "自提",
|
||||
DeliveryType.DineIn => "堂食",
|
||||
_ => "外卖"
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetChannelSort(DeliveryType value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
DeliveryType.Delivery => 0,
|
||||
DeliveryType.Pickup => 1,
|
||||
DeliveryType.DineIn => 2,
|
||||
_ => 9
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetCostCategorySort(string? value)
|
||||
{
|
||||
return (value ?? string.Empty).Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"food" => 0,
|
||||
"labor" => 1,
|
||||
"fixed" => 2,
|
||||
"packaging" => 3,
|
||||
"platform" => 4,
|
||||
_ => 9
|
||||
};
|
||||
}
|
||||
|
||||
private static string ToCostCategoryText(string? value)
|
||||
{
|
||||
return (value ?? string.Empty).Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"food" => "食材",
|
||||
"labor" => "人工",
|
||||
"fixed" => "固定",
|
||||
"packaging" => "包装",
|
||||
"platform" => "平台",
|
||||
_ => "其他"
|
||||
};
|
||||
}
|
||||
|
||||
private static decimal RoundAmount(decimal value)
|
||||
{
|
||||
return decimal.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Overview.Dto;
|
||||
using TakeoutSaaS.Application.App.Finance.Overview.Queries;
|
||||
using TakeoutSaaS.Domain.Finance.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Overview.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 财务概览查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceOverviewDashboardQueryHandler(
|
||||
IFinanceOverviewRepository financeOverviewRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetFinanceOverviewDashboardQuery, FinanceOverviewDashboardDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<FinanceOverviewDashboardDto> Handle(
|
||||
GetFinanceOverviewDashboardQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. 拉取租户上下文并读取快照。
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var snapshot = await financeOverviewRepository.GetDashboardSnapshotAsync(
|
||||
tenantId,
|
||||
request.Dimension,
|
||||
request.StoreId,
|
||||
request.CurrentUtc,
|
||||
cancellationToken);
|
||||
|
||||
// 2. 映射页面 DTO。
|
||||
return FinanceOverviewMapping.ToDashboardDto(snapshot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Finance.Overview.Dto;
|
||||
using TakeoutSaaS.Domain.Finance.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Overview.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询财务概览驾驶舱数据。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceOverviewDashboardQuery : IRequest<FinanceOverviewDashboardDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 统计维度。
|
||||
/// </summary>
|
||||
public FinanceCostDimension Dimension { get; init; } = FinanceCostDimension.Tenant;
|
||||
|
||||
/// <summary>
|
||||
/// 门店标识(门店维度必填)。
|
||||
/// </summary>
|
||||
public long? StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前 UTC 时间。
|
||||
/// </summary>
|
||||
public DateTime CurrentUtc { get; init; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using FluentValidation;
|
||||
using TakeoutSaaS.Application.App.Finance.Overview.Queries;
|
||||
using TakeoutSaaS.Domain.Finance.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Finance.Overview.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// 财务概览查询验证器。
|
||||
/// </summary>
|
||||
public sealed class GetFinanceOverviewDashboardQueryValidator : AbstractValidator<GetFinanceOverviewDashboardQuery>
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化验证规则。
|
||||
/// </summary>
|
||||
public GetFinanceOverviewDashboardQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.Dimension)
|
||||
.Must(value => value is FinanceCostDimension.Tenant or FinanceCostDimension.Store)
|
||||
.WithMessage("dimension 非法");
|
||||
|
||||
RuleFor(x => x)
|
||||
.Must(query =>
|
||||
query.Dimension != FinanceCostDimension.Store ||
|
||||
(query.StoreId.HasValue && query.StoreId.Value > 0))
|
||||
.WithMessage("storeId 非法");
|
||||
|
||||
RuleFor(x => x.CurrentUtc)
|
||||
.Must(value => value.Year is >= 2000 and <= 2100)
|
||||
.WithMessage("currentUtc 非法");
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,11 @@ public sealed record UpdateStoreFeeCommand : IRequest<StoreFeeDto>
|
||||
/// </summary>
|
||||
public decimal DeliveryFee { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 平台服务费率(%)。
|
||||
/// </summary>
|
||||
public decimal PlatformServiceRate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 打包费模式。
|
||||
/// </summary>
|
||||
|
||||
@@ -31,6 +31,11 @@ public sealed record StoreFeeDto
|
||||
/// </summary>
|
||||
public decimal DeliveryFee { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 平台服务费率(%)。
|
||||
/// </summary>
|
||||
public decimal PlatformServiceRate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 餐具费是否启用。
|
||||
/// </summary>
|
||||
|
||||
@@ -53,6 +53,7 @@ public sealed class UpdateStoreFeeCommandHandler(
|
||||
// 3. (空行后) 应用更新字段
|
||||
fee.MinimumOrderAmount = request.MinimumOrderAmount;
|
||||
fee.BaseDeliveryFee = request.DeliveryFee;
|
||||
fee.PlatformServiceRate = request.PlatformServiceRate;
|
||||
fee.PackagingFeeMode = request.PackagingFeeMode;
|
||||
fee.OrderPackagingFeeMode = request.PackagingFeeMode == PackagingFeeMode.Fixed
|
||||
? request.OrderPackagingFeeMode
|
||||
|
||||
@@ -67,6 +67,7 @@ public static class StoreMapping
|
||||
StoreId = fee.StoreId,
|
||||
MinimumOrderAmount = fee.MinimumOrderAmount,
|
||||
DeliveryFee = fee.BaseDeliveryFee,
|
||||
PlatformServiceRate = fee.PlatformServiceRate,
|
||||
CutleryFeeEnabled = fee.CutleryFeeEnabled,
|
||||
CutleryFeeAmount = fee.CutleryFeeAmount,
|
||||
RushFeeEnabled = fee.RushFeeEnabled,
|
||||
|
||||
@@ -17,6 +17,7 @@ public sealed class UpdateStoreFeeCommandValidator : AbstractValidator<UpdateSto
|
||||
RuleFor(x => x.StoreId).GreaterThan(0);
|
||||
RuleFor(x => x.MinimumOrderAmount).GreaterThanOrEqualTo(0).LessThanOrEqualTo(9999.99m);
|
||||
RuleFor(x => x.DeliveryFee).GreaterThanOrEqualTo(0).LessThanOrEqualTo(999.99m);
|
||||
RuleFor(x => x.PlatformServiceRate).GreaterThanOrEqualTo(0).LessThanOrEqualTo(100m);
|
||||
RuleFor(x => x.FreeDeliveryThreshold).GreaterThanOrEqualTo(0).When(x => x.FreeDeliveryThreshold.HasValue);
|
||||
|
||||
RuleFor(x => x.FixedPackagingFee)
|
||||
|
||||
Reference in New Issue
Block a user