diff --git a/29WxOrderWpf/Ctrls/Statistics/Models/ShopRevenueStatisticsModels.cs b/29WxOrderWpf/Ctrls/Statistics/Models/ShopRevenueStatisticsModels.cs
index 3094804..24f5bc9 100644
--- a/29WxOrderWpf/Ctrls/Statistics/Models/ShopRevenueStatisticsModels.cs
+++ b/29WxOrderWpf/Ctrls/Statistics/Models/ShopRevenueStatisticsModels.cs
@@ -1,4 +1,4 @@
-using Newtonsoft.Json;
+using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using WPFCommon.Models;
@@ -6,152 +6,228 @@ using WPFCommon.Models;
namespace _29WxOrderWpf.Ctrls.Statistics.Models
{
///
- /// 店铺营收统计 - 查询参数
+ /// 店铺营收统计 - 查询参数。
+ /// 对应新接口 shop_revenue_statistics/get_shop_order_amount 与
+ /// shop_revenue_statistics/get_shop_stats_details 的请求体(StartDate / EndDate / ShopIds)。
///
public class ShopRevenueStatisticsQuery
{
- /// 开始时间
+ /// 开始日期
public DateTime StartDate { get; set; }
- /// 结束时间
+ /// 结束日期
public DateTime EndDate { get; set; }
- /// 选中的店铺 app_key 列表;空/Null 表示全部店铺
- public List ShopKeys { get; set; }
+ ///
+ /// 选中的店铺 ERP 店铺ID(ShopLoginInfo.ErpShopId)列表;
+ /// 空/Null 表示全部店铺(服务层会取全部店铺的 ErpShopId)。
+ ///
+ public List ShopIds { get; set; }
}
///
- /// 店铺营收统计 - 接口返回的原始明细行。
- /// 与后端 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 = 该店铺按 聚合后的记录列表
+ /// (汇总=1 条;天/周/月=每个自然日/自然周/自然月各 1 条)。
+ /// 注意:接口只返回按 GroupType 聚合后的记录,不返回“整段汇总”,
+ /// 页面上的汇总卡片由客户端对返回记录自行合计(GroupType=Summary 时每店铺一条,直接相加即可)。
+ /// 金额字段单位均为“分”,显示时需除以 100 转为元。
///
- public class ShopRevenueStatisticsRow
+ public class ShopStatsResponse
{
///
- /// 店铺名称
+ /// 店铺ID
///
- [JsonProperty("shop_name")]
- public string ShopName { get; set; }
+ public string ShopId { get; set; }
///
- /// 下单时间(秒级时间戳)
+ /// 销售订单创建日期
///
- [JsonProperty("create_time")]
- [JsonConverter(typeof(UnixDateTimeConverter))]
- public DateTime CreateTime { get; set; }
+ public DateTime StatDate { get; set; }
///
- /// 订单号
+ /// 订单数(有效订单,不含退款订单)
///
- [JsonProperty("order_id")]
- public string OrderId { get; set; }
+ public int OrderCount { get; set; }
///
- /// 订单状态
+ /// 销售总额(分)
///
- [JsonProperty("status")]
- public int Status { get; set; }
-
- ///
- /// 商品总价(分)
- ///
- [JsonProperty("product_price")]
- public int ProductPrice { get; set; }
-
- ///
- /// 订单金额(分)
- ///
- [JsonProperty("order_price")]
- public int OrderPrice { get; set; }
-
- ///
- /// 采购价(分)
- ///
- [JsonProperty("purchase_price")]
- public int PurchasePrice { get; set; }
-
- ///
- /// 运费(分)
- ///
- [JsonProperty("freight")]
- public int Freight { get; set; }
-
- ///
- /// 平台服务费(预计技术服务费,分)
- ///
- [JsonProperty("predict_commission_fee")]
- public int PredictCommissionFee { get; set; }
-
- ///
- /// 达人佣金(分)
- ///
- [JsonProperty("commission_amount")]
- public int CommissionAmount { get; set; }
-
- ///
- /// 优惠金额(分)
- ///
- [JsonProperty("discounted_price")]
- public int DiscountedPrice { get; set; }
+ public long SalesAmount { get; set; }
///
/// 利润(分)
///
- [JsonProperty("profit")]
- public int Profit { get; set; }
+ public long ProfitAmount { get; set; }
///
- /// 采购状态
+ /// 平台佣金(分)
///
- [JsonProperty("purchase_status")]
- public int PurchaseStatus { get; set; }
+ public long PlatformFee { get; set; }
///
- /// 外采金额(分),out_amount为空或0时从中转仓订单表补全
+ /// 退款订单数
+ ///
+ public int RefundOrderCount { get; set; }
+
+ ///
+ /// 已成功退款金额(分,店铺端已退款成功)
+ ///
+ public long RefundDone { get; set; }
+
+ ///
+ /// 退款处理中金额(分,店铺端退款中)
+ ///
+ public long RefundPending { get; set; }
+
+ ///
+ /// 采购单数
+ ///
+ public int PurchaseCount { get; set; }
+
+ ///
+ /// 总花费金额(分,买手号采购付款金额)
+ ///
+ public long TotalPurchase { get; set; }
+
+ ///
+ /// 售后中买手总金额(分)
+ ///
+ public long TotalRefund => DoneRefund + PendingRefund + RejectedRefund;
+
+ ///
+ /// 已成功退款金额(分,买手号采购退款成功)
+ ///
+ public long DoneRefund { get; set; }
+
+ ///
+ /// 未退回金额(分)
+ ///
+ public long PendingRefund { get; set; }
+
+ ///
+ /// 被拒绝金额(分)
+ ///
+ public long RejectedRefund { get; set; }
+ }
+
+ ///
+ /// 销售趋势记录(接口返回):跨店铺合计后的按天趋势,供综合统计页的销售趋势折线图使用。
+ /// 接口返回结构:
+ /// shop_revenue_statistics/GetShopStatsTrend
+ /// -> Result<List<ShopStatsTrendResponse>>
+ /// 请求参数:StartDate / EndDate / ShopIds(不需要 GroupType,固定按天返回)。
+ /// 金额字段单位为“分”,显示时需除以 100 转为元。
+ ///
+ public class ShopStatsTrendResponse
+ {
+ ///
+ /// 统计日期(天)
+ ///
+ public DateTime StatDate { get; set; }
+
+ ///
+ /// 销售额(分)
+ ///
+ public long SalesAmount { get; set; }
+
+ ///
+ /// 订单数
+ ///
+ public int OrderCount { get; set; }
+ }
+
+ ///
+ /// 订单明细(接口返回):订单列表弹窗 ShowOrderDialogView 的数据行。
+ /// 接口返回结构:
+ /// shop_revenue_statistics/GetShopOrderDetails
+ /// -> Result<List<ShopOrderDetailResponse>>
+ /// 请求参数:StartDate / EndDate / ShopIds / OrderType(见 )。
+ /// 金额字段单位均为“分”,显示时需除以 100 转为元。
+ ///
+ public class ShopOrderDetailResponse
+ {
+ ///
+ /// 店铺ID
+ ///
+ public long ShopId { get; set; }
+
+ ///
+ /// 店铺名称
+ ///
+ public string ShopName { get; set; }
+
+ ///
+ /// 商铺订单号
+ ///
+ public string OrderId { get; set; }
+
+ ///
+ /// 订单创建时间
+ ///
+ public DateTime CreateTime { get; set; }
+
+ ///
+ /// 订单金额(分)
+ ///
+ public int OrderPrice { get; set; }
+
+ ///
+ /// 订单退款金额(分),即商铺订单商品明细的退款金额之和;有效订单为 0
+ ///
+ public int OrderRefundAmount { get; set; }
+
+ ///
+ /// 外采金额(分),即该订单下所有买手订单的购买金额之和
///
- [JsonProperty("out_amount")]
public int OutAmount { get; set; }
///
- /// 售后状态(订单明细 refund_status)
+ /// 买手未退回金额(分),即买手订单中退款未成功的退款金额之和;无售后时为 0
///
- [JsonProperty("refund_status")]
- public int RefundStatus { get; set; }
+ public int UnrefundedAmount { get; set; }
///
- /// 售后状态(退款单 status)
+ /// 买手订单信息(一个订单号对应多个买手订单)
///
- [JsonProperty("refund_status_string")]
- public string RefundStatusString { get; set; }
+ public List BuyerOrders { get; set; } = new List();
+ }
+
+ ///
+ /// 买手订单信息(订单明细下的子项,一个商铺订单可对应多个买手订单)
+ ///
+ public class BuyerOrderInfo
+ {
+ ///
+ /// 买手订单ID
+ ///
+ public long Id { get; set; }
+ ///
+ /// 仓库类型 0,1直发
+ ///
+ public int PurchaseType { get; set; }
+ ///
+ /// 外部订单(买手订单)号
+ ///
+ public string OutOrderSn { get; set; }
///
- /// 售后退款金额(分)
+ /// 金额(分)
+ ///
+ public int OutAmount { get; set; }
+
+ public long RefundId { get; set; }
+ ///
+ /// 退款金额(分)
///
- [JsonProperty("refund_amount")]
public int RefundAmount { get; set; }
///
- /// 是否外采:0=未从中转仓订单表查到,1=查到
+ /// 退款状态
///
- [JsonProperty("external_procurement")]
- public int ExternalProcurement { get; set; }
-
-
- ///
- /// 买手退款金额
- ///
- [JsonProperty("buyer_refund_amount")]
- public int BuyerRefundAmount { get; set; }
-
- ///
- /// 买手退款状态
- ///
- [JsonProperty("buyer_refund_status")]
- public string BuyerRefundStatus { get; set; }
-
+ public string RefundStatusText { get; set; }
}
///
@@ -167,7 +243,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// 利润(实际利润)
public decimal Profit { get; set; }
- /// 退款金额(统计期间退款)
+ /// 退款金额(统计期间店铺端退款:已成功 + 处理中)
public decimal RefundAmount { get; set; }
/// 退款订单数
@@ -176,7 +252,10 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// 平台服务费总额(元)
public decimal PlatformFeeTotal { get; set; }
- /// 达人佣金(元)
+ ///
+ /// 达人佣金(元)。
+ /// 新接口未提供该字段,固定为 0,保留属性与页面展示,待后端补充字段后直接接上。
+ ///
public decimal Commission { get; set; }
/// 客单价(平均每单金额)
@@ -239,7 +318,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// 店铺名称
public string ShopName { get; set; }
- /// 订单数
+ /// 订单数(有效订单,不含退款订单)
public int OrderCount { get; set; }
/// 销售额(元)
@@ -255,7 +334,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// 平台服务费总额(元)
public decimal PlatformFeeTotal { get; set; }
- /// 达人佣金(元)
+ /// 达人佣金(元)。新接口未提供该字段,固定为 0。
public decimal Commission { get; set; }
/// 客单价(元)
@@ -280,6 +359,51 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
Month = 2
}
+ ///
+ /// 接口聚合方式(请求参数 GroupType,按数字传值)。
+ /// 每种方式都返回 Dictionary<店铺ID, List<ShopStatsResponse>>:
+ /// 汇总时每个店铺 1 条,天/周/月时每个店铺每个周期 1 条。
+ ///
+ public enum ShopStatsGroupType
+ {
+ ///
+ /// 汇总:每个商铺只返回一条(整个查询范围合计)
+ ///
+ Summary = 0,
+
+ ///
+ /// 天:按自然日聚合,每个商铺每天一条
+ ///
+ Day = 1,
+
+ ///
+ /// 周:按自然周聚合(周一为一周开始),每个商铺每周一条
+ ///
+ Week = 2,
+
+ ///
+ /// 月:按自然月聚合,每个商铺每月一条
+ ///
+ Month = 3
+ }
+
+ ///
+ /// 订单明细类型(订单列表弹窗的入口类型,对应接口参数 OrderType,按数字传值)。
+ /// 数值与后端枚举保持一致:有效订单=1、退款订单=2、买手未退回金额的订单=3
+ /// (后端枚举里的“全部订单 All=0”本页不使用,故不定义)。
+ ///
+ public enum ShopOrderDetailType
+ {
+ /// 有效订单:没有发生售后的订单(对应统计里的订单数 / 销售额 / 利润口径)
+ Valid = 1,
+
+ /// 退款订单:发生过售后的订单(对应统计里的退款订单数口径)
+ Refund = 2,
+
+ /// 买手未退回金额的订单:退款订单中买手款项还没退回的(对应统计里的未退回金额口径)
+ BuyerUnrefunded = 3
+ }
+
///
/// 天/周/月周期统计表格行(金额单位:元)。
/// 按所选店铺(或全部店铺)统计:天统计每天一行;周统计每周一行;月统计每月一行。
@@ -292,7 +416,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// 周期显示文本(天:yyyy-MM-dd;周:yyyy-MM-dd ~ yyyy-MM-dd;月:yyyy-MM)
public string Period { get; set; }
- /// 订单数
+ /// 订单数(有效订单,不含退款订单)
public int OrderCount { get; set; }
/// 销售额(元)
@@ -309,7 +433,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
/// 平台服务费总额(元)
public decimal PlatformFeeTotal { get; set; }
- /// 达人佣金(元)
+ /// 达人佣金(元)。新接口未提供该字段,固定为 0。
public decimal Commission { get; set; }
/// 平均利润率(利润 / 采购金额,百分数,如 12.34 表示 12.34%)
@@ -322,14 +446,51 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Models
public decimal AvgOrderAmount { get; set; }
}
+ ///
+ /// 店铺营收统计 - 一次查询的全部聚合结果(综合统计页使用)。
+ /// 由两次请求组成:
+ /// shop_revenue_statistics/GetShopStats(GroupType=Summary)-> 卡片/店铺统计表/销售占比(每店铺一条,与区间长度无关)
+ /// shop_revenue_statistics/GetShopStatsTrend -> 销售趋势(跨店铺合计的按天序列)
+ ///
+ public class ShopRevenueStatisticsResult
+ {
+ /// 汇总卡片数据
+ public ShopRevenueSummary Summary { get; set; }
+
+ /// 销售趋势(按天,缺失日期补 0)
+ public List Trend { get; set; }
+
+ /// 店铺销售占比(饼图)
+ public List ShopProportions { get; set; }
+
+ /// 店铺统计表格
+ public List ShopStatistics { get; set; }
+ }
+
+ ///
+ /// 店铺营收统计 - 天/周/月周期统计查询结果(周期统计页使用)。
+ /// 接口不返回整段汇总,Summary 由返回的周期数据在客户端合计得出。
+ ///
+ public class ShopPeriodStatisticsResult
+ {
+ /// 汇总卡片数据(整个查询时间段,客户端合计)
+ public ShopRevenueSummary Summary { get; set; }
+
+ /// 周期统计表格(天/周/月各一行)
+ public List Items { get; set; }
+ }
+
///
/// 店铺下拉选项
///
public class ShopOption
{
- /// 店铺 app_key(A.app_key)
- public string AppKey { get; set; }
+ /// 店铺ID(ShopLoginInfo.ShopId)
public string ShopId { get; set; }
+
+ /// ERP 店铺ID(ShopLoginInfo.ErpShopId),统计接口的 ShopIds 使用该字段
+ public string ErpShopId { get; set; }
+
/// 店铺名称
public string ShopName { get; set; }
}
@@ -346,55 +507,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);
- }
- }
}
diff --git a/29WxOrderWpf/Ctrls/Statistics/Services/ShopRevenueStatisticsService.cs b/29WxOrderWpf/Ctrls/Statistics/Services/ShopRevenueStatisticsService.cs
index ee66da0..21d452c 100644
--- a/29WxOrderWpf/Ctrls/Statistics/Services/ShopRevenueStatisticsService.cs
+++ b/29WxOrderWpf/Ctrls/Statistics/Services/ShopRevenueStatisticsService.cs
@@ -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,105 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
{
///
/// 店铺营收统计 - 数据服务调用接口。
- /// 页面只依赖此接口,接入真实数据源时新建实现类(如对接 HTTP 接口 / SQL 查询),
- /// 并通过 ShopRevenueStatisticsViewModel 构造函数注入即可,无需改动页面。
+ /// 页面只依赖此接口,接入其他数据源时新建实现类,
+ /// 并通过 ViewModel 构造函数注入即可,无需改动页面。
///
public interface IShopRevenueStatisticsService
{
///
- /// 查询原始统计明细(接口返回的 A.* / B.* / c.* 原始字段)。
- /// 真实实现通常通过 SQL / HTTP 一次性返回这些行,再由聚合器计算。
+ /// 综合统计查询:返回汇总卡片、销售趋势(按天)、店铺销售占比、店铺统计表格。
+ /// 用 GroupType=Day 一次请求取回按店铺按天的记录,再由客户端汇总,无缓存。
///
- Task> QueryRowsAsync(ShopRevenueStatisticsQuery query);
+ Task QueryAsync(ShopRevenueStatisticsQuery query);
- /// 获取顶部汇总卡片数据
- Task GetSummaryAsync(ShopRevenueStatisticsQuery query);
-
- /// 获取销售趋势(按天)
- Task> GetSalesTrendAsync(ShopRevenueStatisticsQuery query);
-
- /// 获取店铺销售占比(饼图)
- Task> GetShopSalesProportionAsync(ShopRevenueStatisticsQuery query);
-
- /// 获取店铺统计表格
- Task> GetShopStatisticsAsync(ShopRevenueStatisticsQuery query);
-
- /// 获取天/周/月周期统计表格(每天/每周/每月各一行,按所选店铺或全部店铺统计)
- Task> GetPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType);
+ ///
+ /// 天/周/月周期统计查询:返回汇总卡片 + 按周期(天/周/月)聚合的表格数据。
+ /// 接口不返回整段汇总,Summary 由返回的周期记录在客户端合计。
+ ///
+ Task QueryPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType);
/// 获取店铺下拉列表(用于查询条件)
Task> GetShopOptionsAsync();
+ ///
+ /// 订单明细列表(有效订单 / 退款订单 / 买手未退回金额的订单),供订单列表弹窗使用。
+ ///
+ Task> QueryOrderRowsAsync(ShopRevenueStatisticsQuery query, ShopOrderDetailType detailType);
-
+ ///
+ /// 把某笔买手订单标记为退款成功(订单列表弹窗“退款成功”按钮,仅买手未退回金额的订单使用)。
+ ///
+ /// 买手退款表ID(,即表格当前行的 id)
+ Task UpdateBuyerRefundStatusAsync(long id);
}
///
- /// 统计聚合器:把接口返回的原始明细行聚合成页面所需的汇总/趋势/占比/表格数据。
- /// 聚合口径说明(依据接口字段描述):
- /// - 订单状态 status:10-待付款;20-待发货;21-部分发货;30-待收货;100-完成;
- /// 200-全部商品售后之后订单取消;250-未付款用户主动取消或超时未付款订单自动取消。
- /// - 有效订单:已付款(20/21/30/100)且未退款;10/250 未付款不计入销售额。
- /// - 已退款:c.refund_status 为 4/11,或订单状态为 200(全部商品售后后取消)。
+ /// 统计聚合器:把接口返回的 聚合成页面所需的汇总/趋势/占比/表格数据。
+ /// 数据来源:shop_revenue_statistics/GetShopStats(GroupType = Day/Week/Month),
+ /// 接口只返回“按周期聚合的记录”,页面上所有汇总口径都在这里由客户端合计。
+ /// 口径说明(依据接口字段含义):
+ /// - OrderCount 为“有效订单数”,已不含退款订单;因此退款率分母 = OrderCount + RefundOrderCount。
+ /// - 店铺端退款:RefundDone(已成功)+ RefundPending(处理中)→ 退款金额。
+ /// - 买手端(采购)退款:TotalRefund = DoneRefund + PendingRefund + RejectedRefund → 售后采购金额;
+ /// DoneRefund → 已退回采购金额;剩余(待退回)= TotalRefund - DoneRefund。
+ /// - 接口无“达人佣金”字段,Commission 固定为 0(保留属性与页面展示,待后端补字段)。
/// - 金额字段单位均为“分”,除以 100 转为元。
///
- public static class RevenueStatisticsAggregator
+ public static class ShopStatsAggregator
{
- /// 订单状态 - 待付款
- public const int StatusPendingPay = 10;
+ /// 金额换算:分 → 元
+ private const decimal FenPerYuan = 100m;
- /// 订单状态 - 待发货
- public const int StatusWaitSend = 20;
-
- /// 订单状态 - 部分发货
- public const int StatusPartSend = 21;
-
- /// 订单状态 - 待收货
- public const int StatusWaitReceive = 30;
-
- /// 订单状态 - 完成
- public const int StatusFinished = 100;
-
- /// 订单状态 - 全部商品售后之后订单取消
- public const int StatusCancelAfterAfterSale = 200;
-
- /// 订单状态 - 未付款用户主动取消/超时未付款自动取消
- public const int StatusCancelNotPay = 250;
-
- /// 是否已付款的订单(20/21/30/100;10 待付款、250 未付款取消不计入)
- public static bool IsPaidOrder(ShopRevenueStatisticsRow row)
+ /// 分转元
+ 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;
}
- /// 是否已退款(c.refund_status 为 4/11,或订单状态为 200 全部商品售后后取消)
- public static bool IsRefunded(ShopRevenueStatisticsRow row)
+ private static List ToList(IEnumerable stats)
{
- if (row == null) return false;
- return row.RefundStatus > 1 || row.RefundStatusString == "MERCHANT_REFUND_SUCCESS" || row.RefundStatusString == "MERCHANT_RETURN_SUCCESS";
+ return (stats ?? Enumerable.Empty())
+ .Where(a => a != null)
+ .ToList();
}
- /// 是否有效订单(已付款且未退款)
- public static bool IsValidOrder(ShopRevenueStatisticsRow row)
+ private static IEnumerable> GroupByShop(IEnumerable stats)
{
- return !IsRefunded(row);
+ return ToList(stats).GroupBy(a => a.ShopId ?? string.Empty);
}
- /// 买手号退款成功
- public static bool IsBuyerRefundSuccessful(ShopRevenueStatisticsRow row)
+
+ /// 店铺显示名:优先按 ERP 店铺ID匹配,其次按平台店铺ID匹配,都匹配不到时退回店铺ID
+ 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 GetPaymentOrderList(IEnumerable rows)
+
+ /// 汇总卡片(整个查询时间段,所有选中店铺合计)
+ public static ShopRevenueSummary GetSummary(IEnumerable stats)
{
- return (rows ?? new List()).Where(a => a.OutAmount > 0).ToList(); //买手付钱后的订单数量
- }
- /// 汇总卡片
- public static ShopRevenueSummary GetSummary(IEnumerable 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 +120,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,
};
}
- ///
- /// 获取有效订单
- ///
- ///
- ///
- public static List GetValidOrder(IEnumerable rows)
- {
- var list = GetPaymentOrderList(rows); //买手付钱后的订单数量
- return list.Where(IsValidOrder).ToList();
- }
- ///
- /// 获取退款订单
- ///
- ///
- ///
- public static List GetRefundOrder(IEnumerable rows)
- {
- var list = GetPaymentOrderList(rows); //买手付钱后的订单数量
- return list.Where(IsRefunded).ToList();
- }
- ///
- /// 获取退款订单
- ///
- ///
- ///
- public static List GetBuyerRefundRemainingOrder(IEnumerable 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();
+ /// 店铺统计表格(按店铺汇总,销售额倒序)
+ public static List GetShopStatistics(IEnumerable stats)
+ {
+ return GroupByShop(stats)
+ .Select(g => BuildShopStatisticsItem(g.Key, g))
+ .OrderByDescending(i => i.SalesAmount)
+ .ToList();
}
- /// 销售趋势(按天,缺失日期补 0,仅统计有效订单)
- public static List GetSalesTrend(IEnumerable rows, DateTime startDate, DateTime endDate)
+ /// 店铺销售占比(饼图,按店铺汇总,销售额倒序)
+ public static List GetShopSalesProportion(IEnumerable 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;
+ }
+
+ ///
+ /// 销售趋势(按天,缺失日期补 0)。
+ /// 数据来自 GetShopStatsTrend:服务端已经跨店铺合计,这里只做日期规整与补零。
+ ///
+ public static List GetSalesTrend(IEnumerable trend, DateTime startDate, DateTime endDate)
+ {
+ var list = (trend ?? Enumerable.Empty())
+ .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();
@@ -218,95 +192,23 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
return result;
}
- /// 店铺销售占比(饼图,仅统计有效订单)
- public static List GetShopSalesProportion(IEnumerable rows)
+ ///
+ /// 天/周/月周期统计表格(每天/每周/每月各一行,无数据的周期也补一行,金额单位:元)。
+ /// 结果按周期倒序(最新的周期在最上面)。
+ /// 说明:接口按“自然周期”聚合(周从周一起、月从 1 号起),所以筛选也按周期起点比较,
+ /// 否则区间从周三开始时,那一周(周一起)的数据会被误判为越界而丢掉。
+ ///
+ public static List GetPeriodStatistics(IEnumerable 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();
- }
-
- /// 店铺统计表格
- public static List GetShopStatistics(IEnumerable 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();
- }
-
- /// 天/周/月周期统计表格(每天/每周/每月各一行,无数据的周期也补一行,金额单位:元)
- public static List GetPeriodStatistics(IEnumerable rows, DateTime startDate, DateTime endDate, PeriodStatisticType periodType)
- {
- var list = (rows ?? new List()).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();
@@ -321,6 +223,7 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
Period = GetPeriodText(periodStart, periodType),
OrderCount = 0,
SalesAmount = 0m,
+ BuyerPaymentAmount = 0m,
Profit = 0m,
RefundAmount = 0m,
PlatformFeeTotal = 0m,
@@ -336,6 +239,56 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
return result;
}
+ /// 店铺统计行(金额单位:元)
+ private static ShopStatisticsItem BuildShopStatisticsItem(string shopId, IEnumerable 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
+ };
+ }
+
+ /// 周期统计行(金额单位:元)
+ private static ShopPeriodStatisticsItem BuildPeriodStatisticsItem(DateTime periodStart, IEnumerable 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
+ };
+ }
+
/// 枚举 startDate~endDate 覆盖的所有周期开始日期(天/周/月)
private static IEnumerable EnumeratePeriods(DateTime startDate, DateTime endDate, PeriodStatisticType periodType)
{
@@ -387,252 +340,264 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
return periodStart.ToString("yyyy-MM-dd");
}
}
-
}
///
- /// 默认实现:请求真实接口(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 注释)。
///
public class ShopRevenueStatisticsService : IShopRevenueStatisticsService
{
- /// 缓存有效期:订单/退款数据会持续变化,超过该时长后缓存自动失效并重新请求。
- private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(60);
+ /// 统计接口地址(聚合粒度由请求参数 GroupType 决定)
+ private const string GetShopStatsApi = "shop_revenue_statistics/GetShopStats";
- ///
- /// 明细数据缓存(按天缓存,纯时间维度,不含店铺)。
- /// key = 日期(yyyyMMdd),value = 该天全部店铺的明细 + 缓存时间。
- /// 查询时按天命中:未命中的天合并成连续段一次请求,返回后按天拆分写入;
- /// 前台选某一个或某多个店铺时,按所选店铺(ShopName)过滤,复用同一批时间数据。
- ///
- private static readonly Dictionary _rowCache =
- new Dictionary();
- private readonly object _cacheLock = new object();
+ /// 销售趋势接口地址(服务端跨店铺合计,固定按天)
+ private const string GetShopStatsTrendApi = "shop_revenue_statistics/GetShopStatsTrend";
- private sealed class DateRange
+ /// 订单明细接口地址(订单列表弹窗用)
+ private const string GetShopOrderDetailsApi = "shop_revenue_statistics/GetShopOrderDetails";
+
+ /// 更新买手订单退款状态接口地址(id 通过 URL 参数传递)
+ private const string UpdateBuyerRefundStatusApi = "buyer/UpdateBuyerRefundStatsu";
+
+ /// Unix 纪元(本地时区),用于解析以时间戳作为 Key 的返回数据
+ private static readonly DateTime UnixEpoch = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1));
+
+ public async Task 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 Rows;
- public DateTime CachedAt;
- }
-
- private static string CacheKey(DateTime day)
- {
- return day.Date.ToString("yyyyMMdd");
- }
-
- /// 枚举 startDate ~ endDate 之间的每一天(含首尾)
- private static IEnumerable EnumerateDays(DateTime startDate, DateTime endDate)
- {
- for (var d = startDate.Date; d <= endDate.Date; d = d.AddDays(1))
- yield return d;
- }
-
- /// 该天缓存是否有效(存在且未过期);调用方需持有 _cacheLock
- private bool IsValidLocked(DateTime day)
- {
- return _rowCache.TryGetValue(CacheKey(day), out var entry)
- && DateTime.Now - entry.CachedAt <= CacheDuration;
- }
-
- /// 把缺失日期合并为连续日期段(每段一次请求)
- private static List BuildRuns(List missingDays)
- {
- var sorted = missingDays.OrderBy(d => d).ToList();
- var runs = new List();
- 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> QueryRowsAsync(ShopRevenueStatisticsQuery query)
+ public async Task 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 missingDays;
- lock (_cacheLock)
+ /// 页面统计周期 → 接口聚合方式
+ 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();
-
- 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);
}
///
- /// 把一次请求的结果按天写入缓存。
- /// 本次请求覆盖的每一天都以返回数据为准整体替换(无数据的天记为空的已缓存,
- /// 避免“该天无数据”被反复判定为未命中而重复请求)。
+ /// 请求 GetShopStats:返回按 GroupType 聚合的记录
+ /// (汇总时每店铺一条;天/周/月时每店铺每个自然日/自然周/自然月一条)。
///
- private void CacheResponse(DateTime startDate, DateTime endDate, List rows)
+ private static async Task> RequestStatsAsync(ShopRevenueStatisticsQuery query, ShopStatsGroupType groupType)
{
- if (rows == null)
- rows = new List();
+ var shopIds = BuildShopIds(query);
+ if (shopIds.Count == 0)
+ return new List();
- lock (_cacheLock)
+ var url = BaseClass.Build29WxObjString(GetShopStatsApi);
+ var html = await GetHtml.PostJsonAsync(url, BuildPostData(query, shopIds, groupType));
+
+ var ret = Deserialize>>>(html);
+ EnsureSuccess(ret);
+
+ var result = new List();
+ if (ret.Data == null)
+ return result;
+
+ foreach (var kv in ret.Data)
+ result.AddRange(Normalize(kv.Key, kv.Value));
+
+ return result;
+ }
+
+ ///
+ /// 请求 GetShopStatsTrend:跨店铺合计的按天销售趋势(服务端已合计,不需要 GroupType)。
+ ///
+ private static async Task> RequestTrendAsync(ShopRevenueStatisticsQuery query)
+ {
+ var shopIds = BuildShopIds(query);
+ if (shopIds.Count == 0)
+ return new List();
+
+ var url = BaseClass.Build29WxObjString(GetShopStatsTrendApi);
+ var html = await GetHtml.PostJsonAsync(url, BuildTrendPostData(query, shopIds));
+
+ var ret = Deserialize>>(html);
+ EnsureSuccess(ret);
+ return ret.Data ?? new List();
+ }
+
+ ///
+ /// 组装请求体:StartDate / EndDate 传日期,ShopIds 传 ERP 店铺ID 列表,
+ /// GroupType 传枚举数字(0=汇总 1=天 2=周 3=月)。
+ ///
+ private static object BuildPostData(ShopRevenueStatisticsQuery query, List 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());
+ /// 组装趋势接口请求体:StartDate / EndDate + ShopIds(无 GroupType)
+ private static object BuildTrendPostData(ShopRevenueStatisticsQuery query, List 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(), CachedAt = DateTime.Now };
- }
+ ///
+ /// 组装订单明细请求体:StartDate / EndDate + ShopIds + OrderType
+ /// (OrderType 传枚举数字:1=有效订单 2=退款订单 3=买手未退回金额的订单)。
+ ///
+ private static object BuildOrderDetailPostData(ShopRevenueStatisticsQuery query, List shopIds, ShopOrderDetailType detailType)
+ {
+ return UserLogin.GetSignPostData(new
+ {
+ StartDate = query.StartDate,
+ EndDate = query.EndDate,
+ ShopIds = shopIds,
+ OrderType = (int)detailType,
+ });
+ }
+
+ ///
+ /// 查询店铺范围:query.ShopIds 为空表示全部店铺(取全部店铺的 ERP 店铺ID)。
+ /// 接口的 ShopIds 使用 ShopLoginInfo.ErpShopId。
+ ///
+ private static List BuildShopIds(ShopRevenueStatisticsQuery query)
+ {
+ var selected = (query == null || query.ShopIds == null)
+ ? new List()
+ : 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();
+ }
+
+ ///
+ /// 反序列化接口返回;失败时统一抛出可读异常。
+ /// 说明:Newtonsoft 默认按属性名匹配(忽略大小写),因此 PascalCase / camelCase 均可解析。
+ ///
+ private static T Deserialize(string html)
+ {
+ try
+ {
+ return JsonConvert.DeserializeObject(html);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception("查询失败:接口返回数据解析异常", ex);
}
}
- /// 按天从缓存拼装某时间范围的明细(返回新列表,避免外部修改污染缓存)
- private List GetCachedRows(DateTime startDate, DateTime endDate)
+ /// 校验接口返回码
+ private static void EnsureSuccess(Result result)
{
- var result = new List();
- lock (_cacheLock)
+ if (result == null)
+ throw new Exception("查询失败:接口无返回数据");
+ if (result.Code != 200)
+ throw new Exception(string.IsNullOrEmpty(result.Msg) ? "查询失败" : result.Msg);
+ }
+
+ ///
+ /// 摊平某个 Key(店铺ID)下的记录,并用 Key 兜底补齐缺失的 ShopId / StatDate。
+ ///
+ private static List Normalize(string key, List items)
+ {
+ var result = new List();
+ 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;
}
- /// 按所选店铺过滤明细:ShopKeys 为空/Null 返回全部;否则按店铺名(ShopName)精确匹配。
- private static List FilterByShop(List rows, List shopKeys)
+ /// 尝试把返回数据的 Key 解析为日期(支持 yyyy-MM-dd / yyyyMMdd / 秒或毫秒时间戳)
+ private static DateTime? ParseDateFromKey(string key)
{
- if (rows == null)
- return new List();
+ if (string.IsNullOrEmpty(key))
+ return null;
- if (shopKeys == null || shopKeys.Count == 0)
- return new List(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();
+ 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();
- }
-
- /// 清理已过期的缓存条目(调用方需持有 _cacheLock)
- 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);
- }
-
- /// 请求接口一次(可传多个店铺),返回原始明细行
- private async Task> RequestRowsAsync(List 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>>(html, new JsonSerializerSettings
- {
- ContractResolver = new CamelCasePropertyNamesContractResolver()
- });
- if (ret.Code != 200)
- {
- throw new Exception(ret.Msg);
- }
- return ret.Data ?? new List();
-
- }
- catch
- {
- throw new Exception("查询失败");
+ return timestamp.ToString().Length == 13
+ ? UnixEpoch.AddMilliseconds(timestamp)
+ : UnixEpoch.AddSeconds(timestamp);
}
- }
-
- public async Task GetSummaryAsync(ShopRevenueStatisticsQuery query)
- {
- var rows = await QueryRowsAsync(query);
- return RevenueStatisticsAggregator.GetSummary(rows);
- }
-
- public async Task> GetSalesTrendAsync(ShopRevenueStatisticsQuery query)
- {
- var rows = await QueryRowsAsync(query);
- return RevenueStatisticsAggregator.GetSalesTrend(rows, query.StartDate, query.EndDate);
- }
-
- public async Task> GetShopSalesProportionAsync(ShopRevenueStatisticsQuery query)
- {
- var rows = await QueryRowsAsync(query);
- return RevenueStatisticsAggregator.GetShopSalesProportion(rows);
- }
-
- public async Task> GetShopStatisticsAsync(ShopRevenueStatisticsQuery query)
- {
- var rows = await QueryRowsAsync(query);
- return RevenueStatisticsAggregator.GetShopStatistics(rows);
- }
-
- /// 获取天/周/月周期统计表格
- public async Task> GetPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType)
- {
- var rows = await QueryRowsAsync(query);
- return RevenueStatisticsAggregator.GetPeriodStatistics(rows, query.StartDate, query.EndDate, periodType);
+ return null;
}
/// 店铺下拉列表(同步读取,供查询与过滤使用)
@@ -641,8 +606,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 +617,42 @@ namespace _29WxOrderWpf.Ctrls.Statistics.Services
return Task.FromResult(GetShopOptions());
}
+ ///
+ /// 订单明细列表(有效订单 / 退款订单 / 买手未退回金额的订单),供订单列表弹窗使用。
+ /// 请求 shop_revenue_statistics/GetShopOrderDetails:
+ /// StartDate / EndDate / ShopIds / OrderType(OrderType 由 detailType 决定,按数字传值)。
+ ///
+ public async Task> QueryOrderRowsAsync(ShopRevenueStatisticsQuery query, ShopOrderDetailType detailType)
+ {
+ var shopIds = BuildShopIds(query);
+ if (shopIds.Count == 0)
+ return new List();
+
+ var url = BaseClass.Build29WxObjString(GetShopOrderDetailsApi);
+ var html = await GetHtml.PostJsonAsync(url, BuildOrderDetailPostData(query, shopIds, detailType));
+
+ var ret = Deserialize>>(html);
+ EnsureSuccess(ret);
+ return ret.Data ?? new List();
+ }
+
+ ///
+ /// 请求 buyer/UpdateBuyerRefundStatsu 更新买手订单退款状态:
+ /// id 拼在 URL 查询参数上(Build29WxObjString 生成的地址已带 ?obj=…,这里用 &id= 追加);
+ /// 请求体走 UserLogin.GetSignPostData 常规签名(time/code 等)。
+ /// 返回码非 200 时抛出异常,由调用方提示。
+ ///
+ public async Task UpdateBuyerRefundStatusAsync(long id)
+ {
+ if (id == 0)
+ throw new Exception("更新失败:缺少买手订单ID");
+
+ var url = BaseClass.Build29WxObjString(UpdateBuyerRefundStatusApi) + "&id=" + id;
+ var html = await GetHtml.PostJsonAsync(url, UserLogin.GetSignPostData(new { }));
+
+ var ret = Deserialize>(html);
+ EnsureSuccess(ret);
+ return ret.Data;
+ }
}
}
diff --git a/29WxOrderWpf/Ctrls/Statistics/ShowOrderDialogView.xaml b/29WxOrderWpf/Ctrls/Statistics/ShowOrderDialogView.xaml
index 460362c..f667ec3 100644
--- a/29WxOrderWpf/Ctrls/Statistics/ShowOrderDialogView.xaml
+++ b/29WxOrderWpf/Ctrls/Statistics/ShowOrderDialogView.xaml
@@ -1,23 +1,44 @@
-
+
+
+
+
@@ -36,15 +57,15 @@
Width="180"
Placement="Left"
TitleVerticalAlignment="Center">
-
+
-
-
-
-