Merge branch 'main' of github.com:KJD2025/29WxOrder
This commit is contained in:
commit
92fc8cf5b8
8 files changed
+1240
-713
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,228 @@ 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>
|
||||
/// 买手订单ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
/// <summary>
|
||||
/// 仓库类型 0,1直发
|
||||
/// </summary>
|
||||
public int PurchaseType { get; set; }
|
||||
/// <summary>
|
||||
/// 外部订单(买手订单)号
|
||||
/// </summary>
|
||||
public string OutOrderSn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 售后退款金额(分)
|
||||
/// 金额(分)
|
||||
/// </summary>
|
||||
public int OutAmount { get; set; }
|
||||
|
||||
public long RefundId { 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 +243,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 +252,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 +318,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 +334,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 +359,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 +416,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 +433,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 +446,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 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,23 +1,44 @@
|
||||
<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"
|
||||
d:DesignWidth="1000"
|
||||
Focusable="True"
|
||||
PreviewKeyDown="ShowOrderDialogView_PreviewKeyDown"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<wpf:BindingProxy x:Key="ViewModel" Data="{Binding}" />
|
||||
<!-- 单元格文字居中 -->
|
||||
<Style x:Key="CenterCellTextStyle" TargetType="TextBlock">
|
||||
<Setter Property="TextAlignment" Value="Center" />
|
||||
<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 +57,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 +76,7 @@
|
||||
CanUserDeleteRows="False"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserResizeRows="False"
|
||||
CellStyle="{StaticResource SelectedCellStyle}"
|
||||
ColumnHeaderStyle="{StaticResource BaseDataGridColumnHeaderStyle}"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HeadersVisibility="Column"
|
||||
@@ -62,14 +84,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 +110,6 @@
|
||||
<Button
|
||||
Margin="5,0,0,0"
|
||||
Click="CopyOrderId_Click"
|
||||
Foreground="{StaticResource pageColorBrush}"
|
||||
ToolTip="复制订单号">
|
||||
<Button.Style>
|
||||
<Style BasedOn="{StaticResource IconButtonStyle}" TargetType="Button">
|
||||
@@ -113,6 +129,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 +144,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 +152,54 @@
|
||||
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="退款状态" />
|
||||
|
||||
<!-- 操作:仅“买手未退回金额的订单”弹窗显示(列可见性在构造函数里按订单明细类型设置) -->
|
||||
<DataGridTemplateColumn
|
||||
x:Name="colUpdateRefund"
|
||||
Width="*"
|
||||
d:Visibility="Visible"
|
||||
Header="操作"
|
||||
Visibility="Collapsed">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Button
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding Source={StaticResource ViewModel}, Path=Data.UpdateBuyerRefundStatusCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="退款成功"
|
||||
Style="{StaticResource BasePageButtonLightStyle}"
|
||||
ToolTip="把该笔买手订单标记为退款成功" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using _29WxOrderWpf.BaseWindows;
|
||||
using _29WxOrderWpf.BaseWindows;
|
||||
using _29WxOrderWpf.Commands;
|
||||
using _29WxOrderWpf.Common;
|
||||
using _29WxOrderWpf.Ctrls.Statistics.Models;
|
||||
using _29WxOrderWpf.Ctrls.Statistics.Services;
|
||||
using _29WxOrderWpf.ToolCtrls;
|
||||
using NaiBa.Util.Extension;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -11,6 +14,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 +31,41 @@ namespace _29WxOrderWpf.Ctrls.Statistics;
|
||||
/// </summary>
|
||||
public partial class ShowOrderDialogView : UserControl
|
||||
{
|
||||
public ShowOrderDialogView(List<ShopRevenueStatisticsRow> list)
|
||||
private readonly KeyEventHandler shortcutKeyHandler;
|
||||
|
||||
public ShowOrderDialogView(List<ShopOrderDetailResponse> list, ShopOrderDetailType detailType, IShopRevenueStatisticsService statisticsService)
|
||||
{
|
||||
InitializeComponent();
|
||||
this.DataContext = new ShowOrderDialogViewModel(list);
|
||||
this.DataContext = new ShowOrderDialogViewModel(list, statisticsService);
|
||||
|
||||
// “操作”列(退款成功按钮)只在“买手未退回金额的订单”弹窗里显示
|
||||
if (detailType == ShopOrderDetailType.BuyerUnrefunded)
|
||||
colUpdateRefund.Visibility = Visibility.Visible;
|
||||
|
||||
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 +74,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,25 +89,170 @@ 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 : BindableBaseEx
|
||||
{
|
||||
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>买手订单表ID</summary>
|
||||
public long BuyerId { get; set; }
|
||||
|
||||
/// <summary>买手(外部)订单号;无买手订单时为空</summary>
|
||||
public string OutOrderSn { get; set; }
|
||||
|
||||
/// <summary>买手订单金额(分)</summary>
|
||||
public int BuyerOutAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 买手退款表 id
|
||||
/// </summary>
|
||||
public long BuyerRefundId { get; set; }
|
||||
/// <summary>买手订单退款金额(分)</summary>
|
||||
public int BuyerRefundAmount { get; set; }
|
||||
|
||||
/// <summary>买手订单退款状态(更新成功后就地改为“退款成功”)</summary>
|
||||
public string BuyerRefundStatusText
|
||||
{
|
||||
get => GetValue<string>();
|
||||
set => SetValue(value);
|
||||
}
|
||||
|
||||
/// <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,
|
||||
BuyerId = buyerOrder == null ? 0 : buyerOrder.Id,
|
||||
OutOrderSn = buyerOrder == null ? null : buyerOrder.OutOrderSn,
|
||||
BuyerRefundId = buyerOrder == null ? 0 : buyerOrder.RefundId,
|
||||
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; }
|
||||
private readonly IShopRevenueStatisticsService _statisticsService;
|
||||
|
||||
public ObservableCollection<ShopOrderDetailRow> Data { get; set; }
|
||||
public ICollectionView DataView { get; private set; }
|
||||
public ShowOrderDialogViewModel(List<ShopRevenueStatisticsRow> list)
|
||||
public ShowOrderDialogViewModel(List<ShopOrderDetailResponse> list, IShopRevenueStatisticsService statisticsService)
|
||||
{
|
||||
Data = new ObservableCollection<ShopRevenueStatisticsRow>(list ?? new List<ShopRevenueStatisticsRow>());
|
||||
// 按买手订单展平:一笔买手订单一一行,不合并
|
||||
Data = new ObservableCollection<ShopOrderDetailRow>(ShopOrderDetailRow.Flatten(list));
|
||||
DataView = CollectionViewSource.GetDefaultView(Data);
|
||||
DataView.Filter = FilterRow;
|
||||
_statisticsService = statisticsService;
|
||||
}
|
||||
|
||||
private bool FilterRow(object obj)
|
||||
{
|
||||
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 +263,102 @@ 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();
|
||||
}
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
ToastHelper.Show("没有可复制的数据", type: ToastType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string text = BuildCopyText(list);
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
|
||||
ToastHelper.Show("复制成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ToastHelper.Show("复制失败 " + ex.Message, type: ToastType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ICommand UpdateBuyerRefundStatusCommand => new AsyncDelegateCommand<ShopOrderDetailRow>(UpdateBuyerRefundStatus);
|
||||
|
||||
private async Task UpdateBuyerRefundStatus(ShopOrderDetailRow row)
|
||||
{
|
||||
if (row.BuyerRefundId == 0)
|
||||
{
|
||||
ToastHelper.Show("该行缺少买手订单ID,无法更新", type: ToastType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var ret = await _statisticsService.UpdateBuyerRefundStatusAsync(row.BuyerRefundId);
|
||||
if (ret)
|
||||
{
|
||||
row.BuyerRefundStatusText = "退款成功";
|
||||
ToastHelper.Show("操作成功");
|
||||
}
|
||||
else
|
||||
throw new Exception("状态更改失败");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ToastHelper.Show("操作失败 " + ex.Message, type: ToastType.Error);
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
Content = new ShowOrderDialogView(showList, detailType, _service),
|
||||
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="订单列表",
|
||||
Content = new ShowOrderDialogView(showList),
|
||||
DataContext = rows
|
||||
Title = "订单列表",
|
||||
Content = new ShowOrderDialogView(showList, detailType, _service),
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,14 @@ public static class ToastHelper
|
||||
};
|
||||
toast.BeginAnimation(UIElement.OpacityProperty, anim);
|
||||
}
|
||||
|
||||
public static void Show(string message,
|
||||
ToastType type = ToastType.Info, int durationMs = 2000)
|
||||
{
|
||||
Window window = Application.Current.Windows
|
||||
.OfType<Window>()
|
||||
.FirstOrDefault(w => w.IsActive) ?? Application.Current.MainWindow;
|
||||
Show(window, message, type, durationMs);
|
||||
}
|
||||
private static Border CreateToast(string message, ToastType type)
|
||||
{
|
||||
var color = type switch
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace AllBase
|
||||
/// 版本ID
|
||||
/// </summary>
|
||||
|
||||
public static int VersionCount = 306;
|
||||
public static int VersionCount = 310;
|
||||
|
||||
public static string ServerUrl { get; set; } = "http://1.13.195.131:9000";
|
||||
public static string DebugUrl { get; set; } = "http://localhost:9000";
|
||||
|
||||
Reference in new issue
Block a user