修改统计功能 使用新API接口
This commit is contained in:
1 parent
03f71a5d64
commit
8431a8f6b0
6 files changed
+1117
-708
No files matched your search
@@ -1,4 +1,4 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using WPFCommon.Models;
|
||||
@@ -6,152 +6,219 @@ using WPFCommon.Models;
|
||||
namespace _29WxOrderWpf.Ctrls.Statistics.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 店铺营收统计 - 查询参数
|
||||
/// 店铺营收统计 - 查询参数。
|
||||
/// 对应新接口 shop_revenue_statistics/get_shop_order_amount 与
|
||||
/// shop_revenue_statistics/get_shop_stats_details 的请求体(StartDate / EndDate / ShopIds)。
|
||||
/// </summary>
|
||||
public class ShopRevenueStatisticsQuery
|
||||
{
|
||||
/// <summary>开始时间</summary>
|
||||
/// <summary>开始日期</summary>
|
||||
public DateTime StartDate { get; set; }
|
||||
|
||||
/// <summary>结束时间</summary>
|
||||
/// <summary>结束日期</summary>
|
||||
public DateTime EndDate { get; set; }
|
||||
|
||||
/// <summary>选中的店铺 app_key 列表;空/Null 表示全部店铺</summary>
|
||||
public List<string> ShopKeys { get; set; }
|
||||
/// <summary>
|
||||
/// 选中的店铺 ERP 店铺ID(ShopLoginInfo.ErpShopId)列表;
|
||||
/// 空/Null 表示全部店铺(服务层会取全部店铺的 ErpShopId)。
|
||||
/// </summary>
|
||||
public List<string> ShopIds { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 店铺营收统计 - 接口返回的原始明细行。
|
||||
/// 与后端 WXOrderAmountInfo 保持一致(camelCase JSON):
|
||||
/// shopName, createTime, orderId, status, productPrice, orderPrice, purchasePrice, freight,
|
||||
/// predictCommissionFee, commissionAmount, discountedPrice, profit, purchaseStatus,
|
||||
/// outAmount, refundStatus, refundStatusString, refundAmount, externalProcurement
|
||||
/// 金额字段单位均为分(显示时需除以 100)。
|
||||
/// 店铺统计记录(接口返回)。
|
||||
/// 接口返回结构:
|
||||
/// shop_revenue_statistics/GetShopStats -> Result<Dictionary<string, List<ShopStatsResponse>>>
|
||||
/// Key = 店铺ID,Value = 该店铺按 <see cref="ShopStatsGroupType"/> 聚合后的记录列表
|
||||
/// (汇总=1 条;天/周/月=每个自然日/自然周/自然月各 1 条)。
|
||||
/// 注意:接口只返回按 GroupType 聚合后的记录,不返回“整段汇总”,
|
||||
/// 页面上的汇总卡片由客户端对返回记录自行合计(GroupType=Summary 时每店铺一条,直接相加即可)。
|
||||
/// 金额字段单位均为“分”,显示时需除以 100 转为元。
|
||||
/// </summary>
|
||||
public class ShopRevenueStatisticsRow
|
||||
public class ShopStatsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 店铺名称
|
||||
/// 店铺ID
|
||||
/// </summary>
|
||||
[JsonProperty("shop_name")]
|
||||
public string ShopName { get; set; }
|
||||
public string ShopId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下单时间(秒级时间戳)
|
||||
/// 销售订单创建日期
|
||||
/// </summary>
|
||||
[JsonProperty("create_time")]
|
||||
[JsonConverter(typeof(UnixDateTimeConverter))]
|
||||
public DateTime CreateTime { get; set; }
|
||||
public DateTime StatDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单号
|
||||
/// 订单数(有效订单,不含退款订单)
|
||||
/// </summary>
|
||||
[JsonProperty("order_id")]
|
||||
public string OrderId { get; set; }
|
||||
public int OrderCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单状态
|
||||
/// 销售总额(分)
|
||||
/// </summary>
|
||||
[JsonProperty("status")]
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品总价(分)
|
||||
/// </summary>
|
||||
[JsonProperty("product_price")]
|
||||
public int ProductPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单金额(分)
|
||||
/// </summary>
|
||||
[JsonProperty("order_price")]
|
||||
public int OrderPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 采购价(分)
|
||||
/// </summary>
|
||||
[JsonProperty("purchase_price")]
|
||||
public int PurchasePrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 运费(分)
|
||||
/// </summary>
|
||||
[JsonProperty("freight")]
|
||||
public int Freight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 平台服务费(预计技术服务费,分)
|
||||
/// </summary>
|
||||
[JsonProperty("predict_commission_fee")]
|
||||
public int PredictCommissionFee { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 达人佣金(分)
|
||||
/// </summary>
|
||||
[JsonProperty("commission_amount")]
|
||||
public int CommissionAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 优惠金额(分)
|
||||
/// </summary>
|
||||
[JsonProperty("discounted_price")]
|
||||
public int DiscountedPrice { get; set; }
|
||||
public long SalesAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 利润(分)
|
||||
/// </summary>
|
||||
[JsonProperty("profit")]
|
||||
public int Profit { get; set; }
|
||||
public long ProfitAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 采购状态
|
||||
/// 平台佣金(分)
|
||||
/// </summary>
|
||||
[JsonProperty("purchase_status")]
|
||||
public int PurchaseStatus { get; set; }
|
||||
public long PlatformFee { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外采金额(分),out_amount为空或0时从中转仓订单表补全
|
||||
/// 退款订单数
|
||||
/// </summary>
|
||||
public int RefundOrderCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已成功退款金额(分,店铺端已退款成功)
|
||||
/// </summary>
|
||||
public long RefundDone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 退款处理中金额(分,店铺端退款中)
|
||||
/// </summary>
|
||||
public long RefundPending { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 采购单数
|
||||
/// </summary>
|
||||
public int PurchaseCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总花费金额(分,买手号采购付款金额)
|
||||
/// </summary>
|
||||
public long TotalPurchase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 售后中买手总金额(分)
|
||||
/// </summary>
|
||||
public long TotalRefund => DoneRefund + PendingRefund + RejectedRefund;
|
||||
|
||||
/// <summary>
|
||||
/// 已成功退款金额(分,买手号采购退款成功)
|
||||
/// </summary>
|
||||
public long DoneRefund { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 未退回金额(分)
|
||||
/// </summary>
|
||||
public long PendingRefund { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 被拒绝金额(分)
|
||||
/// </summary>
|
||||
public long RejectedRefund { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 销售趋势记录(接口返回):跨店铺合计后的按天趋势,供综合统计页的销售趋势折线图使用。
|
||||
/// 接口返回结构:
|
||||
/// shop_revenue_statistics/GetShopStatsTrend
|
||||
/// -> Result<List<ShopStatsTrendResponse>>
|
||||
/// 请求参数:StartDate / EndDate / ShopIds(不需要 GroupType,固定按天返回)。
|
||||
/// 金额字段单位为“分”,显示时需除以 100 转为元。
|
||||
/// </summary>
|
||||
public class ShopStatsTrendResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 统计日期(天)
|
||||
/// </summary>
|
||||
public DateTime StatDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 销售额(分)
|
||||
/// </summary>
|
||||
public long SalesAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单数
|
||||
/// </summary>
|
||||
public int OrderCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订单明细(接口返回):订单列表弹窗 ShowOrderDialogView 的数据行。
|
||||
/// 接口返回结构:
|
||||
/// shop_revenue_statistics/GetShopOrderDetails
|
||||
/// -> Result<List<ShopOrderDetailResponse>>
|
||||
/// 请求参数:StartDate / EndDate / ShopIds / OrderType(见 <see cref="ShopOrderDetailType"/>)。
|
||||
/// 金额字段单位均为“分”,显示时需除以 100 转为元。
|
||||
/// </summary>
|
||||
public class ShopOrderDetailResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 店铺ID
|
||||
/// </summary>
|
||||
public long ShopId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 店铺名称
|
||||
/// </summary>
|
||||
public string ShopName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商铺订单号
|
||||
/// </summary>
|
||||
public string OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单创建时间
|
||||
/// </summary>
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单金额(分)
|
||||
/// </summary>
|
||||
public int OrderPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单退款金额(分),即商铺订单商品明细的退款金额之和;有效订单为 0
|
||||
/// </summary>
|
||||
public int OrderRefundAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外采金额(分),即该订单下所有买手订单的购买金额之和
|
||||
/// </summary>
|
||||
[JsonProperty("out_amount")]
|
||||
public int OutAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 售后状态(订单明细 refund_status)
|
||||
/// 买手未退回金额(分),即买手订单中退款未成功的退款金额之和;无售后时为 0
|
||||
/// </summary>
|
||||
[JsonProperty("refund_status")]
|
||||
public int RefundStatus { get; set; }
|
||||
public int UnrefundedAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 售后状态(退款单 status)
|
||||
/// 买手订单信息(一个订单号对应多个买手订单)
|
||||
/// </summary>
|
||||
[JsonProperty("refund_status_string")]
|
||||
public string RefundStatusString { get; set; }
|
||||
public List<BuyerOrderInfo> BuyerOrders { get; set; } = new List<BuyerOrderInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 买手订单信息(订单明细下的子项,一个商铺订单可对应多个买手订单)
|
||||
/// </summary>
|
||||
public class BuyerOrderInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 外部订单(买手订单)号
|
||||
/// </summary>
|
||||
public string OutOrderSn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 售后退款金额(分)
|
||||
/// 金额(分)
|
||||
/// </summary>
|
||||
public int OutAmount { 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>
|
||||
/// 买手退款金额
|
||||
/// </summary>
|
||||
[JsonProperty("buyer_refund_amount")]
|
||||
public int BuyerRefundAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 买手退款状态
|
||||
/// </summary>
|
||||
[JsonProperty("buyer_refund_status")]
|
||||
public string BuyerRefundStatus { get; set; }
|
||||
|
||||
public string RefundStatusText { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -167,7 +234,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
|
||||
/// <summary>利润(实际利润)</summary>
|
||||
public decimal Profit { get; set; }
|
||||
|
||||
/// <summary>退款金额(统计期间退款)</summary>
|
||||
/// <summary>退款金额(统计期间店铺端退款:已成功 + 处理中)</summary>
|
||||
public decimal RefundAmount { get; set; }
|
||||
|
||||
/// <summary>退款订单数</summary>
|
||||
@@ -176,7 +243,10 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
|
||||
/// <summary>平台服务费总额(元)</summary>
|
||||
public decimal PlatformFeeTotal { get; set; }
|
||||
|
||||
/// <summary>达人佣金(元)</summary>
|
||||
/// <summary>
|
||||
/// 达人佣金(元)。
|
||||
/// 新接口未提供该字段,固定为 0,保留属性与页面展示,待后端补充字段后直接接上。
|
||||
/// </summary>
|
||||
public decimal Commission { get; set; }
|
||||
|
||||
/// <summary>客单价(平均每单金额)</summary>
|
||||
@@ -239,7 +309,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
|
||||
/// <summary>店铺名称</summary>
|
||||
public string ShopName { get; set; }
|
||||
|
||||
/// <summary>订单数</summary>
|
||||
/// <summary>订单数(有效订单,不含退款订单)</summary>
|
||||
public int OrderCount { get; set; }
|
||||
|
||||
/// <summary>销售额(元)</summary>
|
||||
@@ -255,7 +325,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
|
||||
/// <summary>平台服务费总额(元)</summary>
|
||||
public decimal PlatformFeeTotal { get; set; }
|
||||
|
||||
/// <summary>达人佣金(元)</summary>
|
||||
/// <summary>达人佣金(元)。新接口未提供该字段,固定为 0。</summary>
|
||||
public decimal Commission { get; set; }
|
||||
|
||||
/// <summary>客单价(元)</summary>
|
||||
@@ -280,6 +350,51 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
|
||||
Month = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 接口聚合方式(请求参数 GroupType,按数字传值)。
|
||||
/// 每种方式都返回 Dictionary<店铺ID, List<ShopStatsResponse>>:
|
||||
/// 汇总时每个店铺 1 条,天/周/月时每个店铺每个周期 1 条。
|
||||
/// </summary>
|
||||
public enum ShopStatsGroupType
|
||||
{
|
||||
/// <summary>
|
||||
/// 汇总:每个商铺只返回一条(整个查询范围合计)
|
||||
/// </summary>
|
||||
Summary = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 天:按自然日聚合,每个商铺每天一条
|
||||
/// </summary>
|
||||
Day = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 周:按自然周聚合(周一为一周开始),每个商铺每周一条
|
||||
/// </summary>
|
||||
Week = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 月:按自然月聚合,每个商铺每月一条
|
||||
/// </summary>
|
||||
Month = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订单明细类型(订单列表弹窗的入口类型,对应接口参数 OrderType,按数字传值)。
|
||||
/// 数值与后端枚举保持一致:有效订单=1、退款订单=2、买手未退回金额的订单=3
|
||||
/// (后端枚举里的“全部订单 All=0”本页不使用,故不定义)。
|
||||
/// </summary>
|
||||
public enum ShopOrderDetailType
|
||||
{
|
||||
/// <summary>有效订单:没有发生售后的订单(对应统计里的订单数 / 销售额 / 利润口径)</summary>
|
||||
Valid = 1,
|
||||
|
||||
/// <summary>退款订单:发生过售后的订单(对应统计里的退款订单数口径)</summary>
|
||||
Refund = 2,
|
||||
|
||||
/// <summary>买手未退回金额的订单:退款订单中买手款项还没退回的(对应统计里的未退回金额口径)</summary>
|
||||
BuyerUnrefunded = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 天/周/月周期统计表格行(金额单位:元)。
|
||||
/// 按所选店铺(或全部店铺)统计:天统计每天一行;周统计每周一行;月统计每月一行。
|
||||
@@ -292,7 +407,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
|
||||
/// <summary>周期显示文本(天:yyyy-MM-dd;周:yyyy-MM-dd ~ yyyy-MM-dd;月:yyyy-MM)</summary>
|
||||
public string Period { get; set; }
|
||||
|
||||
/// <summary>订单数</summary>
|
||||
/// <summary>订单数(有效订单,不含退款订单)</summary>
|
||||
public int OrderCount { get; set; }
|
||||
|
||||
/// <summary>销售额(元)</summary>
|
||||
@@ -309,7 +424,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
|
||||
/// <summary>平台服务费总额(元)</summary>
|
||||
public decimal PlatformFeeTotal { get; set; }
|
||||
|
||||
/// <summary>达人佣金(元)</summary>
|
||||
/// <summary>达人佣金(元)。新接口未提供该字段,固定为 0。</summary>
|
||||
public decimal Commission { get; set; }
|
||||
|
||||
/// <summary>平均利润率(利润 / 采购金额,百分数,如 12.34 表示 12.34%)</summary>
|
||||
@@ -322,14 +437,51 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
|
||||
public decimal AvgOrderAmount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 店铺营收统计 - 一次查询的全部聚合结果(综合统计页使用)。
|
||||
/// 由两次请求组成:
|
||||
/// shop_revenue_statistics/GetShopStats(GroupType=Summary)-> 卡片/店铺统计表/销售占比(每店铺一条,与区间长度无关)
|
||||
/// shop_revenue_statistics/GetShopStatsTrend -> 销售趋势(跨店铺合计的按天序列)
|
||||
/// </summary>
|
||||
public class ShopRevenueStatisticsResult
|
||||
{
|
||||
/// <summary>汇总卡片数据</summary>
|
||||
public ShopRevenueSummary Summary { get; set; }
|
||||
|
||||
/// <summary>销售趋势(按天,缺失日期补 0)</summary>
|
||||
public List<RevenueTrendPoint> Trend { get; set; }
|
||||
|
||||
/// <summary>店铺销售占比(饼图)</summary>
|
||||
public List<ShopSalesProportionItem> ShopProportions { get; set; }
|
||||
|
||||
/// <summary>店铺统计表格</summary>
|
||||
public List<ShopStatisticsItem> ShopStatistics { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 店铺营收统计 - 天/周/月周期统计查询结果(周期统计页使用)。
|
||||
/// 接口不返回整段汇总,Summary 由返回的周期数据在客户端合计得出。
|
||||
/// </summary>
|
||||
public class ShopPeriodStatisticsResult
|
||||
{
|
||||
/// <summary>汇总卡片数据(整个查询时间段,客户端合计)</summary>
|
||||
public ShopRevenueSummary Summary { get; set; }
|
||||
|
||||
/// <summary>周期统计表格(天/周/月各一行)</summary>
|
||||
public List<ShopPeriodStatisticsItem> Items { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 店铺下拉选项
|
||||
/// </summary>
|
||||
public class ShopOption
|
||||
{
|
||||
/// <summary>店铺 app_key(A.app_key)</summary>
|
||||
public string AppKey { get; set; }
|
||||
/// <summary>店铺ID(ShopLoginInfo.ShopId)</summary>
|
||||
public string ShopId { get; set; }
|
||||
|
||||
/// <summary>ERP 店铺ID(ShopLoginInfo.ErpShopId),统计接口的 ShopIds 使用该字段</summary>
|
||||
public string ErpShopId { get; set; }
|
||||
|
||||
/// <summary>店铺名称</summary>
|
||||
public string ShopName { get; set; }
|
||||
}
|
||||
@@ -346,55 +498,4 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
class UnixDateTimeConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
// 读取数值(支持 long 和 int 类型)
|
||||
if (reader.TokenType == JsonToken.Integer)
|
||||
{
|
||||
long milliseconds = (long)reader.Value;
|
||||
// Unix 纪元:1970-01-01 UTC
|
||||
//DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
DateTime epoch = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1));
|
||||
if (milliseconds.ToString().Length == 13)
|
||||
{
|
||||
return epoch.AddMilliseconds(milliseconds);
|
||||
}
|
||||
return epoch.AddSeconds(milliseconds);
|
||||
|
||||
|
||||
}
|
||||
// 如果传入的是字符串(如 "1700000000000"),也支持
|
||||
else if (reader.TokenType == JsonToken.String)
|
||||
{
|
||||
if (long.TryParse((string)reader.Value, out long milliseconds))
|
||||
{
|
||||
//DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
DateTime epoch = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1));
|
||||
if (milliseconds.ToString().Length == 13)
|
||||
{
|
||||
return epoch.AddMilliseconds(milliseconds);
|
||||
}
|
||||
return epoch.AddSeconds(milliseconds);
|
||||
}
|
||||
}
|
||||
throw new JsonSerializationException($"无法将 {reader.Value} 转换为 DateTime");
|
||||
}
|
||||
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
// 序列化时转回时间戳
|
||||
//DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
DateTime epoch = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1));
|
||||
long milliseconds = (long)((DateTime)value - epoch).TotalMilliseconds;
|
||||
writer.WriteValue(milliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
using _29WxOrderWpf.Ctrls.Statistics.Models;
|
||||
using _29WxOrderWpf.Ctrls.Statistics.Models;
|
||||
using AllBase;
|
||||
using AllBase.Helper;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Work;
|
||||
using WxOrder.Framework.Commons;
|
||||
@@ -17,127 +13,99 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 店铺营收统计 - 数据服务调用接口。
|
||||
/// 页面只依赖此接口,接入真实数据源时新建实现类(如对接 HTTP 接口 / SQL 查询),
|
||||
/// 并通过 ShopRevenueStatisticsViewModel 构造函数注入即可,无需改动页面。
|
||||
/// 页面只依赖此接口,接入其他数据源时新建实现类,
|
||||
/// 并通过 ViewModel 构造函数注入即可,无需改动页面。
|
||||
/// </summary>
|
||||
public interface IShopRevenueStatisticsService
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询原始统计明细(接口返回的 A.* / B.* / c.* 原始字段)。
|
||||
/// 真实实现通常通过 SQL / HTTP 一次性返回这些行,再由聚合器计算。
|
||||
/// 综合统计查询:返回汇总卡片、销售趋势(按天)、店铺销售占比、店铺统计表格。
|
||||
/// 用 GroupType=Day 一次请求取回按店铺按天的记录,再由客户端汇总,无缓存。
|
||||
/// </summary>
|
||||
Task<List<ShopRevenueStatisticsRow>> QueryRowsAsync(ShopRevenueStatisticsQuery query);
|
||||
Task<ShopRevenueStatisticsResult> QueryAsync(ShopRevenueStatisticsQuery query);
|
||||
|
||||
/// <summary>获取顶部汇总卡片数据</summary>
|
||||
Task<ShopRevenueSummary> GetSummaryAsync(ShopRevenueStatisticsQuery query);
|
||||
|
||||
/// <summary>获取销售趋势(按天)</summary>
|
||||
Task<List<RevenueTrendPoint>> GetSalesTrendAsync(ShopRevenueStatisticsQuery query);
|
||||
|
||||
/// <summary>获取店铺销售占比(饼图)</summary>
|
||||
Task<List<ShopSalesProportionItem>> GetShopSalesProportionAsync(ShopRevenueStatisticsQuery query);
|
||||
|
||||
/// <summary>获取店铺统计表格</summary>
|
||||
Task<List<ShopStatisticsItem>> GetShopStatisticsAsync(ShopRevenueStatisticsQuery query);
|
||||
|
||||
/// <summary>获取天/周/月周期统计表格(每天/每周/每月各一行,按所选店铺或全部店铺统计)</summary>
|
||||
Task<List<ShopPeriodStatisticsItem>> GetPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType);
|
||||
/// <summary>
|
||||
/// 天/周/月周期统计查询:返回汇总卡片 + 按周期(天/周/月)聚合的表格数据。
|
||||
/// 接口不返回整段汇总,Summary 由返回的周期记录在客户端合计。
|
||||
/// </summary>
|
||||
Task<ShopPeriodStatisticsResult> QueryPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType);
|
||||
|
||||
/// <summary>获取店铺下拉列表(用于查询条件)</summary>
|
||||
Task<List<ShopOption>> GetShopOptionsAsync();
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 订单明细列表(有效订单 / 退款订单 / 买手未退回金额的订单),供订单列表弹窗使用。
|
||||
/// </summary>
|
||||
Task<List<ShopOrderDetailResponse>> QueryOrderRowsAsync(ShopRevenueStatisticsQuery query, ShopOrderDetailType detailType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统计聚合器:把接口返回的原始明细行聚合成页面所需的汇总/趋势/占比/表格数据。
|
||||
/// 聚合口径说明(依据接口字段描述):
|
||||
/// - 订单状态 status:10-待付款;20-待发货;21-部分发货;30-待收货;100-完成;
|
||||
/// 200-全部商品售后之后订单取消;250-未付款用户主动取消或超时未付款订单自动取消。
|
||||
/// - 有效订单:已付款(20/21/30/100)且未退款;10/250 未付款不计入销售额。
|
||||
/// - 已退款:c.refund_status 为 4/11,或订单状态为 200(全部商品售后后取消)。
|
||||
/// 统计聚合器:把接口返回的 <see cref="ShopStatsResponse"/> 聚合成页面所需的汇总/趋势/占比/表格数据。
|
||||
/// 数据来源:shop_revenue_statistics/GetShopStats(GroupType = Day/Week/Month),
|
||||
/// 接口只返回“按周期聚合的记录”,页面上所有汇总口径都在这里由客户端合计。
|
||||
/// 口径说明(依据接口字段含义):
|
||||
/// - OrderCount 为“有效订单数”,已不含退款订单;因此退款率分母 = OrderCount + RefundOrderCount。
|
||||
/// - 店铺端退款:RefundDone(已成功)+ RefundPending(处理中)→ 退款金额。
|
||||
/// - 买手端(采购)退款:TotalRefund = DoneRefund + PendingRefund + RejectedRefund → 售后采购金额;
|
||||
/// DoneRefund → 已退回采购金额;剩余(待退回)= TotalRefund - DoneRefund。
|
||||
/// - 接口无“达人佣金”字段,Commission 固定为 0(保留属性与页面展示,待后端补字段)。
|
||||
/// - 金额字段单位均为“分”,除以 100 转为元。
|
||||
/// </summary>
|
||||
public static class RevenueStatisticsAggregator
|
||||
public static class ShopStatsAggregator
|
||||
{
|
||||
/// <summary>订单状态 - 待付款</summary>
|
||||
public const int StatusPendingPay = 10;
|
||||
/// <summary>金额换算:分 → 元</summary>
|
||||
private const decimal FenPerYuan = 100m;
|
||||
|
||||
/// <summary>订单状态 - 待发货</summary>
|
||||
public const int StatusWaitSend = 20;
|
||||
|
||||
/// <summary>订单状态 - 部分发货</summary>
|
||||
public const int StatusPartSend = 21;
|
||||
|
||||
/// <summary>订单状态 - 待收货</summary>
|
||||
public const int StatusWaitReceive = 30;
|
||||
|
||||
/// <summary>订单状态 - 完成</summary>
|
||||
public const int StatusFinished = 100;
|
||||
|
||||
/// <summary>订单状态 - 全部商品售后之后订单取消</summary>
|
||||
public const int StatusCancelAfterAfterSale = 200;
|
||||
|
||||
/// <summary>订单状态 - 未付款用户主动取消/超时未付款自动取消</summary>
|
||||
public const int StatusCancelNotPay = 250;
|
||||
|
||||
/// <summary>是否已付款的订单(20/21/30/100;10 待付款、250 未付款取消不计入)</summary>
|
||||
public static bool IsPaidOrder(ShopRevenueStatisticsRow row)
|
||||
/// <summary>分转元</summary>
|
||||
public static decimal ToYuan(long fen)
|
||||
{
|
||||
if (row == null) return false;
|
||||
return row.Status == StatusWaitSend
|
||||
|| row.Status == StatusPartSend
|
||||
|| row.Status == StatusWaitReceive
|
||||
|| row.Status == StatusFinished;
|
||||
return fen / FenPerYuan;
|
||||
}
|
||||
|
||||
/// <summary>是否已退款(c.refund_status 为 4/11,或订单状态为 200 全部商品售后后取消)</summary>
|
||||
public static bool IsRefunded(ShopRevenueStatisticsRow row)
|
||||
private static List<ShopStatsResponse> ToList(IEnumerable<ShopStatsResponse> stats)
|
||||
{
|
||||
if (row == null) return false;
|
||||
return row.RefundStatus > 1 || row.RefundStatusString == "MERCHANT_REFUND_SUCCESS" || row.RefundStatusString == "MERCHANT_RETURN_SUCCESS";
|
||||
return (stats ?? Enumerable.Empty<ShopStatsResponse>())
|
||||
.Where(a => a != null)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>是否有效订单(已付款且未退款)</summary>
|
||||
public static bool IsValidOrder(ShopRevenueStatisticsRow row)
|
||||
private static IEnumerable<IGrouping<string, ShopStatsResponse>> GroupByShop(IEnumerable<ShopStatsResponse> stats)
|
||||
{
|
||||
return !IsRefunded(row);
|
||||
return ToList(stats).GroupBy(a => a.ShopId ?? string.Empty);
|
||||
}
|
||||
/// <summary>买手号退款成功</summary>
|
||||
public static bool IsBuyerRefundSuccessful(ShopRevenueStatisticsRow row)
|
||||
|
||||
/// <summary>店铺显示名:优先按 ERP 店铺ID匹配,其次按平台店铺ID匹配,都匹配不到时退回店铺ID</summary>
|
||||
public static string ResolveShopName(string shopId)
|
||||
{
|
||||
return row.BuyerRefundStatus == "退款成功";
|
||||
if (string.IsNullOrEmpty(shopId))
|
||||
return "未知店铺";
|
||||
|
||||
var shop = GlobalConfig.ShopLoginList.FirstOrDefault(a => a.ErpShopId == shopId)
|
||||
?? GlobalConfig.ShopLoginList.FirstOrDefault(a => a.ShopId == shopId);
|
||||
|
||||
if (shop == null || string.IsNullOrEmpty(shop.ShopAccount))
|
||||
return shopId;
|
||||
|
||||
return shop.ShopAccount;
|
||||
}
|
||||
private static List<ShopRevenueStatisticsRow> GetPaymentOrderList(IEnumerable<ShopRevenueStatisticsRow> rows)
|
||||
|
||||
/// <summary>汇总卡片(整个查询时间段,所有选中店铺合计)</summary>
|
||||
public static ShopRevenueSummary GetSummary(IEnumerable<ShopStatsResponse> stats)
|
||||
{
|
||||
return (rows ?? new List<ShopRevenueStatisticsRow>()).Where(a => a.OutAmount > 0).ToList(); //买手付钱后的订单数量
|
||||
}
|
||||
/// <summary>汇总卡片</summary>
|
||||
public static ShopRevenueSummary GetSummary(IEnumerable<ShopRevenueStatisticsRow> rows)
|
||||
{
|
||||
var list = GetPaymentOrderList(rows); //买手付钱后的订单数量
|
||||
var valid = list.Where(IsValidOrder).ToList();
|
||||
var refunded = list.Where(IsRefunded).ToList();
|
||||
var list = ToList(stats);
|
||||
|
||||
decimal salesAmount = valid.Sum(r => (decimal)(r.OrderPrice)) / 100m;//有效订单总金额
|
||||
decimal profit = valid.Concat(refunded).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.OutAmount)) / 100m;
|
||||
decimal buyerPaymentAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号付款金额(采购金额)
|
||||
decimal buyerRefundAmount = refunded.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号退款金额(采购退款金额)
|
||||
decimal buyerRefundSuccessedAmount = refunded.Where(IsBuyerRefundSuccessful).Sum(r => (decimal)(r.BuyerRefundAmount)) / 100m; //买手号退款成功金额
|
||||
decimal salesAmount = ToYuan(list.Sum(a => a.SalesAmount));
|
||||
decimal profit = ToYuan(list.Sum(a => a.ProfitAmount));
|
||||
decimal platformFeeTotal = ToYuan(list.Sum(a => a.PlatformFee));
|
||||
decimal refundAmount = ToYuan(list.Sum(a => a.RefundDone) + list.Sum(a => a.RefundPending));
|
||||
decimal buyerPaymentAmount = ToYuan(list.Sum(a => a.TotalPurchase));
|
||||
decimal buyerRefundAmount = ToYuan(list.Sum(a => a.TotalRefund));
|
||||
decimal buyerRefundSuccessedAmount = ToYuan(list.Sum(a => a.DoneRefund));
|
||||
|
||||
int validOrderCount = valid.Select(r => r.OrderId).Distinct().Count();
|
||||
int validOrderCount = list.Sum(a => a.OrderCount); // 新接口 OrderCount 不含退款订单
|
||||
int refundOrderCount = list.Sum(a => a.RefundOrderCount);
|
||||
int totalOrderCount = validOrderCount + refundOrderCount;
|
||||
|
||||
// 总订单数(退款率分母)= 已付款订单 + 已退款订单,不含未付款的 10/250
|
||||
int totalOrderCount = list.Count();
|
||||
int refundOrderCount = refunded.Select(r => r.OrderId).Distinct().Count();
|
||||
Debug.WriteLine($"计算利润:{(salesAmount - buyerPaymentAmount - platformFeeTotal)}");
|
||||
Debug.WriteLine($"正常订单利润:{valid.Sum(r => (decimal)(r.Profit)) / 100}");
|
||||
Debug.WriteLine($"售后订单利润:{refunded.Sum(r => (decimal)(r.Profit)) / 100}");
|
||||
return new ShopRevenueSummary
|
||||
{
|
||||
SalesAmount = salesAmount,
|
||||
@@ -146,65 +114,65 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
|
||||
RefundAmount = refundAmount,
|
||||
RefundOrderCount = refundOrderCount,
|
||||
PlatformFeeTotal = platformFeeTotal,
|
||||
Commission = commission,
|
||||
Commission = 0m, // 新接口无“达人佣金”字段,保留占位
|
||||
AvgOrderAmount = validOrderCount == 0 ? 0m : salesAmount / validOrderCount,
|
||||
RefundRate = totalOrderCount == 0 ? 0m : (decimal)refundOrderCount * 100m / totalOrderCount,
|
||||
AverageProfitRate = purchaseAmount == 0m ? 0m : profit / purchaseAmount * 100m,
|
||||
AverageProfitRate = buyerPaymentAmount == 0m ? 0m : profit / buyerPaymentAmount * 100m,
|
||||
BuyerPaymentAmount = buyerPaymentAmount,
|
||||
BuyerRefundAmount = buyerRefundAmount,
|
||||
BuyerRefundSuccessedAmount = buyerRefundSuccessedAmount,
|
||||
};
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取有效订单
|
||||
/// </summary>
|
||||
/// <param name="rows"></param>
|
||||
/// <returns></returns>
|
||||
public static List<ShopRevenueStatisticsRow> GetValidOrder(IEnumerable<ShopRevenueStatisticsRow> rows)
|
||||
{
|
||||
var list = GetPaymentOrderList(rows); //买手付钱后的订单数量
|
||||
return list.Where(IsValidOrder).ToList();
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取退款订单
|
||||
/// </summary>
|
||||
/// <param name="rows"></param>
|
||||
/// <returns></returns>
|
||||
public static List<ShopRevenueStatisticsRow> GetRefundOrder(IEnumerable<ShopRevenueStatisticsRow> rows)
|
||||
{
|
||||
var list = GetPaymentOrderList(rows); //买手付钱后的订单数量
|
||||
return list.Where(IsRefunded).ToList();
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取退款订单
|
||||
/// </summary>
|
||||
/// <param name="rows"></param>
|
||||
/// <returns></returns>
|
||||
public static List<ShopRevenueStatisticsRow> GetBuyerRefundRemainingOrder(IEnumerable<ShopRevenueStatisticsRow> rows)
|
||||
{
|
||||
var list = GetPaymentOrderList(rows); //买手付钱后的订单数量
|
||||
var refunded = list.Where(IsRefunded).ToList();
|
||||
|
||||
decimal buyerRefundAmount = refunded.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号退款金额(采购退款金额)
|
||||
decimal buyerRefundSuccessedAmount = refunded.Where(a => IsBuyerRefundSuccessful(a) == false).Sum(r => (decimal)(r.BuyerRefundAmount)) / 100m; //买手号退款成功金额
|
||||
|
||||
return refunded.Where(a => IsBuyerRefundSuccessful(a) == false).ToList();
|
||||
/// <summary>店铺统计表格(按店铺汇总,销售额倒序)</summary>
|
||||
public static List<ShopStatisticsItem> GetShopStatistics(IEnumerable<ShopStatsResponse> stats)
|
||||
{
|
||||
return GroupByShop(stats)
|
||||
.Select(g => BuildShopStatisticsItem(g.Key, g))
|
||||
.OrderByDescending(i => i.SalesAmount)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>销售趋势(按天,缺失日期补 0,仅统计有效订单)</summary>
|
||||
public static List<RevenueTrendPoint> GetSalesTrend(IEnumerable<ShopRevenueStatisticsRow> rows, DateTime startDate, DateTime endDate)
|
||||
/// <summary>店铺销售占比(饼图,按店铺汇总,销售额倒序)</summary>
|
||||
public static List<ShopSalesProportionItem> GetShopSalesProportion(IEnumerable<ShopStatsResponse> stats)
|
||||
{
|
||||
var list = GetPaymentOrderList(rows);
|
||||
list = list.Where(IsValidOrder).ToList();
|
||||
var items = GroupByShop(stats)
|
||||
.Select(g => new ShopSalesProportionItem
|
||||
{
|
||||
ShopName = ResolveShopName(g.Key),
|
||||
SalesAmount = ToYuan(g.Sum(a => a.SalesAmount))
|
||||
})
|
||||
.OrderByDescending(i => i.SalesAmount)
|
||||
.ToList();
|
||||
|
||||
decimal total = items.Sum(i => i.SalesAmount);
|
||||
foreach (var item in items)
|
||||
{
|
||||
item.Percentage = total == 0m ? 0d : (double)(item.SalesAmount / total * 100m);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 销售趋势(按天,缺失日期补 0)。
|
||||
/// 数据来自 GetShopStatsTrend:服务端已经跨店铺合计,这里只做日期规整与补零。
|
||||
/// </summary>
|
||||
public static List<RevenueTrendPoint> GetSalesTrend(IEnumerable<ShopStatsTrendResponse> trend, DateTime startDate, DateTime endDate)
|
||||
{
|
||||
var list = (trend ?? Enumerable.Empty<ShopStatsTrendResponse>())
|
||||
.Where(a => a != null && a.StatDate != default(DateTime))
|
||||
.ToList();
|
||||
|
||||
var map = list
|
||||
.GroupBy(r => r.CreateTime.Date)
|
||||
.Where(a => a.StatDate.Date >= startDate.Date && a.StatDate.Date <= endDate.Date)
|
||||
.GroupBy(a => a.StatDate.Date)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => new RevenueTrendPoint
|
||||
{
|
||||
Date = g.Key,
|
||||
SalesAmount = g.Sum(r => (decimal)(r.OrderPrice)) / 100m,
|
||||
OrderCount = g.Select(r => r.OrderId).Distinct().Count()
|
||||
SalesAmount = ToYuan(g.Sum(a => a.SalesAmount)),
|
||||
OrderCount = g.Sum(a => a.OrderCount)
|
||||
});
|
||||
|
||||
var result = new List<RevenueTrendPoint>();
|
||||
@@ -218,95 +186,23 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>店铺销售占比(饼图,仅统计有效订单)</summary>
|
||||
public static List<ShopSalesProportionItem> GetShopSalesProportion(IEnumerable<ShopRevenueStatisticsRow> rows)
|
||||
/// <summary>
|
||||
/// 天/周/月周期统计表格(每天/每周/每月各一行,无数据的周期也补一行,金额单位:元)。
|
||||
/// 结果按周期倒序(最新的周期在最上面)。
|
||||
/// 说明:接口按“自然周期”聚合(周从周一起、月从 1 号起),所以筛选也按周期起点比较,
|
||||
/// 否则区间从周三开始时,那一周(周一起)的数据会被误判为越界而丢掉。
|
||||
/// </summary>
|
||||
public static List<ShopPeriodStatisticsItem> GetPeriodStatistics(IEnumerable<ShopStatsResponse> stats, DateTime startDate, DateTime endDate, PeriodStatisticType periodType)
|
||||
{
|
||||
var list = GetPaymentOrderList(rows);
|
||||
list = list.Where(IsValidOrder).ToList();
|
||||
decimal total = list.Sum(r => (decimal)(r.OrderPrice)) / 100m;
|
||||
var startPeriod = GetPeriodStart(startDate, periodType);
|
||||
var endPeriod = GetPeriodStart(endDate, periodType);
|
||||
|
||||
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)
|
||||
})
|
||||
.OrderByDescending(i => i.SalesAmount)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>店铺统计表格</summary>
|
||||
public static List<ShopStatisticsItem> GetShopStatistics(IEnumerable<ShopRevenueStatisticsRow> rows)
|
||||
{
|
||||
var list = GetPaymentOrderList(rows); //买手付钱后的订单
|
||||
|
||||
return list
|
||||
.GroupBy(r => r.ShopName)
|
||||
.Select(g =>
|
||||
{
|
||||
var valid = g.Where(IsValidOrder).ToList();
|
||||
var refunded = g.Where(IsRefunded).ToList();
|
||||
decimal salesAmount = valid.Sum(r => (decimal)(r.OrderPrice)) / 100m; //与 GetSummary 一致:不含已退款订单的销售额
|
||||
int orderCount = valid.Select(r => r.OrderId).Distinct().Count();
|
||||
decimal profit = valid.Concat(refunded).Sum(r => (decimal)(r.Profit)) / 100m; //与 GetSummary 一致:有效+退款订单利润
|
||||
decimal purchaseAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; //与 GetSummary 一致:买手付款金额(采购金额)
|
||||
decimal buyerPaymentAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号付款金额(采购金额)
|
||||
return new ShopStatisticsItem
|
||||
{
|
||||
ShopName = string.IsNullOrEmpty(g.Key) ? "未知店铺" : g.Key,
|
||||
OrderCount = orderCount,
|
||||
SalesAmount = salesAmount,
|
||||
BuyerPaymentAmount = buyerPaymentAmount,
|
||||
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)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>天/周/月周期统计表格(每天/每周/每月各一行,无数据的周期也补一行,金额单位:元)</summary>
|
||||
public static List<ShopPeriodStatisticsItem> GetPeriodStatistics(IEnumerable<ShopRevenueStatisticsRow> rows, DateTime startDate, DateTime endDate, PeriodStatisticType periodType)
|
||||
{
|
||||
var list = (rows ?? new List<ShopRevenueStatisticsRow>()).Where(a => a.OutAmount > 0).ToList(); //买手付钱后的订单
|
||||
|
||||
// 有数据的周期
|
||||
var map = list
|
||||
.GroupBy(r => GetPeriodStart(r.CreateTime, periodType))
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g =>
|
||||
{
|
||||
var valid = g.Where(IsValidOrder).ToList();
|
||||
var refunded = g.Where(IsRefunded).ToList();
|
||||
decimal salesAmount = valid.Sum(r => (decimal)(r.OrderPrice)) / 100m; //与 GetSummary 一致:不含已退款订单的销售额
|
||||
decimal profit = valid.Concat(refunded).Sum(r => (decimal)(r.Profit)) / 100m; //与 GetSummary 一致:有效+退款订单利润
|
||||
decimal purchaseAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; //与 GetSummary 一致:买手付款金额(采购金额)
|
||||
int orderCount = valid.Select(r => r.OrderId).Distinct().Count();
|
||||
decimal buyerPaymentAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号付款金额(采购金额)
|
||||
|
||||
return new ShopPeriodStatisticsItem
|
||||
{
|
||||
PeriodStart = g.Key,
|
||||
Period = GetPeriodText(g.Key, periodType),
|
||||
OrderCount = orderCount,
|
||||
SalesAmount = salesAmount,
|
||||
BuyerPaymentAmount = buyerPaymentAmount,
|
||||
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
|
||||
};
|
||||
});
|
||||
var map = ToList(stats)
|
||||
.Where(a => a.StatDate != default(DateTime))
|
||||
.Select(a => new { Stat = a, PeriodStart = GetPeriodStart(a.StatDate, periodType) })
|
||||
.Where(a => a.PeriodStart >= startPeriod && a.PeriodStart <= endPeriod)
|
||||
.GroupBy(a => a.PeriodStart)
|
||||
.ToDictionary(g => g.Key, g => BuildPeriodStatisticsItem(g.Key, g.Select(a => a.Stat), periodType));
|
||||
|
||||
// 按周期补齐整段时间:无数据的周期也生成一行(全 0)
|
||||
var result = new List<ShopPeriodStatisticsItem>();
|
||||
@@ -321,6 +217,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
|
||||
Period = GetPeriodText(periodStart, periodType),
|
||||
OrderCount = 0,
|
||||
SalesAmount = 0m,
|
||||
BuyerPaymentAmount = 0m,
|
||||
Profit = 0m,
|
||||
RefundAmount = 0m,
|
||||
PlatformFeeTotal = 0m,
|
||||
@@ -336,6 +233,56 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>店铺统计行(金额单位:元)</summary>
|
||||
private static ShopStatisticsItem BuildShopStatisticsItem(string shopId, IEnumerable<ShopStatsResponse> shopStats)
|
||||
{
|
||||
var list = ToList(shopStats);
|
||||
decimal salesAmount = ToYuan(list.Sum(a => a.SalesAmount));
|
||||
decimal profit = ToYuan(list.Sum(a => a.ProfitAmount));
|
||||
decimal buyerPaymentAmount = ToYuan(list.Sum(a => a.TotalPurchase));
|
||||
int orderCount = list.Sum(a => a.OrderCount);
|
||||
|
||||
return new ShopStatisticsItem
|
||||
{
|
||||
ShopName = ResolveShopName(shopId),
|
||||
OrderCount = orderCount,
|
||||
SalesAmount = salesAmount,
|
||||
BuyerPaymentAmount = buyerPaymentAmount,
|
||||
Profit = profit,
|
||||
RefundAmount = ToYuan(list.Sum(a => a.RefundDone) + list.Sum(a => a.RefundPending)),
|
||||
PlatformFeeTotal = ToYuan(list.Sum(a => a.PlatformFee)),
|
||||
Commission = 0m, // 新接口无“达人佣金”字段,保留占位
|
||||
AvgOrderAmount = orderCount == 0 ? 0m : salesAmount / orderCount,
|
||||
AverageProfitRate = buyerPaymentAmount == 0m ? 0m : profit / buyerPaymentAmount * 100m
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>周期统计行(金额单位:元)</summary>
|
||||
private static ShopPeriodStatisticsItem BuildPeriodStatisticsItem(DateTime periodStart, IEnumerable<ShopStatsResponse> periodStats, PeriodStatisticType periodType)
|
||||
{
|
||||
var list = ToList(periodStats);
|
||||
decimal salesAmount = ToYuan(list.Sum(a => a.SalesAmount));
|
||||
decimal profit = ToYuan(list.Sum(a => a.ProfitAmount));
|
||||
decimal buyerPaymentAmount = ToYuan(list.Sum(a => a.TotalPurchase));
|
||||
int orderCount = list.Sum(a => a.OrderCount);
|
||||
|
||||
return new ShopPeriodStatisticsItem
|
||||
{
|
||||
PeriodStart = periodStart,
|
||||
Period = GetPeriodText(periodStart, periodType),
|
||||
OrderCount = orderCount,
|
||||
SalesAmount = salesAmount,
|
||||
BuyerPaymentAmount = buyerPaymentAmount,
|
||||
Profit = profit,
|
||||
RefundAmount = ToYuan(list.Sum(a => a.RefundDone) + list.Sum(a => a.RefundPending)),
|
||||
PlatformFeeTotal = ToYuan(list.Sum(a => a.PlatformFee)),
|
||||
Commission = 0m, // 新接口无“达人佣金”字段,保留占位
|
||||
AverageProfitRate = buyerPaymentAmount == 0m ? 0m : profit / buyerPaymentAmount * 100m,
|
||||
CustomerCount = orderCount,
|
||||
AvgOrderAmount = orderCount == 0 ? 0m : salesAmount / orderCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>枚举 startDate~endDate 覆盖的所有周期开始日期(天/周/月)</summary>
|
||||
private static IEnumerable<DateTime> EnumeratePeriods(DateTime startDate, DateTime endDate, PeriodStatisticType periodType)
|
||||
{
|
||||
@@ -387,252 +334,261 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
|
||||
return periodStart.ToString("yyyy-MM-dd");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 默认实现:请求真实接口(shop_revenue_statistics/get_shop_order_amount)。
|
||||
/// 默认实现:请求新接口(无缓存,每次查询实时获取)。
|
||||
/// shop_revenue_statistics/GetShopStats
|
||||
/// -> Result<Dictionary<店铺ID, List<ShopStatsResponse>>>
|
||||
/// 请求体:UserLogin.GetSignPostData(new { StartDate, EndDate, ShopIds, GroupType });
|
||||
/// GroupType 决定聚合粒度(0=汇总 1=天 2=周 3=月),汇总时每个店铺只有一条。
|
||||
/// shop_revenue_statistics/GetShopStatsTrend
|
||||
/// -> Result<List<ShopStatsTrendResponse>>
|
||||
/// 请求体:UserLogin.GetSignPostData(new { StartDate, EndDate, ShopIds });
|
||||
/// 服务端跨店铺合计,固定按天返回,用于销售趋势折线图。
|
||||
/// shop_revenue_statistics/GetShopOrderDetails
|
||||
/// -> Result<List<ShopOrderDetailResponse>>
|
||||
/// 请求体:UserLogin.GetSignPostData(new { StartDate, EndDate, ShopIds, OrderType });
|
||||
/// 订单列表弹窗的明细数据(OrderType 见 ShopOrderDetailType)。
|
||||
/// 接口不返回“整段汇总”,页面上的汇总卡片一律由客户端对返回记录合计。
|
||||
/// 接入其他数据源时,新建实现类并注入即可(见 IShopRevenueStatisticsService 注释)。
|
||||
/// </summary>
|
||||
public class ShopRevenueStatisticsService : IShopRevenueStatisticsService
|
||||
{
|
||||
/// <summary>缓存有效期:订单/退款数据会持续变化,超过该时长后缓存自动失效并重新请求。</summary>
|
||||
private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(60);
|
||||
/// <summary>统计接口地址(聚合粒度由请求参数 GroupType 决定)</summary>
|
||||
private const string GetShopStatsApi = "shop_revenue_statistics/GetShopStats";
|
||||
|
||||
/// <summary>
|
||||
/// 明细数据缓存(按天缓存,纯时间维度,不含店铺)。
|
||||
/// key = 日期(yyyyMMdd),value = 该天全部店铺的明细 + 缓存时间。
|
||||
/// 查询时按天命中:未命中的天合并成连续段一次请求,返回后按天拆分写入;
|
||||
/// 前台选某一个或某多个店铺时,按所选店铺(ShopName)过滤,复用同一批时间数据。
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, CacheEntry> _rowCache =
|
||||
new Dictionary<string, CacheEntry>();
|
||||
private readonly object _cacheLock = new object();
|
||||
/// <summary>销售趋势接口地址(服务端跨店铺合计,固定按天)</summary>
|
||||
private const string GetShopStatsTrendApi = "shop_revenue_statistics/GetShopStatsTrend";
|
||||
|
||||
private sealed class DateRange
|
||||
/// <summary>订单明细接口地址(订单列表弹窗用)</summary>
|
||||
private const string GetShopOrderDetailsApi = "shop_revenue_statistics/GetShopOrderDetails";
|
||||
|
||||
/// <summary>Unix 纪元(本地时区),用于解析以时间戳作为 Key 的返回数据</summary>
|
||||
private static readonly DateTime UnixEpoch = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1));
|
||||
|
||||
public async Task<ShopRevenueStatisticsResult> QueryAsync(ShopRevenueStatisticsQuery query)
|
||||
{
|
||||
public DateTime Start;
|
||||
public DateTime End;
|
||||
}
|
||||
// 综合统计页分两次取数,各取所需:
|
||||
// 1) Summary:每店铺一条(与区间长度无关),作为卡片/店铺统计表/销售占比的权威合计
|
||||
// 2) Trend :跨店铺合计的按天序列,只给趋势折线图用(数据量只与天数有关,不随店铺数膨胀)
|
||||
// 顺序请求:签名中的 time/code 由当前时间生成,并发请求有被判重复签名的风险。
|
||||
var summaryStats = await RequestStatsAsync(query, ShopStatsGroupType.Summary);
|
||||
var trend = await RequestTrendAsync(query);
|
||||
|
||||
private sealed class CacheEntry
|
||||
{
|
||||
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)
|
||||
return new ShopRevenueStatisticsResult
|
||||
{
|
||||
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;
|
||||
Summary = ShopStatsAggregator.GetSummary(summaryStats),
|
||||
Trend = ShopStatsAggregator.GetSalesTrend(trend, query.StartDate, query.EndDate),
|
||||
ShopProportions = ShopStatsAggregator.GetShopSalesProportion(summaryStats),
|
||||
ShopStatistics = ShopStatsAggregator.GetShopStatistics(summaryStats),
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<ShopRevenueStatisticsRow>> QueryRowsAsync(ShopRevenueStatisticsQuery query)
|
||||
public async Task<ShopPeriodStatisticsResult> QueryPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType)
|
||||
{
|
||||
var startDay = query.StartDate.Date;
|
||||
var endDay = query.EndDate.Date;
|
||||
if (startDay > endDay)
|
||||
// 按天/周/月粒度取数;接口不返回整段汇总,卡片由返回的周期记录在客户端合计
|
||||
var stats = await RequestStatsAsync(query, ToGroupType(periodType));
|
||||
|
||||
return new ShopPeriodStatisticsResult
|
||||
{
|
||||
var tmp = startDay;
|
||||
startDay = endDay;
|
||||
endDay = tmp;
|
||||
}
|
||||
Summary = ShopStatsAggregator.GetSummary(stats),
|
||||
Items = ShopStatsAggregator.GetPeriodStatistics(stats, query.StartDate, query.EndDate, periodType),
|
||||
};
|
||||
}
|
||||
|
||||
// 1. 找出未命中或已过期的天
|
||||
List<DateTime> missingDays;
|
||||
lock (_cacheLock)
|
||||
/// <summary>页面统计周期 → 接口聚合方式</summary>
|
||||
private static ShopStatsGroupType ToGroupType(PeriodStatisticType periodType)
|
||||
{
|
||||
switch (periodType)
|
||||
{
|
||||
missingDays = EnumerateDays(startDay, endDay)
|
||||
.Where(d => !IsValidLocked(d))
|
||||
.ToList();
|
||||
case PeriodStatisticType.Week:
|
||||
return ShopStatsGroupType.Week;
|
||||
case PeriodStatisticType.Month:
|
||||
return ShopStatsGroupType.Month;
|
||||
default:
|
||||
return ShopStatsGroupType.Day;
|
||||
}
|
||||
|
||||
// 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 rows = await RequestRowsAsync(shopIds, run.Start, run.End);
|
||||
CacheResponse(run.Start, run.End, rows);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 按天从缓存拼装完整范围,再按所选店铺过滤(ShopKeys 为空 = 全部店铺)
|
||||
return FilterByShop(GetCachedRows(startDay, endDay), query.ShopKeys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把一次请求的结果按天写入缓存。
|
||||
/// 本次请求覆盖的每一天都以返回数据为准整体替换(无数据的天记为空的已缓存,
|
||||
/// 避免“该天无数据”被反复判定为未命中而重复请求)。
|
||||
/// 请求 GetShopStats:返回按 GroupType 聚合的记录
|
||||
/// (汇总时每店铺一条;天/周/月时每店铺每个自然日/自然周/自然月一条)。
|
||||
/// </summary>
|
||||
private void CacheResponse(DateTime startDate, DateTime endDate, List<ShopRevenueStatisticsRow> rows)
|
||||
private static async Task<List<ShopStatsResponse>> RequestStatsAsync(ShopRevenueStatisticsQuery query, ShopStatsGroupType groupType)
|
||||
{
|
||||
if (rows == null)
|
||||
rows = new List<ShopRevenueStatisticsRow>();
|
||||
var shopIds = BuildShopIds(query);
|
||||
if (shopIds.Count == 0)
|
||||
return new List<ShopStatsResponse>();
|
||||
|
||||
lock (_cacheLock)
|
||||
var url = BaseClass.Build29WxObjString(GetShopStatsApi);
|
||||
var html = await GetHtml.PostJsonAsync(url, BuildPostData(query, shopIds, groupType));
|
||||
|
||||
var ret = Deserialize<Result<Dictionary<string, List<ShopStatsResponse>>>>(html);
|
||||
EnsureSuccess(ret);
|
||||
|
||||
var result = new List<ShopStatsResponse>();
|
||||
if (ret.Data == null)
|
||||
return result;
|
||||
|
||||
foreach (var kv in ret.Data)
|
||||
result.AddRange(Normalize(kv.Key, kv.Value));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求 GetShopStatsTrend:跨店铺合计的按天销售趋势(服务端已合计,不需要 GroupType)。
|
||||
/// </summary>
|
||||
private static async Task<List<ShopStatsTrendResponse>> RequestTrendAsync(ShopRevenueStatisticsQuery query)
|
||||
{
|
||||
var shopIds = BuildShopIds(query);
|
||||
if (shopIds.Count == 0)
|
||||
return new List<ShopStatsTrendResponse>();
|
||||
|
||||
var url = BaseClass.Build29WxObjString(GetShopStatsTrendApi);
|
||||
var html = await GetHtml.PostJsonAsync(url, BuildTrendPostData(query, shopIds));
|
||||
|
||||
var ret = Deserialize<Result<List<ShopStatsTrendResponse>>>(html);
|
||||
EnsureSuccess(ret);
|
||||
return ret.Data ?? new List<ShopStatsTrendResponse>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 组装请求体:StartDate / EndDate 传日期,ShopIds 传 ERP 店铺ID 列表,
|
||||
/// GroupType 传枚举数字(0=汇总 1=天 2=周 3=月)。
|
||||
/// </summary>
|
||||
private static object BuildPostData(ShopRevenueStatisticsQuery query, List<string> shopIds, ShopStatsGroupType groupType)
|
||||
{
|
||||
return UserLogin.GetSignPostData(new
|
||||
{
|
||||
RemoveExpiredEntriesLocked();
|
||||
StartDate = query.StartDate.Date,
|
||||
EndDate = query.EndDate.Date,
|
||||
ShopIds = shopIds,
|
||||
GroupType = (int)groupType,
|
||||
});
|
||||
}
|
||||
|
||||
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());
|
||||
/// <summary>组装趋势接口请求体:StartDate / EndDate + ShopIds(无 GroupType)</summary>
|
||||
private static object BuildTrendPostData(ShopRevenueStatisticsQuery query, List<string> shopIds)
|
||||
{
|
||||
return UserLogin.GetSignPostData(new
|
||||
{
|
||||
StartDate = query.StartDate.Date,
|
||||
EndDate = query.EndDate.Date,
|
||||
ShopIds = shopIds,
|
||||
});
|
||||
}
|
||||
|
||||
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>
|
||||
/// 组装订单明细请求体:StartDate / EndDate + ShopIds + OrderType
|
||||
/// (OrderType 传枚举数字:1=有效订单 2=退款订单 3=买手未退回金额的订单)。
|
||||
/// </summary>
|
||||
private static object BuildOrderDetailPostData(ShopRevenueStatisticsQuery query, List<string> shopIds, ShopOrderDetailType detailType)
|
||||
{
|
||||
return UserLogin.GetSignPostData(new
|
||||
{
|
||||
StartDate = query.StartDate,
|
||||
EndDate = query.EndDate,
|
||||
ShopIds = shopIds,
|
||||
OrderType = (int)detailType,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询店铺范围:query.ShopIds 为空表示全部店铺(取全部店铺的 ERP 店铺ID)。
|
||||
/// 接口的 ShopIds 使用 ShopLoginInfo.ErpShopId。
|
||||
/// </summary>
|
||||
private static List<string> BuildShopIds(ShopRevenueStatisticsQuery query)
|
||||
{
|
||||
var selected = (query == null || query.ShopIds == null)
|
||||
? new List<string>()
|
||||
: query.ShopIds.Where(a => !string.IsNullOrEmpty(a)).Distinct().ToList();
|
||||
|
||||
if (selected.Count > 0)
|
||||
return selected;
|
||||
|
||||
return GetShopOptions()
|
||||
.Select(a => a.ErpShopId)
|
||||
.Where(a => !string.IsNullOrEmpty(a))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反序列化接口返回;失败时统一抛出可读异常。
|
||||
/// 说明:Newtonsoft 默认按属性名匹配(忽略大小写),因此 PascalCase / camelCase 均可解析。
|
||||
/// </summary>
|
||||
private static T Deserialize<T>(string html)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(html);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("查询失败:接口返回数据解析异常", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>按天从缓存拼装某时间范围的明细(返回新列表,避免外部修改污染缓存)</summary>
|
||||
private List<ShopRevenueStatisticsRow> GetCachedRows(DateTime startDate, DateTime endDate)
|
||||
/// <summary>校验接口返回码</summary>
|
||||
private static void EnsureSuccess(Result result)
|
||||
{
|
||||
var result = new List<ShopRevenueStatisticsRow>();
|
||||
lock (_cacheLock)
|
||||
if (result == null)
|
||||
throw new Exception("查询失败:接口无返回数据");
|
||||
if (result.Code != 200)
|
||||
throw new Exception(string.IsNullOrEmpty(result.Msg) ? "查询失败" : result.Msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 摊平某个 Key(店铺ID)下的记录,并用 Key 兜底补齐缺失的 ShopId / StatDate。
|
||||
/// </summary>
|
||||
private static List<ShopStatsResponse> Normalize(string key, List<ShopStatsResponse> items)
|
||||
{
|
||||
var result = new List<ShopStatsResponse>();
|
||||
if (items == null)
|
||||
return result;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
foreach (var day in EnumerateDays(startDate, endDate))
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
if (string.IsNullOrEmpty(item.ShopId))
|
||||
item.ShopId = key;
|
||||
|
||||
if (item.StatDate == default(DateTime))
|
||||
{
|
||||
if (_rowCache.TryGetValue(CacheKey(day), out var entry))
|
||||
result.AddRange(entry.Rows);
|
||||
var date = ParseDateFromKey(key);
|
||||
if (date.HasValue)
|
||||
item.StatDate = date.Value;
|
||||
}
|
||||
|
||||
result.Add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>按所选店铺过滤明细:ShopKeys 为空/Null 返回全部;否则按店铺名(ShopName)精确匹配。</summary>
|
||||
private static List<ShopRevenueStatisticsRow> FilterByShop(List<ShopRevenueStatisticsRow> rows, List<string> shopKeys)
|
||||
/// <summary>尝试把返回数据的 Key 解析为日期(支持 yyyy-MM-dd / yyyyMMdd / 秒或毫秒时间戳)</summary>
|
||||
private static DateTime? ParseDateFromKey(string key)
|
||||
{
|
||||
if (rows == null)
|
||||
return new List<ShopRevenueStatisticsRow>();
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return null;
|
||||
|
||||
if (shopKeys == null || shopKeys.Count == 0)
|
||||
return new List<ShopRevenueStatisticsRow>(rows);
|
||||
if (DateTime.TryParse(key, out var date))
|
||||
return date;
|
||||
|
||||
var shops = GetShopOptions()
|
||||
.Where(a => shopKeys.Contains(a.AppKey))
|
||||
.Where(a => !string.IsNullOrEmpty(a.ShopName))
|
||||
.ToList();
|
||||
if (shops.Count == 0)
|
||||
return new List<ShopRevenueStatisticsRow>();
|
||||
if (DateTime.TryParseExact(key, "yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out date))
|
||||
return date;
|
||||
|
||||
return rows
|
||||
.Where(r => shops.Any(s => string.Equals(r.ShopName, s.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)
|
||||
{
|
||||
var url = BaseClass.Build29WxObjString("shop_revenue_statistics/get_shop_order_amount");
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(UserLogin.GetSignaturex());
|
||||
sb.Append("&shop_ids=").Append(string.Join(",", shopIds));
|
||||
sb.Append("&start_time=").Append(BaseClass.ConvertDateTimeLong(startDate.Date));
|
||||
sb.Append("&end_time=").Append(BaseClass.ConvertDateTimeLong(endDate.Date.AddDays(1).AddTicks(-1)));
|
||||
var html = await GetHtml.PostUtf8Async(url, sb.ToString());
|
||||
|
||||
try
|
||||
if (long.TryParse(key, out var timestamp))
|
||||
{
|
||||
var ret = JsonConvert.DeserializeObject<Result<List<ShopRevenueStatisticsRow>>>(html, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
});
|
||||
if (ret.Code != 200)
|
||||
{
|
||||
throw new Exception(ret.Msg);
|
||||
}
|
||||
return ret.Data ?? new List<ShopRevenueStatisticsRow>();
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new Exception("查询失败");
|
||||
return timestamp.ToString().Length == 13
|
||||
? UnixEpoch.AddMilliseconds(timestamp)
|
||||
: UnixEpoch.AddSeconds(timestamp);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task<ShopRevenueSummary> GetSummaryAsync(ShopRevenueStatisticsQuery query)
|
||||
{
|
||||
var rows = await QueryRowsAsync(query);
|
||||
return RevenueStatisticsAggregator.GetSummary(rows);
|
||||
}
|
||||
|
||||
public async Task<List<RevenueTrendPoint>> GetSalesTrendAsync(ShopRevenueStatisticsQuery query)
|
||||
{
|
||||
var rows = await QueryRowsAsync(query);
|
||||
return RevenueStatisticsAggregator.GetSalesTrend(rows, query.StartDate, query.EndDate);
|
||||
}
|
||||
|
||||
public async Task<List<ShopSalesProportionItem>> GetShopSalesProportionAsync(ShopRevenueStatisticsQuery query)
|
||||
{
|
||||
var rows = await QueryRowsAsync(query);
|
||||
return RevenueStatisticsAggregator.GetShopSalesProportion(rows);
|
||||
}
|
||||
|
||||
public async Task<List<ShopStatisticsItem>> GetShopStatisticsAsync(ShopRevenueStatisticsQuery query)
|
||||
{
|
||||
var rows = await QueryRowsAsync(query);
|
||||
return RevenueStatisticsAggregator.GetShopStatistics(rows);
|
||||
}
|
||||
|
||||
/// <summary>获取天/周/月周期统计表格</summary>
|
||||
public async Task<List<ShopPeriodStatisticsItem>> GetPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType)
|
||||
{
|
||||
var rows = await QueryRowsAsync(query);
|
||||
return RevenueStatisticsAggregator.GetPeriodStatistics(rows, query.StartDate, query.EndDate, periodType);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>店铺下拉列表(同步读取,供查询与过滤使用)</summary>
|
||||
@@ -641,8 +597,8 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
|
||||
return GlobalConfig.ShopLoginList.Select(a =>
|
||||
new ShopOption()
|
||||
{
|
||||
AppKey = a.ShopId,
|
||||
ShopId = a.ErpShopId,
|
||||
ShopId = a.ShopId,
|
||||
ErpShopId = a.ErpShopId,
|
||||
ShopName = a.ShopAccount
|
||||
}).ToList();
|
||||
}
|
||||
@@ -652,5 +608,23 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
|
||||
return Task.FromResult(GetShopOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订单明细列表(有效订单 / 退款订单 / 买手未退回金额的订单),供订单列表弹窗使用。
|
||||
/// 请求 shop_revenue_statistics/GetShopOrderDetails:
|
||||
/// StartDate / EndDate / ShopIds / OrderType(OrderType 由 detailType 决定,按数字传值)。
|
||||
/// </summary>
|
||||
public async Task<List<ShopOrderDetailResponse>> QueryOrderRowsAsync(ShopRevenueStatisticsQuery query, ShopOrderDetailType detailType)
|
||||
{
|
||||
var shopIds = BuildShopIds(query);
|
||||
if (shopIds.Count == 0)
|
||||
return new List<ShopOrderDetailResponse>();
|
||||
|
||||
var url = BaseClass.Build29WxObjString(GetShopOrderDetailsApi);
|
||||
var html = await GetHtml.PostJsonAsync(url, BuildOrderDetailPostData(query, shopIds, detailType));
|
||||
|
||||
var ret = Deserialize<Result<List<ShopOrderDetailResponse>>>(html);
|
||||
EnsureSuccess(ret);
|
||||
return ret.Data ?? new List<ShopOrderDetailResponse>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
<UserControl
|
||||
<UserControl
|
||||
x:Class="_29WxOrderWpf.Ctrls.Statistics.ShowOrderDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:comn="clr-namespace:_29WxOrderWpf.Common"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:_29WxOrderWpf.Ctrls.Statistics"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:wpf="WPFCommon"
|
||||
comn:DialogWindowProperty.Height="620"
|
||||
comn:DialogWindowProperty.ResizeMode="CanResizeWithGrip"
|
||||
comn:DialogWindowProperty.Width="1320"
|
||||
d:Background="White"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
Focusable="True"
|
||||
PreviewKeyDown="ShowOrderDialogView_PreviewKeyDown"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<!-- 单元格文字居中 -->
|
||||
@@ -18,6 +23,21 @@
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
选中行高亮:用主题色填充选中行,并且不区分表格是否拥有焦点
|
||||
(默认样式的选中色在焦点离开表格后会变成浅灰,点了“复制数据”就看不出选中了哪些行)
|
||||
-->
|
||||
<Style x:Key="SelectedCellStyle" TargetType="DataGridCell">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource pageColorBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
@@ -36,15 +56,15 @@
|
||||
Width="180"
|
||||
Placement="Left"
|
||||
TitleVerticalAlignment="Center">
|
||||
<TextBox Height="22" Text="{Binding OrderId, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBox
|
||||
Height="22"
|
||||
Text="{Binding OrderId, UpdateSourceTrigger=PropertyChanged}"
|
||||
ToolTip="按订单号筛选;表格内可用 Ctrl+A 全选、Ctrl+C 复制数据" />
|
||||
</wpf:TitleContent>
|
||||
<Button
|
||||
Command="{Binding SelectAllCommand}"
|
||||
Content="复制数据"
|
||||
Style="{StaticResource BaseButtonLightStyle}" />
|
||||
</wpf:SpacingStackPanel>
|
||||
</Border>
|
||||
<DataGrid
|
||||
x:Name="dgOrders"
|
||||
Grid.Row="1"
|
||||
d:ItemsSource="{d:SampleData ItemCount=10}"
|
||||
AlternatingRowBackground="#FAFAFA"
|
||||
@@ -55,6 +75,7 @@
|
||||
CanUserDeleteRows="False"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserResizeRows="False"
|
||||
CellStyle="{StaticResource SelectedCellStyle}"
|
||||
ColumnHeaderStyle="{StaticResource BaseDataGridColumnHeaderStyle}"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HeadersVisibility="Column"
|
||||
@@ -62,14 +83,9 @@
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding DataView}"
|
||||
RowHeight="44"
|
||||
SelectionMode="Single"
|
||||
SelectionMode="Extended"
|
||||
SelectionUnit="FullRow"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<DataGrid.RowStyle>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</Style>
|
||||
</DataGrid.RowStyle>
|
||||
<DataGrid.Columns>
|
||||
<!-- 店名 -->
|
||||
<DataGridTextColumn
|
||||
@@ -93,7 +109,6 @@
|
||||
<Button
|
||||
Margin="5,0,0,0"
|
||||
Click="CopyOrderId_Click"
|
||||
Foreground="{StaticResource pageColorBrush}"
|
||||
ToolTip="复制订单号">
|
||||
<Button.Style>
|
||||
<Style BasedOn="{StaticResource IconButtonStyle}" TargetType="Button">
|
||||
@@ -113,6 +128,13 @@
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<!-- 创建时间 -->
|
||||
<DataGridTextColumn
|
||||
Width="1.4*"
|
||||
Binding="{Binding CreateTime, StringFormat={}{0:yyyy-MM-dd HH:mm}}"
|
||||
ElementStyle="{StaticResource CenterCellTextStyle}"
|
||||
Header="创建时间" />
|
||||
|
||||
<!-- 金额(分转元) -->
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
@@ -121,7 +143,7 @@
|
||||
Header="金额" />
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
Binding="{Binding RefundAmount, Converter={StaticResource FenToYuanConverter}, StringFormat={}¥{0:N2}}"
|
||||
Binding="{Binding OrderRefundAmount, Converter={StaticResource FenToYuanConverter}, StringFormat={}¥{0:N2}}"
|
||||
ElementStyle="{StaticResource CenterCellTextStyle}"
|
||||
Header="退款金额" />
|
||||
<DataGridTextColumn
|
||||
@@ -129,6 +151,33 @@
|
||||
Binding="{Binding OutAmount, Converter={StaticResource FenToYuanConverter}, StringFormat={}¥{0:N2}}"
|
||||
ElementStyle="{StaticResource CenterCellTextStyle}"
|
||||
Header="外采金额" />
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
Binding="{Binding UnrefundedAmount, Converter={StaticResource FenToYuanConverter}, StringFormat={}¥{0:N2}}"
|
||||
ElementStyle="{StaticResource CenterCellTextStyle}"
|
||||
Header="未退回金额" />
|
||||
|
||||
<!-- 买手订单:一笔买手订单占一行,不合并 -->
|
||||
<DataGridTextColumn
|
||||
Width="1.6*"
|
||||
Binding="{Binding OutOrderSn}"
|
||||
ElementStyle="{StaticResource CenterCellTextStyle}"
|
||||
Header="买手单号" />
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
Binding="{Binding BuyerOutAmount, Converter={StaticResource FenToYuanConverter}, StringFormat={}¥{0:N2}}"
|
||||
ElementStyle="{StaticResource CenterCellTextStyle}"
|
||||
Header="买手金额" />
|
||||
<DataGridTextColumn
|
||||
Width="*"
|
||||
Binding="{Binding BuyerRefundAmount, Converter={StaticResource FenToYuanConverter}, StringFormat={}¥{0:N2}}"
|
||||
ElementStyle="{StaticResource CenterCellTextStyle}"
|
||||
Header="买手退款" />
|
||||
<DataGridTextColumn
|
||||
Width="1.1*"
|
||||
Binding="{Binding BuyerRefundStatusText}"
|
||||
ElementStyle="{StaticResource CenterCellTextStyle}"
|
||||
Header="退款状态" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using _29WxOrderWpf.BaseWindows;
|
||||
using _29WxOrderWpf.BaseWindows;
|
||||
using _29WxOrderWpf.Commands;
|
||||
using _29WxOrderWpf.Ctrls.Statistics.Models;
|
||||
using _29WxOrderWpf.ToolCtrls;
|
||||
@@ -11,6 +11,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
@@ -27,10 +28,37 @@ namespace _29WxOrderWpf.Ctrls.Statistics;
|
||||
/// </summary>
|
||||
public partial class ShowOrderDialogView : UserControl
|
||||
{
|
||||
public ShowOrderDialogView(List<ShopRevenueStatisticsRow> list)
|
||||
private readonly KeyEventHandler shortcutKeyHandler;
|
||||
|
||||
public ShowOrderDialogView(List<ShopOrderDetailResponse> list)
|
||||
{
|
||||
InitializeComponent();
|
||||
this.DataContext = new ShowOrderDialogViewModel(list);
|
||||
|
||||
shortcutKeyHandler = ShowOrderDialogView_PreviewKeyDown;
|
||||
|
||||
// 快捷键必须挂到弹窗窗口上:弹窗刚打开时键盘焦点在窗口本身(不在本控件的可视树里),
|
||||
// 事件路由中就不会有本控件,只在本控件上挂 PreviewKeyDown 的话,
|
||||
// 必须先用鼠标点一下表格(焦点进到表格里)Ctrl+A/Ctrl+C 才会生效。
|
||||
Loaded += (s, e) => AttachShortcutHandler();
|
||||
Unloaded += (s, e) => DetachShortcutHandler();
|
||||
}
|
||||
|
||||
/// <summary>把快捷键处理器挂到所在窗口(隧道事件,焦点在窗口上的任意位置都能收到)</summary>
|
||||
private void AttachShortcutHandler()
|
||||
{
|
||||
Window window = Window.GetWindow(this);
|
||||
if (window == null)
|
||||
return;
|
||||
|
||||
// 先移除再挂,避免重复挂载后按一次键执行两次
|
||||
window.RemoveHandler(PreviewKeyDownEvent, shortcutKeyHandler);
|
||||
window.AddHandler(PreviewKeyDownEvent, shortcutKeyHandler);
|
||||
}
|
||||
|
||||
private void DetachShortcutHandler()
|
||||
{
|
||||
Window.GetWindow(this)?.RemoveHandler(PreviewKeyDownEvent, shortcutKeyHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -39,7 +67,7 @@ public partial class ShowOrderDialogView : UserControl
|
||||
private void CopyOrderId_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element
|
||||
&& element.DataContext is ShopRevenueStatisticsRow row
|
||||
&& element.DataContext is ShopOrderDetailRow row
|
||||
&& !string.IsNullOrEmpty(row.OrderId))
|
||||
{
|
||||
Window window = Window.GetWindow(this);
|
||||
@@ -54,14 +82,142 @@ public partial class ShowOrderDialogView : UserControl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹窗级快捷键:Ctrl+A 全选(筛选后显示的)行,Ctrl+C 复制数据。
|
||||
/// 焦点在文本框里(如订单号搜索框)时不拦截,保留文本自身的 Ctrl+A/Ctrl+C/Ctrl+V 行为。
|
||||
/// </summary>
|
||||
private void ShowOrderDialogView_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Handled || Keyboard.Modifiers != ModifierKeys.Control)
|
||||
return;
|
||||
|
||||
// 文本框(含搜索框)优先,走文本自己的全选/复制/粘贴
|
||||
if (Keyboard.FocusedElement is TextBoxBase)
|
||||
return;
|
||||
|
||||
if (DataContext is not ShowOrderDialogViewModel viewModel)
|
||||
return;
|
||||
|
||||
if (e.Key == Key.A)
|
||||
{
|
||||
SelectAllRows(viewModel);
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.C)
|
||||
{
|
||||
// 有选中就复制选中的行,没有选中则复制筛选后的全部数据
|
||||
viewModel.CopyRows(GetSelectedRows());
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ctrl+A:全选当前筛选后显示的所有行(被过滤掉的行不会选中)。
|
||||
/// 选中行取自与复制同一份筛选结果,保证“选中的”和“复制的”完全一致。
|
||||
/// </summary>
|
||||
private void SelectAllRows(ShowOrderDialogViewModel viewModel)
|
||||
{
|
||||
// 让键盘焦点回到表格,全选后仍可用键盘继续操作
|
||||
dgOrders.Focus();
|
||||
dgOrders.SelectedItems.Clear();
|
||||
foreach (var row in viewModel.GetFilteredRows())
|
||||
{
|
||||
dgOrders.SelectedItems.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>当前选中的行</summary>
|
||||
private List<ShopOrderDetailRow> GetSelectedRows()
|
||||
{
|
||||
return dgOrders.SelectedItems.OfType<ShopOrderDetailRow>().ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订单明细表格行:把一个商铺订单按买手订单“展平”成多行(有几笔买手订单就几行,
|
||||
/// 没有买手订单时占一行且买手字段为空),不把多笔买手订单合并到同一行。
|
||||
/// 商铺订单级字段(店名/订单号/创建时间/金额等)在多行之间重复,保证每行都是一条完整记录。
|
||||
/// </summary>
|
||||
class ShopOrderDetailRow
|
||||
{
|
||||
public string ShopName { get; set; }
|
||||
|
||||
public string OrderId { get; set; }
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
public int OrderPrice { get; set; }
|
||||
|
||||
public int OrderRefundAmount { get; set; }
|
||||
|
||||
public int OutAmount { get; set; }
|
||||
|
||||
public int UnrefundedAmount { get; set; }
|
||||
|
||||
/// <summary>买手(外部)订单号;无买手订单时为空</summary>
|
||||
public string OutOrderSn { get; set; }
|
||||
|
||||
/// <summary>买手订单金额(分)</summary>
|
||||
public int BuyerOutAmount { get; set; }
|
||||
|
||||
/// <summary>买手订单退款金额(分)</summary>
|
||||
public int BuyerRefundAmount { get; set; }
|
||||
|
||||
/// <summary>买手订单退款状态</summary>
|
||||
public string BuyerRefundStatusText { get; set; }
|
||||
|
||||
/// <summary>把接口返回的订单明细按买手订单展平成表格行</summary>
|
||||
public static List<ShopOrderDetailRow> Flatten(IEnumerable<ShopOrderDetailResponse> list)
|
||||
{
|
||||
var rows = new List<ShopOrderDetailRow>();
|
||||
if (list == null)
|
||||
return rows;
|
||||
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
if (item.BuyerOrders == null || item.BuyerOrders.Count == 0)
|
||||
{
|
||||
rows.Add(Create(item, null));
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var buyerOrder in item.BuyerOrders)
|
||||
rows.Add(Create(item, buyerOrder));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static ShopOrderDetailRow Create(ShopOrderDetailResponse item, BuyerOrderInfo buyerOrder)
|
||||
{
|
||||
return new ShopOrderDetailRow
|
||||
{
|
||||
ShopName = item.ShopName,
|
||||
OrderId = item.OrderId,
|
||||
CreateTime = item.CreateTime,
|
||||
OrderPrice = item.OrderPrice,
|
||||
OrderRefundAmount = item.OrderRefundAmount,
|
||||
OutAmount = item.OutAmount,
|
||||
UnrefundedAmount = item.UnrefundedAmount,
|
||||
OutOrderSn = buyerOrder == null ? null : buyerOrder.OutOrderSn,
|
||||
BuyerOutAmount = buyerOrder == null ? 0 : buyerOrder.OutAmount,
|
||||
BuyerRefundAmount = buyerOrder == null ? 0 : buyerOrder.RefundAmount,
|
||||
BuyerRefundStatusText = buyerOrder == null ? null : buyerOrder.RefundStatusText,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ShowOrderDialogViewModel : BindableBaseEx
|
||||
{
|
||||
public ObservableCollection<ShopRevenueStatisticsRow> Data { get; set; }
|
||||
public ObservableCollection<ShopOrderDetailRow> Data { get; set; }
|
||||
public ICollectionView DataView { get; private set; }
|
||||
public ShowOrderDialogViewModel(List<ShopRevenueStatisticsRow> list)
|
||||
public ShowOrderDialogViewModel(List<ShopOrderDetailResponse> list)
|
||||
{
|
||||
Data = new ObservableCollection<ShopRevenueStatisticsRow>(list ?? new List<ShopRevenueStatisticsRow>());
|
||||
// 按买手订单展平:一笔买手订单一一行,不合并
|
||||
Data = new ObservableCollection<ShopOrderDetailRow>(ShopOrderDetailRow.Flatten(list));
|
||||
DataView = CollectionViewSource.GetDefaultView(Data);
|
||||
DataView.Filter = FilterRow;
|
||||
}
|
||||
@@ -70,9 +226,9 @@ class ShowOrderDialogViewModel : BindableBaseEx
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(OrderId))
|
||||
return true;
|
||||
if (obj is ShopRevenueStatisticsRow row)
|
||||
if (obj is ShopOrderDetailRow row)
|
||||
{
|
||||
return row.OrderId.Contains(OrderId);
|
||||
return (row.OrderId ?? string.Empty).Contains(OrderId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -83,33 +239,74 @@ class ShowOrderDialogViewModel : BindableBaseEx
|
||||
set => SetValue(value, () => DataView.Refresh());
|
||||
}
|
||||
|
||||
public ICommand SelectAllCommand => new DelegateCommand(SelectAll);
|
||||
/// <summary>当前筛选后显示的行(按表格显示顺序)</summary>
|
||||
public List<ShopOrderDetailRow> GetFilteredRows()
|
||||
{
|
||||
return DataView.Cast<ShopOrderDetailRow>().ToList();
|
||||
}
|
||||
|
||||
private void SelectAll()
|
||||
/// <summary>
|
||||
/// 复制指定行到剪贴板(带表头,制表符分隔)。
|
||||
/// rows 为空时复制筛选后的全部数据;复制结果用提示条反馈。
|
||||
/// </summary>
|
||||
public void CopyRows(IEnumerable<ShopOrderDetailRow> rows)
|
||||
{
|
||||
List<ShopOrderDetailRow> list = rows?.ToList() ?? new List<ShopOrderDetailRow>();
|
||||
if (list.Count == 0)
|
||||
{
|
||||
// 没有选中行时,复制筛选后的全部数据
|
||||
list = GetFilteredRows();
|
||||
}
|
||||
|
||||
Window window = Application.Current.Windows
|
||||
.OfType<Window>()
|
||||
.FirstOrDefault(w => w.IsActive) ?? Application.Current.MainWindow;
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
ToastHelper.Show(window, "没有可复制的数据", type: ToastType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string text = BuildCopyText(list);
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
|
||||
ToastHelper.Show(window, "复制成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ToastHelper.Show(window, "复制失败 " + ex.Message, type: ToastType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>把行拼成制表符分隔的文本(第一行是表头)</summary>
|
||||
private string BuildCopyText(IEnumerable<ShopOrderDetailRow> rows)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// 表头
|
||||
sb.AppendLine("店名\t订单\t金额\t退款金额\t外采金额");
|
||||
sb.AppendLine("店名\t订单\t创建时间\t金额\t退款金额\t外采金额\t未退回金额\t买手单号\t买手金额\t买手退款\t退款状态");
|
||||
|
||||
// 数据行
|
||||
foreach (var item in Data)
|
||||
foreach (var item in rows)
|
||||
{
|
||||
string shopName = CsvEscape(item.ShopName);
|
||||
string orderId = item.OrderId;
|
||||
string createTime = item.CreateTime.ToString("yyyy-MM-dd HH:mm");
|
||||
string orderPrice = (item.OrderPrice / 100m).ToString("0.00");
|
||||
string refundAmount = (item.RefundAmount / 100m).ToString("0.00");
|
||||
string refundAmount = (item.OrderRefundAmount / 100m).ToString("0.00");
|
||||
string outAmount = (item.OutAmount / 100m).ToString("0.00");
|
||||
string unrefundedAmount = (item.UnrefundedAmount / 100m).ToString("0.00");
|
||||
string buyerOrderSn = CsvEscape(item.OutOrderSn);
|
||||
string buyerOutAmount = (item.BuyerOutAmount / 100m).ToString("0.00");
|
||||
string buyerRefundAmount = (item.BuyerRefundAmount / 100m).ToString("0.00");
|
||||
string buyerRefundStatus = CsvEscape(item.BuyerRefundStatusText);
|
||||
|
||||
sb.AppendLine($"{shopName}\t{orderId}\t{orderPrice}\t{refundAmount}\t{outAmount}");
|
||||
sb.AppendLine($"{shopName}\t{orderId}\t{createTime}\t{orderPrice}\t{refundAmount}\t{outAmount}\t{unrefundedAmount}\t{buyerOrderSn}\t{buyerOutAmount}\t{buyerRefundAmount}\t{buyerRefundStatus}");
|
||||
}
|
||||
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(sb.ToString()));
|
||||
var window = Application.Current.Windows
|
||||
.OfType<Window>()
|
||||
.FirstOrDefault(w => w.IsActive) ?? Application.Current.MainWindow;
|
||||
|
||||
ToastHelper.Show(window, "复制成功");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string CsvEscape(string value)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using _29WxOrderWpf.Commands;
|
||||
using _29WxOrderWpf.Commands;
|
||||
using _29WxOrderWpf.Common;
|
||||
using _29WxOrderWpf.Ctrls.Statistics.Models;
|
||||
using _29WxOrderWpf.Ctrls.Statistics.Services;
|
||||
@@ -39,6 +39,12 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
|
||||
private int _totalCount;
|
||||
private string _pagerText = string.Empty;
|
||||
|
||||
/// <summary>是否已完成首次初始化(用于避免页签来回切换时重复初始化、重复查询)</summary>
|
||||
private bool _initialized;
|
||||
|
||||
/// <summary>上一次真正生效(并据此计算过日期)的时间范围选项,避免同一个选项被重复应用</summary>
|
||||
private TimeRangeOption _appliedTimeRange;
|
||||
|
||||
public PeriodRevenueStatisticsViewModel(PeriodStatisticType periodType)
|
||||
: this(periodType, new ShopRevenueStatisticsService())
|
||||
{
|
||||
@@ -65,8 +71,18 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 页面首次显示时初始化查询条件并加载数据(由外层 TabControl 的选中事件调用)。
|
||||
/// 重复调用(在“综合统计 / 天统计 / 周统计 / 月统计”页签之间来回切换)会被忽略:
|
||||
/// 否则每次切回来都会把时间范围和店铺重置成默认值,并重新发起一次查询。
|
||||
/// 需要刷新数据时点“查询”按钮即可。
|
||||
/// </summary>
|
||||
public void Init()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
_initialized = true;
|
||||
InitializeAsync();
|
||||
}
|
||||
|
||||
@@ -172,7 +188,16 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
|
||||
set
|
||||
{
|
||||
this.SetValue(x => x.SelectedTimeRange, value);
|
||||
ApplyTimeRange();
|
||||
|
||||
// 只有用户真正切换到“另一个”时间范围选项时才重新计算起止日期。
|
||||
// 页签切换时下拉框会把同一个选项(甚至先 null 再回写)重放一遍,
|
||||
// 若照样重算,就会把用户手工调过的日期覆盖掉(例如自定义区间被改回“本周”)。
|
||||
if (value != null && !ReferenceEquals(_appliedTimeRange, value))
|
||||
{
|
||||
_appliedTimeRange = value;
|
||||
ApplyTimeRange();
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(IsCustomDateRange));
|
||||
}
|
||||
}
|
||||
@@ -481,25 +506,28 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>根据选中店铺生成查询店铺 key 列表:未选/全选/选中“全部店铺”时返回空列表(表示全部店铺)</summary>
|
||||
private List<string> BuildSelectedShopKeys()
|
||||
/// <summary>
|
||||
/// 根据选中店铺生成查询用 ERP 店铺ID 列表(接口 ShopIds 使用 ShopLoginInfo.ErpShopId):
|
||||
/// 未选/全选/选中“全部店铺”时返回空列表(表示全部店铺)。
|
||||
/// </summary>
|
||||
private List<string> BuildSelectedShopIds()
|
||||
{
|
||||
var selected = SelectedShops;
|
||||
if (selected == null || selected.Count == 0)
|
||||
return new List<string>();
|
||||
|
||||
var keys = selected.Select(s => s == null ? string.Empty : s.AppKey).ToList();
|
||||
if (keys.Any(string.IsNullOrEmpty))
|
||||
var ids = selected.Select(s => s == null ? string.Empty : s.ErpShopId).ToList();
|
||||
if (ids.Any(string.IsNullOrEmpty))
|
||||
return new List<string>();
|
||||
|
||||
if (selected.Count >= ShopOptions.Count)
|
||||
return new List<string>();
|
||||
|
||||
return keys;
|
||||
return ids;
|
||||
}
|
||||
|
||||
/// <summary>执行查询,刷新周期统计表格并回到第一页</summary>
|
||||
private async Task LoadAsync()
|
||||
/// <summary>组装查询参数(起止日期已保证先后顺序)</summary>
|
||||
private ShopRevenueStatisticsQuery BuildQuery()
|
||||
{
|
||||
if (StartDate > EndDate)
|
||||
{
|
||||
@@ -508,19 +536,23 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
|
||||
EndDate = temp;
|
||||
}
|
||||
|
||||
return new ShopRevenueStatisticsQuery
|
||||
{
|
||||
StartDate = StartDate,
|
||||
EndDate = EndDate,
|
||||
ShopIds = BuildSelectedShopIds(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>执行查询,刷新周期统计表格并回到第一页</summary>
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
var query = new ShopRevenueStatisticsQuery
|
||||
{
|
||||
StartDate = StartDate,
|
||||
EndDate = EndDate,
|
||||
ShopKeys = BuildSelectedShopKeys(),
|
||||
};
|
||||
|
||||
var table = await _service.GetPeriodStatisticsAsync(query, _periodType);
|
||||
Summary = await _service.GetSummaryAsync(query);
|
||||
_allItems = table ?? new List<ShopPeriodStatisticsItem>();
|
||||
var result = await _service.QueryPeriodStatisticsAsync(BuildQuery(), _periodType);
|
||||
Summary = result.Summary;
|
||||
_allItems = result.Items ?? new List<ShopPeriodStatisticsItem>();
|
||||
TotalCount = _allItems.Count;
|
||||
TotalPages = TotalCount == 0 ? 1 : (int)Math.Ceiling(TotalCount / (double)PageSize);
|
||||
GoToPage(1);
|
||||
@@ -553,35 +585,51 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
|
||||
|
||||
private async Task ShowOrderList(string type)
|
||||
{
|
||||
var query = new ShopRevenueStatisticsQuery
|
||||
var detailType = ParseDetailType(type);
|
||||
List<ShopOrderDetailResponse> showList;
|
||||
// 明细接口数据量大、耗时长,期间显示页面统一的加载遮罩,避免点了没反应
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
StartDate = StartDate,
|
||||
EndDate = EndDate,
|
||||
ShopKeys = BuildSelectedShopKeys(),
|
||||
};
|
||||
var rows = await _service.QueryRowsAsync(query);
|
||||
List<ShopRevenueStatisticsRow> showList = new List<ShopRevenueStatisticsRow>();
|
||||
switch (type)
|
||||
showList = await _service.QueryOrderRowsAsync(BuildQuery(), detailType)
|
||||
?? new List<ShopOrderDetailResponse>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
case "ValidOrderCount":
|
||||
showList = RevenueStatisticsAggregator.GetValidOrder(rows);
|
||||
break; //有效订单
|
||||
case "RefundOrderCount":
|
||||
showList = RevenueStatisticsAggregator.GetRefundOrder(rows);
|
||||
break;//退款订单
|
||||
case "BuyerRefundRemainingAmount":
|
||||
showList = RevenueStatisticsAggregator.GetBuyerRefundRemainingOrder(rows);
|
||||
break;//待退回采购金额
|
||||
default:
|
||||
break;
|
||||
MessageBox.Show("订单明细查询失败:" + ex.Message, "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
if (showList.Count == 0)
|
||||
{
|
||||
MessageBox.Show("未查询到符合条件的订单明细。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
new DialogWindow()
|
||||
{
|
||||
Title = "订单列表",
|
||||
Content = new ShowOrderDialogView(showList),
|
||||
DataContext = rows
|
||||
DataContext = showList
|
||||
}.ShowDialog();
|
||||
}
|
||||
|
||||
/// <summary>卡片“明细”按钮参数 → 订单明细类型(对应接口参数 OrderType)</summary>
|
||||
private static ShopOrderDetailType ParseDetailType(string type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case "RefundOrderCount":
|
||||
return ShopOrderDetailType.Refund; //退款订单
|
||||
case "BuyerRefundRemainingAmount":
|
||||
return ShopOrderDetailType.BuyerUnrefunded; //买手未退回金额的订单
|
||||
default:
|
||||
return ShopOrderDetailType.Valid; //有效订单
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using _29WxOrderWpf.Commands;
|
||||
using _29WxOrderWpf.Commands;
|
||||
using _29WxOrderWpf.Common;
|
||||
using _29WxOrderWpf.Ctrls.Statistics.Models;
|
||||
using _29WxOrderWpf.Ctrls.Statistics.Services;
|
||||
@@ -12,6 +12,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Work.BaseForms;
|
||||
|
||||
namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
|
||||
{
|
||||
@@ -31,6 +32,9 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
|
||||
private ObservableCollection<ShopOption> _selectedShops;
|
||||
private string _selectedPlatform;
|
||||
|
||||
/// <summary>上一次真正生效(并据此计算过日期)的时间范围选项,避免同一个选项被重复应用</summary>
|
||||
private TimeRangeOption _appliedTimeRange;
|
||||
|
||||
public ShopRevenueStatisticsViewModel()
|
||||
: this(new ShopRevenueStatisticsService())
|
||||
{
|
||||
@@ -70,7 +74,16 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
|
||||
set
|
||||
{
|
||||
this.SetValue(x => x.SelectedTimeRange, value);
|
||||
ApplyTimeRange();
|
||||
|
||||
// 只有用户真正切换到“另一个”时间范围选项时才重新计算起止日期。
|
||||
// 页签切换时下拉框会把同一个选项(甚至先 null 再回写)重放一遍,
|
||||
// 若照样重算,就会把用户手工调过的日期覆盖掉。
|
||||
if (value != null && !ReferenceEquals(_appliedTimeRange, value))
|
||||
{
|
||||
_appliedTimeRange = value;
|
||||
ApplyTimeRange();
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(IsCustomDateRange));
|
||||
}
|
||||
}
|
||||
@@ -235,57 +248,66 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>根据选中店铺生成查询店铺 key 列表:未选/全选/选中“全部店铺”时返回空列表(表示全部店铺)</summary>
|
||||
private List<string> BuildSelectedShopKeys()
|
||||
/// <summary>
|
||||
/// 根据选中店铺生成查询用 ERP 店铺ID 列表(接口 ShopIds 使用 ShopLoginInfo.ErpShopId):
|
||||
/// 未选/全选/选中“全部店铺”时返回空列表(表示全部店铺)。
|
||||
/// </summary>
|
||||
private List<string> BuildSelectedShopIds()
|
||||
{
|
||||
var selected = SelectedShops;
|
||||
if (selected == null || selected.Count == 0)
|
||||
return new List<string>();
|
||||
|
||||
var keys = selected.Select(s => s == null ? string.Empty : s.AppKey).ToList();
|
||||
if (keys.Any(string.IsNullOrEmpty))
|
||||
var ids = selected.Select(s => s == null ? string.Empty : s.ErpShopId).ToList();
|
||||
if (ids.Any(string.IsNullOrEmpty))
|
||||
return new List<string>();
|
||||
|
||||
if (selected.Count >= ShopOptions.Count)
|
||||
return new List<string>();
|
||||
|
||||
return keys;
|
||||
return ids;
|
||||
}
|
||||
|
||||
/// <summary>执行查询,刷新页面所有统计数据</summary>
|
||||
private async Task LoadAsync()
|
||||
/// <summary>组装查询参数(起止日期已保证先后顺序)</summary>
|
||||
private ShopRevenueStatisticsQuery BuildQuery()
|
||||
{
|
||||
if (StartDate > EndDate)
|
||||
if (StartDate == EndDate)
|
||||
{
|
||||
EndDate = EndDate.AddDays(1).AddSeconds(-1);
|
||||
}
|
||||
else if (StartDate > EndDate)
|
||||
{
|
||||
var temp = StartDate;
|
||||
StartDate = EndDate;
|
||||
EndDate = temp;
|
||||
}
|
||||
|
||||
|
||||
return new ShopRevenueStatisticsQuery
|
||||
{
|
||||
StartDate = StartDate,
|
||||
EndDate = EndDate,
|
||||
ShopIds = BuildSelectedShopIds(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>执行查询,刷新页面所有统计数据</summary>
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
var query = new ShopRevenueStatisticsQuery
|
||||
{
|
||||
StartDate = StartDate,
|
||||
EndDate = EndDate,
|
||||
ShopKeys = BuildSelectedShopKeys(),
|
||||
};
|
||||
var result = await _service.QueryAsync(BuildQuery());
|
||||
|
||||
Summary = await _service.GetSummaryAsync(query);
|
||||
|
||||
var trend = await _service.GetSalesTrendAsync(query);
|
||||
TrendData = new ObservableCollection<RevenueTrendPoint>(trend);
|
||||
|
||||
var proportion = await _service.GetShopSalesProportionAsync(query);
|
||||
ShopProportionData = new ObservableCollection<ShopSalesProportionItem>(proportion);
|
||||
|
||||
var table = await _service.GetShopStatisticsAsync(query);
|
||||
ShopStatistics = new ObservableCollection<ShopStatisticsItem>(table);
|
||||
Summary = result.Summary;
|
||||
TrendData = new ObservableCollection<RevenueTrendPoint>(result.Trend ?? new List<RevenueTrendPoint>());
|
||||
ShopProportionData = new ObservableCollection<ShopSalesProportionItem>(result.ShopProportions ?? new List<ShopSalesProportionItem>());
|
||||
ShopStatistics = new ObservableCollection<ShopStatisticsItem>(result.ShopStatistics ?? new List<ShopStatisticsItem>());
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 查询异常:保留上一次数据,可在此接入日志/错误提示
|
||||
// 查询异常:保留上一次数据,并弹框提示
|
||||
BaseMsgForm.ShowError("查询失败:" + ex.Message, "提示");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -309,35 +331,53 @@ namespace _29WxOrderWpf.Ctrls.Statistics.ViewModels
|
||||
|
||||
private async Task ShowOrderList(string type)
|
||||
{
|
||||
var query = new ShopRevenueStatisticsQuery
|
||||
var detailType = ParseDetailType(type);
|
||||
List<ShopOrderDetailResponse> showList;
|
||||
// 明细接口数据量大、耗时长,期间显示页面统一的加载遮罩,避免点了没反应
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
StartDate = StartDate,
|
||||
EndDate = EndDate,
|
||||
ShopKeys = BuildSelectedShopKeys(),
|
||||
};
|
||||
var rows = await _service.QueryRowsAsync(query);
|
||||
List<ShopRevenueStatisticsRow> showList = new List<ShopRevenueStatisticsRow>();
|
||||
switch (type)
|
||||
{
|
||||
case "ValidOrderCount":
|
||||
showList = RevenueStatisticsAggregator.GetValidOrder(rows);
|
||||
break; //有效订单
|
||||
case "RefundOrderCount":
|
||||
showList = RevenueStatisticsAggregator.GetRefundOrder(rows);
|
||||
break;//退款订单
|
||||
case "BuyerRefundRemainingAmount":
|
||||
showList = RevenueStatisticsAggregator.GetBuyerRefundRemainingOrder(rows);
|
||||
break;//待退回采购金额
|
||||
default:
|
||||
break;
|
||||
showList = await _service.QueryOrderRowsAsync(BuildQuery(), detailType)
|
||||
?? new List<ShopOrderDetailResponse>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
BaseMsgForm.ShowError("订单明细查询失败:" + ex.Message, "提示");
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
|
||||
if (showList.Count == 0)
|
||||
{
|
||||
BaseMsgForm.ShowInfo("未查询到符合条件的订单明细。", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
new DialogWindow(GetWindow())
|
||||
{
|
||||
Title="订单列表",
|
||||
Title = "订单列表",
|
||||
Content = new ShowOrderDialogView(showList),
|
||||
DataContext = rows
|
||||
DataContext = showList
|
||||
}.Show();
|
||||
}
|
||||
|
||||
/// <summary>卡片“明细”按钮参数 → 订单明细类型(对应接口参数 OrderType)</summary>
|
||||
private static ShopOrderDetailType ParseDetailType(string type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case "RefundOrderCount":
|
||||
return ShopOrderDetailType.Refund; //退款订单
|
||||
case "BuyerRefundRemainingAmount":
|
||||
return ShopOrderDetailType.BuyerUnrefunded; //买手未退回金额的订单
|
||||
default:
|
||||
return ShopOrderDetailType.Valid; //有效订单
|
||||
}
|
||||
}
|
||||
|
||||
public Func<Window> GetWindow;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user