619 lines
24 KiB
C#
619 lines
24 KiB
C#
using _29WxOrderWpf.Ctrls.Statistics.Models;
|
||
using AllBase;
|
||
using AllBase.Helper;
|
||
using Newtonsoft.Json;
|
||
using Newtonsoft.Json.Linq;
|
||
using Newtonsoft.Json.Serialization;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using Work;
|
||
using WxOrder.Framework.Commons;
|
||
|
||
namespace _29WxOrderWpf.Ctrls.Statistics.Services
|
||
{
|
||
/// <summary>
|
||
/// 店铺营收统计 - 数据服务调用接口。
|
||
/// 页面只依赖此接口,接入真实数据源时新建实现类(如对接 HTTP 接口 / SQL 查询),
|
||
/// 并通过 ShopRevenueStatisticsViewModel 构造函数注入即可,无需改动页面。
|
||
/// </summary>
|
||
public interface IShopRevenueStatisticsService
|
||
{
|
||
/// <summary>
|
||
/// 查询原始统计明细(接口返回的 A.* / B.* / c.* 原始字段)。
|
||
/// 真实实现通常通过 SQL / HTTP 一次性返回这些行,再由聚合器计算。
|
||
/// </summary>
|
||
Task<List<ShopRevenueStatisticsRow>> QueryRowsAsync(ShopRevenueStatisticsQuery query);
|
||
|
||
/// <summary>获取顶部汇总卡片数据</summary>
|
||
Task<ShopRevenueSummary> GetSummaryAsync(ShopRevenueStatisticsQuery query);
|
||
|
||
/// <summary>获取销售趋势(按天)</summary>
|
||
Task<List<RevenueTrendPoint>> GetSalesTrendAsync(ShopRevenueStatisticsQuery query);
|
||
|
||
/// <summary>获取店铺销售占比(饼图)</summary>
|
||
Task<List<ShopSalesProportionItem>> GetShopSalesProportionAsync(ShopRevenueStatisticsQuery query);
|
||
|
||
/// <summary>获取店铺统计表格</summary>
|
||
Task<List<ShopStatisticsItem>> GetShopStatisticsAsync(ShopRevenueStatisticsQuery query);
|
||
|
||
/// <summary>获取天/周/月周期统计表格(每天/每周/每月各一行,按所选店铺或全部店铺统计)</summary>
|
||
Task<List<ShopPeriodStatisticsItem>> GetPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType);
|
||
|
||
/// <summary>获取店铺下拉列表(用于查询条件)</summary>
|
||
Task<List<ShopOption>> GetShopOptionsAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统计聚合器:把接口返回的原始明细行聚合成页面所需的汇总/趋势/占比/表格数据。
|
||
/// 聚合口径说明(依据接口字段描述):
|
||
/// - 订单状态 status:10-待付款;20-待发货;21-部分发货;30-待收货;100-完成;
|
||
/// 200-全部商品售后之后订单取消;250-未付款用户主动取消或超时未付款订单自动取消。
|
||
/// - 有效订单:已付款(20/21/30/100)且未退款;10/250 未付款不计入销售额。
|
||
/// - 已退款:c.refund_status 为 4/11,或订单状态为 200(全部商品售后后取消)。
|
||
/// - 金额字段单位均为“分”,除以 100 转为元。
|
||
/// </summary>
|
||
public static class RevenueStatisticsAggregator
|
||
{
|
||
/// <summary>订单状态 - 待付款</summary>
|
||
public const int StatusPendingPay = 10;
|
||
|
||
/// <summary>订单状态 - 待发货</summary>
|
||
public const int StatusWaitSend = 20;
|
||
|
||
/// <summary>订单状态 - 部分发货</summary>
|
||
public const int StatusPartSend = 21;
|
||
|
||
/// <summary>订单状态 - 待收货</summary>
|
||
public const int StatusWaitReceive = 30;
|
||
|
||
/// <summary>订单状态 - 完成</summary>
|
||
public const int StatusFinished = 100;
|
||
|
||
/// <summary>订单状态 - 全部商品售后之后订单取消</summary>
|
||
public const int StatusCancelAfterAfterSale = 200;
|
||
|
||
/// <summary>订单状态 - 未付款用户主动取消/超时未付款自动取消</summary>
|
||
public const int StatusCancelNotPay = 250;
|
||
|
||
/// <summary>是否已付款的订单(20/21/30/100;10 待付款、250 未付款取消不计入)</summary>
|
||
public static bool IsPaidOrder(ShopRevenueStatisticsRow row)
|
||
{
|
||
if (row == null) return false;
|
||
return row.Status == StatusWaitSend
|
||
|| row.Status == StatusPartSend
|
||
|| row.Status == StatusWaitReceive
|
||
|| row.Status == StatusFinished;
|
||
}
|
||
|
||
/// <summary>是否已退款(c.refund_status 为 4/11,或订单状态为 200 全部商品售后后取消)</summary>
|
||
public static bool IsRefunded(ShopRevenueStatisticsRow row)
|
||
{
|
||
if (row == null) return false;
|
||
return row.RefundStatus > 1 || row.RefundStatusString == "MERCHANT_REFUND_SUCCESS" || row.RefundStatusString == "MERCHANT_RETURN_SUCCESS";
|
||
}
|
||
|
||
/// <summary>是否有效订单(已付款且未退款)</summary>
|
||
public static bool IsValidOrder(ShopRevenueStatisticsRow row)
|
||
{
|
||
return !IsRefunded(row);
|
||
}
|
||
/// <summary>买手号退款成功</summary>
|
||
public static bool IsBuyerRefundSuccessful(ShopRevenueStatisticsRow row)
|
||
{
|
||
return row.BuyerRefundStatus == "退款成功";
|
||
}
|
||
private static List<ShopRevenueStatisticsRow> GetPaymentOrderList(IEnumerable<ShopRevenueStatisticsRow> rows)
|
||
{
|
||
return (rows ?? new List<ShopRevenueStatisticsRow>()).Where(a => a.OutAmount > 0).ToList(); //买手付钱后的订单数量
|
||
}
|
||
/// <summary>汇总卡片</summary>
|
||
public static ShopRevenueSummary GetSummary(IEnumerable<ShopRevenueStatisticsRow> rows)
|
||
{
|
||
var list = GetPaymentOrderList(rows); //买手付钱后的订单数量
|
||
var valid = list.Where(IsValidOrder).ToList();
|
||
var refunded = list.Where(IsRefunded).ToList();
|
||
|
||
decimal salesAmount = valid.Sum(r => (decimal)(r.OrderPrice)) / 100m;//有效订单总金额
|
||
decimal profit = valid.Concat(refunded).Sum(r => (decimal)(r.Profit)) / 100m;
|
||
decimal refundAmount = refunded.Sum(r => (decimal)(r.OrderPrice)) / 100m;
|
||
decimal platformFeeTotal = valid.Sum(r => (decimal)(r.PredictCommissionFee)) / 100m;
|
||
decimal commission = valid.Sum(r => (decimal)(r.CommissionAmount)) / 100m;
|
||
decimal purchaseAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m;
|
||
decimal buyerPaymentAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号付款金额(采购金额)
|
||
decimal buyerRefundAmount = refunded.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号退款金额(采购退款金额)
|
||
decimal buyerRefundSuccessedAmount = refunded.Where(IsBuyerRefundSuccessful).Sum(r => (decimal)(r.BuyerRefundAmount)) / 100m; //买手号退款成功金额
|
||
|
||
int validOrderCount = valid.Select(r => r.OrderId).Distinct().Count();
|
||
|
||
// 总订单数(退款率分母)= 已付款订单 + 已退款订单,不含未付款的 10/250
|
||
int totalOrderCount = list.Count();
|
||
int refundOrderCount = refunded.Select(r => r.OrderId).Distinct().Count();
|
||
Debug.WriteLine($"计算利润:{(salesAmount - buyerPaymentAmount - platformFeeTotal)}");
|
||
Debug.WriteLine($"正常订单利润:{valid.Sum(r => (decimal)(r.Profit))/100}");
|
||
Debug.WriteLine($"售后订单利润:{refunded.Sum(r => (decimal)(r.Profit))/100}");
|
||
return new ShopRevenueSummary
|
||
{
|
||
SalesAmount = salesAmount,
|
||
ValidOrderCount = validOrderCount,
|
||
Profit = profit,
|
||
RefundAmount = refundAmount,
|
||
RefundOrderCount = refundOrderCount,
|
||
PlatformFeeTotal = platformFeeTotal,
|
||
Commission = commission,
|
||
AvgOrderAmount = validOrderCount == 0 ? 0m : salesAmount / validOrderCount,
|
||
RefundRate = totalOrderCount == 0 ? 0m : (decimal)refundOrderCount * 100m / totalOrderCount,
|
||
AverageProfitRate = purchaseAmount == 0m ? 0m : profit / purchaseAmount * 100m,
|
||
BuyerPaymentAmount = buyerPaymentAmount,
|
||
BuyerRefundAmount = buyerRefundAmount,
|
||
BuyerRefundSuccessedAmount = buyerRefundSuccessedAmount,
|
||
};
|
||
}
|
||
|
||
/// <summary>销售趋势(按天,缺失日期补 0,仅统计有效订单)</summary>
|
||
public static List<RevenueTrendPoint> GetSalesTrend(IEnumerable<ShopRevenueStatisticsRow> rows, DateTime startDate, DateTime endDate)
|
||
{
|
||
var list = GetPaymentOrderList(rows);
|
||
list = list.Where(IsValidOrder).ToList();
|
||
var map = list
|
||
.GroupBy(r => r.CreateTime.Date)
|
||
.ToDictionary(
|
||
g => g.Key,
|
||
g => new RevenueTrendPoint
|
||
{
|
||
Date = g.Key,
|
||
SalesAmount = g.Sum(r => (decimal)(r.OrderPrice)) / 100m,
|
||
OrderCount = g.Select(r => r.OrderId).Distinct().Count()
|
||
});
|
||
|
||
var result = new List<RevenueTrendPoint>();
|
||
for (var d = startDate.Date; d <= endDate.Date; d = d.AddDays(1))
|
||
{
|
||
if (map.TryGetValue(d, out var point))
|
||
result.Add(point);
|
||
else
|
||
result.Add(new RevenueTrendPoint { Date = d, SalesAmount = 0m, OrderCount = 0 });
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>店铺销售占比(饼图,仅统计有效订单)</summary>
|
||
public static List<ShopSalesProportionItem> GetShopSalesProportion(IEnumerable<ShopRevenueStatisticsRow> rows)
|
||
{
|
||
var list = GetPaymentOrderList(rows);
|
||
list = list.Where(IsValidOrder).ToList();
|
||
decimal total = list.Sum(r => (decimal)(r.OrderPrice)) / 100m;
|
||
|
||
return list
|
||
.GroupBy(r => r.ShopName)
|
||
.Select(g => new ShopSalesProportionItem
|
||
{
|
||
ShopName = string.IsNullOrEmpty(g.Key) ? "未知店铺" : g.Key,
|
||
SalesAmount = g.Sum(r => (decimal)(r.OrderPrice)) / 100m,
|
||
Percentage = total == 0m ? 0d : (double)(g.Sum(r => (decimal)(r.OrderPrice)) / 100m / total * 100m)
|
||
})
|
||
.OrderByDescending(i => i.SalesAmount)
|
||
.ToList();
|
||
}
|
||
|
||
/// <summary>店铺统计表格</summary>
|
||
public static List<ShopStatisticsItem> GetShopStatistics(IEnumerable<ShopRevenueStatisticsRow> rows)
|
||
{
|
||
var list = GetPaymentOrderList(rows); //买手付钱后的订单
|
||
|
||
return list
|
||
.GroupBy(r => r.ShopName)
|
||
.Select(g =>
|
||
{
|
||
var valid = g.Where(IsValidOrder).ToList();
|
||
var refunded = g.Where(IsRefunded).ToList();
|
||
decimal salesAmount = valid.Sum(r => (decimal)(r.OrderPrice)) / 100m; //与 GetSummary 一致:不含已退款订单的销售额
|
||
int orderCount = valid.Select(r => r.OrderId).Distinct().Count();
|
||
decimal profit = valid.Concat(refunded).Sum(r => (decimal)(r.Profit)) / 100m; //与 GetSummary 一致:有效+退款订单利润
|
||
decimal purchaseAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; //与 GetSummary 一致:买手付款金额(采购金额)
|
||
decimal buyerPaymentAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号付款金额(采购金额)
|
||
return new ShopStatisticsItem
|
||
{
|
||
ShopName = string.IsNullOrEmpty(g.Key) ? "未知店铺" : g.Key,
|
||
OrderCount = orderCount,
|
||
SalesAmount = salesAmount,
|
||
BuyerPaymentAmount = buyerPaymentAmount,
|
||
Profit = profit,
|
||
RefundAmount = refunded.Sum(r => (decimal)(r.OrderPrice)) / 100m,
|
||
PlatformFeeTotal = valid.Sum(r => (decimal)(r.PredictCommissionFee)) / 100m,
|
||
Commission = valid.Sum(r => (decimal)(r.CommissionAmount)) / 100m,
|
||
AvgOrderAmount = orderCount == 0 ? 0m : salesAmount / orderCount,
|
||
AverageProfitRate = purchaseAmount == 0m ? 0m : profit / purchaseAmount * 100m
|
||
};
|
||
})
|
||
.OrderByDescending(i => i.SalesAmount)
|
||
.ToList();
|
||
}
|
||
|
||
/// <summary>天/周/月周期统计表格(每天/每周/每月各一行,无数据的周期也补一行,金额单位:元)</summary>
|
||
public static List<ShopPeriodStatisticsItem> GetPeriodStatistics(IEnumerable<ShopRevenueStatisticsRow> rows, DateTime startDate, DateTime endDate, PeriodStatisticType periodType)
|
||
{
|
||
var list = (rows ?? new List<ShopRevenueStatisticsRow>()).Where(a => a.OutAmount > 0).ToList(); //买手付钱后的订单
|
||
|
||
// 有数据的周期
|
||
var map = list
|
||
.GroupBy(r => GetPeriodStart(r.CreateTime, periodType))
|
||
.ToDictionary(
|
||
g => g.Key,
|
||
g =>
|
||
{
|
||
var valid = g.Where(IsValidOrder).ToList();
|
||
var refunded = g.Where(IsRefunded).ToList();
|
||
decimal salesAmount = valid.Sum(r => (decimal)(r.OrderPrice)) / 100m; //与 GetSummary 一致:不含已退款订单的销售额
|
||
decimal profit = valid.Concat(refunded).Sum(r => (decimal)(r.Profit)) / 100m; //与 GetSummary 一致:有效+退款订单利润
|
||
decimal purchaseAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; //与 GetSummary 一致:买手付款金额(采购金额)
|
||
int orderCount = valid.Select(r => r.OrderId).Distinct().Count();
|
||
decimal buyerPaymentAmount = valid.Sum(r => (decimal)(r.OutAmount)) / 100m; //买手号付款金额(采购金额)
|
||
|
||
return new ShopPeriodStatisticsItem
|
||
{
|
||
PeriodStart = g.Key,
|
||
Period = GetPeriodText(g.Key, periodType),
|
||
OrderCount = orderCount,
|
||
SalesAmount = salesAmount,
|
||
BuyerPaymentAmount = buyerPaymentAmount,
|
||
Profit = profit,
|
||
RefundAmount = refunded.Sum(r => (decimal)(r.OrderPrice)) / 100m,
|
||
PlatformFeeTotal = valid.Sum(r => (decimal)(r.PredictCommissionFee)) / 100m,
|
||
Commission = valid.Sum(r => (decimal)(r.CommissionAmount)) / 100m,
|
||
AverageProfitRate = purchaseAmount == 0m ? 0m : profit / purchaseAmount * 100m,
|
||
CustomerCount = orderCount,
|
||
AvgOrderAmount = orderCount == 0 ? 0m : salesAmount / orderCount
|
||
};
|
||
});
|
||
|
||
// 按周期补齐整段时间:无数据的周期也生成一行(全 0)
|
||
var result = new List<ShopPeriodStatisticsItem>();
|
||
foreach (var periodStart in EnumeratePeriods(startDate, endDate, periodType))
|
||
{
|
||
if (map.TryGetValue(periodStart, out var item))
|
||
result.Add(item);
|
||
else
|
||
result.Add(new ShopPeriodStatisticsItem
|
||
{
|
||
PeriodStart = periodStart,
|
||
Period = GetPeriodText(periodStart, periodType),
|
||
OrderCount = 0,
|
||
SalesAmount = 0m,
|
||
Profit = 0m,
|
||
RefundAmount = 0m,
|
||
PlatformFeeTotal = 0m,
|
||
Commission = 0m,
|
||
AverageProfitRate = 0m,
|
||
CustomerCount = 0,
|
||
AvgOrderAmount = 0m
|
||
});
|
||
}
|
||
|
||
// 倒序排列:最新的周期在最上面
|
||
result.Reverse();
|
||
return result;
|
||
}
|
||
|
||
/// <summary>枚举 startDate~endDate 覆盖的所有周期开始日期(天/周/月)</summary>
|
||
private static IEnumerable<DateTime> EnumeratePeriods(DateTime startDate, DateTime endDate, PeriodStatisticType periodType)
|
||
{
|
||
var start = GetPeriodStart(startDate, periodType);
|
||
var end = GetPeriodStart(endDate, periodType);
|
||
switch (periodType)
|
||
{
|
||
case PeriodStatisticType.Week:
|
||
for (var d = start; d <= end; d = d.AddDays(7))
|
||
yield return d;
|
||
break;
|
||
case PeriodStatisticType.Month:
|
||
for (var d = start; d <= end; d = d.AddMonths(1))
|
||
yield return d;
|
||
break;
|
||
default:
|
||
for (var d = start; d <= end; d = d.AddDays(1))
|
||
yield return d;
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>周期开始日期:天=当天;周=周一起始;月=当月1号</summary>
|
||
private static DateTime GetPeriodStart(DateTime time, PeriodStatisticType periodType)
|
||
{
|
||
switch (periodType)
|
||
{
|
||
case PeriodStatisticType.Week:
|
||
var d = time.Date;
|
||
int diff = d.DayOfWeek == DayOfWeek.Sunday ? 6 : (int)d.DayOfWeek - 1;
|
||
return d.AddDays(-diff);
|
||
case PeriodStatisticType.Month:
|
||
return new DateTime(time.Year, time.Month, 1);
|
||
default:
|
||
return time.Date;
|
||
}
|
||
}
|
||
|
||
/// <summary>周期显示文本:天=yyyy-MM-dd;周=yyyy-MM-dd ~ yyyy-MM-dd;月=yyyy-MM</summary>
|
||
private static string GetPeriodText(DateTime periodStart, PeriodStatisticType periodType)
|
||
{
|
||
switch (periodType)
|
||
{
|
||
case PeriodStatisticType.Week:
|
||
return periodStart.ToString("yyyy-MM-dd") + " ~ " + periodStart.AddDays(6).ToString("yyyy-MM-dd");
|
||
case PeriodStatisticType.Month:
|
||
return periodStart.ToString("yyyy-MM");
|
||
default:
|
||
return periodStart.ToString("yyyy-MM-dd");
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// 默认实现:请求真实接口(shop_revenue_statistics/get_shop_order_amount)。
|
||
/// 接入其他数据源时,新建实现类并注入即可(见 IShopRevenueStatisticsService 注释)。
|
||
/// </summary>
|
||
public class ShopRevenueStatisticsService : IShopRevenueStatisticsService
|
||
{
|
||
/// <summary>缓存有效期:订单/退款数据会持续变化,超过该时长后缓存自动失效并重新请求。</summary>
|
||
private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(60);
|
||
|
||
/// <summary>
|
||
/// 明细数据缓存(按天缓存,纯时间维度,不含店铺)。
|
||
/// key = 日期(yyyyMMdd),value = 该天全部店铺的明细 + 缓存时间。
|
||
/// 查询时按天命中:未命中的天合并成连续段一次请求,返回后按天拆分写入;
|
||
/// 前台选某一个或某多个店铺时,按所选店铺(ShopName)过滤,复用同一批时间数据。
|
||
/// </summary>
|
||
private static readonly Dictionary<string, CacheEntry> _rowCache =
|
||
new Dictionary<string, CacheEntry>();
|
||
private readonly object _cacheLock = new object();
|
||
|
||
private sealed class DateRange
|
||
{
|
||
public DateTime Start;
|
||
public DateTime End;
|
||
}
|
||
|
||
private sealed class CacheEntry
|
||
{
|
||
public List<ShopRevenueStatisticsRow> Rows;
|
||
public DateTime CachedAt;
|
||
}
|
||
|
||
private static string CacheKey(DateTime day)
|
||
{
|
||
return day.Date.ToString("yyyyMMdd");
|
||
}
|
||
|
||
/// <summary>枚举 startDate ~ endDate 之间的每一天(含首尾)</summary>
|
||
private static IEnumerable<DateTime> EnumerateDays(DateTime startDate, DateTime endDate)
|
||
{
|
||
for (var d = startDate.Date; d <= endDate.Date; d = d.AddDays(1))
|
||
yield return d;
|
||
}
|
||
|
||
/// <summary>该天缓存是否有效(存在且未过期);调用方需持有 _cacheLock</summary>
|
||
private bool IsValidLocked(DateTime day)
|
||
{
|
||
return _rowCache.TryGetValue(CacheKey(day), out var entry)
|
||
&& DateTime.Now - entry.CachedAt <= CacheDuration;
|
||
}
|
||
|
||
/// <summary>把缺失日期合并为连续日期段(每段一次请求)</summary>
|
||
private static List<DateRange> BuildRuns(List<DateTime> missingDays)
|
||
{
|
||
var sorted = missingDays.OrderBy(d => d).ToList();
|
||
var runs = new List<DateRange>();
|
||
int i = 0;
|
||
while (i < sorted.Count)
|
||
{
|
||
var start = sorted[i];
|
||
var end = start;
|
||
i++;
|
||
while (i < sorted.Count && sorted[i] == end.AddDays(1))
|
||
{
|
||
end = sorted[i];
|
||
i++;
|
||
}
|
||
runs.Add(new DateRange { Start = start, End = end });
|
||
}
|
||
return runs;
|
||
}
|
||
|
||
public async Task<List<ShopRevenueStatisticsRow>> QueryRowsAsync(ShopRevenueStatisticsQuery query)
|
||
{
|
||
var startDay = query.StartDate.Date;
|
||
var endDay = query.EndDate.Date;
|
||
if (startDay > endDay)
|
||
{
|
||
var tmp = startDay;
|
||
startDay = endDay;
|
||
endDay = tmp;
|
||
}
|
||
|
||
// 1. 找出未命中或已过期的天
|
||
List<DateTime> missingDays;
|
||
lock (_cacheLock)
|
||
{
|
||
missingDays = EnumerateDays(startDay, endDay)
|
||
.Where(d => !IsValidLocked(d))
|
||
.ToList();
|
||
}
|
||
|
||
// 2. 有缺失:按连续段请求整个时间范围的全部店铺明细,并按天写入缓存
|
||
if (missingDays.Count > 0)
|
||
{
|
||
var shopIds = GetShopOptions().Select(a => a.ShopId).ToList();
|
||
if (shopIds.Count == 0)
|
||
return new List<ShopRevenueStatisticsRow>();
|
||
|
||
foreach (var run in BuildRuns(missingDays))
|
||
{
|
||
var rows = await RequestRowsAsync(shopIds, run.Start, run.End);
|
||
CacheResponse(run.Start, run.End, rows);
|
||
}
|
||
}
|
||
|
||
// 3. 按天从缓存拼装完整范围,再按所选店铺过滤(ShopKeys 为空 = 全部店铺)
|
||
return FilterByShop(GetCachedRows(startDay, endDay), query.ShopKeys);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把一次请求的结果按天写入缓存。
|
||
/// 本次请求覆盖的每一天都以返回数据为准整体替换(无数据的天记为空的已缓存,
|
||
/// 避免“该天无数据”被反复判定为未命中而重复请求)。
|
||
/// </summary>
|
||
private void CacheResponse(DateTime startDate, DateTime endDate, List<ShopRevenueStatisticsRow> rows)
|
||
{
|
||
if (rows == null)
|
||
rows = new List<ShopRevenueStatisticsRow>();
|
||
|
||
lock (_cacheLock)
|
||
{
|
||
RemoveExpiredEntriesLocked();
|
||
|
||
var byDay = rows
|
||
.Where(r => r.CreateTime.Date >= startDate.Date && r.CreateTime.Date <= endDate.Date)
|
||
.GroupBy(r => r.CreateTime.Date)
|
||
.ToDictionary(g => g.Key, g => g.ToList());
|
||
|
||
foreach (var day in EnumerateDays(startDate, endDate))
|
||
{
|
||
byDay.TryGetValue(day, out var dayRows);
|
||
_rowCache[CacheKey(day)] = new CacheEntry { Rows = dayRows ?? new List<ShopRevenueStatisticsRow>(), CachedAt = DateTime.Now };
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>按天从缓存拼装某时间范围的明细(返回新列表,避免外部修改污染缓存)</summary>
|
||
private List<ShopRevenueStatisticsRow> GetCachedRows(DateTime startDate, DateTime endDate)
|
||
{
|
||
var result = new List<ShopRevenueStatisticsRow>();
|
||
lock (_cacheLock)
|
||
{
|
||
foreach (var day in EnumerateDays(startDate, endDate))
|
||
{
|
||
if (_rowCache.TryGetValue(CacheKey(day), out var entry))
|
||
result.AddRange(entry.Rows);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>按所选店铺过滤明细:ShopKeys 为空/Null 返回全部;否则按店铺名(ShopName)精确匹配。</summary>
|
||
private static List<ShopRevenueStatisticsRow> FilterByShop(List<ShopRevenueStatisticsRow> rows, List<string> shopKeys)
|
||
{
|
||
if (rows == null)
|
||
return new List<ShopRevenueStatisticsRow>();
|
||
|
||
if (shopKeys == null || shopKeys.Count == 0)
|
||
return new List<ShopRevenueStatisticsRow>(rows);
|
||
|
||
var shops = GetShopOptions()
|
||
.Where(a => shopKeys.Contains(a.AppKey))
|
||
.Where(a => !string.IsNullOrEmpty(a.ShopName))
|
||
.ToList();
|
||
if (shops.Count == 0)
|
||
return new List<ShopRevenueStatisticsRow>();
|
||
|
||
return rows
|
||
.Where(r => shops.Any(s => string.Equals(r.ShopName, s.ShopName, StringComparison.OrdinalIgnoreCase)))
|
||
.ToList();
|
||
}
|
||
|
||
/// <summary>清理已过期的缓存条目(调用方需持有 _cacheLock)</summary>
|
||
private void RemoveExpiredEntriesLocked()
|
||
{
|
||
var expiredKeys = _rowCache
|
||
.Where(p => DateTime.Now - p.Value.CachedAt > CacheDuration)
|
||
.Select(p => p.Key)
|
||
.ToList();
|
||
foreach (var key in expiredKeys)
|
||
_rowCache.Remove(key);
|
||
}
|
||
|
||
/// <summary>请求接口一次(可传多个店铺),返回原始明细行</summary>
|
||
private async Task<List<ShopRevenueStatisticsRow>> RequestRowsAsync(List<string> shopIds, DateTime startDate, DateTime endDate)
|
||
{
|
||
var url = BaseClass.Build29WxObjString("shop_revenue_statistics/get_shop_order_amount");
|
||
var sb = new StringBuilder();
|
||
sb.Append(UserLogin.GetSignaturex());
|
||
sb.Append("&shop_ids=").Append(string.Join(",", shopIds));
|
||
sb.Append("&start_time=").Append(BaseClass.ConvertDateTimeLong(startDate.Date));
|
||
sb.Append("&end_time=").Append(BaseClass.ConvertDateTimeLong(endDate.Date.AddDays(1).AddTicks(-1)));
|
||
var html = await GetHtml.PostUtf8Async(url, sb.ToString());
|
||
|
||
try
|
||
{
|
||
var ret = JsonConvert.DeserializeObject<Result<List<ShopRevenueStatisticsRow>>>(html, new JsonSerializerSettings
|
||
{
|
||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||
});
|
||
if (ret.Code != 200)
|
||
{
|
||
throw new Exception(ret.Msg);
|
||
}
|
||
return ret.Data ?? new List<ShopRevenueStatisticsRow>();
|
||
|
||
}
|
||
catch
|
||
{
|
||
throw new Exception("查询失败");
|
||
}
|
||
|
||
}
|
||
|
||
public async Task<ShopRevenueSummary> GetSummaryAsync(ShopRevenueStatisticsQuery query)
|
||
{
|
||
var rows = await QueryRowsAsync(query);
|
||
return RevenueStatisticsAggregator.GetSummary(rows);
|
||
}
|
||
|
||
public async Task<List<RevenueTrendPoint>> GetSalesTrendAsync(ShopRevenueStatisticsQuery query)
|
||
{
|
||
var rows = await QueryRowsAsync(query);
|
||
return RevenueStatisticsAggregator.GetSalesTrend(rows, query.StartDate, query.EndDate);
|
||
}
|
||
|
||
public async Task<List<ShopSalesProportionItem>> GetShopSalesProportionAsync(ShopRevenueStatisticsQuery query)
|
||
{
|
||
var rows = await QueryRowsAsync(query);
|
||
return RevenueStatisticsAggregator.GetShopSalesProportion(rows);
|
||
}
|
||
|
||
public async Task<List<ShopStatisticsItem>> GetShopStatisticsAsync(ShopRevenueStatisticsQuery query)
|
||
{
|
||
var rows = await QueryRowsAsync(query);
|
||
return RevenueStatisticsAggregator.GetShopStatistics(rows);
|
||
}
|
||
|
||
/// <summary>获取天/周/月周期统计表格</summary>
|
||
public async Task<List<ShopPeriodStatisticsItem>> GetPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType)
|
||
{
|
||
var rows = await QueryRowsAsync(query);
|
||
return RevenueStatisticsAggregator.GetPeriodStatistics(rows, query.StartDate, query.EndDate, periodType);
|
||
}
|
||
|
||
/// <summary>店铺下拉列表(同步读取,供查询与过滤使用)</summary>
|
||
private static List<ShopOption> GetShopOptions()
|
||
{
|
||
return GlobalConfig.ShopLoginList.Select(a =>
|
||
new ShopOption()
|
||
{
|
||
AppKey = a.ShopId,
|
||
ShopId = a.ErpShopId,
|
||
ShopName = a.ShopAccount
|
||
}).ToList();
|
||
}
|
||
|
||
public Task<List<ShopOption>> GetShopOptionsAsync()
|
||
{
|
||
return Task.FromResult(GetShopOptions());
|
||
}
|
||
|
||
}
|
||
}
|