From 22c7ecd4d894a6d8a7df3d107cfb8caf54f94725 Mon Sep 17 00:00:00 2001 From: chfei Date: Thu, 8 Oct 2026 12:02:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E6=96=B0=E6=8E=A5=E5=8F=A3=20=E5=A2=9E=E5=8A=A0=E6=AF=8F?= =?UTF-8?q?=E6=97=A502=E6=97=B6=E8=87=AA=E5=8A=A8=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WX/WXShopRevenueStatisticsController.cs | 83 +- KJD_WebService/Global.asax.cs | 124 +-- KJD_WebService/KJD_WebService.csproj | 9 +- .../Model/WxModel/ShopRevenueStatistics.cs | 194 ++++ KJD_WebService/Nlog.config | 3 + .../Requests/Shops/ShopOrderDetailType.cs | 27 + .../Requests/Shops/ShopStatsGroupType.cs | 27 + .../Requests/Shops/ShopStatsRequest.cs | 26 + .../Shops/ShopOrderDetailResponse.cs | 61 ++ .../Responses/Shops/ShopStatsResponse.cs | 89 ++ .../Responses/Shops/ShopStatsTrendResponse.cs | 24 + .../Shops/ShopRevenueStatisticsService.cs | 843 ++++++++++++++++-- KJD_WebService/Services/Shops/ShopService.cs | 22 +- KJD_WebService/Services/Shops/ShopStatsJob.cs | 28 + KJD_WebService/Web.config | 4 +- KJD_WebService/WebApiConfig.cs | 7 +- .../Order/Entitys/AfterSaleStatus.cs | 99 ++ .../Entitys/OrderPddBuyerAfterSaleEntity.cs | 123 +++ Store.Contracts/Order/Entitys/PurchaseType.cs | 23 + .../WeiExternalProcurementLogEntity.cs | 4 +- .../Shops/BuyerDailyStatsEntity.cs | 84 ++ Store.Contracts/Shops/ShopDailyStatsEntity.cs | 87 ++ test/WebServiceTest/ShopTest.cs | 157 +++- test/WebServiceTest/UserTest.cs | 2 +- 24 files changed, 1895 insertions(+), 255 deletions(-) create mode 100644 KJD_WebService/Model/WxModel/ShopRevenueStatistics.cs create mode 100644 KJD_WebService/Requests/Shops/ShopOrderDetailType.cs create mode 100644 KJD_WebService/Requests/Shops/ShopStatsGroupType.cs create mode 100644 KJD_WebService/Requests/Shops/ShopStatsRequest.cs create mode 100644 KJD_WebService/Responses/Shops/ShopOrderDetailResponse.cs create mode 100644 KJD_WebService/Responses/Shops/ShopStatsResponse.cs create mode 100644 KJD_WebService/Responses/Shops/ShopStatsTrendResponse.cs create mode 100644 KJD_WebService/Services/Shops/ShopStatsJob.cs create mode 100644 Store.Contracts/Order/Entitys/AfterSaleStatus.cs create mode 100644 Store.Contracts/Order/Entitys/OrderPddBuyerAfterSaleEntity.cs create mode 100644 Store.Contracts/Order/Entitys/PurchaseType.cs create mode 100644 Store.Contracts/Shops/BuyerDailyStatsEntity.cs create mode 100644 Store.Contracts/Shops/ShopDailyStatsEntity.cs diff --git a/KJD_WebService/Controllers/WX/WXShopRevenueStatisticsController.cs b/KJD_WebService/Controllers/WX/WXShopRevenueStatisticsController.cs index e7f4d39..94af99b 100644 --- a/KJD_WebService/Controllers/WX/WXShopRevenueStatisticsController.cs +++ b/KJD_WebService/Controllers/WX/WXShopRevenueStatisticsController.cs @@ -1,14 +1,19 @@ using KJD_Web.BLL.BXX; using KJD_Web.BLL.WXXD; +using KJD_Web.Requests.Shops; +using KJD_Web.Responses.Shops; +using KJD_Web.Services.Shops; using KJD_Web.Utils; +using Newtonsoft.Json.Converters; +using Store.Framework.Models; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Web; -using System.Web.Services; using System.Web.Http; +using System.Web.Services; namespace KJD_Web.Controllers.WX { @@ -16,6 +21,12 @@ namespace KJD_Web.Controllers.WX [Route("{action}")] public class WXShopRevenueStatisticsController : AsmxApiController { + private readonly ShopRevenueStatisticsService _statsService; + + public WXShopRevenueStatisticsController(ShopRevenueStatisticsService statsService) + { + _statsService = statsService; + } public IHttpActionResult get_shop_order_amount() { string jsontext = GetRequestJsonString(); @@ -25,5 +36,75 @@ namespace KJD_Web.Controllers.WX return JsonString(result); } + /// + /// 按天统计(兼容旧接口,等价于 GetShopStats 传 天) + /// + public IHttpActionResult GetShopStatsDetails(ShopStatsRequest request) + { + return GetShopStats(request, ShopStatsGroupType.Day); + } + public IHttpActionResult GetShopStats(ShopStatsRequest request) + { + return GetShopStats(request, request.GroupType); + } + + /// + /// 商铺销售趋势:按天返回所有商铺合计的销售额和订单数(无数据的天补 0) + /// + public IHttpActionResult GetShopStatsTrend(ShopStatsRequest request) + { + List ret; + try + { + if (request.ShopIds == null || request.ShopIds.Count == 0) + ret = _statsService.GetShopStatsTrend(request.StartDate, request.EndDate, request.UserName); + else + ret = _statsService.GetShopStatsTrend(request.StartDate, request.EndDate, "", request.ShopIds); + return Ok(ApiResult.Success(ret)); + } + catch (Exception ex) + { + return Ok(ApiResult.Fail("查询失败", ResultCode.ServerError)); + } + } + + /// + /// 订单明细统一入口:有效订单 / 退款订单 / 买手未退回金额的订单,三种列表返回同一结构 + /// 请求参数 OrderType:All(默认全部) / Valid(有效订单) / Refund(退款订单) / BuyerUnrefunded(买手未退回金额) + /// 时间范围为订单创建时间,商铺ID为空时按用户名的商铺查询 + /// + public IHttpActionResult GetShopOrderDetails(ShopStatsRequest request) + { + List ret; + try + { + if (request.ShopIds == null || request.ShopIds.Count == 0) + ret = _statsService.GetShopOrderDetails(request.StartDate, request.EndDate, request.OrderType, request.UserName); + else + ret = _statsService.GetShopOrderDetails(request.StartDate, request.EndDate, request.OrderType, "", request.ShopIds); + return Ok(ApiResult.Success(ret)); + } + catch (Exception ex) + { + return Ok(ApiResult.Fail("查询失败", ResultCode.ServerError)); + } + } + + private IHttpActionResult GetShopStats(ShopStatsRequest request, ShopStatsGroupType groupType) + { + Dictionary> ret; + try + { + if (request.ShopIds == null || request.ShopIds.Count == 0) + ret = _statsService.GetShopStats(request.StartDate, request.EndDate, request.UserName, null, groupType); + else + ret = _statsService.GetShopStats(request.StartDate, request.EndDate, "", request.ShopIds, groupType); + return Ok(ApiResult.Success(ret)); + } + catch (Exception ex) + { + return Ok(ApiResult.Fail("查询失败", ResultCode.ServerError)); + } + } } } diff --git a/KJD_WebService/Global.asax.cs b/KJD_WebService/Global.asax.cs index 429b543..1accdf3 100644 --- a/KJD_WebService/Global.asax.cs +++ b/KJD_WebService/Global.asax.cs @@ -8,6 +8,7 @@ using MAX.Core.DependencyInjection; using MAX.Core.Logger; using MAX.Framework.Logger; using MySql.Data.MySqlClient; +using Newtonsoft.Json.Converters; using NLog; using Store.Framework.DependencyInjection; using System; @@ -26,6 +27,7 @@ namespace KJD_Web { protected void Application_Start(object sender, EventArgs e) { + NLogger.Instance.Debug("应用程序启动"); NLogger.Instance.Info("应用程序启动"); var container = ContainerConfig.BuildContainer(); @@ -54,128 +56,6 @@ namespace KJD_Web Context.Response.Write("Service is running."); Context.Response.End(); } - string hPath = Request.Url.LocalPath.ToString(); - //if (!hPath.Contains(".asmx") && hPath.Contains("/user/")) - //{ - //hPath = hPath.Replace("/user/", "/UserService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/shop/")) - //{ - //hPath = hPath.Replace("/shop/", "/ShopService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/update/")) - //{ - //hPath = hPath.Replace("/update/", "/UpdateService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/order/")) - //{ - //hPath = hPath.Replace("/order/", "/OrderService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/product/")) - //{ - //hPath = hPath.Replace("/product/", "/ProductService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/product_third/")) - //{ - //hPath = hPath.Replace("/product_third/", "/ProductThirdService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/price/")) - //{ - //hPath = hPath.Replace("/price/", "/PriceService.asmx/"); - //Context.RewritePath(hPath); - //} - - - //if (!hPath.Contains(".asmx") && hPath.Contains("/bxx_user/")) - //{ - //hPath = hPath.Replace("/bxx_user/", "/BXXUserService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/bxx_shop/")) - //{ - //hPath = hPath.Replace("/bxx_shop/", "/BXXShopService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/update/")) - //{ - // hPath = hPath.Replace("/update/", "/UpdateService.asmx/"); - // Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/bxx_product/")) - //{ - //hPath = hPath.Replace("/bxx_product/", "/BXXProductService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/bxx_product_third/")) - //{ - //hPath = hPath.Replace("/bxx_product_third/", "/BXXProductThirdService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/bxx_price/")) - //{ - //hPath = hPath.Replace("/bxx_price/", "/BXXPriceService.asmx/"); - //Context.RewritePath(hPath); - //} - - - //if (!hPath.Contains(".asmx") && hPath.Contains("/dyxd_user/")) - //{ - //hPath = hPath.Replace("/dyxd_user/", "/DYUserService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/dyxd_shop/")) - //{ - //hPath = hPath.Replace("/dyxd_shop/", "/DYShopService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/update/")) - //{ - // hPath = hPath.Replace("/update/", "/UpdateService.asmx/"); - // Context.RewritePath(hPath); - //} - - - - - //if (!hPath.Contains(".asmx") && hPath.Contains("/wxxd_user/")) - //{ - // //hPath = hPath.Replace("/wxxd_user/", "/WXUserService.asmx/"); - // //Context.RewritePath(hPath, false); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/wxxd_shop/")) - //{ - // hPath = hPath.Replace("/wxxd_shop/", "/WXShopService.asmx/"); - // Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/update/")) - //{ - // hPath = hPath.Replace("/update/", "/UpdateService.asmx/"); - // Context.RewritePath(hPath); - //} - //if (!hPath.Contains(".asmx") && hPath.Contains("/wxxd_product/")) - //{ - //hPath = hPath.Replace("/wxxd_product/", "/WXProductService.asmx/"); - //Context.RewritePath(hPath); - //} - - //else if (!hPath.Contains(".asmx") && hPath.Contains("/wxxd_order/")) - //{ - //hPath = hPath.Replace("/wxxd_order/", "/WXOrderService.asmx/"); - //Context.RewritePath(hPath); - //} - //else if (!hPath.Contains(".asmx") && hPath.Contains("/wxxd_shop_revenue_statistics/")) - //{ - //hPath = hPath.Replace("/wxxd_shop_revenue_statistics/", "/WXShopRevenueStatistics.asmx/"); - //Context.RewritePath(hPath); - //} - // 请求录制:捕获请求链接与请求内容,用于迁移后的回放校验 - //try { ApiRequestLogger.BeginRequest(Context); } catch { } } protected void Application_AuthenticateRequest(object sender, EventArgs e) diff --git a/KJD_WebService/KJD_WebService.csproj b/KJD_WebService/KJD_WebService.csproj index 1854533..c5ebd1e 100644 --- a/KJD_WebService/KJD_WebService.csproj +++ b/KJD_WebService/KJD_WebService.csproj @@ -1,4 +1,4 @@ - + @@ -217,11 +217,18 @@ + + + + + + + diff --git a/KJD_WebService/Model/WxModel/ShopRevenueStatistics.cs b/KJD_WebService/Model/WxModel/ShopRevenueStatistics.cs new file mode 100644 index 0000000..8deef6c --- /dev/null +++ b/KJD_WebService/Model/WxModel/ShopRevenueStatistics.cs @@ -0,0 +1,194 @@ +using KJD_Web.Utils; +using MySqlX.XDevAPI.Relational; +using Store.Contracts.Order.Entitys; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace KJD_Web.Model.WxModel; + +public class ShopRevenueStatistics +{ + public class StatisticsBase + { + public long ShopId { get; set; } + public string OrderId { get; set; } + + } + /// + /// 订单信息 + /// + public class OrderInfo : StatisticsBase + { + public string ShopName { get; set; } + + /// + /// 外采金额(单位:分,整数存储) + /// + public int OutAmount => BuyerOrders.Sum(a => a.OutAmount); + /// + /// 订单金额,单位为分, + /// + public int OrderPrice { get; set; } + + /// + /// 平台服务费(微信小店:预计技术服务费),单位为分 + /// + public int PredictCommissionFee { get; set; } + + /// + /// 达人佣金(微信小店:分账金额),单位为分 + /// + public int CommissionAmount { get; set; } + + /// + /// 利润 + /// + public int Profit { get; set; } + /// + /// 采购类型 + /// + public PurchaseType PurchaseType { get; set; } + + public List Items { get; set; } = []; + /// + /// 买手订单信息 + /// + public List BuyerOrders { get; set; } = []; + + public bool HasRefund => Items.Any(x => x.IsAfterSale); + + public DateTime CreateTime { get; set; } + + public DateTime UpdateTime { get; set; } + } + public class OrderItemInfo + { + public string OrderId { get; set; } + public string SkuId { get; set; } + /// + /// SKU价格 + /// + public int SkuPrice { get; set; } + + /// + /// 是否退款完成 + /// + public bool IsRefundSuccessed + { + get + { + return AfterSaleStatus == AfterSaleStatus.MERCHANT_REFUND_SUCCESS || AfterSaleStatus == AfterSaleStatus.MERCHANT_RETURN_SUCCESS; + } + } + /// + /// 是否是售后订单 + /// + public bool IsAfterSale + { + get + { + return AfterSaleStatus != AfterSaleStatus.NONE; + } + } + /// + /// 退款状态 + /// 售后状态 1:无售后或售后关闭,2:售后处理中,3:退款中,4: 退款成功 11 退款完成订单已取消 + /// + public int RefundStatus { get; set; } + public AfterSaleStatus AfterSaleStatus { get; set; } = AfterSaleStatus.NONE; + /// + /// 退款金额 + /// + public int RefundAmount { get; set; } + } + public class OrderFlatInfo + { + public long ShopId { get; set; } + public string OrderId { get; set; } + public int CreateTime { get; set; } + + public int UpdateTime { get; set; } + public string ShopName { get; set; } + public int OutAmount { get; set; } + public int OrderPrice { get; set; } + public int PredictCommissionFee { get; set; } + public int CommissionAmount { get; set; } + + // 子表字段 + public string SkuId { get; set; } + public int SkuPrice { get; set; } + public int CashCouponDiscountedPrice { get; set; } + public int RefundStatus { get; set; } + } + + + /// + /// 订单退款 + /// + public class OrderRefundInfo : StatisticsBase + { + public string SkuId { get; set; } + /// + /// 退款状态 + /// + public string Status { get; set; } + /// + /// 退款金额 + /// + public int RefundAmount { get; set; } + public int CreateTime { get; set; } + } + + + /// + /// 买手订单 + /// + public class BuyerOrderInfo : StatisticsBase + { + /// + /// 买手订单 + /// + public string SkuId { get; set; } + public string OutOrderSn { get; set; } + /// + /// 购买金额(分) + /// + public int OutAmount { get; set; } + ///// + ///// 订单作废状态:0=正常,1=已作废,2=作废失败 + ///// + //public int Status { get; set; } + + + + + /// + /// 退款金额(分) + /// + public int RefundAmount { get; set; } + /// + /// 退款状态 + /// + public string RefundStatusText { get; set; } + + + } + /// + /// 更新要素信息 + /// + public class UpdateStatsInfo + { + /// + /// 商铺Id + /// + public string ShopId { get; set; } + /// + /// 订单创建时间 + /// + public int OrderCreateAt { get; set; } + + public DateTime OrderCreateDate => TimeUtils.StampToDateTime(OrderCreateAt).Date; + } +} diff --git a/KJD_WebService/Nlog.config b/KJD_WebService/Nlog.config index d938001..88fb1ac 100644 --- a/KJD_WebService/Nlog.config +++ b/KJD_WebService/Nlog.config @@ -64,6 +64,9 @@ + + + diff --git a/KJD_WebService/Requests/Shops/ShopOrderDetailType.cs b/KJD_WebService/Requests/Shops/ShopOrderDetailType.cs new file mode 100644 index 0000000..a176aaf --- /dev/null +++ b/KJD_WebService/Requests/Shops/ShopOrderDetailType.cs @@ -0,0 +1,27 @@ +namespace KJD_Web.Requests.Shops; + +/// +/// 订单明细类型(统一明细入口 GetShopOrderDetails 使用) +/// +public enum ShopOrderDetailType +{ + /// + /// 全部订单 + /// + All = 0, + + /// + /// 有效订单:没有发生售后的订单(对应统计里的订单数 / 销售额 / 利润口径) + /// + Valid = 1, + + /// + /// 退款订单:发生过售后的订单(对应统计里的退款订单数口径) + /// + Refund = 2, + + /// + /// 买手未退回金额的订单:退款订单中买手款项还没退回的(对应统计里的未退回金额口径) + /// + BuyerUnrefunded = 3, +} diff --git a/KJD_WebService/Requests/Shops/ShopStatsGroupType.cs b/KJD_WebService/Requests/Shops/ShopStatsGroupType.cs new file mode 100644 index 0000000..93944c5 --- /dev/null +++ b/KJD_WebService/Requests/Shops/ShopStatsGroupType.cs @@ -0,0 +1,27 @@ +namespace KJD_Web.Requests.Shops; + +/// +/// 商铺统计聚合维度 +/// +public enum ShopStatsGroupType +{ + /// + /// 汇总:每个商铺只返回一条(整个查询范围合计) + /// + Summary = 0, + + /// + /// 天:按自然日聚合,每个商铺每天一条 + /// + Day = 1, + + /// + /// 周:按自然周聚合(周一为一周开始),每个商铺每周一条 + /// + Week = 2, + + /// + /// 月:按自然月聚合,每个商铺每月一条 + /// + Month = 3, +} diff --git a/KJD_WebService/Requests/Shops/ShopStatsRequest.cs b/KJD_WebService/Requests/Shops/ShopStatsRequest.cs new file mode 100644 index 0000000..3ef9ead --- /dev/null +++ b/KJD_WebService/Requests/Shops/ShopStatsRequest.cs @@ -0,0 +1,26 @@ +using Store.Framework.Dtos; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace KJD_Web.Requests.Shops; + +public class ShopStatsRequest: SignRequestBase +{ + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + + public List ShopIds { get; set; } + + /// + /// 聚合维度:汇总(默认) / 天 / 周 / 月 + /// + public ShopStatsGroupType GroupType { get; set; } = ShopStatsGroupType.Summary; + + /// + /// 订单明细类型(统一明细入口 GetShopOrderDetails 使用): + /// 全部(默认) / 有效订单 / 退款订单 / 买手未退回金额的订单 + /// + public ShopOrderDetailType OrderType { get; set; } = ShopOrderDetailType.All; +} \ No newline at end of file diff --git a/KJD_WebService/Responses/Shops/ShopOrderDetailResponse.cs b/KJD_WebService/Responses/Shops/ShopOrderDetailResponse.cs new file mode 100644 index 0000000..3aa2edd --- /dev/null +++ b/KJD_WebService/Responses/Shops/ShopOrderDetailResponse.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using static KJD_Web.Model.WxModel.ShopRevenueStatistics; + +namespace KJD_Web.Responses.Shops; + +/// +/// 订单明细(有效订单 / 退款订单 / 买手未退回金额的订单 共用的返回结构) +/// 一个商铺订单可能对应多个买手订单(见 BuyerOrders) +/// 金额单位:分 +/// +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; } + + /// + /// 外采金额(分),即该订单下所有买手订单的购买金额之和 + /// + public int OutAmount { get; set; } + + /// + /// 买手未退回金额(分),即买手订单中退款未成功的退款金额之和;无售后时为 0 + /// + public int UnrefundedAmount { get; set; } + + /// + /// 买手订单信息(一个订单号对应多个买手订单) + /// 字段含义:OutOrderSn=外部订单(买手订单)号,OutAmount=金额,RefundAmount=退款金额,RefundStatusText=退款状态 + /// + public List BuyerOrders { get; set; } = []; +} diff --git a/KJD_WebService/Responses/Shops/ShopStatsResponse.cs b/KJD_WebService/Responses/Shops/ShopStatsResponse.cs new file mode 100644 index 0000000..6c968c2 --- /dev/null +++ b/KJD_WebService/Responses/Shops/ShopStatsResponse.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace KJD_Web.Responses.Shops; + +/// +/// 商铺统计记录 +/// +public class ShopStatsResponse +{ + /// + /// 店铺ID + /// + public string ShopId { get; set; } + + /// + /// 销售订单创建日期 + /// + public DateTime StatDate { get; set; } + + /// + /// 订单数 + /// + public int OrderCount { get; set; } + + /// + /// 销售总额 + /// + public long SalesAmount { get; set; } + + /// + /// 利润 + /// + public long ProfitAmount { get; set; } + + /// + /// 平台佣金 + /// + public long PlatformFee { get; set; } + + /// + /// 退款订单数 + /// + 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; } +} \ No newline at end of file diff --git a/KJD_WebService/Responses/Shops/ShopStatsTrendResponse.cs b/KJD_WebService/Responses/Shops/ShopStatsTrendResponse.cs new file mode 100644 index 0000000..bcd68c6 --- /dev/null +++ b/KJD_WebService/Responses/Shops/ShopStatsTrendResponse.cs @@ -0,0 +1,24 @@ +using System; + +namespace KJD_Web.Responses.Shops; + +/// +/// 商铺销售趋势(按天,所有商铺合计) +/// +public class ShopStatsTrendResponse +{ + /// + /// 统计日期(天) + /// + public DateTime StatDate { get; set; } + + /// + /// 销售额(分) + /// + public long SalesAmount { get; set; } + + /// + /// 订单数 + /// + public int OrderCount { get; set; } +} diff --git a/KJD_WebService/Services/Shops/ShopRevenueStatisticsService.cs b/KJD_WebService/Services/Shops/ShopRevenueStatisticsService.cs index 0061434..91636b7 100644 --- a/KJD_WebService/Services/Shops/ShopRevenueStatisticsService.cs +++ b/KJD_WebService/Services/Shops/ShopRevenueStatisticsService.cs @@ -1,12 +1,23 @@ -using KJD_Web.Model.WxModel; +using KJD_Web.Model.WxModel; +using KJD_Web.Requests.Shops; +using KJD_Web.Responses.Shops; +using KJD_Web.Services.Users; using KJD_Web.Utils; using MAX.Core.DependencyInjection; using MAX.Core.Logger; +using MySqlX.XDevAPI.Relational; using Store.Contracts.Order.Entitys; +using Store.Contracts.Shops; +using Store.Contracts.User.Interfaces; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Runtime.Remoting.Metadata.W3cXsd2001; +using System.Text.RegularExpressions; using System.Web; +using static FreeSql.Internal.GlobalFilter; +using static KJD_Web.Model.WxModel.ShopRevenueStatistics; namespace KJD_Web.Services.Shops; @@ -15,137 +26,779 @@ namespace KJD_Web.Services.Shops; /// public class ShopRevenueStatisticsService : ITransientDependency { + private const int BatchSize = 500; private readonly IFreeSql _freeSql; + private readonly ILoggerFacade _logger; + private readonly IUserService _userService; + private readonly ShopService _shopService; - public ShopRevenueStatisticsService(IFreeSql freeSql, ILoggerFacade logger) + public ShopRevenueStatisticsService(IFreeSql freeSql, ILoggerFacade logger, IUserService userService, ShopService shopService) { _freeSql = freeSql; + _logger = logger; + _userService = userService; + _shopService = shopService; + } + /// + /// 获取商铺统计 + /// 汇总:每个商铺只返回一条(整个查询范围合计,StatDate 为查询结束日期) + /// 天/周/月:按自然日 / 自然周(周一为开始)/ 自然月聚合,每个周期一条(StatDate 为该周期的起始日期) + /// + /// 统计开始时间(含) + /// 统计结束时间(含) + /// 用户名(未指定商铺ID时,按该用户名下的商铺统计) + /// 指定商铺ID集合(为空时按用户名统计) + /// 聚合维度,默认汇总 + /// 商铺ID -> 该商铺按维度聚合后的统计列表 + public Dictionary> GetShopStats(DateTime startTime, DateTime endTime, string userName, List shopIds = null, ShopStatsGroupType groupType = ShopStatsGroupType.Summary) + { + var result = new Dictionary>(); + + (Dictionary> shopGroups, Dictionary> buyerGroups) ret; + if (shopIds != null && shopIds.Count > 0) + ret = getShopStats(startTime, endTime, shopIds); + else + ret = getShopStatsByUserName(startTime, endTime, userName); + if (ret.shopGroups == null || ret.buyerGroups == null) return result; + + // 汇总维度下所有日期归到同一组,用查询结束日期作为该组的标识 + DateTime summaryKey = endTime.Date; + + foreach (var id in ret.shopGroups.Keys.Union(ret.buyerGroups.Keys)) + { + ret.shopGroups.TryGetValue(id, out var sList); + ret.buyerGroups.TryGetValue(id, out var bList); + sList = sList ?? new List(); + bList = bList ?? new List(); + + // 同一商铺内先按聚合维度分组(同一周期内的多条明细在下面求和) + var sDict = sList + .GroupBy(x => GetGroupKey(x.StatDate, groupType, summaryKey)) + .ToDictionary(g => g.Key, g => g.ToList()); + var bDict = bList + .GroupBy(x => GetGroupKey(x.StatDate, groupType, summaryKey)) + .ToDictionary(g => g.Key, g => g.ToList()); + + var list = new List(); + foreach (var key in sDict.Keys.Union(bDict.Keys).OrderBy(d => d)) + { + var resp = new ShopStatsResponse { ShopId = id, StatDate = key }; + + if (sDict.TryGetValue(key, out var ss)) + { + resp.OrderCount = ss.Sum(x => x.OrderCount); + resp.SalesAmount = ss.Sum(x => x.SalesAmount); + resp.ProfitAmount = ss.Sum(x => x.ProfitAmount); + resp.PlatformFee = ss.Sum(x => x.PlatformFee); + resp.RefundOrderCount = ss.Sum(x => x.RefundOrderCount); + resp.RefundDone = ss.Sum(x => x.RefundDone); + resp.RefundPending = ss.Sum(x => x.RefundPending); + } + + if (bDict.TryGetValue(key, out var bb)) + { + resp.PurchaseCount = bb.Sum(x => x.PurchaseCount); + resp.TotalPurchase = bb.Sum(x => x.TotalPurchase); + resp.DoneRefund = bb.Sum(x => x.DoneRefund); + resp.PendingRefund = bb.Sum(x => x.PendingRefund); + resp.RejectedRefund = bb.Sum(x => x.RejectedRefund); + } + + list.Add(resp); + } + + result[id] = list; + } + return result; + } - public void GetShopOrderAmountRange(List shopIds, DateTime startTime, DateTime endTime) + /// + /// 获取商铺销售趋势(按天,所有商铺合计) + /// 返回查询范围内每一天的销售额和订单数,没有数据的天补 0,保证日期连续 + /// + /// 统计开始时间(含) + /// 统计结束时间(含) + /// 用户名(未指定商铺ID时,按该用户名下的商铺统计) + /// 指定商铺ID集合(为空时按用户名统计) + public List GetShopStatsTrend(DateTime startTime, DateTime endTime, string userName, List shopIds = null) { - int startTimestamp = TimeUtils.DateTimeToTimestampSeconds(startTime); - int endTimestamp = TimeUtils.DateTimeToTimestampSeconds(endTime); + var result = new List(); + var ids = shopIds != null && shopIds.Count > 0 ? shopIds : getShopIdsByUserName(userName); + if (ids == null || ids.Count == 0) return result; + if (endTime.Date < startTime.Date) return result; - // 1. 先查订单(含 item 的 INNER JOIN 可以放在数据库里做) - var ordersQuery = _freeSql.Select() - .InnerJoin((a, b) => a.OrderId == b.OrderId) - .Where((a, b) => a.CreateTime >= startTimestamp && a.CreateTime <= endTimestamp) - .Where((a, b) => a.Status > 10) - .Where((a, b) => shopIds.Contains(a.ShopId)) - .OrderBy((a, b) => a.CreateTime); - var sql = ordersQuery.ToSql(); - var orders = ordersQuery.ToList((a, b) => new WXOrderAmountInfo + // 所有商铺先按天合计 + var byDate = loadShopStats(startTime, endTime, ids) + .GroupBy(x => x.StatDate.Date) + .ToDictionary(g => g.Key, g => new ShopStatsTrendResponse + { + StatDate = g.Key, + SalesAmount = g.Sum(x => x.SalesAmount), + OrderCount = g.Sum(x => x.OrderCount), + }); + + // 再按天逐个输出,缺失的日期补 0 + for (var date = startTime.Date; date <= endTime.Date; date = date.AddDays(1)) { - shop_name = a.ShopName, - create_time = a.CreateTime.Value, - order_id = a.OrderId, - status = a.Status.Value, - product_price = a.ProductPrice.Value, - order_price = a.OrderPrice.Value, - purchase_price = a.PurchasePrice.Value, - freight = a.Freight.Value, - predict_commission_fee = a.PredictCommissionFee.Value, - commission_amount = a.CommissionAmount, - discounted_price = a.DiscountedPrice.Value, - profit = a.Profit.Value, - purchase_status = a.PurchaseStatus, - out_amount = a.OutAmount, - refund_status = b.RefundStatus - }); + if (!byDate.TryGetValue(date, out var item)) + item = new ShopStatsTrendResponse { StatDate = date }; - // 2. 只按订单号查退款,分批避免 IN 过长 - var orderIds = orders.Select(o => o.order_id).Distinct().ToList(); - var refunds = new List(); - - for (int i = 0; i < orderIds.Count; i += 1000) - { - var batch = orderIds.Skip(i).Take(1000).ToArray(); - var ids = batch.ToList(); - refunds.AddRange( - _freeSql.Select() - .Where(r => ids.Contains(r.OrderId)) - .ToList(r => new WeiRefundEntity - { - Id = r.Id, - OrderId = r.OrderId, - Status = r.Status, - RefundAmount = r.RefundAmount - })); + result.Add(item); } - // 3. 内存里 group,每个订单取 id 最大的那条 - var latestRefund = refunds - .GroupBy(r => r.OrderId) - .ToDictionary(g => g.Key, g => g.OrderByDescending(r => r.Id).First()); + return result; + } - // 4. 回填(等价 LEFT JOIN + IFNULL) - foreach (var o in orders) + /// + /// 订单明细统一入口:有效订单 / 退款订单 / 买手未退回金额的订单,三种列表返回同一结构 + /// 按订单创建时间范围 + 商铺ID查询(未指定商铺ID时按用户名下的商铺查询) + /// 有效订单:没有发生售后的订单;退款订单:发生过售后的订单;买手未退回:退款订单中买手款项还没退回的 + /// 未退回:买手订单存在退款信息(退款金额 > 0 或退款状态非空)且退款未成功(不是"退款成功"), + /// 未退回金额 = 这些买手订单的退款金额之和(与统计里的"未退回金额/PendingRefund"口径一致) + /// 一个订单号可能对应多个买手订单(BuyerOrders),金额单位:分 + /// 与统计口径保持一致:只统计单商品订单(多商品订单视为刷单,统计时已排除) + /// + /// 订单创建开始时间(含) + /// 订单创建结束时间(含) + /// 明细类型:全部 / 有效订单 / 退款订单 / 买手未退回金额的订单 + /// 用户名(未指定商铺ID时,按该用户名下的商铺查询) + /// 指定商铺ID集合(为空时按用户名查询) + /// 按订单创建时间倒序的订单明细 + public List GetShopOrderDetails(DateTime startTime, DateTime endTime, ShopOrderDetailType orderType = ShopOrderDetailType.All, string userName = null, List shopIds = null) + { + var result = new List(); + if (endTime < startTime) return result; + + var ids = shopIds != null && shopIds.Count > 0 ? shopIds : getShopIdsByUserName(userName); + if (ids == null || ids.Count == 0) return result; + + // 订单表的 shop_id 是数字,这里做一次转换 + var orderShopIds = new List(); + foreach (var id in ids) { - if (latestRefund.TryGetValue(o.order_id, out var r)) + if (long.TryParse(id, out long shopId) && !orderShopIds.Contains(shopId)) + orderShopIds.Add(shopId); + } + if (orderShopIds.Count == 0) return result; + + int startStamp = TimeUtils.DateTimeToTimestampSeconds(startTime); + int endStamp = TimeUtils.DateTimeToTimestampSeconds(endTime) + 1; // 结束时间含当天/当秒 + var orders = LoadOrders(startStamp, endStamp, orderShopIds); + if (orders.Count == 0) return result; + + // 加载买手订单(直拍和中转仓都要查),并回填商铺订单的退款金额 + LoadBuyerOrder(orders, true); + FillRefunds(orders); + + foreach (var order in orders) + { + var buyerOrders = order.BuyerOrders ?? new List(); + bool hasRefund = order.HasRefund; + if (orderType == ShopOrderDetailType.BuyerUnrefunded) { - o.refund_status_string = r.Status; - o.refund_amount = r.RefundAmount.Value; + buyerOrders = buyerOrders.Where(a => !IsRefundDone(a.RefundStatusText)).ToList(); } - else + int unrefundedAmount = GetBuyerUnrefundedAmount(buyerOrders); + switch (orderType) { - o.refund_amount = 0; + case ShopOrderDetailType.Valid: // 有效订单:没有售后 + if (hasRefund) continue; + break; + case ShopOrderDetailType.Refund: // 退款订单:发生过售后 + if (!hasRefund) continue; + break; + case ShopOrderDetailType.BuyerUnrefunded: // 买手未退回金额的订单 + if (!hasRefund || unrefundedAmount <= 0) continue; + break; + } + + result.Add(new ShopOrderDetailResponse + { + ShopId = order.ShopId, + ShopName = order.ShopName, + OrderId = order.OrderId, + CreateTime = order.CreateTime, + OrderPrice = order.OrderPrice, + OrderRefundAmount = order.Items.Sum(a => a.RefundAmount), + OutAmount = order.OutAmount, + UnrefundedAmount = unrefundedAmount, + BuyerOrders = buyerOrders, + }); } + return result.OrderByDescending(a => a.CreateTime).ToList(); + } - var transitOutAmountMap = new Dictionary(); - // needLookupOrderIds:先跑一遍订单列表,out_amount 为空或0的订单号 - var needLookupOrderIds = orders - .Where(o => o.out_amount == 0) - .Select(o => o.order_id) - .Distinct() + /// + /// 买手未退回金额:不是"退款成功" 状态的买手订单退款金额之和 + /// 只算有退款信息的(退款金额 > 0 或退款状态非空),没发生退款的正常订单不计入 + /// + private static int GetBuyerUnrefundedAmount(List buyerOrders) + { + if (buyerOrders == null || buyerOrders.Count == 0) return 0; + + return buyerOrders + .Where(a => !IsRefundDone(a.RefundStatusText)) + .Where(a => a.RefundAmount > 0 || !string.IsNullOrEmpty(a.RefundStatusText)) + .Sum(a => a.RefundAmount); + } + + /// + /// 取某个日期在指定聚合维度下所属周期的起始日期 + /// + /// 明细日期 + /// 聚合维度 + /// 汇总维度使用的分组标识 + private static DateTime GetGroupKey(DateTime date, ShopStatsGroupType groupType, DateTime summaryKey) + { + switch (groupType) + { + case ShopStatsGroupType.Day: + return date.Date; + case ShopStatsGroupType.Week: + // 周一为一周的开始 + return date.Date.AddDays(-(((int)date.DayOfWeek + 6) % 7)); + case ShopStatsGroupType.Month: + return new DateTime(date.Year, date.Month, 1); + default: + // 汇总:所有日期归到同一组 + return summaryKey; + } + } + + private (Dictionary> shopGroups, Dictionary> buyerGroups) getShopStatsByUserName(DateTime startTime, DateTime endTime, string userName) + { + var shopIds = getShopIdsByUserName(userName); + if (shopIds == null) return (null, null); + + return getShopStats(startTime, endTime, shopIds); + + } + + /// 按用户名取商铺ID集合,取不到返回 null + private List getShopIdsByUserName(string userName) + { + if (string.IsNullOrEmpty(userName)) return null; + + var shopIds = _shopService.GetShopIds(userName).Select(a => a.ToString()).ToList(); + return shopIds.Count == 0 ? null : shopIds; + } + + /// 加载指定商铺在某时间范围内的商铺日统计 + private List loadShopStats(DateTime startTime, DateTime endTime, List shopIds) + { + return _freeSql.Select() + .Where(a => shopIds.Contains(a.ShopId)) + .Where(a => a.StatDate >= startTime) + .Where(a => a.StatDate <= endTime) .ToList(); - if (needLookupOrderIds.Count > 0) - { - for (int i = 0; i < orderIds.Count; i += 1000) - { - var batch = orderIds.Skip(i).Take(1000).ToArray(); - var ids = batch.ToList(); - var transitList = _freeSql.Select() - .Where(t => ids.Contains(t.OrderNo)) - .ToList(t => new { t.OrderNo, t.OutAmount }); + } - foreach (var t in transitList) + private (Dictionary> shopGroups, Dictionary> buyerGroups) getShopStats(DateTime startTime, DateTime endTime, List shopIds) + { + if (shopIds == null || shopIds.Count() == 0) return (null, null); + + var shopStats = loadShopStats(startTime, endTime, shopIds); + + var buyerStats = _freeSql.Select() + .Where(a => shopIds.Contains(a.ShopId)) + .Where(a => a.StatDate >= startTime) + .Where(a => a.StatDate <= endTime) + .ToList(); + + // 先按商铺分组 + Dictionary> shopGroups = shopStats.GroupBy(x => x.ShopId).ToDictionary(g => g.Key, g => g.ToList()); + Dictionary> buyerGroups = buyerStats.GroupBy(x => x.ShopId).ToDictionary(g => g.Key, g => g.ToList()); + + return (shopGroups, buyerGroups); + } + + /// 商铺统计指定天(同时会统计买手数据) + public void ShopDailyStats(DateTime date, List shopId = null) + { + + var orders = LoadOrders(date, shopId); + LoadBuyerOrder(orders); + FillRefunds(orders); + CalcProfit(orders); + + SaveShopStats(date.Date, orders); + SaveBuyerStats(date.Date, orders); + + } + + /// 买手统计指定天 + public void BuyerDailyStats(DateTime date, List shopId = null) + { + var orders = LoadOrders(date, shopId); + LoadBuyerOrder(orders); + SaveBuyerStats(date.Date, orders); + } + + /// + /// 更新 + /// + + public void UpdateStats(DateTime date) + { + /* + 商铺新增订单创建时间 + 商铺退款状态更新时间 + 买手购买记录增加创建时间 + 买手退款表更新时间 + */ + DateTime startDate = date.Date; // 当天0点 + DateTime endDate = date.Date.AddDays(1);// 次日0点 + int startUnix = TimeUtils.DateTimeToTimestampSeconds(startDate); + int endUnix = TimeUtils.DateTimeToTimestampSeconds(endDate); + List allUpdateOrderIds = new List(); + + //获取新增订单 + allUpdateOrderIds.AddRange(_freeSql.Select() + .Where(a => a.CreateTime >= startUnix && a.CreateTime < endUnix) + .ToList(a => a.OrderId)); + + //获取退款信息 + allUpdateOrderIds.AddRange(_freeSql.Select() + .Where(a => a.UpdateTime >= startUnix && a.UpdateTime < endUnix) + .ToList(a => a.OrderId)); + + //获取买手购买 + //云仓 + allUpdateOrderIds.AddRange(_freeSql.Select() + .Where(a => a.CreatedTime >= startDate && a.CreatedTime < endDate) + .ToList(a => a.OrderNo)); + //直发 + allUpdateOrderIds.AddRange(_freeSql.Select() + .Where(a => a.AddTime >= startDate && a.AddTime < endDate) + .ToList(a => a.WxOrderId)); + + //买手退款 + allUpdateOrderIds.AddRange(_freeSql.Select() + .Where(a => a.MdyTime >= startDate && a.MdyTime < endDate) + .ToList(a => a.WxOrderId)); + + allUpdateOrderIds = allUpdateOrderIds.Distinct().ToList();//合并 + + + List allUpdateStats = new List(); + if (allUpdateOrderIds.Count > 0) + { + //查询订单归属商铺和时间 + foreach (string[] batch in Batch(allUpdateOrderIds, BatchSize)) + { + allUpdateStats.AddRange(_freeSql.Select() + .Where(a => batch.Contains(a.OrderId)) + .ToList(a => new UpdateStatsInfo + { + ShopId = a.ShopId.ToString(), + OrderCreateAt = a.CreateTime ?? 0, + })); + if (allUpdateStats.Any(a => a.OrderCreateDate < DateTime.Now.AddMonths(-1))) { - if (string.IsNullOrEmpty(t.OrderNo)) continue; - if (!transitOutAmountMap.ContainsKey(t.OrderNo)) - transitOutAmountMap[t.OrderNo] = (int)(t.OutAmount ?? 0); + var list = allUpdateStats.Where(a => a.OrderCreateDate < DateTime.Now.AddMonths(-1)).ToList(); } } + } - - - foreach (var o in orders) + //重新计算指定商铺指定时间 + if (allUpdateStats.Count > 0) { - // 1. 外采金额补全 - if (o.out_amount == 0 && transitOutAmountMap.TryGetValue(o.order_id, out var transitAmount)) + + // 1. 按日期分组,找出"ShopId 数量 > 80%"的日期 + Dictionary> dict = allUpdateStats + .GroupBy(x => x.OrderCreateDate) + .ToDictionary(a => a.Key, a => a.Select(a => long.Parse(a.ShopId)).ToList()); + // 用 HashSet 加速 Contains + // 2. 这些日期走"全量执行":对所有商铺跑 + foreach (var item in dict) { - o.external_procurement = 1; - o.out_amount = transitAmount; + if (item.Value.Count > 1000) + { + ShopDailyStats(item.Key); + } + else + { + ShopDailyStats(item.Key, item.Value); + } + string log = $"更新统计:{item.Key.ToString("yyyy/MM/dd")} {item.Value.Count}条商铺"; + _logger.Info(log); + Debug.WriteLine(log); } - // 2. 退款信息补全(latestRefund 是前面按订单号分组取最新退款得到的字典) - if (latestRefund.TryGetValue(o.order_id, out var r)) - { - o.refund_status_string = r.Status.ToString(); // 按实际类型调整 - o.refund_amount = (int)(r.RefundAmount ?? 0); - } + } + } - // 利润 = 订单金额 - 外采金额 - 平台服务费 - 达人佣金(为空按0处理) - o.profit = o.order_price - o.out_amount - o.predict_commission_fee - o.commission_amount; - if (o.refund_status > 1 && o.refund_amount > 0)//退款利润计算 + #region 数据加载 + + /// 加载某天的订单(只保留单商品订单) + private List LoadOrders(DateTime date, List shopId) + { + int start = TimeUtils.DateTimeToTimestampSeconds(date.Date); // 当天0点 + int end = start + 86400; // 次日0点 + + return LoadOrders(start, end, shopId); + } + + /// + /// 按时间范围加载订单(秒级时间戳,左闭右开 [startStamp, endStamp)) + /// 只保留单商品订单;shopIds 为空时不限制商铺 + /// + private List LoadOrders(int startStamp, int endStamp, List shopIds) + { + var query = _freeSql.Select() + .InnerJoin((a, b) => a.OrderId == b.OrderId) + .Where((a, b) => a.CreateTime >= startStamp && a.CreateTime < endStamp) + .Where((a, b) => a.Status > 10); + + if (shopIds != null && shopIds.Count > 0) + query = query.Where((a, b) => shopIds.Contains(a.ShopId)); + + var flatList = query.ToList((a, b) => new OrderFlatInfo + { + OrderId = a.OrderId, + ShopId = a.ShopId, + ShopName = a.ShopName, + OutAmount = a.OutAmount, + OrderPrice = a.OrderPrice ?? 0, + PredictCommissionFee = a.PredictCommissionFee ?? 0, + CommissionAmount = a.CommissionAmount, + CreateTime = a.CreateTime ?? 0, + UpdateTime = a.UpdateTime ?? 0, + + SkuId = b.SkuId, + SkuPrice = b.EstimatePrice ?? 0, + CashCouponDiscountedPrice = b.CashCouponDiscountedPrice ?? 0, + RefundStatus = b.RefundStatus, + }); + + var orders = new List(); + foreach (var g in flatList.GroupBy(x => x.OrderId)) + { + var rows = g.ToList(); + if (rows.Count > 1) continue; // 暂时不记录多商品订单,可能是刷单 + + var first = rows[0]; + orders.Add(new OrderInfo { - o.profit = o.order_price - o.refund_amount; + ShopId = first.ShopId, + OrderId = first.OrderId, + ShopName = first.ShopName, + PurchaseType = first.OutAmount > 0 ? PurchaseType.DirectPurchase : PurchaseType.FromWarehouse, + OrderPrice = first.OrderPrice, + PredictCommissionFee = first.PredictCommissionFee, + CommissionAmount = first.CommissionAmount, + CreateTime = TimeUtils.StampToDateTime(first.CreateTime), + UpdateTime = TimeUtils.StampToDateTime(first.UpdateTime), + Items = rows.Select(x => new OrderItemInfo + { + OrderId = x.OrderId, + SkuId = x.SkuId, + SkuPrice = x.SkuPrice + x.CashCouponDiscountedPrice, + RefundStatus = x.RefundStatus + }).ToList() + }); + } + return orders; + } + + /// + /// 加载买手订单信息 + /// + /// 商铺订单 + /// + /// true:按订单的采购类型区分直拍 / 中转仓,两张买手表都能查到(查询明细用) + /// false:沿用历史口径,按 OutAmount 是否为 0 判断(统计任务保持原行为不变) + /// + private void LoadBuyerOrder(List orders, bool byPurchaseType = false) + { + var result = new List(); + + + //查询中转仓购买表 + var outWarehouseOrderIds = orders + .Where(a => byPurchaseType ? a.PurchaseType == PurchaseType.FromWarehouse : a.OutAmount == 0) + .Select(a => a.OrderId).Distinct().ToList(); + if (outWarehouseOrderIds.Count > 0) + { + //查询中转仓购买表 + foreach (string[] batch in Batch(outWarehouseOrderIds, BatchSize)) + { + result.AddRange(_freeSql.Select() + .Where(a => batch.Contains(a.OrderNo)) + .OrderBy(a => a.Id) // + .ToList(a => new BuyerOrderInfo + { + OrderId = a.OrderNo, + SkuId = a.OrderSkuId, + OutOrderSn = a.OutOrderNo, + OutAmount = a.OutAmount ?? 0, + })); } } + + //查询直拍表 + var outDirectOrderIds = orders + .Where(a => byPurchaseType ? a.PurchaseType == PurchaseType.DirectPurchase : a.OutAmount > 0) + .Select(a => a.OrderId).Distinct().ToList(); + foreach (string[] batch in Batch(outDirectOrderIds, BatchSize)) + { + result.AddRange(_freeSql.Select() + .Where(a => a.WxSendType == 1) + .Where(a => batch.Contains(a.WxOrderId)) + .ToList(a => new BuyerOrderInfo + { + OrderId = a.WxOrderId, + SkuId = a.WxSkuId, + OutOrderSn = a.OrderSn, + OutAmount = a.OrderAmount ?? 0, + })); + + } + var orderDict = orders.ToDictionary(o => o.OrderId); + foreach (var b in result) + { + if (orderDict.TryGetValue(b.OrderId, out var o)) + { + b.ShopId = o.ShopId; + o.BuyerOrders.Add(b); + } + } + + + + + + //买手退款信息 + var ids = orders + .Where(a => a.OutAmount > 0 && a.Items.Any(b => b.RefundStatus > 1)) + .Select(a => a.OrderId).Distinct().ToList(); + + var refundlist = new List(); + foreach (var batch in Batch(ids, BatchSize)) + { + refundlist.AddRange(_freeSql.Select() + .Where(a => batch.Contains(a.WxOrderId)) + .OrderBy(a => a.Id) + .ToList(a => new BuyerOrderInfo + { + OrderId = a.WxOrderId, + SkuId = a.WxSkuId, + OutOrderSn = a.OrderSn, + OutAmount = a.OrderAmount ?? 0, + RefundAmount = a.RefundAmount ?? 0, + RefundStatusText = a.StatusText + })); + } + + //填充买手订单退款信息 + foreach (var r in refundlist) + { + if (orderDict.TryGetValue(r.OrderId, out var o)) + { + r.ShopId = o.ShopId; + var buyerOrder = o.BuyerOrders.FirstOrDefault(a => a.OutOrderSn == r.OutOrderSn); + if (buyerOrder != null)//补充退款信息 + { + buyerOrder.RefundAmount = r.RefundAmount; + buyerOrder.RefundStatusText = r.RefundStatusText; + } + else + { + o.BuyerOrders.Add(r); + } + + } + } } + + /// 回填商铺订单的退款信息 + private void FillRefunds(List orders) + { + var refundOrderIds = orders + .Where(a => a.Items.Any(b => b.RefundStatus > 1)) + .Select(a => a.OrderId).Distinct().ToList(); + if (refundOrderIds.Count == 0) return; + + var allRefunds = new List(); + foreach (var batch in Batch(refundOrderIds, BatchSize)) + { + allRefunds.AddRange(_freeSql.Select() + .Where(a => batch.Contains(a.OrderId)) + .ToList(a => new OrderRefundInfo + { + OrderId = a.OrderId, + SkuId = a.SkuId, + RefundAmount = a.RefundAmount ?? 0, + Status = a.Status, + CreateTime = a.CreateTime ?? 0 + })); + } + + // 按 (OrderId, SkuId) 分组,取 CreateTime 最新一条 + var refundDict = allRefunds + .GroupBy(x => (x.OrderId, x.SkuId)) + .ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.CreateTime).First()); + + foreach (var item in orders.SelectMany(o => o.Items)) + { + if (!refundDict.TryGetValue((item.OrderId, item.SkuId), out var refund)) continue; + + item.RefundAmount = refund.RefundAmount; + if (Enum.TryParse(refund.Status, out AfterSaleStatus v)) + item.AfterSaleStatus = v; + } + } + + #endregion + + #region 计算与保存 + /// + /// 计算利润 + /// + /// + private static void CalcProfit(List orders) + { + foreach (var order in orders) + { + order.Profit = order.HasRefund + ? order.OrderPrice - order.Items.Sum(a => a.RefundAmount) + : order.OrderPrice - order.OutAmount - order.PredictCommissionFee - order.CommissionAmount; + } + } + + private void SaveShopStats(DateTime statDate, List orders) + { + var statsList = new List(); + + foreach (var shopGroup in orders.GroupBy(o => o.ShopId)) + { + var valid = shopGroup.Where(a => !a.HasRefund).ToList(); + var afterSale = shopGroup.Where(a => a.HasRefund).ToList(); + var afterSaleItems = afterSale.SelectMany(o => o.Items).ToList(); + + statsList.Add(new ShopDailyStatsEntity + { + ShopId = shopGroup.Key.ToString(), + StatDate = statDate, + OrderCount = valid.Count, + SalesAmount = valid.Sum(o => (long)o.OrderPrice), + ProfitAmount = valid.Sum(o => (long)o.Profit), + PlatformFee = valid.Sum(o => (long)o.PredictCommissionFee), + RefundOrderCount = afterSale.Count, + RefundDone = afterSaleItems.Where(i => i.IsRefundSuccessed) + .Sum(i => (long)i.RefundAmount), + RefundPending = afterSaleItems.Where(i => !i.IsRefundSuccessed && i.IsAfterSale) + .Sum(i => (long)i.RefundAmount), + }); + } + + if (statsList.Count == 0) return; // 空集合不要提交 + + _freeSql.InsertOrUpdate() + .SetSource(statsList, a => new { a.ShopId, a.StatDate }) + .ExecuteAffrows(); + } + + private void SaveBuyerStats(DateTime statDate, List orders) + { + if (orders == null || orders.Count == 0) return; + + + var statsList = new List(); + // 按店铺分组,每个店铺下展开所有买手订单 + foreach (var shopGroup in orders.GroupBy(o => o.ShopId)) + { + var buyerOrders = shopGroup + .Where(o => o.BuyerOrders != null) + .SelectMany(o => o.BuyerOrders) + .ToList(); + + if (buyerOrders.Count == 0) continue; + // 该店铺当天没有采购单,不写入 + // 同一个外部订单可能有多条记录,按 OutOrderSn 再分组 + // OutOrderSn 为空时用 SkuId 兜底,避免多个空单号被合并成一个 + var outOrderGroups = buyerOrders + .GroupBy(b => b.OutOrderSn) + .ToList(); + + + long done = 0, pending = 0, rejected = 0; + long totalPurchase = 0; + foreach (var g in outOrderGroups) + { + //购买金额:同一外部订单的多条记录可能是退款记录重复展开,购买金额只算一次 + totalPurchase += g.Max(x => x.OutAmount); + + // 只要有一条退款成功,整个外部订单视为退款成功 + var successRow = g.FirstOrDefault(x => IsRefundDone(x.RefundStatusText)); + if (successRow != null) + { + done += successRow.RefundAmount; + continue; + } + + // 没有任何退款信息,说明没发生退款 + var refundRows = g.Where(x => x.RefundAmount > 0 || !string.IsNullOrEmpty(x.RefundStatusText)).ToList(); + if (refundRows.Count == 0) continue; + + // 没有成功记录:视为未成功,金额取该订单下最大的退款金额,避免重复累加 + long refundAmount = refundRows.Max(x => x.RefundAmount); + if (refundRows.Any(x => IsRefundRejected(x.RefundStatusText))) + rejected += refundAmount; + else + pending += refundAmount; + } + statsList.Add(new BuyerDailyStatsEntity + { + ShopId = shopGroup.Key.ToString(), + BuyerId = "", + StatDate = statDate.Date, + PurchaseCount = outOrderGroups.Count, // 按外部订单数统计 + TotalPurchase = totalPurchase, + DoneRefund = done, + PendingRefund = pending, + RejectedRefund = rejected, + }); + } + if (statsList.Count == 0) return; + + _freeSql.InsertOrUpdate() + .SetSource(statsList, a => new { a.ShopId, a.StatDate }) + .ExecuteAffrows(); + } + + + + #endregion + + #region 辅助 + + /// .NET Framework 没有 Enumerable.Chunk,自己分批 + private static IEnumerable Batch(IList source, int size) + { + for (int i = 0; i < source.Count; i += size) + yield return source.Skip(i).Take(size).ToArray(); + } + + private static bool IsRefundDone(string statusText) + { + return !string.IsNullOrEmpty(statusText) && statusText == "退款成功"; + } + + private static bool IsRefundRejected(string statusText) + { + // TODO: 目前业务上暂不区分"被拒绝",需要时放开下面逻辑 + return false; + //return !string.IsNullOrEmpty(statusText) + // && (statusText.Contains("拒绝") || statusText.Contains("驳回") || statusText.Contains("失败")); + } + + #endregion + } \ No newline at end of file diff --git a/KJD_WebService/Services/Shops/ShopService.cs b/KJD_WebService/Services/Shops/ShopService.cs index 43a15e9..4349b63 100644 --- a/KJD_WebService/Services/Shops/ShopService.cs +++ b/KJD_WebService/Services/Shops/ShopService.cs @@ -21,11 +21,15 @@ public class ShopService : ITransientDependency _logger = logger; _userService = userService; } + public long GetShopCount() + { + return _freeSql.Select().Count(); + } /// /// 修改商铺永不过期 /// - public bool UpdateShopNeverExpires(string userName, string shopId,string ownerUser, bool isNever) + public bool UpdateShopNeverExpires(string userName, string shopId, string ownerUser, bool isNever) { if (string.IsNullOrEmpty(userName)) return false; if (string.IsNullOrEmpty(shopId)) return false; @@ -39,4 +43,20 @@ public class ShopService : ITransientDependency .ExecuteAffrows(); return affrows == 1; } + + public List GetShops(string userName) + { + return _freeSql.Select() + .Where(a => a.UserName == userName) + .ToList(); + } + + public List GetShopIds(string userName) + { + var shops = GetShops(userName); + var shopAppkeys = shops.Select(a => a.ShopId).Distinct().ToList(); + return _freeSql.Select() + .Where(a => shopAppkeys.Contains(a.AppKey)) + .ToList(a => a.Id); + } } \ No newline at end of file diff --git a/KJD_WebService/Services/Shops/ShopStatsJob.cs b/KJD_WebService/Services/Shops/ShopStatsJob.cs new file mode 100644 index 0000000..7db69e4 --- /dev/null +++ b/KJD_WebService/Services/Shops/ShopStatsJob.cs @@ -0,0 +1,28 @@ +using MAX.Core.DependencyInjection; +using Store.Framework.Jobs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Web; + +namespace KJD_Web.Services.Shops; + +/// +/// 更新商铺统计 +/// +[Job("ShopStats", "0 2 * * *")] +public class ShopStatsJob : IScheduledJobs, ITransientDependency +{ + private readonly ShopRevenueStatisticsService _shopRevenueStatisticsService; + + public ShopStatsJob(ShopRevenueStatisticsService shopRevenueStatisticsService) + { + _shopRevenueStatisticsService = shopRevenueStatisticsService; + } + public Task RunAsync() + { + _shopRevenueStatisticsService.UpdateStats(DateTime.Now.AddDays(-1).Date); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/KJD_WebService/Web.config b/KJD_WebService/Web.config index d8c0792..5ffe245 100644 --- a/KJD_WebService/Web.config +++ b/KJD_WebService/Web.config @@ -5,8 +5,8 @@ --> - - + + diff --git a/KJD_WebService/WebApiConfig.cs b/KJD_WebService/WebApiConfig.cs index e5d6a7a..5aedcd9 100644 --- a/KJD_WebService/WebApiConfig.cs +++ b/KJD_WebService/WebApiConfig.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; using System.Linq; @@ -28,7 +29,9 @@ namespace KJD_Web var jsonFormatter = config.Formatters.JsonFormatter; jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - + jsonFormatter.SerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat; + //jsonFormatter.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + jsonFormatter.SerializerSettings.Formatting = Formatting.None; //config.Filters.Add(new SignValidateFilterAttribute()); config.Formatters.Remove(config.Formatters.XmlFormatter); diff --git a/Store.Contracts/Order/Entitys/AfterSaleStatus.cs b/Store.Contracts/Order/Entitys/AfterSaleStatus.cs new file mode 100644 index 0000000..42571d3 --- /dev/null +++ b/Store.Contracts/Order/Entitys/AfterSaleStatus.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Store.Contracts.Order.Entitys; + +/// +/// 售后状态枚举 +/// +public enum AfterSaleStatus +{ + NONE, + /// + /// 用户取消申请 + /// + USER_CANCELD, + + /// + /// 商家受理中 + /// + MERCHANT_PROCESSING, + + /// + /// 商家拒绝退款 + /// + MERCHANT_REJECT_REFUND, + + /// + /// 商家拒绝退货退款 + /// + MERCHANT_REJECT_RETURN, + + /// + /// 待买家退货 + /// + USER_WAIT_RETURN, + + /// + /// 退货退款关闭 + /// + RETURN_CLOSED, + + /// + /// 待商家收货 + /// + MERCHANT_WAIT_RECEIPT, + + /// + /// 商家逾期未退款 + /// + MERCHANT_OVERDUE_REFUND, + + /// + /// 退款完成 + /// + MERCHANT_REFUND_SUCCESS, + + /// + /// 退货退款完成 + /// + MERCHANT_RETURN_SUCCESS, + + /// + /// 平台退款中 + /// + PLATFORM_REFUNDING, + + /// + /// 平台退款失败 + /// + PLATFORM_REFUND_FAIL, + + /// + /// 待用户确认 + /// + USER_WAIT_CONFIRM, + + /// + /// 商家打款失败,客服关闭售后 + /// + MERCHANT_REFUND_RETRY_FAIL, + + /// + /// 售后关闭 + /// + MERCHANT_FAIL, + + /// + /// 待用户处理商家协商 + /// + USER_WAIT_CONFIRM_UPDATE, + + /// + /// 待用户处理商家代发起的售后申请 + /// + USER_WAIT_HANDLE_MERCHANT_AFTER_SALE +} diff --git a/Store.Contracts/Order/Entitys/OrderPddBuyerAfterSaleEntity.cs b/Store.Contracts/Order/Entitys/OrderPddBuyerAfterSaleEntity.cs new file mode 100644 index 0000000..ff4603e --- /dev/null +++ b/Store.Contracts/Order/Entitys/OrderPddBuyerAfterSaleEntity.cs @@ -0,0 +1,123 @@ +using FreeSql.DataAnnotations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Store.Contracts.Order.Entitys; + +/// +/// 拼多多买手售后单表 +/// +[Table(Name = "wxxd.order_pdd_buyer_after_sale")] +public class OrderPddBuyerAfterSaleEntity +{ + /// + /// id + /// + [Column(Name = "id", IsPrimary = true, IsIdentity = true)] + public long Id { get; set; } + + /// + /// 微信订单 + /// + [Column(Name = "wx_order_id")] + public string WxOrderId { get; set; } + + /// + /// 微信订单sku + /// + [Column(Name = "wx_sku_id")] + public string WxSkuId { get; set; } + + /// + /// 微信订单发货类型 0 中转仓,1店铺后台直发 + /// + [Column(Name = "wx_send_type")] + public byte WxSendType { get; set; } + + /// + /// 中转单创建时间 + /// + [Column(Name = "TransitCreateTime")] + public DateTime? TransitCreateTime { get; set; } + + /// + /// 中转仓取号状态 + /// + [Column(Name = "pre_allocated_order_no")] + public int? PreAllocatedOrderNo { get; set; } + + /// + /// 买手编号 + /// + [Column(Name = "buyerId")] + public string BuyerId { get; set; } + + /// + /// 买手名称 + /// + [Column(Name = "buyerName")] + public string BuyerName { get; set; } + + /// + /// 售后单号 + /// + [Column(Name = "afterSalesId")] + public string AfterSalesId { get; set; } + + /// + /// 买手单号 + /// + [Column(Name = "orderSn")] + public string OrderSn { get; set; } + + /// + /// 拍单订单金额 单位分 + /// + [Column(Name = "orderAmount")] + public int? OrderAmount { get; set; } + + /// + /// 售后金额 单位分 + /// + [Column(Name = "refundAmount")] + public int? RefundAmount { get; set; } + + /// + /// 拍单日期 yyMMdd + /// + [Column(Name = "orderDate")] + public long? OrderDate { get; set; } + + /// + /// 售后状态 + /// + [Column(Name = "statusText")] + public string StatusText { get; set; } + + /// + /// 退货地址 + /// + [Column(Name = "afterSaleAddress")] + public string AfterSaleAddress { get; set; } + + /// + /// 买手订单详情 + /// + [Column(Name = "order_detail")] + public string OrderDetail { get; set; } + + /// + /// add_time + /// + [Column(Name = "add_time")] + public DateTime AddTime { get; set; } + + /// + /// mdy_time + /// + [Column(Name = "mdy_time")] + public DateTime MdyTime { get; set; } +} diff --git a/Store.Contracts/Order/Entitys/PurchaseType.cs b/Store.Contracts/Order/Entitys/PurchaseType.cs new file mode 100644 index 0000000..985ec89 --- /dev/null +++ b/Store.Contracts/Order/Entitys/PurchaseType.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Store.Contracts.Order.Entitys; + +/// +/// 采购类型 +/// +public enum PurchaseType +{ + /// + /// 从仓库采购 + /// + FromWarehouse = 0, + + /// + /// 直接购买 + /// + DirectPurchase = 1 +} diff --git a/Store.Contracts/Order/Entitys/WeiExternalProcurementLogEntity.cs b/Store.Contracts/Order/Entitys/WeiExternalProcurementLogEntity.cs index 5aeb931..0d63e8e 100644 --- a/Store.Contracts/Order/Entitys/WeiExternalProcurementLogEntity.cs +++ b/Store.Contracts/Order/Entitys/WeiExternalProcurementLogEntity.cs @@ -10,7 +10,7 @@ namespace Store.Contracts.Order.Entitys; /// /// 微信外采取号日志表 /// -[Table(Name = "wxxd.oms_wei_external_procurement_log")] +[Table(Name = "erp.oms_wei_external_procurement_log")] public class WeiExternalProcurementLogEntity { /// @@ -161,7 +161,7 @@ public class WeiExternalProcurementLogEntity /// 订单作废状态:0=正常,1=已作废,2=作废失败 /// [Column(Name = "order_void_status")] - public byte OrderVoidStatus { get; set; } + public int OrderVoidStatus { get; set; } /// /// 虚拟单号 diff --git a/Store.Contracts/Shops/BuyerDailyStatsEntity.cs b/Store.Contracts/Shops/BuyerDailyStatsEntity.cs new file mode 100644 index 0000000..be128f7 --- /dev/null +++ b/Store.Contracts/Shops/BuyerDailyStatsEntity.cs @@ -0,0 +1,84 @@ +using FreeSql.DataAnnotations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Store.Contracts.Shops; + +/// +/// 买手每日统计表 +/// +[Table(Name = "wxxd.buyer_daily_stats")] +public class BuyerDailyStatsEntity +{ + /// + /// 自增主键 + /// + [Column(Name = "id", IsPrimary = true, IsIdentity = true)] + public long Id { get; set; } + + /// + /// 店铺ID + /// + [Column(Name = "shop_id")] + public string ShopId { get; set; } + + /// + /// 买手ID + /// + [Column(Name = "buyer_id")] + public string BuyerId { get; set; } + + /// + /// 采购订单创建日期 + /// + [Column(Name = "stat_date")] + public DateTime StatDate { get; set; } + + /// + /// 采购单数 + /// + [Column(Name = "purchase_count")] + public int PurchaseCount { get; set; } + + /// + /// 总花费金额 + /// + [Column(Name = "total_purchase")] + public long TotalPurchase { get; set; } + /// + /// 售后中买手总金额 + /// + public long TotalRefund => DoneRefund + PendingRefund + RejectedRefund; + /// + /// 已成功退款金额 + /// + [Column(Name = "done_refund")] + public long DoneRefund { get; set; } + + /// + /// 未退回金额 + /// + [Column(Name = "pending_refund")] + public long PendingRefund { get; set; } + + /// + /// 被拒绝金额 + /// + [Column(Name = "rejected_refund")] + public long RejectedRefund { get; set; } + + /// + /// 创建时间 + /// + [Column(Name = "created_at", ServerTime = DateTimeKind.Local, CanUpdate = false)] + public DateTime CreatedAt { get; set; } + + /// + /// 最后刷新时间 + /// + [Column(Name = "updated_at", ServerTime = DateTimeKind.Local)] + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/Store.Contracts/Shops/ShopDailyStatsEntity.cs b/Store.Contracts/Shops/ShopDailyStatsEntity.cs new file mode 100644 index 0000000..1cb79a1 --- /dev/null +++ b/Store.Contracts/Shops/ShopDailyStatsEntity.cs @@ -0,0 +1,87 @@ +using FreeSql.DataAnnotations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Store.Contracts.Shops; + +/// +/// 店铺每日统计表 +/// +[Table(Name = "wxxd.shop_daily_stats")] +public class ShopDailyStatsEntity +{ + /// + /// 自增主键 + /// + [Column(Name = "id", IsPrimary = true, IsIdentity = true)] + public long Id { get; set; } + + /// + /// 店铺ID + /// + [Column(Name = "shop_id")] + public string ShopId { get; set; } + + /// + /// 销售订单创建日期 + /// + [Column(Name = "stat_date")] + public DateTime StatDate { get; set; } + + /// + /// 订单数 + /// + [Column(Name = "order_count")] + public int OrderCount { get; set; } + + /// + /// 销售总额 + /// + [Column(Name = "sales_amount")] + public long SalesAmount { get; set; } + + /// + /// 利润 + /// + [Column(Name = "profit_amount")] + public long ProfitAmount { get; set; } + + /// + /// 平台佣金 + /// + [Column(Name = "platform_fee")] + public long PlatformFee { get; set; } + + /// + /// 退款订单数 + /// + [Column(Name = "refund_order_count")] + public int RefundOrderCount { get; set; } + + /// + /// 已成功退款金额 + /// + [Column(Name = "refund_done")] + public long RefundDone { get; set; } + + /// + /// 退款处理中金额 + /// + [Column(Name = "refund_pending")] + public long RefundPending { get; set; } + + /// + /// 创建时间 + /// + [Column(Name = "created_at", ServerTime = DateTimeKind.Local, CanUpdate = false)] + public DateTime CreatedAt { get; set; } + + /// + /// 最后刷新时间 + /// + [Column(Name = "updated_at", ServerTime = DateTimeKind.Local)] + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/test/WebServiceTest/ShopTest.cs b/test/WebServiceTest/ShopTest.cs index a504214..6ddbe11 100644 --- a/test/WebServiceTest/ShopTest.cs +++ b/test/WebServiceTest/ShopTest.cs @@ -1,8 +1,9 @@ -using Autofac; +using Autofac; using Autofac.Integration.WebApi; using Autofac.Integration.WebApi; using KJD_Web; using KJD_Web; +using KJD_Web.Requests.Shops; using KJD_Web.Services.Shops; using KJD_Web.Services.Users; using Newtonsoft.Json; @@ -14,6 +15,7 @@ using System.Collections.Generic; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices.ComTypes; using System.Text; using System.Threading.Tasks; using System.Timers; @@ -44,36 +46,135 @@ internal class ShopTest } [Test] - public void TestAm() + //[TestCase(1960967267443631281)] + //[TestCase(1960967267443631212)] + //[TestCase(1960967267443631213)] + //[TestCase(1960967267443631215)] + //[TestCase(1960967267443631216)] + //[TestCase(1960967267443631218)] + //[TestCase(1960967267443631219)] + //[TestCase(1960967267443631221)] + //[TestCase(1960967267443631222)] + //[TestCase(1960967267443631223)] + //[TestCase(1960967267443631224)] + //[TestCase(1960967267443631226)] + //[TestCase(1960967267443631227)] + //[TestCase(1960967267443631228)] + //[TestCase(1960967267443630057)] + //[TestCase(1960967267443629984)] + //[TestCase(1960967267443629858)] + [TestCase(1960967267443629855)] + //[TestCase(1960967267443629809)] + //[TestCase(1960967267443629737)] + //[TestCase(1960967267443627649)] + //[TestCase(1960967267443627648)] + public void TestAm(long id) { - List ShopIds = new List - { - 1960967267443631281, - 1960967267443631212, - 1960967267443631213, - 1960967267443631215, - 1960967267443631216, - 1960967267443631218, - 1960967267443631219, - 1960967267443631221, - 1960967267443631222, - 1960967267443631223, - 1960967267443631224, - 1960967267443631226, - 1960967267443631227, - 1960967267443631228, - 1960967267443630057, - 1960967267443629984, - 1960967267443629858, - 1960967267443629855, - 1960967267443629809, - 1960967267443629737, - 1960967267443627649, - 1960967267443627648 - }; + // List ShopIds = new List + // { + //1960967267443631281, + //1960967267443631212, + //1960967267443631213, + //1960967267443631215, + //1960967267443631216, + //1960967267443631218, + //1960967267443631219, + //1960967267443631221, + //1960967267443631222, + //1960967267443631223, + //1960967267443631224, + //1960967267443631226, + //1960967267443631227, + //1960967267443631228, + //1960967267443630057, + //1960967267443629984, + //1960967267443629858, + //1960967267443629855, + //1960967267443629809, + //1960967267443629737, + //1960967267443627649, + //1960967267443627648 + // }; var shopService = _container.Resolve(); - shopService.GetShopOrderAmountRange(ShopIds, DateTime.Now.AddMonths(-1), DateTime.Now); + //shopService.GetShopOrderAmountRange(id, DateTime.Now.AddDays(-10)); + } + + + [Test] + public void GetShopSalesOrderTest() + { + var shopService = _container.Resolve(); + DateTime startDate = DateTime.Parse("2026/08/01"); + DateTime endDate = DateTime.Parse("2026/10/07"); + DateTime date = startDate; + while (date <= endDate) + { + Console.WriteLine($"开始:{date.ToLongDateString()}"); + try + { + shopService.ShopDailyStats(date); + } + catch + { + Console.WriteLine($"错误:{date.ToLongDateString()}"); + } + date = date.AddDays(1); + } + + + } + [Test] + public void GetBuyerDailyStatsTest() + { + var shopService = _container.Resolve(); + shopService.BuyerDailyStats(DateTime.Parse("2026/10/02"), [1960967267443620449]); + } + [Test] + public void UpdateStatsTest() + { + var shopService = _container.Resolve(); + shopService.UpdateStats(DateTime.Now.AddDays(-1).Date); + } + + [Test] + public void GetShopStatsTest() + { + var shopService = _container.Resolve(); + var data = shopService.GetShopStats(DateTime.Parse("2026/10/01"), DateTime.Parse("2026/10/07"), "18990991234", null, ShopStatsGroupType.Day); + var sb = new System.Text.StringBuilder(); + if (data == null || data.Count == 0) + { + sb.AppendLine("没有统计数据"); + } + else + { + Func yuan = fen => (fen / 100m).ToString("N2"); + + foreach (var shopId in data.Keys) + { + sb.AppendLine("==================== 商铺 " + shopId + " ===================="); + foreach (var r in data[shopId]) + { + sb.AppendLine("[日期] " + r.StatDate.ToString("yyyy-MM-dd")); + sb.AppendLine("[销售] 订单数: " + r.OrderCount + + " | 销售额: " + yuan(r.SalesAmount) + + " | 利润: " + yuan(r.ProfitAmount) + + " | 佣金: " + yuan(r.PlatformFee)); + sb.AppendLine("[售后] 退款单: " + r.RefundOrderCount + + " | 已退款: " + yuan(r.RefundDone) + + " | 退款中: " + yuan(r.RefundPending)); + sb.AppendLine("[买手] 采购单: " + r.PurchaseCount + + " | 采购额: " + yuan(r.TotalPurchase) + + " | 已退: " + yuan(r.DoneRefund) + + " | 未退: " + yuan(r.PendingRefund) + + " | 拒绝: " + yuan(r.RejectedRefund) + + " | 买手退款合计: " + yuan(r.TotalRefund)); + sb.AppendLine(); + } + } + } + Console.WriteLine(sb.ToString()); } } diff --git a/test/WebServiceTest/UserTest.cs b/test/WebServiceTest/UserTest.cs index 4f773e6..c8f6f57 100644 --- a/test/WebServiceTest/UserTest.cs +++ b/test/WebServiceTest/UserTest.cs @@ -94,7 +94,7 @@ internal class UserTest public void GetUserAmountSummate() { var userService = _container.Resolve(); - var data = userService.GetUserAmountSummary("15395107196"); + var data = userService.GetUserAmountSummary("17734871710"); Console.WriteLine(JsonConvert.SerializeObject(data)); } //[Test]