Files
KJD_WebService/KJD_WebService/Services/Shops/ShopRevenueStatisticsService.cs
T
2026-10-08 18:55:51 +08:00

823 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 商铺统计
/// </summary>
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, IUserService userService, ShopService shopService)
{
_freeSql = freeSql;
_logger = logger;
_userService = userService;
_shopService = shopService;
}
/// <summary>
/// 获取商铺统计
/// 汇总:每个商铺只返回一条(整个查询范围合计,StatDate 为查询结束日期)
/// 天/周/月:按自然日 / 自然周(周一为开始)/ 自然月聚合,每个周期一条(StatDate 为该周期的起始日期)
/// </summary>
/// <param name="startTime">统计开始时间(含)</param>
/// <param name="endTime">统计结束时间(含)</param>
/// <param name="userName">用户名(未指定商铺ID时,按该用户名下的商铺统计)</param>
/// <param name="shopIds">指定商铺ID集合(为空时按用户名统计)</param>
/// <param name="groupType">聚合维度,默认汇总</param>
/// <returns>商铺ID -> 该商铺按维度聚合后的统计列表</returns>
public Dictionary<string, List<ShopStatsResponse>> GetShopStats(DateTime startTime, DateTime endTime, string userName, List<string> shopIds = null, ShopStatsGroupType groupType = ShopStatsGroupType.Summary)
{
var result = new Dictionary<string, List<ShopStatsResponse>>();
(Dictionary<string, List<ShopDailyStatsEntity>> shopGroups, Dictionary<string, List<BuyerDailyStatsEntity>> 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<ShopDailyStatsEntity>();
bList = bList ?? new List<BuyerDailyStatsEntity>();
// 同一商铺内先按聚合维度分组(同一周期内的多条明细在下面求和)
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<ShopStatsResponse>();
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;
}
/// <summary>
/// 获取商铺销售趋势(按天,所有商铺合计)
/// 返回查询范围内每一天的销售额和订单数,没有数据的天补 0,保证日期连续
/// </summary>
/// <param name="startTime">统计开始时间(含)</param>
/// <param name="endTime">统计结束时间(含)</param>
/// <param name="userName">用户名(未指定商铺ID时,按该用户名下的商铺统计)</param>
/// <param name="shopIds">指定商铺ID集合(为空时按用户名统计)</param>
public List<ShopStatsTrendResponse> GetShopStatsTrend(DateTime startTime, DateTime endTime, string userName, List<string> shopIds = null)
{
var result = new List<ShopStatsTrendResponse>();
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;
// 所有商铺先按天合计
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))
{
if (!byDate.TryGetValue(date, out var item))
item = new ShopStatsTrendResponse { StatDate = date };
result.Add(item);
}
return result;
}
/// <summary>
/// 订单明细统一入口:有效订单 / 退款订单 / 买手未退回金额的订单,三种列表返回同一结构
/// 按订单创建时间范围 + 商铺ID查询(未指定商铺ID时按用户名下的商铺查询)
/// 有效订单:没有发生售后的订单;退款订单:发生过售后的订单;买手未退回:退款订单中买手款项还没退回的
/// 未退回:买手订单存在退款信息(退款金额 &gt; 0 或退款状态非空)且退款未成功(不是"退款成功"),
/// 未退回金额 = 这些买手订单的退款金额之和(与统计里的"未退回金额/PendingRefund"口径一致)
/// 一个订单号可能对应多个买手订单(BuyerOrders),金额单位:分
/// 与统计口径保持一致:只统计单商品订单(多商品订单视为刷单,统计时已排除)
/// </summary>
/// <param name="startTime">订单创建开始时间(含)</param>
/// <param name="endTime">订单创建结束时间(含)</param>
/// <param name="orderType">明细类型:全部 / 有效订单 / 退款订单 / 买手未退回金额的订单</param>
/// <param name="userName">用户名(未指定商铺ID时,按该用户名下的商铺查询)</param>
/// <param name="shopIds">指定商铺ID集合(为空时按用户名查询)</param>
/// <returns>按订单创建时间倒序的订单明细</returns>
public List<ShopOrderDetailResponse> GetShopOrderDetails(DateTime startTime, DateTime endTime, ShopOrderDetailType orderType = ShopOrderDetailType.All, string userName = null, List<string> shopIds = null)
{
var result = new List<ShopOrderDetailResponse>();
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<long>();
foreach (var id in ids)
{
if (long.TryParse(id, out long shopId) && !orderShopIds.Contains(shopId))
orderShopIds.Add(shopId);
}
if (orderShopIds.Count == 0) return result;
if (startTime == endTime)
{
endTime = startTime.Date.AddDays(1).AddSeconds(-1);
}
int startStamp = TimeUtils.DateTimeToTimestampSeconds(startTime);
int endStamp = TimeUtils.DateTimeToTimestampSeconds(endTime);
var orders = LoadOrders(startStamp, endStamp, orderShopIds);
if (orders.Count == 0) return result;
// 加载买手订单(直拍和中转仓都要查),并回填商铺订单的退款金额
LoadBuyerOrder(orders);
FillRefunds(orders);
foreach (var order in orders)
{
var buyerOrders = order.BuyerOrders ?? new List<BuyerOrderInfo>();
bool hasRefund = order.HasRefund;
if (orderType == ShopOrderDetailType.BuyerUnrefunded)
{
buyerOrders = buyerOrders.Where(a => !IsRefundDone(a.RefundStatusText)).ToList();
}
int unrefundedAmount = GetBuyerUnrefundedAmount(buyerOrders);
switch (orderType)
{
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();
}
/// <summary>
/// 买手未退回金额:不是"退款成功" 状态的买手订单退款金额之和
/// 只算有退款信息的(退款金额 &gt; 0 或退款状态非空),没发生退款的正常订单不计入
/// </summary>
private static int GetBuyerUnrefundedAmount(List<BuyerOrderInfo> 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);
}
/// <summary>
/// 取某个日期在指定聚合维度下所属周期的起始日期
/// </summary>
/// <param name="date">明细日期</param>
/// <param name="groupType">聚合维度</param>
/// <param name="summaryKey">汇总维度使用的分组标识</param>
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<string, List<ShopDailyStatsEntity>> shopGroups, Dictionary<string, List<BuyerDailyStatsEntity>> buyerGroups) getShopStatsByUserName(DateTime startTime, DateTime endTime, string userName)
{
var shopIds = getShopIdsByUserName(userName);
if (shopIds == null) return (null, null);
return getShopStats(startTime, endTime, shopIds);
}
/// <summary>按用户名取商铺ID集合,取不到返回 null</summary>
private List<string> 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;
}
/// <summary>加载指定商铺在某时间范围内的商铺日统计</summary>
private List<ShopDailyStatsEntity> loadShopStats(DateTime startTime, DateTime endTime, List<string> shopIds)
{
return _freeSql.Select<ShopDailyStatsEntity>()
.Where(a => shopIds.Contains(a.ShopId))
.Where(a => a.StatDate >= startTime)
.Where(a => a.StatDate <= endTime)
.ToList();
}
private (Dictionary<string, List<ShopDailyStatsEntity>> shopGroups, Dictionary<string, List<BuyerDailyStatsEntity>> buyerGroups) getShopStats(DateTime startTime, DateTime endTime, List<string> shopIds)
{
if (shopIds == null || shopIds.Count() == 0) return (null, null);
var shopStats = loadShopStats(startTime, endTime, shopIds);
var buyerStats = _freeSql.Select<BuyerDailyStatsEntity>()
.Where(a => shopIds.Contains(a.ShopId))
.Where(a => a.StatDate >= startTime)
.Where(a => a.StatDate <= endTime)
.ToList();
// 先按商铺分组
Dictionary<string, List<ShopDailyStatsEntity>> shopGroups = shopStats.GroupBy(x => x.ShopId).ToDictionary(g => g.Key, g => g.ToList());
Dictionary<string, List<BuyerDailyStatsEntity>> buyerGroups = buyerStats.GroupBy(x => x.ShopId).ToDictionary(g => g.Key, g => g.ToList());
return (shopGroups, buyerGroups);
}
/// <summary>商铺统计指定天(同时会统计买手数据)</summary>
public void ShopDailyStats(DateTime date, List<long> shopId = null)
{
var orders = LoadOrders(date, shopId);
LoadBuyerOrder(orders);
FillRefunds(orders);
CalcProfit(orders);
SaveShopStats(date.Date, orders);
SaveBuyerStats(date.Date, orders);
}
/// <summary>买手统计指定天</summary>
public void BuyerDailyStats(DateTime date, List<long> shopId = null)
{
var orders = LoadOrders(date, shopId);
LoadBuyerOrder(orders);
SaveBuyerStats(date.Date, orders);
}
/// <summary>
/// 更新
/// </summary>
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<string> allUpdateOrderIds = new List<string>();
//获取新增订单
allUpdateOrderIds.AddRange(_freeSql.Select<WeiOrderEntity>()
.Where(a => a.CreateTime >= startUnix && a.CreateTime < endUnix)
.ToList(a => a.OrderId));
//获取退款信息
allUpdateOrderIds.AddRange(_freeSql.Select<WeiRefundEntity>()
.Where(a => a.UpdateTime >= startUnix && a.UpdateTime < endUnix)
.ToList(a => a.OrderId));
//获取买手购买
//云仓
allUpdateOrderIds.AddRange(_freeSql.Select<WeiExternalProcurementLogEntity>()
.Where(a => a.CreatedTime >= startDate && a.CreatedTime < endDate)
.ToList(a => a.OrderNo));
//直发
allUpdateOrderIds.AddRange(_freeSql.Select<OrderPddBuyerOrderEntity>()
.Where(a => a.AddTime >= startDate && a.AddTime < endDate)
.ToList(a => a.WxOrderId));
//买手退款
allUpdateOrderIds.AddRange(_freeSql.Select<OrderPddBuyerAfterSaleEntity>()
.Where(a => a.MdyTime >= startDate && a.MdyTime < endDate)
.ToList(a => a.WxOrderId));
allUpdateOrderIds = allUpdateOrderIds.Distinct().ToList();//合并
List<UpdateStatsInfo> allUpdateStats = new List<UpdateStatsInfo>();
if (allUpdateOrderIds.Count > 0)
{
//查询订单归属商铺和时间
foreach (string[] batch in Batch(allUpdateOrderIds, BatchSize))
{
allUpdateStats.AddRange(_freeSql.Select<WeiOrderEntity>()
.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)))
{
var list = allUpdateStats.Where(a => a.OrderCreateDate < DateTime.Now.AddMonths(-1)).ToList();
}
}
}
//重新计算指定商铺指定时间
if (allUpdateStats.Count > 0)
{
// 1. 按日期分组,找出"ShopId 数量 > 80%"的日期
Dictionary<DateTime, List<long>> 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)
{
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);
}
}
}
#region 数据加载
/// <summary>加载某天的订单(只保留单商品订单)</summary>
private List<OrderInfo> LoadOrders(DateTime date, List<long> shopId)
{
int start = TimeUtils.DateTimeToTimestampSeconds(date.Date); // 当天0点
int end = start + 86400; // 次日0点
return LoadOrders(start, end, shopId);
}
/// <summary>
/// 按时间范围加载订单(秒级时间戳,左闭右开 [startStamp, endStamp))
/// 只保留单商品订单;shopIds 为空时不限制商铺
/// </summary>
private List<OrderInfo> LoadOrders(int startStamp, int endStamp, List<long> shopIds)
{
var query = _freeSql.Select<WeiOrderEntity, WeiOrderItemEntity>()
.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<OrderInfo>();
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
{
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;
}
/// <summary>
/// 加载买手订单信息
/// </summary>
/// <param name="orders">商铺订单</param>
/// <param name="byPurchaseType">
/// true:按订单的采购类型区分直拍 / 中转仓,两张买手表都能查到(查询明细用)
/// false:沿用历史口径,按 OutAmount 是否为 0 判断(统计任务保持原行为不变)
/// </param>
private void LoadBuyerOrder(List<OrderInfo> orders)
{
var result = new List<BuyerOrderInfo>();
//查询中转仓购买表
var outWarehouseOrderIds = orders
.Where(a => a.PurchaseType == PurchaseType.FromWarehouse)
.Select(a => a.OrderId).Distinct().ToList();
if (outWarehouseOrderIds.Count > 0)
{
//查询中转仓购买表
foreach (string[] batch in Batch(outWarehouseOrderIds, BatchSize))
{
result.AddRange(_freeSql.Select<WeiExternalProcurementLogEntity>()
.Where(a => batch.Contains(a.OrderNo))
.OrderBy(a => a.Id) //
.ToList(a => new BuyerOrderInfo
{
Id = a.Id,
PurchaseType = PurchaseType.FromWarehouse,
OrderId = a.OrderNo,
SkuId = a.OrderSkuId,
OutOrderSn = a.OutOrderNo,
OutAmount = a.OutAmount ?? 0,
}));
}
}
//查询直拍表
var outDirectOrderIds = orders
.Where(a => a.PurchaseType == PurchaseType.DirectPurchase)
.Select(a => a.OrderId).Distinct().ToList();
foreach (string[] batch in Batch(outDirectOrderIds, BatchSize))
{
result.AddRange(_freeSql.Select<OrderPddBuyerOrderEntity>()
.Where(a => a.WxSendType == 1)
.Where(a => batch.Contains(a.WxOrderId))
.ToList(a => new BuyerOrderInfo
{
Id = a.Id,
PurchaseType = PurchaseType.DirectPurchase,
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<BuyerOrderInfo>();
foreach (var batch in Batch(ids, BatchSize))
{
refundlist.AddRange(_freeSql.Select<OrderPddBuyerAfterSaleEntity>()
.Where(a => batch.Contains(a.WxOrderId))
.OrderBy(a => a.Id)
.ToList(a => new BuyerOrderInfo
{
RefundId = a.Id,
OrderId = a.WxOrderId,
SkuId = a.WxSkuId,
OutOrderSn = a.OrderSn,
OutAmount = a.OrderAmount ?? 0,
RefundAmount = a.RefundAmount ?? (a.OrderAmount ?? 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.RefundId = r.RefundId;
buyerOrder.RefundAmount = r.RefundAmount;
buyerOrder.RefundStatusText = r.RefundStatusText;
}
else
{
o.BuyerOrders.Add(r);
}
}
}
//移除未购买的订单
for (int i = orders.Count - 1; i >= 0; i--)
{
if (orders[i].BuyerOrders.Count == 0)
orders.RemoveAt(i);
}
}
/// <summary>回填商铺订单的退款信息</summary>
private void FillRefunds(List<OrderInfo> 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<OrderRefundInfo>();
foreach (var batch in Batch(refundOrderIds, BatchSize))
{
allRefunds.AddRange(_freeSql.Select<WeiRefundEntity>()
.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 计算与保存
/// <summary>
/// 计算利润
/// </summary>
/// <param name="orders"></param>
private static void CalcProfit(List<OrderInfo> 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<OrderInfo> orders)
{
var statsList = new List<ShopDailyStatsEntity>();
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<ShopDailyStatsEntity>()
.SetSource(statsList, a => new { a.ShopId, a.StatDate })
.ExecuteAffrows();
}
private void SaveBuyerStats(DateTime statDate, List<OrderInfo> orders)
{
if (orders == null || orders.Count == 0) return;
var statsList = new List<BuyerDailyStatsEntity>();
// 按店铺分组,每个店铺下展开所有买手订单
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();
if (buyerOrders.Any(a => a.OutOrderSn == "261007-181907044681346"))
{
}
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<BuyerDailyStatsEntity>()
.SetSource(statsList, a => new { a.ShopId, a.StatDate })
.ExecuteAffrows();
}
#endregion
#region 辅助
/// <summary>.NET Framework 没有 Enumerable.Chunk,自己分批</summary>
private static IEnumerable<string[]> Batch(IList<string> 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
}