统计页面 修改 增加统计Border。优化部分统计DataGrid的列展示

This commit is contained in:
chfei committed 2026-08-07 10:07:02 +08:00
1 parent f2d619eafb
commit b6b72e6bc3
9 files changed
+883 -687

No files matched your search

@@ -15,80 +15,128 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// <summary>结束时间</summary>
public DateTime EndDate { get; set; }
/// <summary>店铺 app_key(A.app_key),空/Null 表示全部店铺</summary>
/// <summary>店铺 shop_id(B.shop_id),空/Null 表示全部店铺</summary>
public string ShopKey { get; set; }
}
/// <summary>
/// 店铺营收统计 - 接口返回的原始明细行。
/// 对应接口返回字段:
/// A.app_key, B.create_time, B.order_id, B.status,
/// B.product_price, B.order_price, B.purchase_price, B.freight,
/// B.predict_commission_fee, B.commission_amount, B.discounted_price, B.profit,
/// B.purchase_status, c.refund_status
/// 其中 A 为店铺表,B 为订单表,c 为售后/退款表。
/// 金额字段单位均为“分”,展示时需除以 100。
/// 与后端 WXOrderAmountInfo 保持一致(camelCase JSON):
/// shopName, createTime, orderId, status, productPrice, orderPrice, purchasePrice, freight,
/// predictCommissionFee, commissionAmount, discountedPrice, profit, purchaseStatus,
/// outAmount, refundStatus, refundStatusString, refundAmount, externalProcurement
/// 金额字段单位均为分(显示时需除以 100)。
/// </summary>
public class ShopRevenueStatisticsRow
{
/// <summary>A.app_key 店铺 app_key</summary>
[JsonProperty("app_key")]
public string AppKey { get; set; }
/// <summary>
/// 店铺名称
/// </summary>
[JsonProperty("shop_name")]
public string ShopName { get; set; }
/// <summary>B.create_time 订单创建时间(接口为秒级时间戳,由 UnixDateTimeConverter 转换为 DateTime)</summary>
/// <summary>
/// 下单时间(秒级时间戳)
/// </summary>
[JsonProperty("create_time")]
[JsonConverter(typeof(UnixDateTimeConverter))]
public DateTime CreateTime { get; set; }
/// <summary>B.order_id 订单号</summary>
/// <summary>
/// 订单号
/// </summary>
[JsonProperty("order_id")]
public string OrderId { get; set; }
/// <summary>B.status 订单状态:10-待付款;20-待发货;21-部分发货;30-待收货;100-完成;200-全部商品售后之后订单取消;250-未付款用户主动取消或超时未付款订单自动取消</summary>
/// <summary>
/// 订单状态
/// </summary>
[JsonProperty("status")]
public int Status { get; set; }
/// <summary>B.product_price 商品总价(分)</summary>
/// <summary>
/// 商品总价(分)
/// </summary>
[JsonProperty("product_price")]
public int ProductPrice { get; set; }
/// <summary>B.order_price 订单金额(分),计算公式:order_price = original_order_price - discounted_price - deduction_price - change_down_price</summary>
/// <summary>
/// 订单金额(分)
/// </summary>
[JsonProperty("order_price")]
public int OrderPrice { get; set; }
/// <summary>B.purchase_price 采购价(分)</summary>
/// <summary>
/// 采购价(分)
/// </summary>
[JsonProperty("purchase_price")]
public int PurchasePrice { get; set; }
/// <summary>B.freight 运费(分)</summary>
/// <summary>
/// 运费(分)
/// </summary>
[JsonProperty("freight")]
public int Freight { get; set; }
/// <summary>B.predict_commission_fee 平台服务费(微信小店:预计技术服务费)(分)</summary>
/// <summary>
/// 平台服务费(预计技术服务费,分)
/// </summary>
[JsonProperty("predict_commission_fee")]
public int PredictCommissionFee { get; set; }
/// <summary>B.commission_amount 达人佣金(微信小店:分账金额)(分)</summary>
/// <summary>
/// 达人佣金(分)
/// </summary>
[JsonProperty("commission_amount")]
public int CommissionAmount { get; set; }
/// <summary>B.discounted_price 优惠券优惠金额(分)</summary>
/// <summary>
/// 优惠金额(分)
/// </summary>
[JsonProperty("discounted_price")]
public int DiscountedPrice { get; set; }
/// <summary>B.profit 利润(分),计算公式:实付金额 - 采购价 - 平台服务费 - 达人佣金(采购价、达人佣金为空值则按 0)</summary>
/// <summary>
/// 利润(分)
/// </summary>
[JsonProperty("profit")]
public int Profit { get; set; }
/// <summary>B.purchase_status 采购状态:0-未采购;1-采购(默认值 0,推送仓库时修改为 1)</summary>
/// <summary>
/// 采购状态
/// </summary>
[JsonProperty("purchase_status")]
public int PurchaseStatus { get; set; }
/// <summary>c.refund_status 退款状态(4/11 视为已退款,与 RevenueStatisticsAggregator.IsRefunded 保持一致)</summary>
/// <summary>
/// 外采金额(分),out_amount为空或0时从中转仓订单表补全
/// </summary>
[JsonProperty("out_amount")]
public int OutAmount { get; set; }
/// <summary>
/// 售后状态(订单明细 refund_status)
/// </summary>
[JsonProperty("refund_status")]
public int RefundStatus { get; set; }
/// <summary>店铺名称(由 app_key 关联店铺表得到,展示用,非接口原始字段)</summary>
[JsonProperty("shop_name")]
public string ShopName { get; set; }
/// <summary>
/// 售后状态(退款单 status)
/// </summary>
[JsonProperty("refund_status_string")]
public string RefundStatusString { get; set; }
/// <summary>
/// 售后退款金额(分)
/// </summary>
[JsonProperty("refund_amount")]
public int RefundAmount { get; set; }
/// <summary>
/// 是否外采:0=未从中转仓订单表查到,1=查到
/// </summary>
[JsonProperty("external_procurement")]
public int ExternalProcurement { get; set; }
}
/// <summary>
@@ -98,7 +146,6 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
{
/// <summary>销售额(最终有效销售额)</summary>
public decimal SalesAmount { get; set; }
/// <summary>有效订单数(已扣除退款订单)</summary>
public int ValidOrderCount { get; set; }
@@ -111,14 +158,20 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// <summary>商品销量(有效销量)</summary>
public int ProductSalesVolume { get; set; }
/// <summary>客户数(成交客户)</summary>
public int CustomerCount { get; set; }
/// <summary>平台服务费总额(元)</summary>
public decimal PlatformFeeTotal { get; set; }
/// <summary>达人佣金(元)</summary>
public decimal Commission { get; set; }
/// <summary>客单价(平均每单金额)</summary>
public decimal AvgOrderAmount { get; set; }
/// <summary>退款率(退款订单占比,百分数,如 2.31 表示 2.31%)</summary>
public decimal RefundRate { get; set; }
/// <summary>平均利润率(利润 / 采购金额,百分数,如 12.34 表示 12.34%)</summary>
public decimal AverageProfitRate { get; set; }
}
/// <summary>
@@ -174,11 +227,17 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// <summary>退款金额(元)</summary>
public decimal RefundAmount { get; set; }
/// <summary>客户数</summary>
public int CustomerCount { get; set; }
/// <summary>平台服务费总额(元)</summary>
public decimal PlatformFeeTotal { get; set; }
/// <summary>达人佣金(元)</summary>
public decimal Commission { get; set; }
/// <summary>客单价(元)</summary>
public decimal AvgOrderAmount { get; set; }
/// <summary>平均利润率(利润 / 采购金额,百分数,如 12.34 表示 12.34%)</summary>
public decimal AverageProfitRate { get; set; }
}
/// <summary>
@@ -220,6 +279,15 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// <summary>退款金额(元)</summary>
public decimal RefundAmount { get; set; }
/// <summary>平台服务费总额(元)</summary>
public decimal PlatformFeeTotal { get; set; }
/// <summary>达人佣金(元)</summary>
public decimal Commission { get; set; }
/// <summary>平均利润率(利润 / 采购金额,百分数,如 12.34 表示 12.34%)</summary>
public decimal AverageProfitRate { get; set; }
/// <summary>客户数</summary>
public int CustomerCount { get; set; }
@@ -234,7 +302,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
{
/// <summary>店铺 app_key(A.app_key)</summary>
public string AppKey { get; set; }
public string ShopId { get; set; }
/// <summary>店铺名称</summary>
public string ShopName { get; set; }
}
@@ -299,4 +367,4 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
writer.WriteValue(milliseconds);
}
}
}
}
@@ -4,6 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:stat="clr-namespace:_29WxOrderWpf.Ctrls.Statistics"
xmlns:vm="clr-namespace:_29WxOrderWpf.Ctrls.Statistics.ViewModels"
d:Background="#F5F7FB"
d:DataContext="{d:DesignInstance Type=vm:PeriodRevenueStatisticsViewModel}"
@@ -13,11 +14,6 @@
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="boolToVisibilityConverter" />
<!-- 日期只允许从日历选择,禁止手工输入 -->
<Style TargetType="DatePickerTextBox">
<Setter Property="IsReadOnly" Value="True" />
</Style>
<!-- 卡片样式(白底、圆角、阴影) -->
<Style x:Key="CardBorderStyle" TargetType="Border">
<Setter Property="Background" Value="White" />
@@ -50,219 +46,160 @@
</UserControl.Resources>
<Grid Background="#F5F7FB">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="12">
<!-- ============ 查询条件工具栏 ============ -->
<Border
Grid.Row="0"
Margin="6,6,6,0"
Padding="14,10"
Style="{StaticResource CardBorderStyle}">
<WrapPanel>
<TextBlock
Margin="6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="时间" />
<ComboBox IsEditable="False"
Width="110"
Height="32"
VerticalContentAlignment="Center"
DisplayMemberPath="Text"
ItemsSource="{Binding TimeRangeOptions}"
SelectedItem="{Binding SelectedTimeRange, Mode=TwoWay}"
Style="{StaticResource BaseComboBoxStyle}" />
<TextBlock
Margin="14,0,6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="日期" />
<DatePicker
Height="32"
VerticalContentAlignment="Center"
SelectedDate="{Binding StartDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Margin="6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="~" />
<DatePicker
Height="32"
VerticalContentAlignment="Center"
SelectedDate="{Binding EndDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Margin="14,0,6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="店铺" />
<ComboBox IsEditable="False"
Width="130"
Height="32"
VerticalContentAlignment="Center"
DisplayMemberPath="ShopName"
ItemsSource="{Binding ShopOptions}"
SelectedItem="{Binding SelectedShop, Mode=TwoWay}"
Style="{StaticResource BaseComboBoxStyle}" />
<Button
Height="32"
Margin="18,0,0,0"
Padding="16,0"
Command="{Binding QueryCommand}"
Content=" 查询 "
Style="{StaticResource BasePageButtonStyle}" />
<Button
Height="32"
Margin="8,0,0,0"
Padding="16,0"
Command="{Binding ResetCommand}"
Content=" 重置 "
Style="{StaticResource BasePageButtonLightStyle}" />
</WrapPanel>
</Border>
<!-- ============ 查询条件工具栏(共享 UserControl) ============ -->
<stat:RevenueStatisticsSearchBar />
<!-- ============ 周期×店铺统计表格 ============ -->
<Border
Grid.Row="1"
Margin="6,8,6,12"
Style="{StaticResource CardBorderStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- ============ 汇总卡片(共享 UserControl) ============ -->
<stat:RevenueSummaryCards />
<TextBlock
Grid.Row="0"
Margin="0,0,0,10"
Style="{StaticResource PanelTitleStyle}"
Text="{Binding PageTitle}" />
<!-- ============ 周期统计表格 ============ -->
<Border
Margin="6,8,6,12"
Style="{StaticResource CardBorderStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DataGrid
Grid.Row="1"
AlternatingRowBackground="#FAFAFA"
AutoGenerateColumns="False"
Background="Transparent"
BorderThickness="0"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserReorderColumns="False"
CanUserResizeRows="False"
ColumnHeaderStyle="{StaticResource BaseDataGridColumnHeaderStyle}"
FontSize="13"
Foreground="#333333"
GridLinesVisibility="Horizontal"
HeadersVisibility="Column"
HorizontalGridLinesBrush="#EEEEEE"
IsReadOnly="True"
ItemsSource="{Binding Statistics}"
RowHeight="46"
ScrollViewer.CanContentScroll="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectionMode="Single"
SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTextColumn
Width="1.4*"
Binding="{Binding Period}"
ElementStyle="{StaticResource CenterCellTextStyle}">
<DataGridTextColumn.Header>
<TextBlock
FontSize="14"
FontWeight="Bold"
Foreground="{DynamicResource mainForegroundColorBrush}"
Text="{Binding DataContext.PeriodColumnHeader, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn
Width="*"
Binding="{Binding OrderCount, StringFormat={}{0:N0}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="订单数" />
<DataGridTextColumn
Width="*"
Binding="{Binding SalesAmount, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="销售额" />
<DataGridTextColumn
Width="*"
Binding="{Binding Profit, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="利润" />
<DataGridTextColumn
Width="*"
Binding="{Binding RefundAmount, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="退款金额" />
<DataGridTextColumn
Width="*"
Binding="{Binding CustomerCount, StringFormat={}{0:N0}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="客户数" />
<DataGridTextColumn
Width="*"
Binding="{Binding AvgOrderAmount, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="客单价" />
</DataGrid.Columns>
</DataGrid>
<TextBlock
Grid.Row="0"
Margin="0,0,0,10"
Style="{StaticResource PanelTitleStyle}"
Text="{Binding PageTitle}" />
<!-- ============ 底部翻页 ============ -->
<Border
Grid.Row="2"
Margin="0,10,0,0"
Padding="0,4"
Background="#F8F9FB"
CornerRadius="6">
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<TextBlock
Margin="0,0,12,0"
VerticalAlignment="Center"
FontSize="13"
Foreground="#666666"
Text="{Binding PagerText}" />
<Button
Height="28"
Margin="4,0,0,0"
Padding="12,0"
Command="{Binding FirstPageCommand}"
Content="首页"
Style="{StaticResource BasePageButtonLightStyle}" />
<Button
Height="28"
Margin="4,0,0,0"
Padding="12,0"
Command="{Binding PrevPageCommand}"
Content="上一页"
Style="{StaticResource BasePageButtonLightStyle}" />
<Button
Height="28"
Margin="4,0,0,0"
Padding="12,0"
Command="{Binding NextPageCommand}"
Content="下一页"
Style="{StaticResource BasePageButtonLightStyle}" />
<Button
Height="28"
Margin="4,0,0,0"
Padding="12,0"
Command="{Binding LastPageCommand}"
Content="末页"
Style="{StaticResource BasePageButtonLightStyle}" />
</StackPanel>
</Border>
</Grid>
</Border>
</Grid>
<DataGrid
Grid.Row="1"
AlternatingRowBackground="#FAFAFA"
AutoGenerateColumns="False"
Background="Transparent"
BorderThickness="0"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserReorderColumns="False"
CanUserResizeRows="False"
ColumnHeaderStyle="{StaticResource BaseDataGridColumnHeaderStyle}"
FontSize="13"
Foreground="#333333"
GridLinesVisibility="Horizontal"
HeadersVisibility="Column"
HorizontalGridLinesBrush="#EEEEEE"
IsReadOnly="True"
ItemsSource="{Binding Statistics}"
RowHeight="46"
ScrollViewer.CanContentScroll="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
SelectionMode="Single"
SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTextColumn
Width="1.4*"
Binding="{Binding Period}"
ElementStyle="{StaticResource CenterCellTextStyle}">
<DataGridTextColumn.Header>
<TextBlock
FontSize="14"
FontWeight="Bold"
Foreground="{DynamicResource mainForegroundColorBrush}"
Text="{Binding DataContext.PeriodColumnHeader, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn
Width="*"
Binding="{Binding OrderCount, StringFormat={}{0:N0}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="订单数" />
<DataGridTextColumn
Width="*"
Binding="{Binding SalesAmount, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="销售额" />
<DataGridTextColumn
Width="*"
Binding="{Binding Profit, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="利润" />
<DataGridTextColumn
Width="*"
Binding="{Binding RefundAmount, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="退款金额" />
<DataGridTextColumn
Width="*"
Binding="{Binding PlatformFeeTotal, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="平台服务费总额" />
<DataGridTextColumn
Width="*"
Binding="{Binding Commission, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="达人佣金" />
<DataGridTextColumn
Width="*"
Binding="{Binding AverageProfitRate, StringFormat={}{0:0.00}%}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="平均利润率" />
<DataGridTextColumn
Width="*"
Binding="{Binding AvgOrderAmount, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="客单价" />
</DataGrid.Columns>
</DataGrid>
<!-- ============ 底部翻页 ============ -->
<Border
Grid.Row="2"
Margin="0,10,0,0"
Padding="0,4"
Background="#F8F9FB"
CornerRadius="6">
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<TextBlock
Margin="0,0,12,0"
VerticalAlignment="Center"
FontSize="13"
Foreground="#666666"
Text="{Binding PagerText}" />
<Button
Height="28"
Margin="4,0,0,0"
Padding="12,0"
Command="{Binding FirstPageCommand}"
Content="首页"
Style="{StaticResource BasePageButtonLightStyle}" />
<Button
Height="28"
Margin="4,0,0,0"
Padding="12,0"
Command="{Binding PrevPageCommand}"
Content="上一页"
Style="{StaticResource BasePageButtonLightStyle}" />
<Button
Height="28"
Margin="4,0,0,0"
Padding="12,0"
Command="{Binding NextPageCommand}"
Content="下一页"
Style="{StaticResource BasePageButtonLightStyle}" />
<Button
Height="28"
Margin="4,0,0,0"
Padding="12,0"
Command="{Binding LastPageCommand}"
Content="末页"
Style="{StaticResource BasePageButtonLightStyle}" />
</StackPanel>
</Border>
</Grid>
</Border>
</StackPanel>
</ScrollViewer>
<!-- ============ 加载遮罩 ============ -->
<Border Background="#66FFFFFF" Visibility="{Binding IsLoading, Converter={StaticResource boolToVisibilityConverter}}" d:Visibility="Collapsed">
@@ -0,0 +1,103 @@
<UserControl
x:Class="_29WxOrderWpf.Ctrls.Statistics.RevenueStatisticsSearchBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:_29WxOrderWpf.Ctrls.Statistics.ViewModels"
d:DataContext="{d:DesignInstance Type=vm:ShopRevenueStatisticsViewModel,IsDesignTimeCreatable=True}"
mc:Ignorable="d">
<UserControl.Resources>
<!-- 日期只允许从日历选择,禁止手工输入 -->
<Style TargetType="DatePickerTextBox">
<Setter Property="IsReadOnly" Value="True" />
</Style>
<!-- 卡片样式(白底、圆角、阴影) -->
<Style x:Key="CardBorderStyle" TargetType="Border">
<Setter Property="Background" Value="White" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="18" />
<Setter Property="Margin" Value="4" />
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect
BlurRadius="10"
Opacity="0.08"
ShadowDepth="2"
Color="#000000" />
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Border
Margin="6,6,6,0"
Padding="14,10"
Style="{StaticResource CardBorderStyle}">
<WrapPanel>
<TextBlock
Margin="6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="时间" />
<ComboBox IsEnabled="False"
Width="110"
Height="32"
VerticalContentAlignment="Center"
DisplayMemberPath="Text"
ItemsSource="{Binding TimeRangeOptions}"
SelectedItem="{Binding SelectedTimeRange, Mode=TwoWay}"
Style="{StaticResource BaseComboBoxStyle}" />
<TextBlock
Margin="14,0,6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="日期" />
<DatePicker
Height="32"
VerticalContentAlignment="Center"
SelectedDate="{Binding StartDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Margin="6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="~" />
<DatePicker
Height="32"
VerticalContentAlignment="Center"
SelectedDate="{Binding EndDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Margin="14,0,6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="店铺" />
<ComboBox IsEnabled="False"
Width="130"
Height="32"
VerticalContentAlignment="Center"
DisplayMemberPath="ShopName"
ItemsSource="{Binding ShopOptions}"
SelectedItem="{Binding SelectedShop, Mode=TwoWay}"
Style="{StaticResource BaseComboBoxStyle}" />
<Button
Height="32"
Margin="18,0,0,0"
Padding="16,0"
Command="{Binding QueryCommand}"
Content=" 查询 "
Style="{StaticResource BasePageButtonStyle}" />
<Button
Height="32"
Margin="8,0,0,0"
Padding="16,0"
Command="{Binding ResetCommand}"
Content=" 重置 "
Style="{StaticResource BasePageButtonLightStyle}" />
</WrapPanel>
</Border>
</UserControl>
@@ -0,0 +1,16 @@
using System.Windows.Controls;
namespace _29WxOrderWpf.Ctrls.Statistics
{
/// <summary>
/// 营收统计 - 查询条件工具栏(时间范围 + 日期区间 + 店铺 + 查询/重置)。
/// 通过继承父级 DataContext 绑定,供综合统计 / 天周月周期统计共用。
/// </summary>
public partial class RevenueStatisticsSearchBar : UserControl
{
public RevenueStatisticsSearchBar()
{
InitializeComponent();
}
}
}
@@ -0,0 +1,283 @@
<UserControl
x:Class="_29WxOrderWpf.Ctrls.Statistics.RevenueSummaryCards"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:_29WxOrderWpf.Ctrls.Statistics.ViewModels"
d:DataContext="{d:DesignInstance Type=vm:ShopRevenueStatisticsViewModel,IsDesignTimeCreatable=True}"
mc:Ignorable="d">
<UserControl.Resources>
<!-- 卡片样式(白底、圆角、阴影) -->
<Style x:Key="CardBorderStyle" TargetType="Border">
<Setter Property="Background" Value="White" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="18" />
<Setter Property="Margin" Value="4" />
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect
BlurRadius="10"
Opacity="0.08"
ShadowDepth="2"
Color="#000000" />
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<!-- 汇总卡片(2 行 x 5 列) -->
<Grid Margin="3,8,3,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 销售额 -->
<Border
Grid.Row="0"
Grid.Column="0"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="销售额" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.SalesAmount, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="最终有效销售额" />
</StackPanel>
</Border>
<!-- 有效订单 -->
<Border
Grid.Row="0"
Grid.Column="1"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="有效订单" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.ValidOrderCount, StringFormat={}{0:N0}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="已扣除退款订单" />
</StackPanel>
</Border>
<!-- 利润 -->
<Border
Grid.Row="0"
Grid.Column="2"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="利润" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.Profit, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="实际利润" />
</StackPanel>
</Border>
<!-- 退款金额 -->
<Border
Grid.Row="0"
Grid.Column="3"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="退款金额" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.RefundAmount, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="统计期间退款" />
</StackPanel>
</Border>
<!-- 商品销量 -->
<Border
Grid.Row="0"
Grid.Column="4"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="商品销量" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.ProductSalesVolume, StringFormat={}{0:N0}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="有效销量" />
</StackPanel>
</Border>
<!-- 平台服务费总额 -->
<Border
Grid.Row="1"
Grid.Column="0"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="平台服务费总额" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.PlatformFeeTotal, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="预计技术服务费" />
</StackPanel>
</Border>
<!-- 达人佣金 -->
<Border
Grid.Row="1"
Grid.Column="1"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="达人佣金" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.Commission, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="佣金支出" />
</StackPanel>
</Border>
<!-- 客单价 -->
<Border
Grid.Row="1"
Grid.Column="2"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="客单价" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.AvgOrderAmount, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="平均每单金额" />
</StackPanel>
</Border>
<!-- 退款率 -->
<Border
Grid.Row="1"
Grid.Column="3"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="退款率" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.RefundRate, StringFormat={}{0:0.00}%}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="退款订单占比" />
</StackPanel>
</Border>
<!-- 平均利润率 -->
<Border
Grid.Row="1"
Grid.Column="4"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="平均利润率" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.AverageProfitRate, StringFormat={}{0:0.00}%}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="利润 / 采购金额" />
</StackPanel>
</Border>
</Grid>
</UserControl>
@@ -0,0 +1,17 @@
using System.Windows.Controls;
namespace _29WxOrderWpf.Ctrls.Statistics
{
/// <summary>
/// 营收统计 - 顶部汇总卡片(2 行 x 5 列)。
/// 通过继承父级 DataContext 绑定 Summary(ShopRevenueSummary),
/// 供综合统计 / 天周月周期统计共用。
/// </summary>
public partial class RevenueSummaryCards : UserControl
{
public RevenueSummaryCards()
{
InitializeComponent();
}
}
}
@@ -108,9 +108,12 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
var valid = list.Where(IsValidOrder).ToList();
var refunded = list.Where(IsRefunded).ToList();
decimal salesAmount = valid.Sum(r => (decimal)r.OrderPrice) / 100m;
decimal profit = valid.Sum(r => (decimal)r.Profit) / 100m;
decimal refundAmount = refunded.Sum(r => (decimal)r.OrderPrice) / 100m;
decimal salesAmount = valid.Sum(r => (decimal)(r.OrderPrice)) / 100m;
decimal profit = valid.Sum(r => (decimal)(r.Profit )) / 100m;
decimal refundAmount = refunded.Sum(r => (decimal)(r.OrderPrice)) / 100m;
decimal platformFeeTotal = valid.Sum(r => (decimal)(r.PredictCommissionFee)) / 100m;
decimal commission = valid.Sum(r => (decimal)(r.CommissionAmount)) / 100m;
decimal purchaseAmount = valid.Sum(r => (decimal)(r.PurchasePrice)) / 100m;
int validOrderCount = valid.Select(r => r.OrderId).Distinct().Count();
// 总订单数(退款率分母)= 已付款订单 + 已退款订单(含售后取消 200),不含未付款的 10/250
int totalOrderCount = list.Where(r => IsPaidOrder(r) || IsRefunded(r)).Select(r => r.OrderId).Distinct().Count();
@@ -123,9 +126,11 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
Profit = profit,
RefundAmount = refundAmount,
ProductSalesVolume = valid.Count,
CustomerCount = validOrderCount,
PlatformFeeTotal = platformFeeTotal,
Commission = commission,
AvgOrderAmount = validOrderCount == 0 ? 0m : salesAmount / validOrderCount,
RefundRate = totalOrderCount == 0 ? 0m : (decimal)refundOrderCount * 100m / totalOrderCount
RefundRate = totalOrderCount == 0 ? 0m : (decimal)refundOrderCount * 100m / totalOrderCount,
AverageProfitRate = purchaseAmount == 0m ? 0m : profit / purchaseAmount * 100m
};
}
@@ -140,7 +145,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
g => new RevenueTrendPoint
{
Date = g.Key,
SalesAmount = g.Sum(r => (decimal)r.OrderPrice) / 100m,
SalesAmount = g.Sum(r => (decimal)(r.OrderPrice)) / 100m,
OrderCount = g.Select(r => r.OrderId).Distinct().Count()
});
@@ -159,15 +164,15 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
public static List<ShopSalesProportionItem> GetShopSalesProportion(IEnumerable<ShopRevenueStatisticsRow> rows)
{
var list = (rows ?? new List<ShopRevenueStatisticsRow>()).Where(IsValidOrder).ToList();
decimal total = list.Sum(r => (decimal)r.OrderPrice) / 100m;
decimal total = list.Sum(r => (decimal)(r.OrderPrice )) / 100m;
return list
.GroupBy(r => r.ShopName)
.Select(g => new ShopSalesProportionItem
{
ShopName = string.IsNullOrEmpty(g.Key) ? "未知店铺" : g.Key,
SalesAmount = g.Sum(r => (decimal)r.OrderPrice) / 100m,
Percentage = total == 0m ? 0d : (double)(g.Sum(r => (decimal)r.OrderPrice) / 100m / total * 100m)
SalesAmount = g.Sum(r => (decimal)(r.OrderPrice )) / 100m,
Percentage = total == 0m ? 0d : (double)(g.Sum(r => (decimal)(r.OrderPrice)) / 100m / total * 100m)
})
.OrderByDescending(i => i.SalesAmount)
.ToList();
@@ -184,17 +189,21 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
{
var valid = g.Where(IsValidOrder).ToList();
var refunded = g.Where(IsRefunded).ToList();
decimal salesAmount = valid.Sum(r => (decimal)r.OrderPrice) / 100m;
decimal salesAmount = valid.Sum(r => (decimal)(r.OrderPrice)) / 100m;
int orderCount = valid.Select(r => r.OrderId).Distinct().Count();
decimal profit = valid.Sum(r => (decimal)(r.Profit)) / 100m;
decimal purchaseAmount = valid.Sum(r => (decimal)(r.PurchasePrice)) / 100m;
return new ShopStatisticsItem
{
ShopName = string.IsNullOrEmpty(g.Key) ? "未知店铺" : g.Key,
OrderCount = orderCount,
SalesAmount = salesAmount,
Profit = valid.Sum(r => (decimal)r.Profit) / 100m,
RefundAmount = refunded.Sum(r => (decimal)r.OrderPrice) / 100m,
CustomerCount = orderCount,
AvgOrderAmount = orderCount == 0 ? 0m : salesAmount / orderCount
Profit = profit,
RefundAmount = refunded.Sum(r => (decimal)(r.OrderPrice)) / 100m,
PlatformFeeTotal = valid.Sum(r => (decimal)(r.PredictCommissionFee)) / 100m,
Commission = valid.Sum(r => (decimal)(r.CommissionAmount)) / 100m,
AvgOrderAmount = orderCount == 0 ? 0m : salesAmount / orderCount,
AverageProfitRate = purchaseAmount == 0m ? 0m : profit / purchaseAmount * 100m
};
})
.OrderByDescending(i => i.SalesAmount)
@@ -215,7 +224,9 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
{
var valid = g.Where(IsValidOrder).ToList();
var refunded = g.Where(IsRefunded).ToList();
decimal salesAmount = valid.Sum(r => (decimal)r.OrderPrice) / 100m;
decimal salesAmount = valid.Sum(r => (decimal)(r.OrderPrice)) / 100m;
decimal profit = valid.Sum(r => (decimal)(r.Profit)) / 100m;
decimal purchaseAmount = valid.Sum(r => (decimal)(r.PurchasePrice)) / 100m;
int orderCount = valid.Select(r => r.OrderId).Distinct().Count();
return new ShopPeriodStatisticsItem
{
@@ -223,8 +234,11 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
Period = GetPeriodText(g.Key, periodType),
OrderCount = orderCount,
SalesAmount = salesAmount,
Profit = valid.Sum(r => (decimal)r.Profit) / 100m,
RefundAmount = refunded.Sum(r => (decimal)r.OrderPrice) / 100m,
Profit = profit,
RefundAmount = refunded.Sum(r => (decimal)(r.OrderPrice)) / 100m,
PlatformFeeTotal = valid.Sum(r => (decimal)(r.PredictCommissionFee)) / 100m,
Commission = valid.Sum(r => (decimal)(r.CommissionAmount)) / 100m,
AverageProfitRate = purchaseAmount == 0m ? 0m : profit / purchaseAmount * 100m,
CustomerCount = orderCount,
AvgOrderAmount = orderCount == 0 ? 0m : salesAmount / orderCount
};
@@ -245,6 +259,9 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
SalesAmount = 0m,
Profit = 0m,
RefundAmount = 0m,
PlatformFeeTotal = 0m,
Commission = 0m,
AverageProfitRate = 0m,
CustomerCount = 0,
AvgOrderAmount = 0m
});
@@ -310,19 +327,22 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
}
/// <summary>
/// 默认实现:返回模拟数据,用于页面演示/联调。
/// 接入真实接口时,新建实现类并注入即可(见 IShopRevenueStatisticsService 注释)。
/// 默认实现:请求真实接口(shop_revenue_statistics/get_shop_order_amount)。
/// 接入其他数据源时,新建实现类并注入即可(见 IShopRevenueStatisticsService 注释)。
/// </summary>
public class ShopRevenueStatisticsService : IShopRevenueStatisticsService
{
/// <summary>缓存有效期:订单/退款数据会持续变化,超过该时长后缓存自动失效并重新请求。</summary>
private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(60);
/// <summary>
/// 明细数据缓存。
/// key = 店铺id|yyyyMMdd(店铺 × 天),value = 该店铺当天的明细行。
/// 查询按“店铺 × 日期”命中;完全未命中时所有店铺 + 整个时间范围一次请求,
/// 部分命中时只请求缺失的店铺/日期段。
/// 明细数据缓存(按天缓存,纯时间维度,不含店铺)。
/// key = 日期(yyyyMMdd),value = 该天全部店铺的明细 + 缓存时间。
/// 查询时按天命中:未命中的天合并成连续段一次请求,返回后按天拆分写入;
/// 前台选某一个或某多个店铺时,按所选店铺(ShopName)过滤,复用同一批时间数据。
/// </summary>
private readonly Dictionary<string, List<ShopRevenueStatisticsRow>> _rowCache =
new Dictionary<string, List<ShopRevenueStatisticsRow>>();
private readonly Dictionary<string, CacheEntry> _rowCache =
new Dictionary<string, CacheEntry>();
private readonly object _cacheLock = new object();
private sealed class DateRange
@@ -331,91 +351,161 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
public DateTime End;
}
private static string CacheKey(string shopId, DateTime day)
private sealed class CacheEntry
{
return shopId + "|" + day.ToString("yyyyMMdd");
public List<ShopRevenueStatisticsRow> Rows;
public DateTime CachedAt;
}
private static string CacheKey(DateTime day)
{
return day.Date.ToString("yyyyMMdd");
}
/// <summary>枚举 startDate ~ endDate 之间的每一天(含首尾)</summary>
private static IEnumerable<DateTime> EnumerateDays(DateTime startDate, DateTime endDate)
{
for (var d = startDate.Date; d <= endDate.Date; d = d.AddDays(1))
yield return d;
}
/// <summary>该天缓存是否有效(存在且未过期);调用方需持有 _cacheLock</summary>
private bool IsValidLocked(DateTime day)
{
return _rowCache.TryGetValue(CacheKey(day), out var entry)
&& DateTime.Now - entry.CachedAt <= CacheDuration;
}
/// <summary>把缺失日期合并为连续日期段(每段一次请求)</summary>
private static List<DateRange> BuildRuns(List<DateTime> missingDays)
{
var sorted = missingDays.OrderBy(d => d).ToList();
var runs = new List<DateRange>();
int i = 0;
while (i < sorted.Count)
{
var start = sorted[i];
var end = start;
i++;
while (i < sorted.Count && sorted[i] == end.AddDays(1))
{
end = sorted[i];
i++;
}
runs.Add(new DateRange { Start = start, End = end });
}
return runs;
}
public async Task<List<ShopRevenueStatisticsRow>> QueryRowsAsync(ShopRevenueStatisticsQuery query)
{
// 1. 展开本次查询涉及的店铺(ShopKey 为空 = 全部店铺)
List<string> shopIds;
if (string.IsNullOrEmpty(query.ShopKey))
shopIds = (await GetShopOptionsAsync()).Select(a => a.AppKey).ToList();
else
shopIds = new List<string> { query.ShopKey };
if (shopIds.Count == 0)
return new List<ShopRevenueStatisticsRow>();
var startDay = query.StartDate.Date;
var endDay = query.EndDate.Date;
int totalDays = (int)(endDay - startDay).Days + 1;
if (startDay > endDay)
{
var tmp = startDay;
startDay = endDay;
endDay = tmp;
}
// 2. 命中检测:按 店铺 × 天 找出缺失项
var missingByShop = new Dictionary<string, List<DateTime>>();
int missingCount = 0;
// 1. 找出未命中或已过期的天
List<DateTime> missingDays;
lock (_cacheLock)
{
foreach (var shopId in shopIds)
missingDays = EnumerateDays(startDay, endDay)
.Where(d => !IsValidLocked(d))
.ToList();
}
// 2. 有缺失:按连续段请求整个时间范围的全部店铺明细,并按天写入缓存
if (missingDays.Count > 0)
{
var shopIds = GetShopOptions().Select(a => a.ShopId).ToList();
if (shopIds.Count == 0)
return new List<ShopRevenueStatisticsRow>();
foreach (var run in BuildRuns(missingDays))
{
var missingDays = new List<DateTime>();
for (var d = startDay; d <= endDay; d = d.AddDays(1))
{
if (!_rowCache.ContainsKey(CacheKey(shopId, d)))
missingDays.Add(d);
}
missingCount += missingDays.Count;
missingByShop[shopId] = missingDays;
var rows = await RequestRowsAsync(shopIds, run.Start, run.End);
CacheResponse(run.Start, run.End, rows);
}
}
// 3. 全部命中:直接从缓存拼装返回
if (missingCount == 0)
return GetCachedRows(shopIds, startDay, endDay);
// 4. 完全未命中:所有店铺 + 整个时间范围一次请求
if (missingCount == shopIds.Count * totalDays)
{
var allRows = await RequestRowsAsync(shopIds, startDay, endDay);
CacheResponse(shopIds, startDay, endDay, allRows, null);
var cached = GetCachedRows(shopIds, startDay, endDay);
// 若接口行内未返回 app_key 导致无法入库,则直接返回本次请求结果
return cached.Count > 0 ? cached : allRows;
}
// 5. 部分命中:只请求未命中的(店铺 × 连续缺失日期段)
foreach (var pair in missingByShop)
{
if (pair.Value.Count == 0)
continue;
foreach (var run in BuildRuns(pair.Value))
{
var rows = await RequestRowsAsync(new List<string> { pair.Key }, run.Start, run.End);
CacheResponse(new List<string> { pair.Key }, run.Start, run.End, rows, pair.Key);
}
}
return GetCachedRows(shopIds, startDay, endDay);
// 3. 按天从缓存拼装完整范围,再按所选店铺过滤(ShopKey 为空 = 全部店铺)
return FilterByShop(GetCachedRows(startDay, endDay), query.ShopKey);
}
/// <summary>按 店铺 × 日期 从缓存拼装结果</summary>
private List<ShopRevenueStatisticsRow> GetCachedRows(List<string> shopIds, DateTime startDate, DateTime endDate)
/// <summary>
/// 把一次请求的结果按天写入缓存。
/// 本次请求覆盖的每一天都以返回数据为准整体替换(无数据的天记为空的已缓存,
/// 避免“该天无数据”被反复判定为未命中而重复请求)。
/// </summary>
private void CacheResponse(DateTime startDate, DateTime endDate, List<ShopRevenueStatisticsRow> rows)
{
if (rows == null)
rows = new List<ShopRevenueStatisticsRow>();
lock (_cacheLock)
{
RemoveExpiredEntriesLocked();
var byDay = rows
.Where(r => r.CreateTime.Date >= startDate.Date && r.CreateTime.Date <= endDate.Date)
.GroupBy(r => r.CreateTime.Date)
.ToDictionary(g => g.Key, g => g.ToList());
foreach (var day in EnumerateDays(startDate, endDate))
{
byDay.TryGetValue(day, out var dayRows);
_rowCache[CacheKey(day)] = new CacheEntry { Rows = dayRows ?? new List<ShopRevenueStatisticsRow>(), CachedAt = DateTime.Now };
}
}
}
/// <summary>按天从缓存拼装某时间范围的明细(返回新列表,避免外部修改污染缓存)</summary>
private List<ShopRevenueStatisticsRow> GetCachedRows(DateTime startDate, DateTime endDate)
{
var result = new List<ShopRevenueStatisticsRow>();
lock (_cacheLock)
{
for (var d = startDate.Date; d <= endDate.Date; d = d.AddDays(1))
foreach (var day in EnumerateDays(startDate, endDate))
{
foreach (var shopId in shopIds)
{
if (_rowCache.TryGetValue(CacheKey(shopId, d), out var list))
result.AddRange(list);
}
if (_rowCache.TryGetValue(CacheKey(day), out var entry))
result.AddRange(entry.Rows);
}
}
return result;
}
/// <summary>按所选店铺过滤明细:ShopKey 为空返回全部;否则按店铺名(ShopName)精确匹配。</summary>
private static List<ShopRevenueStatisticsRow> FilterByShop(List<ShopRevenueStatisticsRow> rows, string shopKey)
{
if (rows == null)
return new List<ShopRevenueStatisticsRow>();
if (string.IsNullOrEmpty(shopKey))
return new List<ShopRevenueStatisticsRow>(rows);
var shop = GetShopOptions().FirstOrDefault(a => a.AppKey == shopKey);
if (shop == null || string.IsNullOrEmpty(shop.ShopName))
return new List<ShopRevenueStatisticsRow>();
return rows
.Where(r => string.Equals(r.ShopName, shop.ShopName, StringComparison.OrdinalIgnoreCase))
.ToList();
}
/// <summary>清理已过期的缓存条目(调用方需持有 _cacheLock)</summary>
private void RemoveExpiredEntriesLocked()
{
var expiredKeys = _rowCache
.Where(p => DateTime.Now - p.Value.CachedAt > CacheDuration)
.Select(p => p.Key)
.ToList();
foreach (var key in expiredKeys)
_rowCache.Remove(key);
}
/// <summary>请求接口一次(可传多个店铺),返回原始明细行</summary>
private async Task<List<ShopRevenueStatisticsRow>> RequestRowsAsync(List<string> shopIds, DateTime startDate, DateTime endDate)
{
@@ -447,81 +537,6 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
}
/// <summary>
/// 把一次请求的结果写入缓存(按 店铺id|日期 分桶)。
/// 除有数据的 店铺×天 外,会把本次请求覆盖但无数据的 店铺×天 也标记为已缓存(空列表),
/// 避免“该店铺当天无数据”被反复判定为未命中而重复请求数据库。
/// </summary>
private void CacheResponse(List<string> shopIds, DateTime startDate, DateTime endDate, List<ShopRevenueStatisticsRow> rows, string fallbackShopId)
{
if (rows == null)
rows = new List<ShopRevenueStatisticsRow>();
// 能否按 店铺×天 归属:
// 单店铺请求一定可以(用请求的店铺兜底);多店铺请求要求行内带 app_key;
// 响应为空时也视为可归属(说明覆盖的 店铺×天 都无数据)。
bool canAttribute = shopIds.Count == 1
|| rows.Count == 0
|| rows.Any(r => !string.IsNullOrEmpty(r.AppKey));
lock (_cacheLock)
{
if (canAttribute)
{
// 覆盖的 店铺×天 全部落缓存键(无数据的天记为空的已缓存)
for (var d = startDate.Date; d <= endDate.Date; d = d.AddDays(1))
{
foreach (var shopId in shopIds)
{
var key = CacheKey(shopId, d);
if (!_rowCache.TryGetValue(key, out var list))
{
list = new List<ShopRevenueStatisticsRow>();
_rowCache[key] = list;
}
}
}
}
// 有数据的行按 店铺×天 追加
foreach (var row in rows)
{
// 行内未带 app_key 时,用本次请求的店铺兜底
var shopId = string.IsNullOrEmpty(row.AppKey) ? fallbackShopId : row.AppKey;
if (string.IsNullOrEmpty(shopId))
continue;
var key = CacheKey(shopId, row.CreateTime.Date);
if (!_rowCache.TryGetValue(key, out var list))
{
list = new List<ShopRevenueStatisticsRow>();
_rowCache[key] = list;
}
list.Add(row);
}
}
}
/// <summary>把缺失日期合并为连续日期段(每段一次请求)</summary>
private static List<DateRange> BuildRuns(List<DateTime> missingDays)
{
var sorted = missingDays.OrderBy(d => d).ToList();
var runs = new List<DateRange>();
int i = 0;
while (i < sorted.Count)
{
var start = sorted[i];
var end = start;
i++;
while (i < sorted.Count && sorted[i] == end.AddDays(1))
{
end = sorted[i];
i++;
}
runs.Add(new DateRange { Start = start, End = end });
}
return runs;
}
public async Task<ShopRevenueSummary> GetSummaryAsync(ShopRevenueStatisticsQuery query)
{
var rows = await QueryRowsAsync(query);
@@ -553,16 +568,21 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
return RevenueStatisticsAggregator.GetPeriodStatistics(rows, query.StartDate, query.EndDate, periodType);
}
public Task<List<ShopOption>> GetShopOptionsAsync()
/// <summary>店铺下拉列表(同步读取,供查询与过滤使用)</summary>
private static List<ShopOption> GetShopOptions()
{
var list = GlobalConfig.ShopLoginList.Select(a =>
return GlobalConfig.ShopLoginList.Select(a =>
new ShopOption()
{
AppKey = a.ShopId,
ShopId = a.ErpShopId,
ShopName = a.ShopAccount
}).ToList();
}
return Task.FromResult(list);
public Task<List<ShopOption>> GetShopOptionsAsync()
{
return Task.FromResult(GetShopOptions());
}
}
@@ -130,282 +130,11 @@
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="12">
<!-- ============ 查询条件工具栏 ============ -->
<Border
Margin="6,6,6,0"
Padding="14,10"
Style="{StaticResource CardBorderStyle}">
<WrapPanel>
<TextBlock
Margin="6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="时间" />
<ComboBox
Width="110"
Height="32"
VerticalContentAlignment="Center"
DisplayMemberPath="Text"
ItemsSource="{Binding TimeRangeOptions}"
SelectedItem="{Binding SelectedTimeRange, Mode=TwoWay}"
Style="{StaticResource BaseComboBoxStyle}" />
<TextBlock
Margin="14,0,6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="日期" />
<DatePicker
Height="32"
VerticalContentAlignment="Center"
SelectedDate="{Binding StartDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Margin="6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="~" />
<DatePicker
Height="32"
VerticalContentAlignment="Center"
SelectedDate="{Binding EndDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Margin="14,0,6,0"
VerticalAlignment="Center"
FontSize="14"
Foreground="#666666"
Text="店铺" />
<ComboBox
Width="130"
Height="32"
VerticalContentAlignment="Center"
DisplayMemberPath="ShopName"
ItemsSource="{Binding ShopOptions}"
SelectedItem="{Binding SelectedShop, Mode=TwoWay}"
Style="{StaticResource BaseComboBoxStyle}" />
<Button
Height="32"
Margin="18,0,0,0"
Padding="16,0"
Command="{Binding QueryCommand}"
Content=" 查询 "
Style="{StaticResource BasePageButtonStyle}" />
<Button
Height="32"
Margin="8,0,0,0"
Padding="16,0"
Command="{Binding ResetCommand}"
Content=" 重置 "
Style="{StaticResource BasePageButtonLightStyle}" />
</WrapPanel>
</Border>
<!-- ============ 查询条件工具栏(共享 UserControl) ============ -->
<stat:RevenueStatisticsSearchBar />
<!-- ============ 汇总卡片(2 行 x 4 列) ============ -->
<Grid Margin="3,8,3,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 销售额 -->
<Border
Grid.Row="0"
Grid.Column="0"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="销售额" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.SalesAmount, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="最终有效销售额" />
</StackPanel>
</Border>
<!-- 有效订单 -->
<Border
Grid.Row="0"
Grid.Column="1"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="有效订单" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.ValidOrderCount, StringFormat={}{0:N0}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="已扣除退款订单" />
</StackPanel>
</Border>
<!-- 利润 -->
<Border
Grid.Row="0"
Grid.Column="2"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="利润" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.Profit, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="实际利润" />
</StackPanel>
</Border>
<!-- 退款金额 -->
<Border
Grid.Row="0"
Grid.Column="3"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="退款金额" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.RefundAmount, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="统计期间退款" />
</StackPanel>
</Border>
<!-- 商品销量 -->
<Border
Grid.Row="1"
Grid.Column="0"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="商品销量" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.ProductSalesVolume, StringFormat={}{0:N0}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="有效销量" />
</StackPanel>
</Border>
<!-- 客户数 -->
<Border
Grid.Row="1"
Grid.Column="1"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="客户数" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.CustomerCount, StringFormat={}{0:N0}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="成交客户" />
</StackPanel>
</Border>
<!-- 客单价 -->
<Border
Grid.Row="1"
Grid.Column="2"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="客单价" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.AvgOrderAmount, StringFormat={}¥{0:N2}}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="平均每单金额" />
</StackPanel>
</Border>
<!-- 退款率 -->
<Border
Grid.Row="1"
Grid.Column="3"
Style="{StaticResource CardBorderStyle}">
<StackPanel>
<TextBlock
FontSize="14"
Foreground="#888888"
Text="退款率" />
<TextBlock
Margin="0,12,0,0"
FontSize="30"
FontWeight="Bold"
Foreground="#333333"
Text="{Binding Summary.RefundRate, StringFormat={}{0:0.00}%}" />
<TextBlock
Margin="0,10,0,0"
FontSize="12"
Foreground="#999999"
Text="退款订单占比" />
</StackPanel>
</Border>
</Grid>
<!-- ============ 汇总卡片(共享 UserControl) ============ -->
<stat:RevenueSummaryCards />
<!-- ============ 图表区域 ============ -->
<Grid Margin="3,8,3,0">
@@ -500,9 +229,19 @@
Header="退款金额" />
<DataGridTextColumn
Width="*"
Binding="{Binding CustomerCount, StringFormat={}{0:N0}}"
Binding="{Binding PlatformFeeTotal, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="客户数" />
Header="平台服务费总额" />
<DataGridTextColumn
Width="*"
Binding="{Binding Commission, StringFormat={}¥{0:N2}}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="达人佣金" />
<DataGridTextColumn
Width="*"
Binding="{Binding AverageProfitRate, StringFormat={}{0:0.00}%}"
ElementStyle="{StaticResource CenterCellTextStyle}"
Header="平均利润率" />
<DataGridTextColumn
Width="*"
Binding="{Binding AvgOrderAmount, StringFormat={}¥{0:N2}}"
@@ -551,4 +290,4 @@
</StackPanel>
</Border>
</Grid>
</UserControl>
</UserControl>
@@ -30,6 +30,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
private readonly DelegateCommand _lastPageCommand;
private ShopOption _selectedShop;
private ShopRevenueSummary _summary;
private ObservableCollection<ShopPeriodStatisticsItem> _statistics;
private List<ShopPeriodStatisticsItem> _allItems = new List<ShopPeriodStatisticsItem>();
private int _pageIndex = 1;
@@ -209,6 +210,17 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
#region 统计结果
/// <summary>汇总卡片数据</summary>
public ShopRevenueSummary Summary
{
get { return _summary; }
private set
{
_summary = value;
OnPropertyChanged(nameof(Summary));
}
}
/// <summary>当前页的周期×店铺统计表格数据</summary>
public ObservableCollection<ShopPeriodStatisticsItem> Statistics
{
@@ -486,6 +498,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
};
var table = await _service.GetPeriodStatisticsAsync(query, _periodType);
Summary = await _service.GetSummaryAsync(query);
_allItems = table ?? new List<ShopPeriodStatisticsItem>();
TotalCount = _allItems.Count;
TotalPages = TotalCount == 0 ? 1 : (int)Math.Ceiling(TotalCount / (double)PageSize);