Files
29WxOrder/29WxOrderWpf/Ctrls/Statistics/Services/ShopRevenueStatisticsService.cs
T
chfei cc0904c728 增加 利润明细
移除有效订单明细
增加待退回金额明细 列表备注功能
2026-10-09 17:15:50 +08:00

677 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using _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&lt;Dictionary&lt;店铺ID, List&lt;ShopStatsResponse&gt;&gt;&gt;
/// 请求体:UserLogin.GetSignPostData(new { StartDate, EndDate, ShopIds, GroupType });
/// GroupType 决定聚合粒度(0=汇总 1=天 2=周 3=月),汇总时每个店铺只有一条。
/// shop_revenue_statistics/GetShopStatsTrend
/// -> Result&lt;List&lt;ShopStatsTrendResponse&gt;&gt;
/// 请求体:UserLogin.GetSignPostData(new { StartDate, EndDate, ShopIds });
/// 服务端跨店铺合计,固定按天返回,用于销售趋势折线图。
/// shop_revenue_statistics/GetShopOrderDetails
/// -> Result&lt;List&lt;ShopOrderDetailResponse&gt;&gt;
/// 请求体: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;
}
}
}