677 lines
26 KiB
C#
677 lines
26 KiB
C#
using _29WxOrderWpf.Ctrls.Statistics.Models;
|
||
using AllBase;
|
||
using AllBase.Helper;
|
||
using Newtonsoft.Json;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using Work;
|
||
using WxOrder.Framework.Commons;
|
||
|
||
namespace _29WxOrderWpf.Ctrls.Statistics.Services
|
||
{
|
||
/// <summary>
|
||
/// 店铺营收统计 - 数据服务调用接口。
|
||
/// 页面只依赖此接口,接入其他数据源时新建实现类,
|
||
/// 并通过 ViewModel 构造函数注入即可,无需改动页面。
|
||
/// </summary>
|
||
public interface IShopRevenueStatisticsService
|
||
{
|
||
/// <summary>
|
||
/// 综合统计查询:返回汇总卡片、销售趋势(按天)、店铺销售占比、店铺统计表格。
|
||
/// 用 GroupType=Day 一次请求取回按店铺按天的记录,再由客户端汇总,无缓存。
|
||
/// </summary>
|
||
Task<ShopRevenueStatisticsResult> QueryAsync(ShopRevenueStatisticsQuery query);
|
||
|
||
/// <summary>
|
||
/// 天/周/月周期统计查询:返回汇总卡片 + 按周期(天/周/月)聚合的表格数据。
|
||
/// 接口不返回整段汇总,Summary 由返回的周期记录在客户端合计。
|
||
/// </summary>
|
||
Task<ShopPeriodStatisticsResult> QueryPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType);
|
||
|
||
/// <summary>获取店铺下拉列表(用于查询条件)</summary>
|
||
Task<List<ShopOption>> GetShopOptionsAsync();
|
||
|
||
/// <summary>
|
||
/// 订单明细列表(有效订单 / 退款订单 / 买手未退回金额的订单),供订单列表弹窗使用。
|
||
/// </summary>
|
||
Task<List<ShopOrderDetailResponse>> QueryOrderRowsAsync(ShopRevenueStatisticsQuery query, ShopOrderDetailType detailType);
|
||
|
||
/// <summary>
|
||
/// 把某笔买手订单标记为退款成功(订单列表弹窗“退款成功”按钮,仅买手未退回金额的订单使用)。
|
||
/// </summary>
|
||
/// <param name="id">买手退款表ID(<see cref="BuyerOrderInfo.RefundId"/>,即表格当前行的 id)</param>
|
||
Task<bool> UpdateBuyerRefundStatusAsync(long id);
|
||
Task<bool> UpdateBuyerAfterOrderRemark(long id, string remark);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统计聚合器:把接口返回的 <see cref="ShopStatsResponse"/> 聚合成页面所需的汇总/趋势/占比/表格数据。
|
||
/// 数据来源:shop_revenue_statistics/GetShopStats(GroupType = Day/Week/Month),
|
||
/// 接口只返回“按周期聚合的记录”,页面上所有汇总口径都在这里由客户端合计。
|
||
/// 口径说明(依据接口字段含义):
|
||
/// - OrderCount 为“有效订单数”,已不含退款订单;因此退款率分母 = OrderCount + RefundOrderCount。
|
||
/// - 店铺端退款:RefundDone(已成功)+ RefundPending(处理中)→ 退款金额。
|
||
/// - 买手端(采购)退款:TotalRefund = DoneRefund + PendingRefund + RejectedRefund → 售后采购金额;
|
||
/// DoneRefund → 已退回采购金额;剩余(待退回)= TotalRefund - DoneRefund。
|
||
/// - 接口无“达人佣金”字段,Commission 固定为 0(保留属性与页面展示,待后端补字段)。
|
||
/// - 金额字段单位均为“分”,除以 100 转为元。
|
||
/// </summary>
|
||
public static class ShopStatsAggregator
|
||
{
|
||
/// <summary>金额换算:分 → 元</summary>
|
||
private const decimal FenPerYuan = 100m;
|
||
|
||
/// <summary>分转元</summary>
|
||
public static decimal ToYuan(long fen)
|
||
{
|
||
return fen / FenPerYuan;
|
||
}
|
||
|
||
private static List<ShopStatsResponse> ToList(IEnumerable<ShopStatsResponse> stats)
|
||
{
|
||
return (stats ?? Enumerable.Empty<ShopStatsResponse>())
|
||
.Where(a => a != null)
|
||
.ToList();
|
||
}
|
||
|
||
private static IEnumerable<IGrouping<string, ShopStatsResponse>> GroupByShop(IEnumerable<ShopStatsResponse> stats)
|
||
{
|
||
return ToList(stats).GroupBy(a => a.ShopId ?? string.Empty);
|
||
}
|
||
|
||
/// <summary>店铺显示名:优先按 ERP 店铺ID匹配,其次按平台店铺ID匹配,都匹配不到时退回店铺ID</summary>
|
||
public static string ResolveShopName(string shopId)
|
||
{
|
||
if (string.IsNullOrEmpty(shopId))
|
||
return "未知店铺";
|
||
|
||
var shop = GlobalConfig.ShopLoginList.FirstOrDefault(a => a.ErpShopId == shopId)
|
||
?? GlobalConfig.ShopLoginList.FirstOrDefault(a => a.ShopId == shopId);
|
||
|
||
if (shop == null || string.IsNullOrEmpty(shop.ShopAccount))
|
||
return shopId;
|
||
|
||
return shop.ShopAccount;
|
||
}
|
||
|
||
/// <summary>汇总卡片(整个查询时间段,所有选中店铺合计)</summary>
|
||
public static ShopRevenueSummary GetSummary(IEnumerable<ShopStatsResponse> stats)
|
||
{
|
||
var list = ToList(stats);
|
||
|
||
decimal salesAmount = ToYuan(list.Sum(a => a.SalesAmount));
|
||
decimal profit = ToYuan(list.Sum(a => a.ProfitAmount));
|
||
decimal platformFeeTotal = ToYuan(list.Sum(a => a.PlatformFee));
|
||
decimal refundAmount = ToYuan(list.Sum(a => a.RefundDone) + list.Sum(a => a.RefundPending));
|
||
decimal buyerPaymentAmount = ToYuan(list.Sum(a => a.TotalPurchase));
|
||
decimal buyerRefundAmount = ToYuan(list.Sum(a => a.TotalRefund));
|
||
decimal buyerRefundSuccessedAmount = ToYuan(list.Sum(a => a.DoneRefund));
|
||
|
||
int validOrderCount = list.Sum(a => a.OrderCount); // 新接口 OrderCount 不含退款订单
|
||
int refundOrderCount = list.Sum(a => a.RefundOrderCount);
|
||
int totalOrderCount = validOrderCount + refundOrderCount;
|
||
|
||
return new ShopRevenueSummary
|
||
{
|
||
SalesAmount = salesAmount,
|
||
ValidOrderCount = validOrderCount,
|
||
Profit = profit,
|
||
RefundAmount = refundAmount,
|
||
RefundOrderCount = refundOrderCount,
|
||
PlatformFeeTotal = platformFeeTotal,
|
||
Commission = 0m, // 新接口无“达人佣金”字段,保留占位
|
||
AvgOrderAmount = validOrderCount == 0 ? 0m : salesAmount / validOrderCount,
|
||
RefundRate = totalOrderCount == 0 ? 0m : (decimal)refundOrderCount * 100m / totalOrderCount,
|
||
AverageProfitRate = buyerPaymentAmount == 0m ? 0m : profit / buyerPaymentAmount * 100m,
|
||
BuyerPaymentAmount = buyerPaymentAmount,
|
||
BuyerRefundAmount = buyerRefundAmount,
|
||
BuyerRefundSuccessedAmount = buyerRefundSuccessedAmount,
|
||
};
|
||
}
|
||
|
||
/// <summary>店铺统计表格(按店铺汇总,销售额倒序)</summary>
|
||
public static List<ShopStatisticsItem> GetShopStatistics(IEnumerable<ShopStatsResponse> stats)
|
||
{
|
||
return GroupByShop(stats)
|
||
.Select(g => BuildShopStatisticsItem(g.Key, g))
|
||
.OrderByDescending(i => i.SalesAmount)
|
||
.ToList();
|
||
}
|
||
|
||
/// <summary>店铺销售占比(饼图,按店铺汇总,销售额倒序)</summary>
|
||
public static List<ShopSalesProportionItem> GetShopSalesProportion(IEnumerable<ShopStatsResponse> stats)
|
||
{
|
||
var items = GroupByShop(stats)
|
||
.Select(g => new ShopSalesProportionItem
|
||
{
|
||
ShopName = ResolveShopName(g.Key),
|
||
SalesAmount = ToYuan(g.Sum(a => a.SalesAmount))
|
||
})
|
||
.OrderByDescending(i => i.SalesAmount)
|
||
.ToList();
|
||
|
||
decimal total = items.Sum(i => i.SalesAmount);
|
||
foreach (var item in items)
|
||
{
|
||
item.Percentage = total == 0m ? 0d : (double)(item.SalesAmount / total * 100m);
|
||
}
|
||
return items;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 销售趋势(按天,缺失日期补 0)。
|
||
/// 数据来自 GetShopStatsTrend:服务端已经跨店铺合计,这里只做日期规整与补零。
|
||
/// </summary>
|
||
public static List<RevenueTrendPoint> GetSalesTrend(IEnumerable<ShopStatsTrendResponse> trend, DateTime startDate, DateTime endDate)
|
||
{
|
||
var list = (trend ?? Enumerable.Empty<ShopStatsTrendResponse>())
|
||
.Where(a => a != null && a.StatDate != default(DateTime))
|
||
.ToList();
|
||
|
||
var map = list
|
||
.Where(a => a.StatDate.Date >= startDate.Date && a.StatDate.Date <= endDate.Date)
|
||
.GroupBy(a => a.StatDate.Date)
|
||
.ToDictionary(
|
||
g => g.Key,
|
||
g => new RevenueTrendPoint
|
||
{
|
||
Date = g.Key,
|
||
SalesAmount = ToYuan(g.Sum(a => a.SalesAmount)),
|
||
OrderCount = g.Sum(a => a.OrderCount)
|
||
});
|
||
|
||
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>
|
||
/// 天/周/月周期统计表格(每天/每周/每月各一行,无数据的周期也补一行,金额单位:元)。
|
||
/// 结果按周期倒序(最新的周期在最上面)。
|
||
/// 说明:接口按“自然周期”聚合(周从周一起、月从 1 号起),所以筛选也按周期起点比较,
|
||
/// 否则区间从周三开始时,那一周(周一起)的数据会被误判为越界而丢掉。
|
||
/// </summary>
|
||
public static List<ShopPeriodStatisticsItem> GetPeriodStatistics(IEnumerable<ShopStatsResponse> stats, DateTime startDate, DateTime endDate, PeriodStatisticType periodType)
|
||
{
|
||
var startPeriod = GetPeriodStart(startDate, periodType);
|
||
var endPeriod = GetPeriodStart(endDate, periodType);
|
||
|
||
var map = ToList(stats)
|
||
.Where(a => a.StatDate != default(DateTime))
|
||
.Select(a => new { Stat = a, PeriodStart = GetPeriodStart(a.StatDate, periodType) })
|
||
.Where(a => a.PeriodStart >= startPeriod && a.PeriodStart <= endPeriod)
|
||
.GroupBy(a => a.PeriodStart)
|
||
.ToDictionary(g => g.Key, g => BuildPeriodStatisticsItem(g.Key, g.Select(a => a.Stat), periodType));
|
||
|
||
// 按周期补齐整段时间:无数据的周期也生成一行(全 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,
|
||
BuyerPaymentAmount = 0m,
|
||
Profit = 0m,
|
||
RefundAmount = 0m,
|
||
PlatformFeeTotal = 0m,
|
||
Commission = 0m,
|
||
AverageProfitRate = 0m,
|
||
CustomerCount = 0,
|
||
AvgOrderAmount = 0m
|
||
});
|
||
}
|
||
|
||
// 倒序排列:最新的周期在最上面
|
||
result.Reverse();
|
||
return result;
|
||
}
|
||
|
||
/// <summary>店铺统计行(金额单位:元)</summary>
|
||
private static ShopStatisticsItem BuildShopStatisticsItem(string shopId, IEnumerable<ShopStatsResponse> shopStats)
|
||
{
|
||
var list = ToList(shopStats);
|
||
decimal salesAmount = ToYuan(list.Sum(a => a.SalesAmount));
|
||
decimal profit = ToYuan(list.Sum(a => a.ProfitAmount));
|
||
decimal buyerPaymentAmount = ToYuan(list.Sum(a => a.TotalPurchase));
|
||
int orderCount = list.Sum(a => a.OrderCount);
|
||
|
||
return new ShopStatisticsItem
|
||
{
|
||
ShopName = ResolveShopName(shopId),
|
||
OrderCount = orderCount,
|
||
SalesAmount = salesAmount,
|
||
BuyerPaymentAmount = buyerPaymentAmount,
|
||
Profit = profit,
|
||
RefundAmount = ToYuan(list.Sum(a => a.RefundDone) + list.Sum(a => a.RefundPending)),
|
||
PlatformFeeTotal = ToYuan(list.Sum(a => a.PlatformFee)),
|
||
Commission = 0m, // 新接口无“达人佣金”字段,保留占位
|
||
AvgOrderAmount = orderCount == 0 ? 0m : salesAmount / orderCount,
|
||
AverageProfitRate = buyerPaymentAmount == 0m ? 0m : profit / buyerPaymentAmount * 100m
|
||
};
|
||
}
|
||
|
||
/// <summary>周期统计行(金额单位:元)</summary>
|
||
private static ShopPeriodStatisticsItem BuildPeriodStatisticsItem(DateTime periodStart, IEnumerable<ShopStatsResponse> periodStats, PeriodStatisticType periodType)
|
||
{
|
||
var list = ToList(periodStats);
|
||
decimal salesAmount = ToYuan(list.Sum(a => a.SalesAmount));
|
||
decimal profit = ToYuan(list.Sum(a => a.ProfitAmount));
|
||
decimal buyerPaymentAmount = ToYuan(list.Sum(a => a.TotalPurchase));
|
||
int orderCount = list.Sum(a => a.OrderCount);
|
||
|
||
return new ShopPeriodStatisticsItem
|
||
{
|
||
PeriodStart = periodStart,
|
||
Period = GetPeriodText(periodStart, periodType),
|
||
OrderCount = orderCount,
|
||
SalesAmount = salesAmount,
|
||
BuyerPaymentAmount = buyerPaymentAmount,
|
||
Profit = profit,
|
||
RefundAmount = ToYuan(list.Sum(a => a.RefundDone) + list.Sum(a => a.RefundPending)),
|
||
PlatformFeeTotal = ToYuan(list.Sum(a => a.PlatformFee)),
|
||
Commission = 0m, // 新接口无“达人佣金”字段,保留占位
|
||
AverageProfitRate = buyerPaymentAmount == 0m ? 0m : profit / buyerPaymentAmount * 100m,
|
||
CustomerCount = orderCount,
|
||
AvgOrderAmount = orderCount == 0 ? 0m : salesAmount / orderCount
|
||
};
|
||
}
|
||
|
||
/// <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/GetShopStats
|
||
/// -> Result<Dictionary<店铺ID, List<ShopStatsResponse>>>
|
||
/// 请求体:UserLogin.GetSignPostData(new { StartDate, EndDate, ShopIds, GroupType });
|
||
/// GroupType 决定聚合粒度(0=汇总 1=天 2=周 3=月),汇总时每个店铺只有一条。
|
||
/// shop_revenue_statistics/GetShopStatsTrend
|
||
/// -> Result<List<ShopStatsTrendResponse>>
|
||
/// 请求体:UserLogin.GetSignPostData(new { StartDate, EndDate, ShopIds });
|
||
/// 服务端跨店铺合计,固定按天返回,用于销售趋势折线图。
|
||
/// shop_revenue_statistics/GetShopOrderDetails
|
||
/// -> Result<List<ShopOrderDetailResponse>>
|
||
/// 请求体:UserLogin.GetSignPostData(new { StartDate, EndDate, ShopIds, OrderType });
|
||
/// 订单列表弹窗的明细数据(OrderType 见 ShopOrderDetailType)。
|
||
/// 接口不返回“整段汇总”,页面上的汇总卡片一律由客户端对返回记录合计。
|
||
/// 接入其他数据源时,新建实现类并注入即可(见 IShopRevenueStatisticsService 注释)。
|
||
/// </summary>
|
||
public class ShopRevenueStatisticsService : IShopRevenueStatisticsService
|
||
{
|
||
/// <summary>统计接口地址(聚合粒度由请求参数 GroupType 决定)</summary>
|
||
private const string GetShopStatsApi = "shop_revenue_statistics/GetShopStats";
|
||
|
||
/// <summary>销售趋势接口地址(服务端跨店铺合计,固定按天)</summary>
|
||
private const string GetShopStatsTrendApi = "shop_revenue_statistics/GetShopStatsTrend";
|
||
|
||
/// <summary>订单明细接口地址(订单列表弹窗用)</summary>
|
||
private const string GetShopOrderDetailsApi = "shop_revenue_statistics/GetShopOrderDetails";
|
||
|
||
/// <summary>更新买手订单退款状态接口地址(id 通过 URL 参数传递)</summary>
|
||
private const string UpdateBuyerRefundStatusApi = "buyer/UpdateBuyerRefundStatsu";
|
||
|
||
/// <summary>Unix 纪元(本地时区),用于解析以时间戳作为 Key 的返回数据</summary>
|
||
private static readonly DateTime UnixEpoch = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1));
|
||
|
||
public async Task<ShopRevenueStatisticsResult> QueryAsync(ShopRevenueStatisticsQuery query)
|
||
{
|
||
// 综合统计页分两次取数,各取所需:
|
||
// 1) Summary:每店铺一条(与区间长度无关),作为卡片/店铺统计表/销售占比的权威合计
|
||
// 2) Trend :跨店铺合计的按天序列,只给趋势折线图用(数据量只与天数有关,不随店铺数膨胀)
|
||
// 顺序请求:签名中的 time/code 由当前时间生成,并发请求有被判重复签名的风险。
|
||
var summaryStats = await RequestStatsAsync(query, ShopStatsGroupType.Summary);
|
||
var trend = await RequestTrendAsync(query);
|
||
|
||
return new ShopRevenueStatisticsResult
|
||
{
|
||
Summary = ShopStatsAggregator.GetSummary(summaryStats),
|
||
Trend = ShopStatsAggregator.GetSalesTrend(trend, query.StartDate, query.EndDate),
|
||
ShopProportions = ShopStatsAggregator.GetShopSalesProportion(summaryStats),
|
||
ShopStatistics = ShopStatsAggregator.GetShopStatistics(summaryStats),
|
||
};
|
||
}
|
||
|
||
public async Task<ShopPeriodStatisticsResult> QueryPeriodStatisticsAsync(ShopRevenueStatisticsQuery query, PeriodStatisticType periodType)
|
||
{
|
||
// 按天/周/月粒度取数;接口不返回整段汇总,卡片由返回的周期记录在客户端合计
|
||
var stats = await RequestStatsAsync(query, ToGroupType(periodType));
|
||
|
||
return new ShopPeriodStatisticsResult
|
||
{
|
||
Summary = ShopStatsAggregator.GetSummary(stats),
|
||
Items = ShopStatsAggregator.GetPeriodStatistics(stats, query.StartDate, query.EndDate, periodType),
|
||
};
|
||
}
|
||
|
||
/// <summary>页面统计周期 → 接口聚合方式</summary>
|
||
private static ShopStatsGroupType ToGroupType(PeriodStatisticType periodType)
|
||
{
|
||
switch (periodType)
|
||
{
|
||
case PeriodStatisticType.Week:
|
||
return ShopStatsGroupType.Week;
|
||
case PeriodStatisticType.Month:
|
||
return ShopStatsGroupType.Month;
|
||
default:
|
||
return ShopStatsGroupType.Day;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 请求 GetShopStats:返回按 GroupType 聚合的记录
|
||
/// (汇总时每店铺一条;天/周/月时每店铺每个自然日/自然周/自然月一条)。
|
||
/// </summary>
|
||
private static async Task<List<ShopStatsResponse>> RequestStatsAsync(ShopRevenueStatisticsQuery query, ShopStatsGroupType groupType)
|
||
{
|
||
var shopIds = BuildShopIds(query);
|
||
if (shopIds.Count == 0)
|
||
return new List<ShopStatsResponse>();
|
||
|
||
var url = BaseClass.Build29WxObjString(GetShopStatsApi);
|
||
var html = await GetHtml.PostJsonAsync(url, BuildPostData(query, shopIds, groupType));
|
||
|
||
var ret = Deserialize<Result<Dictionary<string, List<ShopStatsResponse>>>>(html);
|
||
EnsureSuccess(ret);
|
||
|
||
var result = new List<ShopStatsResponse>();
|
||
if (ret.Data == null)
|
||
return result;
|
||
|
||
foreach (var kv in ret.Data)
|
||
result.AddRange(Normalize(kv.Key, kv.Value));
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 请求 GetShopStatsTrend:跨店铺合计的按天销售趋势(服务端已合计,不需要 GroupType)。
|
||
/// </summary>
|
||
private static async Task<List<ShopStatsTrendResponse>> RequestTrendAsync(ShopRevenueStatisticsQuery query)
|
||
{
|
||
var shopIds = BuildShopIds(query);
|
||
if (shopIds.Count == 0)
|
||
return new List<ShopStatsTrendResponse>();
|
||
|
||
var url = BaseClass.Build29WxObjString(GetShopStatsTrendApi);
|
||
var html = await GetHtml.PostJsonAsync(url, BuildTrendPostData(query, shopIds));
|
||
|
||
var ret = Deserialize<Result<List<ShopStatsTrendResponse>>>(html);
|
||
EnsureSuccess(ret);
|
||
return ret.Data ?? new List<ShopStatsTrendResponse>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 组装请求体:StartDate / EndDate 传日期,ShopIds 传 ERP 店铺ID 列表,
|
||
/// GroupType 传枚举数字(0=汇总 1=天 2=周 3=月)。
|
||
/// </summary>
|
||
private static object BuildPostData(ShopRevenueStatisticsQuery query, List<string> shopIds, ShopStatsGroupType groupType)
|
||
{
|
||
return UserLogin.GetSignPostData(new
|
||
{
|
||
StartDate = query.StartDate.Date,
|
||
EndDate = query.EndDate.Date,
|
||
ShopIds = shopIds,
|
||
GroupType = (int)groupType,
|
||
});
|
||
}
|
||
|
||
/// <summary>组装趋势接口请求体:StartDate / EndDate + ShopIds(无 GroupType)</summary>
|
||
private static object BuildTrendPostData(ShopRevenueStatisticsQuery query, List<string> shopIds)
|
||
{
|
||
return UserLogin.GetSignPostData(new
|
||
{
|
||
StartDate = query.StartDate.Date,
|
||
EndDate = query.EndDate.Date,
|
||
ShopIds = shopIds,
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 组装订单明细请求体:StartDate / EndDate + ShopIds + OrderType
|
||
/// (OrderType 传枚举数字:1=有效订单 2=退款订单 3=买手未退回金额的订单)。
|
||
/// </summary>
|
||
private static object BuildOrderDetailPostData(ShopRevenueStatisticsQuery query, List<string> shopIds, ShopOrderDetailType detailType)
|
||
{
|
||
return UserLogin.GetSignPostData(new
|
||
{
|
||
StartDate = query.StartDate,
|
||
EndDate = query.EndDate,
|
||
ShopIds = shopIds,
|
||
OrderType = (int)detailType,
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询店铺范围:query.ShopIds 为空表示全部店铺(取全部店铺的 ERP 店铺ID)。
|
||
/// 接口的 ShopIds 使用 ShopLoginInfo.ErpShopId。
|
||
/// </summary>
|
||
private static List<string> BuildShopIds(ShopRevenueStatisticsQuery query)
|
||
{
|
||
var selected = (query == null || query.ShopIds == null)
|
||
? new List<string>()
|
||
: query.ShopIds.Where(a => !string.IsNullOrEmpty(a)).Distinct().ToList();
|
||
|
||
if (selected.Count > 0)
|
||
return selected;
|
||
|
||
return GetShopOptions()
|
||
.Select(a => a.ErpShopId)
|
||
.Where(a => !string.IsNullOrEmpty(a))
|
||
.Distinct()
|
||
.ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 反序列化接口返回;失败时统一抛出可读异常。
|
||
/// 说明:Newtonsoft 默认按属性名匹配(忽略大小写),因此 PascalCase / camelCase 均可解析。
|
||
/// </summary>
|
||
private static T Deserialize<T>(string html)
|
||
{
|
||
try
|
||
{
|
||
return JsonConvert.DeserializeObject<T>(html);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new Exception("查询失败:接口返回数据解析异常", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>校验接口返回码</summary>
|
||
private static void EnsureSuccess(Result result)
|
||
{
|
||
if (result == null)
|
||
throw new Exception("查询失败:接口无返回数据");
|
||
if (result.Code != 200)
|
||
throw new Exception(string.IsNullOrEmpty(result.Msg) ? "查询失败" : result.Msg);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 摊平某个 Key(店铺ID)下的记录,并用 Key 兜底补齐缺失的 ShopId / StatDate。
|
||
/// </summary>
|
||
private static List<ShopStatsResponse> Normalize(string key, List<ShopStatsResponse> items)
|
||
{
|
||
var result = new List<ShopStatsResponse>();
|
||
if (items == null)
|
||
return result;
|
||
|
||
foreach (var item in items)
|
||
{
|
||
if (item == null)
|
||
continue;
|
||
|
||
if (string.IsNullOrEmpty(item.ShopId))
|
||
item.ShopId = key;
|
||
|
||
if (item.StatDate == default(DateTime))
|
||
{
|
||
var date = ParseDateFromKey(key);
|
||
if (date.HasValue)
|
||
item.StatDate = date.Value;
|
||
}
|
||
|
||
result.Add(item);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>尝试把返回数据的 Key 解析为日期(支持 yyyy-MM-dd / yyyyMMdd / 秒或毫秒时间戳)</summary>
|
||
private static DateTime? ParseDateFromKey(string key)
|
||
{
|
||
if (string.IsNullOrEmpty(key))
|
||
return null;
|
||
|
||
if (DateTime.TryParse(key, out var date))
|
||
return date;
|
||
|
||
if (DateTime.TryParseExact(key, "yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out date))
|
||
return date;
|
||
|
||
if (long.TryParse(key, out var timestamp))
|
||
{
|
||
return timestamp.ToString().Length == 13
|
||
? UnixEpoch.AddMilliseconds(timestamp)
|
||
: UnixEpoch.AddSeconds(timestamp);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>店铺下拉列表(同步读取,供查询与过滤使用)</summary>
|
||
private static List<ShopOption> GetShopOptions()
|
||
{
|
||
return GlobalConfig.ShopLoginList.Select(a =>
|
||
new ShopOption()
|
||
{
|
||
ShopId = a.ShopId,
|
||
ErpShopId = a.ErpShopId,
|
||
ShopName = a.ShopAccount
|
||
}).ToList();
|
||
}
|
||
|
||
public Task<List<ShopOption>> GetShopOptionsAsync()
|
||
{
|
||
return Task.FromResult(GetShopOptions());
|
||
}
|
||
|
||
/// <summary>
|
||
/// 订单明细列表(有效订单 / 退款订单 / 买手未退回金额的订单),供订单列表弹窗使用。
|
||
/// 请求 shop_revenue_statistics/GetShopOrderDetails:
|
||
/// StartDate / EndDate / ShopIds / OrderType(OrderType 由 detailType 决定,按数字传值)。
|
||
/// </summary>
|
||
public async Task<List<ShopOrderDetailResponse>> QueryOrderRowsAsync(ShopRevenueStatisticsQuery query, ShopOrderDetailType detailType)
|
||
{
|
||
var shopIds = BuildShopIds(query);
|
||
if (shopIds.Count == 0)
|
||
return new List<ShopOrderDetailResponse>();
|
||
|
||
var url = BaseClass.Build29WxObjString(GetShopOrderDetailsApi);
|
||
var html = await GetHtml.PostJsonAsync(url, BuildOrderDetailPostData(query, shopIds, detailType));
|
||
|
||
var ret = Deserialize<Result<List<ShopOrderDetailResponse>>>(html);
|
||
EnsureSuccess(ret);
|
||
return ret.Data ?? new List<ShopOrderDetailResponse>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 请求 buyer/UpdateBuyerRefundStatsu 更新买手订单退款状态:
|
||
/// id 拼在 URL 查询参数上(Build29WxObjString 生成的地址已带 ?obj=…,这里用 &id= 追加);
|
||
/// 请求体走 UserLogin.GetSignPostData 常规签名(time/code 等)。
|
||
/// 返回码非 200 时抛出异常,由调用方提示。
|
||
/// </summary>
|
||
public async Task<bool> UpdateBuyerRefundStatusAsync(long id)
|
||
{
|
||
if (id == 0)
|
||
throw new Exception("更新失败:缺少买手订单ID");
|
||
|
||
var url = BaseClass.Build29WxObjString(UpdateBuyerRefundStatusApi) + "&id=" + id;
|
||
var html = await GetHtml.PostJsonAsync(url, UserLogin.GetSignPostData(new { }));
|
||
|
||
var ret = Deserialize<Result<bool>>(html);
|
||
EnsureSuccess(ret);
|
||
return ret.Data;
|
||
}
|
||
|
||
public async Task<bool> UpdateBuyerAfterOrderRemark(long id, string remark)
|
||
{
|
||
if (id == 0)
|
||
throw new Exception("更新失败:缺少买手订单ID");
|
||
|
||
var url = BaseClass.Build29WxObjString("buyer/UpdateBuyerAfterRemark");
|
||
var html = await GetHtml.PostJsonAsync(url, UserLogin.GetSignPostData(new
|
||
{
|
||
id = id,
|
||
remark
|
||
}));
|
||
|
||
var ret = Deserialize<Result<bool>>(html);
|
||
EnsureSuccess(ret);
|
||
return ret.Data;
|
||
}
|
||
}
|
||
}
|