823 lines
28 KiB
C#
823 lines
28 KiB
C#
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时按用户名下的商铺查询)
|
||
/// 有效订单:没有发生售后的订单;退款订单:发生过售后的订单;买手未退回:退款订单中买手款项还没退回的
|
||
/// 未退回:买手订单存在退款信息(退款金额 > 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>
|
||
/// 买手未退回金额:不是"退款成功" 状态的买手订单退款金额之和
|
||
/// 只算有退款信息的(退款金额 > 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
|
||
|
||
} |