diff --git a/KJD_WebService/BLL/WXXD/WXShopRevenueStatisticsManage.cs b/KJD_WebService/BLL/WXXD/WXShopRevenueStatisticsManage.cs index 5d8dbef..5880ae3 100644 --- a/KJD_WebService/BLL/WXXD/WXShopRevenueStatisticsManage.cs +++ b/KJD_WebService/BLL/WXXD/WXShopRevenueStatisticsManage.cs @@ -84,6 +84,7 @@ namespace KJD_Web.BLL.WXXD sb.Append("A.profit, "); sb.Append("A.purchase_status, "); sb.Append("A.out_amount, "); + sb.Append("A.out_order_no,"); sb.Append("B.refund_status, "); sb.Append("c.`status` as refund_status_string, "); sb.Append("IFNULL(C.refund_amount, 0) AS refund_amount "); @@ -178,15 +179,18 @@ namespace KJD_Web.BLL.WXXD NullValueHandling = NullValueHandling.Ignore }); } - /// /// 将订单行转换为金额详情:out_amount 为空或0时从中转仓订单表补全,并计算利润 /// + /// + /// 中转仓订单金额字典表 + /// private WXOrderAmountInfo BuildOrderAmountInfo(DataRow row, Dictionary transitOutAmountMap) { string orderId = row["order_id"].ParseToString(); int outAmount = row["out_amount"].ParseToInt(); + string out_order_no = row["out_order_no"].ParseToString(); int externalProcurement = 0; if (outAmount == 0) { @@ -197,6 +201,10 @@ namespace KJD_Web.BLL.WXXD outAmount = transitAmount; } } + if (!string.IsNullOrEmpty(out_order_no)) + { + externalProcurement = 1; + } int orderPrice = row["order_price"].ParseToInt(); int predictCommissionFee = row["predict_commission_fee"].ParseToInt(); @@ -226,6 +234,7 @@ namespace KJD_Web.BLL.WXXD profit = profit, purchase_status = row["purchase_status"].ParseToInt(), out_amount = outAmount, + out_order_no = out_order_no, refund_status = row["refund_status"].ParseToInt(), refund_status_string = row["refund_status_string"].ParseToString(), refund_amount = row["refund_amount"].ParseToInt(), @@ -234,7 +243,7 @@ namespace KJD_Web.BLL.WXXD } /// - /// 买手号退款金额:退款订单(refund_status>1)且非外采时,从买手售后表(wxxd.order_pdd_buyer_after_sale)查询退款金额(refundAmount)填入 + /// 直拍买手号退款金额:退款订单(refund_status>1)且非外采时,从买手售后表(wxxd.order_pdd_buyer_after_sale)查询退款金额(refundAmount)填入 /// private void FillBuyerRefundAmount(List orderList, string conn) { diff --git a/KJD_WebService/KJD_WebService.csproj b/KJD_WebService/KJD_WebService.csproj index 14fa6d8..1854533 100644 --- a/KJD_WebService/KJD_WebService.csproj +++ b/KJD_WebService/KJD_WebService.csproj @@ -266,6 +266,7 @@ + diff --git a/KJD_WebService/Model/WxModel/WXOrderAmountInfo.cs b/KJD_WebService/Model/WxModel/WXOrderAmountInfo.cs index 79f3bd7..380eee0 100644 --- a/KJD_WebService/Model/WxModel/WXOrderAmountInfo.cs +++ b/KJD_WebService/Model/WxModel/WXOrderAmountInfo.cs @@ -67,7 +67,16 @@ namespace KJD_Web.Model.WxModel /// public int out_amount { get; set; } /// + /// 外采订单号 + /// + public string out_order_no { get; set; } + /// /// 售后状态(订单明细 refund_status) + /// 售后状态 1:无售后或售后关闭, + /// 2:售后处理中, + /// 3:退款中, + /// 4:退款成功 + /// 11 退款完成订单已取消 /// public int refund_status { get; set; } /// diff --git a/KJD_WebService/Services/Shops/ShopRevenueStatisticsService.cs b/KJD_WebService/Services/Shops/ShopRevenueStatisticsService.cs new file mode 100644 index 0000000..0061434 --- /dev/null +++ b/KJD_WebService/Services/Shops/ShopRevenueStatisticsService.cs @@ -0,0 +1,151 @@ +using KJD_Web.Model.WxModel; +using KJD_Web.Utils; +using MAX.Core.DependencyInjection; +using MAX.Core.Logger; +using Store.Contracts.Order.Entitys; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace KJD_Web.Services.Shops; + +/// +/// 商铺统计 +/// +public class ShopRevenueStatisticsService : ITransientDependency +{ + private readonly IFreeSql _freeSql; + + public ShopRevenueStatisticsService(IFreeSql freeSql, ILoggerFacade logger) + { + _freeSql = freeSql; + } + + public void GetShopOrderAmountRange(List shopIds, DateTime startTime, DateTime endTime) + { + int startTimestamp = TimeUtils.DateTimeToTimestampSeconds(startTime); + int endTimestamp = TimeUtils.DateTimeToTimestampSeconds(endTime); + + + // 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 + { + 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 + }); + + // 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 + })); + } + + // 3. 内存里 group,每个订单取 id 最大的那条 + var latestRefund = refunds + .GroupBy(r => r.OrderId) + .ToDictionary(g => g.Key, g => g.OrderByDescending(r => r.Id).First()); + + // 4. 回填(等价 LEFT JOIN + IFNULL) + foreach (var o in orders) + { + if (latestRefund.TryGetValue(o.order_id, out var r)) + { + o.refund_status_string = r.Status; + o.refund_amount = r.RefundAmount.Value; + } + else + { + o.refund_amount = 0; + } + } + + + var transitOutAmountMap = new Dictionary(); + // needLookupOrderIds:先跑一遍订单列表,out_amount 为空或0的订单号 + var needLookupOrderIds = orders + .Where(o => o.out_amount == 0) + .Select(o => o.order_id) + .Distinct() + .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) + { + if (string.IsNullOrEmpty(t.OrderNo)) continue; + if (!transitOutAmountMap.ContainsKey(t.OrderNo)) + transitOutAmountMap[t.OrderNo] = (int)(t.OutAmount ?? 0); + } + } + } + + + foreach (var o in orders) + { + // 1. 外采金额补全 + if (o.out_amount == 0 && transitOutAmountMap.TryGetValue(o.order_id, out var transitAmount)) + { + o.external_procurement = 1; + o.out_amount = transitAmount; + } + + // 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)//退款利润计算 + { + o.profit = o.order_price - o.refund_amount; + } + } + + + } +} \ No newline at end of file diff --git a/Store.Contracts/Order/Entitys/WeiExternalProcurementLogEntity.cs b/Store.Contracts/Order/Entitys/WeiExternalProcurementLogEntity.cs new file mode 100644 index 0000000..5aeb931 --- /dev/null +++ b/Store.Contracts/Order/Entitys/WeiExternalProcurementLogEntity.cs @@ -0,0 +1,285 @@ +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.oms_wei_external_procurement_log")] +public class WeiExternalProcurementLogEntity +{ + /// + /// 主键ID + /// + [Column(Name = "id", IsPrimary = true, IsIdentity = true)] + public long Id { get; set; } + + /// + /// 外部订单号 + /// + [Column(Name = "out_order_no")] + public string OutOrderNo { get; set; } + + /// + /// 平台订单号 + /// + [Column(Name = "order_no")] + public string OrderNo { get; set; } + + /// + /// 订单sku id + /// + [Column(Name = "order_sku_id")] + public string OrderSkuId { get; set; } + + /// + /// 买家账号 + /// + [Column(Name = "buyer_account")] + public string BuyerAccount { get; set; } + + /// + /// 外部金额(分) + /// + [Column(Name = "out_amount")] + public int? OutAmount { get; set; } + + /// + /// 采购人备注 + /// + [Column(Name = "purchaser_note")] + public string PurchaserNote { get; set; } + + /// + /// 快递公司1 + /// + [Column(Name = "express_1_company")] + public string Express1Company { get; set; } + + /// + /// 快递单号1 + /// + [Column(Name = "express_1_waybill")] + public string Express1Waybill { get; set; } + + /// + /// 快递公司2 + /// + [Column(Name = "express_2_company")] + public string Express2Company { get; set; } + + /// + /// 快递单号2 + /// + [Column(Name = "express_2_waybill")] + public string Express2Waybill { get; set; } + + /// + /// 退款状态 + /// + [Column(Name = "refund_status")] + public string RefundStatus { get; set; } + + /// + /// 取货时间 + /// + [Column(Name = "pickup_time")] + public DateTime? PickupTime { get; set; } + + /// + /// 中转仓状态:0 未使用,1 已创建,2 取号失败,3 处理中,4创建异常 + /// + [Column(Name = "transfer_warehouse_status")] + public byte? TransferWarehouseStatus { get; set; } + + /// + /// 取号失败原因 + /// + [Column(Name = "raw_message")] + public string RawMessage { get; set; } + + /// + /// 创建时间 + /// + [Column(Name = "created_time")] + public DateTime? CreatedTime { get; set; } + + /// + /// 更新时间 + /// + [Column(Name = "updated_time")] + public DateTime? UpdatedTime { get; set; } + + /// + /// 中转仓库编码 + /// + [Column(Name = "platform")] + public string Platform { get; set; } + + /// + /// 订单平台 + /// + [Column(Name = "order_platform_code")] + public string OrderPlatformCode { get; set; } + + /// + /// 唯一值编码 + /// + [Column(Name = "order_code")] + public string OrderCode { get; set; } + + /// + /// 1正常取号 2提前取号 3取号更正 + /// + [Column(Name = "pre_allocated_order_no")] + public int? PreAllocatedOrderNo { get; set; } + + /// + /// 是否转寄:0否,1是 + /// + [Column(Name = "is_transfer_ship")] + public byte IsTransferShip { get; set; } + + /// + /// 随机溯源码 + /// + [Column(Name = "random_code")] + public string RandomCode { get; set; } + + /// + /// 中转仓售后状态 + /// + [Column(Name = "transfer_refund_status")] + public int? TransferRefundStatus { get; set; } + + /// + /// 订单作废状态:0=正常,1=已作废,2=作废失败 + /// + [Column(Name = "order_void_status")] + public byte OrderVoidStatus { get; set; } + + /// + /// 虚拟单号 + /// + [Column(Name = "virtual_waybill_no")] + public string VirtualWaybillNo { get; set; } + + /// + /// 虚拟快递公司 + /// + [Column(Name = "virtual_express_company")] + public string VirtualExpressCompany { get; set; } + + /// + /// 0: 未入库, 1: 已入库, 2: 已出库, 3:拦截到仓 + /// + [Column(Name = "warehouse_status")] + public int WarehouseStatus { get; set; } + + /// + /// 入库时间 + /// + [Column(Name = "inbound_time")] + public DateTime? InboundTime { get; set; } + + /// + /// 出库时间 + /// + [Column(Name = "outbound_time")] + public DateTime? OutboundTime { get; set; } + + /// + /// 是否有退货: 0=无,1=有 + /// + [Column(Name = "has_return")] + public byte? HasReturn { get; set; } + + /// + /// 订单初始金额(单位:分) + /// + [Column(Name = "postage")] + public int? Postage { get; set; } + + /// + /// 重试取号是否成功:0=未尝试 1=成功 2=失败 + /// + [Column(Name = "retry_acquire_success")] + public byte RetryAcquireSuccess { get; set; } + + /// + /// 超重费用,单位:分 + /// + [Column(Name = "over_weight_fee")] + public int? OverWeightFee { get; set; } + + /// + /// 99拒收成功退回 0未申请拒收 1已申请拒收 2申请拒收失败 3申请拒收成功(不一定真成功) 5取消拒收 + /// + [Column(Name = "has_reject")] + public byte HasReject { get; set; } + + /// + /// 拦截到仓时间 + /// + [Column(Name = "blocked_arrival_time")] + public DateTime? BlockedArrivalTime { get; set; } + + /// + /// 拒收时间 + /// + [Column(Name = "rejected_time")] + public DateTime? RejectedTime { get; set; } + + /// + /// 拦截退回状态:0未处理,1已退回,2退回失败 + /// + [Column(Name = "intercept_return_status")] + public int? InterceptReturnStatus { get; set; } + + /// + /// 拦截退回时间 + /// + [Column(Name = "intercept_return_time")] + public DateTime? InterceptReturnTime { get; set; } + + /// + /// 拦截退回操作信息/回调信息 + /// + [Column(Name = "intercept_return_info")] + public string InterceptReturnInfo { get; set; } + + /// + /// 拦截改地址状态:0未处理,1修改成功,2修改失败 + /// + [Column(Name = "intercept_address_status")] + public byte? InterceptAddressStatus { get; set; } + + /// + /// 拦截改地址时间 + /// + [Column(Name = "intercept_address_time")] + public DateTime? InterceptAddressTime { get; set; } + + /// + /// 拦截改地址操作信息/回调信息 + /// + [Column(Name = "intercept_address_info")] + public string InterceptAddressInfo { get; set; } + + /// + /// 是否手动发货:0自动发货,1手动发货 + /// + [Column(Name = "is_manual_shipment")] + public int IsManualShipment { get; set; } + + /// + /// 手动发货处理状态:0未处理,1成功,2失败 + /// + [Column(Name = "manual_shipment_status")] + public int? ManualShipmentStatus { get; set; } +} \ No newline at end of file diff --git a/Store.Contracts/Order/Entitys/WeiRefundEntity.cs b/Store.Contracts/Order/Entitys/WeiRefundEntity.cs new file mode 100644 index 0000000..bf9a1ac --- /dev/null +++ b/Store.Contracts/Order/Entitys/WeiRefundEntity.cs @@ -0,0 +1,237 @@ +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 = "erp.oms_wei_refund")] +public class WeiRefundEntity +{ + /// + /// id + /// + [Column(Name = "id", IsPrimary = true, IsIdentity = true)] + public long Id { get; set; } + + /// + /// 店铺id + /// + [Column(Name = "shop_id")] + public long? ShopId { get; set; } + + /// + /// 售后单号 + /// + [Column(Name = "after_sale_order_id")] + public string AfterSaleOrderId { get; set; } + + /// + /// 售后单当前状态,参考: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 待用户处理商家代发起的售后申请 + /// + [Column(Name = "status")] + public string Status { get; set; } + + /// + /// 买家身份标识 + /// + [Column(Name = "openid")] + public string Openid { get; set; } + + /// + /// 买家在开放平台的唯一标识符,若当前视频号小店已绑定到微信开放平台账号下会返回 + /// + [Column(Name = "unionid")] + public string Unionid { get; set; } + + /// + /// 商品spuid + /// + [Column(Name = "product_id")] + public string ProductId { get; set; } + + /// + /// 商品skuid + /// + [Column(Name = "sku_id")] + public string SkuId { get; set; } + + /// + /// 售后数量 + /// + [Column(Name = "count")] + public int? Count { get; set; } + + /// + /// 是否极速退款RefundInfo + /// + [Column(Name = "fast_refund")] + public string FastRefund { get; set; } + + /// + /// 标明售后单退款直接原因, 枚举值参考 RefundReason + /// + [Column(Name = "refund_reason")] + public int? RefundReason { get; set; } + + /// + /// 退款金额(分) + /// + [Column(Name = "refund_amount")] + public int? RefundAmount { get; set; } + + /// + /// 快递单号 + /// + [Column(Name = "return_waybill_id")] + public string ReturnWaybillId { get; set; } + + /// + /// 物流公司id + /// + [Column(Name = "return_delivery_id")] + public string ReturnDeliveryId { get; set; } + + /// + /// 物流公司名称 + /// + [Column(Name = "return_delivery_name")] + public string ReturnDeliveryName { get; set; } + + /// + /// 售后相关商品信息JSON + /// + [Column(Name = "merchant_upload_info")] + public string MerchantUploadInfo { get; set; } + + /// + /// 售后单创建时间戳 + /// + [Column(Name = "create_time")] + public int? CreateTime { get; set; } + + /// + /// 售后单更新时间戳 + /// + [Column(Name = "update_time")] + public int? UpdateTime { get; set; } + + /// + /// 退款原因 + /// + [Column(Name = "reason")] + public string Reason { get; set; } + + /// + /// 退款原因解释 + /// + [Column(Name = "reason_text")] + public string ReasonText { get; set; } + + /// + /// 售后类型。REFUND:退款;RETURN:退货退款。 + /// + [Column(Name = "type")] + public string Type { get; set; } + + /// + /// 订单号,该字段可用于获取订单 + /// + [Column(Name = "order_id")] + public string OrderId { get; set; } + + /// + /// detail json + /// + [Column(Name = "details")] + public string Details { get; set; } + + /// + /// 纠纷id,该字段可用于获取纠纷信息 + /// + [Column(Name = "complaint_id")] + public string ComplaintId { get; set; } + + /// + /// 微信支付退款的响应 + /// + [Column(Name = "refund_resp")] + public string RefundResp { get; set; } + + /// + /// 推送状态(0未推送;1推送成功;2推送失败) + /// + [Column(Name = "pull_status")] + public int? PullStatus { get; set; } + + /// + /// 订单审核时间 + /// + [Column(Name = "pull_time")] + public DateTime? PullTime { get; set; } + + /// + /// 商品id(o_goods外键) + /// + [Column(Name = "erp_goods_id")] + public long ErpGoodsId { get; set; } + + /// + /// 商品skuid(o_goods_sku外键) + /// + [Column(Name = "erp_goods_sku_id")] + public long ErpGoodsSkuId { get; set; } + + /// + /// 商户id + /// + [Column(Name = "merchant_id")] + public long MerchantId { get; set; } + + /// + /// 仓库受理状态 + /// + [Column(Name = "warehouse_status")] + public int WarehouseStatus { get; set; } + + /// + /// 退货收件人姓名 + /// + [Column(Name = "return_user_name")] + public string ReturnUserName { get; set; } + + /// + /// 退货省份 + /// + [Column(Name = "return_province")] + public string ReturnProvince { get; set; } + + /// + /// 退货城市 + /// + [Column(Name = "return_city")] + public string ReturnCity { get; set; } + + /// + /// 退货区县 + /// + [Column(Name = "return_county")] + public string ReturnCounty { get; set; } + + /// + /// 退货详细地址 + /// + [Column(Name = "return_detail")] + public string ReturnDetail { get; set; } + + /// + /// 退货联系电话 + /// + [Column(Name = "return_tel")] + public string ReturnTel { get; set; } +} \ No newline at end of file diff --git a/test/WebServiceTest/ShopTest.cs b/test/WebServiceTest/ShopTest.cs new file mode 100644 index 0000000..a504214 --- /dev/null +++ b/test/WebServiceTest/ShopTest.cs @@ -0,0 +1,79 @@ +using Autofac; +using Autofac.Integration.WebApi; +using Autofac.Integration.WebApi; +using KJD_Web; +using KJD_Web; +using KJD_Web.Services.Shops; +using KJD_Web.Services.Users; +using Newtonsoft.Json; +using Store.Framework.DependencyInjection; +using Store.Framework.DependencyInjection; +using System; +using System; +using System.Collections.Generic; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Timers; +using System.Web.Http; + + +namespace WebServiceTest; + +internal class ShopTest +{ + private static NUnitDebugListener? _listener; + private IContainer _container; + [SetUp] + public void Setup() + { + _listener = new NUnitDebugListener(); + Debug.Listeners.Add(_listener); + var config = new HttpConfiguration(); + _container = ContainerConfig.BuildContainer(); + config.DependencyResolver = new AutofacWebApiDependencyResolver(_container); + WebApiConfig.Register(config); + } + [TearDown] + public void TearDown() + { + _container?.Dispose(); + _listener.Dispose(); + } + + [Test] + public void TestAm() + { + 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); + } +}