570 lines
20 KiB
C#
570 lines
20 KiB
C#
using _29WxOrderWpf.Common;
|
||
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.Linq;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using Work;
|
||
|
||
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 == 4 || row.RefundStatus == 11 || row.Status == StatusCancelAfterAfterSale;
|
||
}
|
||
|
||
/// <summary>是否有效订单(已付款且未退款)</summary>
|
||
public static bool IsValidOrder(ShopRevenueStatisticsRow row)
|
||
{
|
||
return IsPaidOrder(row) && !IsRefunded(row);
|
||
}
|
||
|
||
/// <summary>汇总卡片</summary>
|
||
public static ShopRevenueSummary GetSummary(IEnumerable<ShopRevenueStatisticsRow> rows)
|
||
{
|
||
var list = (rows ?? new List<ShopRevenueStatisticsRow>()).ToList();
|
||
var valid = list.Where(IsValidOrder).ToList();
|
||
var refunded = list.Where(IsRefunded).ToList();
|
||
|
||
decimal salesAmount = valid.Sum(r => (decimal)r.OrderPrice) / 100m;
|
||
decimal profit = valid.Sum(r => (decimal)r.Profit) / 100m;
|
||
decimal refundAmount = refunded.Sum(r => (decimal)r.OrderPrice) / 100m;
|
||
int validOrderCount = valid.Select(r => r.OrderId).Distinct().Count();
|
||
// 总订单数(退款率分母)= 已付款订单 + 已退款订单(含售后取消 200),不含未付款的 10/250
|
||
int totalOrderCount = list.Where(r => IsPaidOrder(r) || IsRefunded(r)).Select(r => r.OrderId).Distinct().Count();
|
||
int refundOrderCount = refunded.Select(r => r.OrderId).Distinct().Count();
|
||
|
||
return new ShopRevenueSummary
|
||
{
|
||
SalesAmount = salesAmount,
|
||
ValidOrderCount = validOrderCount,
|
||
Profit = profit,
|
||
RefundAmount = refundAmount,
|
||
ProductSalesVolume = valid.Count,
|
||
CustomerCount = validOrderCount,
|
||
AvgOrderAmount = validOrderCount == 0 ? 0m : salesAmount / validOrderCount,
|
||
RefundRate = totalOrderCount == 0 ? 0m : (decimal)refundOrderCount * 100m / totalOrderCount
|
||
};
|
||
}
|
||
|
||
/// <summary>销售趋势(按天,缺失日期补 0,仅统计有效订单)</summary>
|
||
public static List<RevenueTrendPoint> GetSalesTrend(IEnumerable<ShopRevenueStatisticsRow> rows, DateTime startDate, DateTime endDate)
|
||
{
|
||
var list = (rows ?? new List<ShopRevenueStatisticsRow>()).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 = (rows ?? new List<ShopRevenueStatisticsRow>()).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 = (rows ?? new List<ShopRevenueStatisticsRow>()).ToList();
|
||
|
||
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;
|
||
int orderCount = valid.Select(r => r.OrderId).Distinct().Count();
|
||
return new ShopStatisticsItem
|
||
{
|
||
ShopName = string.IsNullOrEmpty(g.Key) ? "未知店铺" : g.Key,
|
||
OrderCount = orderCount,
|
||
SalesAmount = salesAmount,
|
||
Profit = valid.Sum(r => (decimal)r.Profit) / 100m,
|
||
RefundAmount = refunded.Sum(r => (decimal)r.OrderPrice) / 100m,
|
||
CustomerCount = orderCount,
|
||
AvgOrderAmount = orderCount == 0 ? 0m : salesAmount / orderCount
|
||
};
|
||
})
|
||
.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>()).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;
|
||
int orderCount = valid.Select(r => r.OrderId).Distinct().Count();
|
||
return new ShopPeriodStatisticsItem
|
||
{
|
||
PeriodStart = g.Key,
|
||
Period = GetPeriodText(g.Key, periodType),
|
||
OrderCount = orderCount,
|
||
SalesAmount = salesAmount,
|
||
Profit = valid.Sum(r => (decimal)r.Profit) / 100m,
|
||
RefundAmount = refunded.Sum(r => (decimal)r.OrderPrice) / 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,
|
||
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>
|
||
/// 默认实现:返回模拟数据,用于页面演示/联调。
|
||
/// 接入真实接口时,新建实现类并注入即可(见 IShopRevenueStatisticsService 注释)。
|
||
/// </summary>
|
||
public class ShopRevenueStatisticsService : IShopRevenueStatisticsService
|
||
{
|
||
/// <summary>
|
||
/// 明细数据缓存。
|
||
/// key = 店铺id|yyyyMMdd(店铺 × 天),value = 该店铺当天的明细行。
|
||
/// 查询按“店铺 × 日期”命中;完全未命中时所有店铺 + 整个时间范围一次请求,
|
||
/// 部分命中时只请求缺失的店铺/日期段。
|
||
/// </summary>
|
||
private readonly Dictionary<string, List<ShopRevenueStatisticsRow>> _rowCache =
|
||
new Dictionary<string, List<ShopRevenueStatisticsRow>>();
|
||
private readonly object _cacheLock = new object();
|
||
|
||
private sealed class DateRange
|
||
{
|
||
public DateTime Start;
|
||
public DateTime End;
|
||
}
|
||
|
||
private static string CacheKey(string shopId, DateTime day)
|
||
{
|
||
return shopId + "|" + day.ToString("yyyyMMdd");
|
||
}
|
||
|
||
public async Task<List<ShopRevenueStatisticsRow>> QueryRowsAsync(ShopRevenueStatisticsQuery query)
|
||
{
|
||
// 1. 展开本次查询涉及的店铺(ShopKey 为空 = 全部店铺)
|
||
List<string> shopIds;
|
||
if (string.IsNullOrEmpty(query.ShopKey))
|
||
shopIds = (await GetShopOptionsAsync()).Select(a => a.AppKey).ToList();
|
||
else
|
||
shopIds = new List<string> { query.ShopKey };
|
||
if (shopIds.Count == 0)
|
||
return new List<ShopRevenueStatisticsRow>();
|
||
|
||
var startDay = query.StartDate.Date;
|
||
var endDay = query.EndDate.Date;
|
||
int totalDays = (int)(endDay - startDay).Days + 1;
|
||
|
||
// 2. 命中检测:按 店铺 × 天 找出缺失项
|
||
var missingByShop = new Dictionary<string, List<DateTime>>();
|
||
int missingCount = 0;
|
||
lock (_cacheLock)
|
||
{
|
||
foreach (var shopId in shopIds)
|
||
{
|
||
var missingDays = new List<DateTime>();
|
||
for (var d = startDay; d <= endDay; d = d.AddDays(1))
|
||
{
|
||
if (!_rowCache.ContainsKey(CacheKey(shopId, d)))
|
||
missingDays.Add(d);
|
||
}
|
||
missingCount += missingDays.Count;
|
||
missingByShop[shopId] = missingDays;
|
||
}
|
||
}
|
||
|
||
// 3. 全部命中:直接从缓存拼装返回
|
||
if (missingCount == 0)
|
||
return GetCachedRows(shopIds, startDay, endDay);
|
||
|
||
// 4. 完全未命中:所有店铺 + 整个时间范围一次请求
|
||
if (missingCount == shopIds.Count * totalDays)
|
||
{
|
||
var allRows = await RequestRowsAsync(shopIds, startDay, endDay);
|
||
CacheResponse(shopIds, startDay, endDay, allRows, null);
|
||
var cached = GetCachedRows(shopIds, startDay, endDay);
|
||
// 若接口行内未返回 app_key 导致无法入库,则直接返回本次请求结果
|
||
return cached.Count > 0 ? cached : allRows;
|
||
}
|
||
|
||
// 5. 部分命中:只请求未命中的(店铺 × 连续缺失日期段)
|
||
foreach (var pair in missingByShop)
|
||
{
|
||
if (pair.Value.Count == 0)
|
||
continue;
|
||
foreach (var run in BuildRuns(pair.Value))
|
||
{
|
||
var rows = await RequestRowsAsync(new List<string> { pair.Key }, run.Start, run.End);
|
||
CacheResponse(new List<string> { pair.Key }, run.Start, run.End, rows, pair.Key);
|
||
}
|
||
}
|
||
|
||
return GetCachedRows(shopIds, startDay, endDay);
|
||
}
|
||
|
||
/// <summary>按 店铺 × 日期 从缓存拼装结果</summary>
|
||
private List<ShopRevenueStatisticsRow> GetCachedRows(List<string> shopIds, DateTime startDate, DateTime endDate)
|
||
{
|
||
var result = new List<ShopRevenueStatisticsRow>();
|
||
lock (_cacheLock)
|
||
{
|
||
for (var d = startDate.Date; d <= endDate.Date; d = d.AddDays(1))
|
||
{
|
||
foreach (var shopId in shopIds)
|
||
{
|
||
if (_rowCache.TryGetValue(CacheKey(shopId, d), out var list))
|
||
result.AddRange(list);
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <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("查询失败");
|
||
}
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把一次请求的结果写入缓存(按 店铺id|日期 分桶)。
|
||
/// 除有数据的 店铺×天 外,会把本次请求覆盖但无数据的 店铺×天 也标记为已缓存(空列表),
|
||
/// 避免“该店铺当天无数据”被反复判定为未命中而重复请求数据库。
|
||
/// </summary>
|
||
private void CacheResponse(List<string> shopIds, DateTime startDate, DateTime endDate, List<ShopRevenueStatisticsRow> rows, string fallbackShopId)
|
||
{
|
||
if (rows == null)
|
||
rows = new List<ShopRevenueStatisticsRow>();
|
||
|
||
// 能否按 店铺×天 归属:
|
||
// 单店铺请求一定可以(用请求的店铺兜底);多店铺请求要求行内带 app_key;
|
||
// 响应为空时也视为可归属(说明覆盖的 店铺×天 都无数据)。
|
||
bool canAttribute = shopIds.Count == 1
|
||
|| rows.Count == 0
|
||
|| rows.Any(r => !string.IsNullOrEmpty(r.AppKey));
|
||
|
||
lock (_cacheLock)
|
||
{
|
||
if (canAttribute)
|
||
{
|
||
// 覆盖的 店铺×天 全部落缓存键(无数据的天记为空的已缓存)
|
||
for (var d = startDate.Date; d <= endDate.Date; d = d.AddDays(1))
|
||
{
|
||
foreach (var shopId in shopIds)
|
||
{
|
||
var key = CacheKey(shopId, d);
|
||
if (!_rowCache.TryGetValue(key, out var list))
|
||
{
|
||
list = new List<ShopRevenueStatisticsRow>();
|
||
_rowCache[key] = list;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 有数据的行按 店铺×天 追加
|
||
foreach (var row in rows)
|
||
{
|
||
// 行内未带 app_key 时,用本次请求的店铺兜底
|
||
var shopId = string.IsNullOrEmpty(row.AppKey) ? fallbackShopId : row.AppKey;
|
||
if (string.IsNullOrEmpty(shopId))
|
||
continue;
|
||
var key = CacheKey(shopId, row.CreateTime.Date);
|
||
if (!_rowCache.TryGetValue(key, out var list))
|
||
{
|
||
list = new List<ShopRevenueStatisticsRow>();
|
||
_rowCache[key] = list;
|
||
}
|
||
list.Add(row);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <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<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);
|
||
}
|
||
|
||
public Task<List<ShopOption>> GetShopOptionsAsync()
|
||
{
|
||
var list = GlobalConfig.ShopLoginList.Select(a =>
|
||
new ShopOption()
|
||
{
|
||
AppKey = a.ShopId,
|
||
ShopName = a.ShopAccount
|
||
}).ToList();
|
||
|
||
return Task.FromResult(list);
|
||
}
|
||
|
||
}
|
||
}
|