# 订单指标系统计算实现计划 ## 项目目标 实现复杂的业务指标系统,包括24小时换单完成率、订单考核时间、标签率计算等指标。这些指标涉及多表关联、时间逻辑判断和复杂的业务规则。 --- ## 时区说明 **重要**: - **到仓时间 (ReceiptTime)**: UTC-5 时区 - **标签推送时间 (LabelRetrievedAt)**: UTC+0 时区 - **扫描时间 (CreatedAt)**: UTC+0 时区 - **其他所有时间字段**: UTC+0 时区 - **数据拉取时间**: UTC-5 时区显示 时间比较时需进行时区转换。 --- ## 核心业务逻辑分析 ### 1. 标签率计算(Label Rate) **定义**: - **第一阶段**(未开始扫描): 标签率 = 当前有标签的订单数 / 该交接单关联的总订单数 - **第二阶段**(已开始扫描): 标签率 = 在第一条扫描记录时间之前有标签的订单数 / 该交接单关联的总订单数(**固定不变**) **计算逻辑**: ``` 1. 获取交接单关联的所有订单 2. 检查是否存在该交接单的扫描记录 - 如果不存在:标签率 = 当前有标签的订单数 / 总订单数 - 如果存在: a. 找到第一条扫描记录的时间(任意结果) b. 统计在此时间点前 LabelRetrievedAt <= 第一扫描时间 的订单 c. 标签率 = 满足条件的订单数 / 总订单数 3. 标签率一旦固定后就不再变化 ``` **关键理解**: - 标签率是交接单在**现场开始扫描时**的一个**时间切片快照** - 此后即使新增了更多有标签的订单,标签率也不变 - 标签率 >= 80% 为"高标签率",< 80% 为"低标签率" **数据关系**: - 交接单号 ← → BillOfLadingNumber / MasterPackageNumber (需要在订单表中查找) - 订单中的 LabelRetrievedAt 与 第一条扫描记录的 CreatedAt 比较 --- ### 2. 考核时间计算(Assessment Time) **场景A:标签率 >= 80%** - 前置条件:有到货时间(ReceiptTime),且标签率 >= 80% - 规则: - 如果收货时间 <= 当天 16:00 → 考核时间 = 次日 16:00 - 如果收货时间 > 当天 16:00 → 考核时间 = 次日 23:59 **场景B:标签率 < 80%** - 前置条件:标签率 < 80% - 规则:以包裹换单完成时间作为考核时间 - 换单完成时间 = 该订单的第一条扫描成功记录(Result=0)的时间 **考核记录日期**: 哪天完成考核,记录在哪天 --- ### 3. 24小时换单完成率(24H Label Exchange Completion Rate) **分子**: 标签率 >= 80% 的订单中,在考核时间内有扫描成功记录(Result=0)的订单数 **分母**: 标签率 >= 80% 的订单总数 **公式**: 完成率 = (考核时间内成功扫描的订单数) / (标签率 >= 80% 的订单总数) * 100% **时间判断**: 以 UTC 时间对比 --- ### 4. 考核订单群体(Assessment Order Group) **定义**: 有到货时间 AND 标签率 >= 80% 的那部分有标签订单 **包含条件**: - ReceiptTime 不为 NULL(有收货时间) - 标签率 >= 80%(或更严格的条件:订单有标签且标签率 >= 80%) --- ### 5. 额外指标定义 #### 5.1 当天新增换单数(Daily New Replace Count) **定义**: 到仓时间是当天(UTC-5)的交接单中有标签的总订单数 **计算逻辑**: ``` 1. 选取 ReceiptTime 在当天(UTC-5)的所有到货交接单(注意时区转换) 2. 获取这些交接单关联的所有订单(通过 BillOfLadingNumber 或 MasterPackageNumber) 3. 筛选其中 Label 不为 NULL 且非空的订单 4. 统计总数 ``` **代码示例**: ```csharp public async Task GetDailyNewReplaceCountAsync(DateTime date) { // date 是 UTC-5 格式的日期 var dateStart = GetUtc5DateStart(date); // 返回 UTC 时间表示 var dateEnd = GetUtc5DateEnd(date); // 返回 UTC 时间表示 // 获取该日期内到仓的所有交接单(ReceiptTime 在 UTC-5 当天) var arrivalForms = await _arrivalHandoverFormService .GetArrivalHandoverFormsByDateRangeAsync(dateStart, dateEnd); if (arrivalForms.Count == 0) return 0; var handoverNumbers = arrivalForms.Select(f => f.HandoverNumber).ToList(); // 获取这些交接单关联的所有订单 var orders = await _labelReplaceRepository .GetOrdersByHandoverNumbersAsync(handoverNumbers); // 统计有标签的订单数(Label 不为 NULL 且非空) int count = orders.Count(o => !string.IsNullOrEmpty(o.Label)); return count; } ``` #### 5.2 累计要换的总单数(Cumulative Total Replace Count) **定义**: 历史上所有有标签但是没有扫描完成记录的订单(不包含当天新增) **计算逻辑**: ``` 1. 获取所有 Label 不为 NULL 的订单 2. 排除当天新增的订单(ReceiptTime 是当天的) 3. 筛选这些订单中,没有成功扫描记录(Result=0)的 4. 统计数量 ``` #### 5.3 当天应该换单数(Daily Should Replace Count) **定义**: 标签率 >= 80% 且到仓时间是当天的订单总数 **计算逻辑**: ``` 1. 选取 ReceiptTime 在当天(UTC-5)的所有到货交接单 2. 计算每个交接单的标签率 3. 筛选标签率 >= 80% 的交接单 4. 统计这些交接单关联的订单总数 ``` #### 5.4 当日换单完成数(Daily Completion Count) **定义**: 扫描完成时间是当日(UTC-5)的订单总数 **计算逻辑**: ``` 1. 获取所有有成功扫描记录的订单(Result=0,即 ScanResult.ReturnedLabel) 2. 注意:CreatedAt 是 UTC+0,需要转换为 UTC-5 后判断是否在当日 3. 筛选其中扫描时间(转换后)对应当天(UTC-5)的订单 4. 去重统计(同一订单即使有多条成功扫描也只计算一次) ``` **代码示例**: ```csharp public async Task GetDailyCompletionCountAsync(DateTime date) { // date 是 UTC-5 格式的日期 var dateStart = GetUtc5DateStart(date); // UTC 时间 var dateEnd = GetUtc5DateEnd(date); // UTC 时间 // 获取该日期范围内有成功扫描记录的订单 var successfulScans = await _labelScanService .GetScanRecordsByDateRangeAsync(dateStart, dateEnd, ScanResult.ReturnedLabel); if (successfulScans == null || successfulScans.Count == 0) return 0; // 去重统计订单数 var uniqueWaybills = successfulScans .Select(s => s.NeutralWaybillNumber) .Distinct() .Count(); return uniqueWaybills; } ``` **重要**:在 Repository 层 GetScanRecordsByDateRangeAsync 方法中,对 CreatedAt(UTC+0)进行查询时,参数 dateStart 和 dateEnd 已是 UTC 时间,可直接作为 SQL WHERE 条件。 #### 5.5 当日STOP数(Daily STOP Count) **定义**: 扫描结果是完成(Result=0)且描述包含"STOP"关键字的扫描记录,其创建时间是当日(UTC-5)的订单总数 **计算逻辑**: ``` 1. 获取当日(UTC-5)所有成功扫描记录(Result=0,即 ScanResult.ReturnedLabel) 2. 筛选其中 Description 包含"STOP"关键字的记录 3. 去重统计对应的订单数 ``` **代码示例**: ```csharp public async Task GetDailyStopCountAsync(DateTime date) { // date 是 UTC-5 格式的日期 var dateStart = GetUtc5DateStart(date); // UTC 时间 var dateEnd = GetUtc5DateEnd(date); // UTC 时间 // 获取该日期范围内所有成功扫描记录 var successfulScans = await _labelScanService .GetScanRecordsByDateRangeAsync(dateStart, dateEnd, ScanResult.ReturnedLabel); if (successfulScans == null || successfulScans.Count == 0) return 0; // 筛选 Description 包含 "STOP" 的记录,去重统计 var stopCount = successfulScans .Where(s => s.Description != null && s.Description.Contains("STOP", StringComparison.OrdinalIgnoreCase)) .Select(s => s.NeutralWaybillNumber) .Distinct() .Count(); return stopCount; } ``` #### 5.6 当日标签推送数(Daily Label Push Count) **定义**: 标签推送时间是当天(UTC-5)的订单数 **计算逻辑**: ``` 1. 获取所有 LabelRetrievedAt 不为 NULL 的订单 2. 将 LabelRetrievedAt(UTC+0)转换为 UTC-5 3. 筛选时间在当天的订单 4. 统计数量 ``` #### 5.7 当天换单完成率(Daily Completion Rate) **定义**: 当日换单完成数 / 当天应该换单数 **计算逻辑**: ``` 完成率 = (Daily Completion Count) / (Daily Should Replace Count) * 100% ``` #### 5.8 当日扫描数(Daily Scan Count) **定义**: 扫描时间是当天(UTC-5)的扫描记录总数(不去重) **计算逻辑**: ``` 1. 获取当日(UTC-5)内所有扫描记录(任意结果) 2. 统计记录总数(不进行去重) ``` **代码示例**: ```csharp public async Task GetDailyScanCountAsync(DateTime date) { // date 是 UTC-5 格式的日期 var dateStart = GetUtc5DateStart(date); // UTC 时间 var dateEnd = GetUtc5DateEnd(date); // UTC 时间 // 获取该日期范围内的所有扫描记录 var allScans = await _labelScanService .GetScanRecordsByDateRangeAsync(dateStart, dateEnd, resultFilter: null); return allScans?.Count ?? 0; } ``` #### 5.9 16点前到仓包裹数(Before Noon Arrived Count) **定义**: 到仓时间在当天 16:00 之前(UTC-5)的订单总数 **说明**:此指标用于统计分时段到仓的包裹数,用于后续分析和考核时间的关联。 **计算逻辑**: ``` 1. 选取 ReceiptTime 在当天(UTC-5)且时间 <= 16:00 的订单 2. 统计数量 ``` #### 5.10 16点后到仓包裹数(Afternoon Arrived Count) **定义**: 到仓时间在当天 16:00 之后(UTC-5)的订单总数 **说明**:与 16点前到仓包裹数配套,用于分时段统计。 **计算逻辑**: ``` 1. 选取 ReceiptTime 在当天(UTC-5)且时间 > 16:00 的订单 2. 统计数量 ``` #### 5.11 16点前考核通过包裹数(Before Noon Passed Count) **定义**: 16点前到仓且标签率 >= 80% 的订单中,在考核时间内完成的订单数 **说明**:配套 5.9 指标,统计早上到仓的订单中有多少在次日 16:00 前完成考核。 **计算逻辑**: ``` 1. 获取当天 ReceiptTime <= 16:00 且标签率 >= 80% 的订单 2. 这些订单的考核时间为次日 16:00 3. 统计其中有成功扫描记录且时间 <= 次日 16:00 的订单 ``` #### 5.12 16点后考核通过包裹数(Afternoon Passed Count) **定义**: 16点后到仓且标签率 >= 80% 的订单中,在考核时间内完成的订单数 **说明**:配套 5.10 指标,统计下午到仓的订单中有多少在次日 23:59 前完成考核。 **计算逻辑**: ``` 1. 获取当天 ReceiptTime > 16:00 且标签率 >= 80% 的订单 2. 这些订单的考核时间为次日 23:59 3. 统计其中有成功扫描记录且时间 <= 次日 23:59 的订单 ``` --- ## 实现方案 ### 第一阶段:数据模型与DTO扩展 #### 1.1 新增DTO类 **MetricsCalculationDto.cs** - 存储中间计算结果 ``` - NeutralWaybillNumber - LabelRate (第一次扫描时的标签率) - FirstScanTime (第一条扫描记录的时间) - FirstScanResult (第一条扫描记录的结果) - ReceiptTime (收货时间) - AssessmentTime (考核时间) - IsHighLabelRate (标签率 >= 80%) - CompletedOnTime (是否在考核时间内完成) - CompletionTime (实际完成时间) - HandoverNumber (交接单号) - ArrivalDate (到仓日期) ``` **LabelRateMetricsDto.cs** - 交接单级别的标签率 ``` - HandoverNumber (交接单号) - TotalOrderCount (总订单数) - LabeledOrderCount (有标签的订单数) - LabelRate (标签率百分比) - FirstScanTime (现场首次扫描时间) ``` **Daily24HCompletionRateDto.cs** - 日统计完成率 ``` - Date (日期, UTC-5) - DailyNewReplaceCount (当天新增换单数) - CumulativeTotalReplaceCount (累计要换的总单数) - UnfinishedFailureCount (换单失败未完结订单) - DailyFailureCount (当日换单失败) - DailySuccessCount (当日换单成功数) - DailyStopCount (当日STOP数) - DailyShouldReplaceCount (当天应该换单数) - HighLabelRateOrderCount (标签率 >= 80% 的订单数) - CompletedOnTimeCount (按时完成的订单数) - Rate24Hour (24小时完成率百分比) - DailyCompletionRate (当天换单完成率百分比) - DailyLabelPushCount (当日标签推送数) - DailyScanCount (当日扫描数) - BeforeNoonArrivedCount (16点前到仓包裹数) - AfternoonArrivedCount (16点后到仓包裹数) - BeforeNoonPassedCount (16点前考核通过包裹数) - AfternoonPassedCount (16点后考核通过包裹数) - DataFetchTime (数据拉取时间, UTC-5) ``` #### 1.2 新增Entity(可选,如果需要持久化计算结果) **MetricsCalculationResultEntity.cs** - 计算结果缓存表 ``` - Id (主键) - HandoverNumber - NeutralWaybillNumber - LabelRate - AssessmentTime - AssessmentDate - CompletedOnTime - CreatedAt - CalculatedAt ``` --- ### 第二阶段:Repository层扩展 在 `ILabelReplaceRepository` 中添加方法: #### 2.1 LabelReplaceRepository 扩展方法 ```csharp // 获取指定交接单号对应的所有订单 Task> GetOrdersByHandoverNumberAsync(string handoverNumber) // 获取指定交接单号列表对应的所有订单 Task> GetOrdersByHandoverNumbersAsync(List handoverNumbers) // 获取所有有标签的订单(不分页) Task> GetAllOrdersWithLabelsAsync() // 获取指定日期范围内有标签的订单 Task> GetOrdersWithLabelsByDateRangeAsync( DateTime startDate, DateTime endDate) ``` #### 2.2 LabelScanRepository 扩展方法 ```csharp // 获取指定日期范围内的扫描记录(指定结果) Task> GetScanRecordsByDateRangeAsync( DateTime startDate, DateTime endDate, ScanResult? resultFilter = null) // 获取指定中性面单号列表的所有扫描记录 Task> GetScanRecordsByNeutralWaybillNumbersAsync( List neutralWaybillNumbers) // 获取指定中性面单号的第一条扫描记录 Task GetFirstScanRecordByWaybillNumberAsync(string neutralWaybillNumber) // 批量获取多个中性面单号的第一条扫描记录 Task> GetFirstScanRecordsByWaybillNumbersAsync( List neutralWaybillNumbers) // 检查指定时间前是否有成功扫描 Task HasSuccessScanBeforeAsync(string neutralWaybillNumber, DateTime beforeTime) // 获取指定中性面单号的第一条成功扫描 Task GetFirstSuccessScanAsync(string neutralWaybillNumber) ``` #### 2.3 ArrivalHandoverFormRepository 扩展方法 ```csharp // 获取指定日期范围内的到货交接单 Task> GetArrivalHandoverFormsByDateRangeAsync( DateTime startDate, DateTime endDate) ``` --- #### 2.4 关键实现提示 **时区转换注意事项**: - ReceiptTime(UTC-5)进行时间比较前,需转换为 UTC 再进行 SQL 比较 - LabelRetrievedAt(UTC+0)直接比较 - CreatedAt(UTC+0)直接比较 - 在应用层判断"16:00"时,需使用本地时间(UTC-5)进行判断 **标签率计算的核心逻辑**: - 找到交接单关联的所有订单 - 检查是否存在该交接单的扫描记录 - 如果存在,找到第一条扫描记录的时间点,统计该时间点前有标签的订单数 - 如果不存在,统计当前有标签的订单数 - 标签率 = 满足条件的订单数 / 总数 --- ### 第三阶段:Service层实现 #### 3.1 新建 `IMetricsCalculationService` 接口 ```csharp public interface IMetricsCalculationService { // 时间转换辅助方法 DateTime ConvertUtcToUtc5(DateTime utcTime); DateTime ConvertUtc5ToUtc(DateTime utc5Time); DateTime GetUtc5Today(); DateTime GetUtc5DateStart(DateTime utc5Date); DateTime GetUtc5DateEnd(DateTime utc5Date); // 基础指标计算 Task GetLabelRateAsync(string handoverNumber); Task GetOrderMetricsAsync(string neutralWaybillNumber); // 当日指标计算 Task GetDailyNewReplaceCountAsync(DateTime date); Task GetCumulativeTotalReplaceCountAsync(DateTime date); Task GetDailyCompletionCountAsync(DateTime date); Task GetDailyStopCountAsync(DateTime date); Task GetDailyLabelPushCountAsync(DateTime date); Task GetDailyUnfinishedFailureCountAsync(); Task GetDailyFailureCountAsync(DateTime date); Task GetDailySuccessCountAsync(DateTime date); Task GetDailyShouldReplaceCountAsync(DateTime date); Task GetBeforeNoonArrivedCountAsync(DateTime date); Task GetAfternoonArrivedCountAsync(DateTime date); Task GetBeforeNoonPassedCountAsync(DateTime date); Task GetAfternoonPassedCountAsync(DateTime date); Task GetDailyScanCountAsync(DateTime date); // 24小时和每日完成率 Task Calculate24HCompletionRateAsync(DateTime date); Task CalculateDailyCompletionRateAsync(DateTime date); // 完整日统计 Task GetDailySummaryAsync(DateTime date); // 批量计算多个订单的指标 Task> GetBatchOrderMetricsAsync(List neutralWaybillNumbers); // 获取指定日期范围的每日统计 Task> GetDailySummariesAsync( DateTime startDate, DateTime endDate); // 重新计算并缓存某个交接单的标签率 Task RecalculateAndCacheLabelRateAsync(string handoverNumber); } ``` #### 3.2 实现 `MetricsCalculationService` **时区处理辅助方法**: ```csharp private const int UTC_5_OFFSET = -5; public DateTime ConvertUtcToUtc5(DateTime utcTime) { return utcTime.AddHours(UTC_5_OFFSET); } public DateTime ConvertUtc5ToUtc(DateTime utc5Time) { return utc5Time.AddHours(-UTC_5_OFFSET); } public DateTime GetUtc5Today() { var now = DateTime.UtcNow; var utc5Now = ConvertUtcToUtc5(now); return utc5Now.Date; } public DateTime GetUtc5DateStart(DateTime utc5Date) { // 返回 UTC-5 日期的开始时间(0:00:00),但以 UTC 时间表示 var utc5Start = utc5Date.Date; return ConvertUtc5ToUtc(utc5Start); } public DateTime GetUtc5DateEnd(DateTime utc5Date) { // 返回 UTC-5 日期的结束时间(23:59:59),但以 UTC 时间表示 var utc5End = utc5Date.Date.AddDays(1).AddSeconds(-1); return ConvertUtc5ToUtc(utc5End); } ``` **当日新增换单数计算**: ```csharp public async Task GetDailyNewReplaceCountAsync(DateTime date) { // date 是 UTC-5 格式的日期 var dateStart = GetUtc5DateStart(date); var dateEnd = GetUtc5DateEnd(date); // 获取该日期内到仓的所有交接单 var arrivalForms = await _arrivalHandoverFormService .GetArrivalHandoverFormsByDateRangeAsync(dateStart, dateEnd); if (arrivalForms.Count == 0) return 0; var handoverNumbers = arrivalForms.Select(f => f.HandoverNumber).ToList(); // 获取这些交接单关联的所有订单 var orders = await _labelReplaceRepository .GetOrdersByHandoverNumbersAsync(handoverNumbers); // 统计有标签的订单数 int count = orders.Count(o => !string.IsNullOrEmpty(o.Label)); return count; } ``` **累计要换的总单数计算**: ```csharp public async Task GetCumulativeTotalReplaceCountAsync(DateTime date) { // date 是 UTC-5 格式的日期 var dateStart = GetUtc5DateStart(date); // 获取所有有标签的订单 var labeledOrders = await _labelReplaceRepository .GetAllOrdersWithLabelsAsync(); // 排除当天新增的订单 var pastOrders = labeledOrders .Where(o => o.LabelRetrievedAt < dateStart) .ToList(); // 获取所有这些订单的扫描记录 var waybills = pastOrders.Select(o => o.NeutralWaybillNumber).ToList(); var scans = await _labelScanService .GetScanRecordsByNeutralWaybillNumbersAsync(waybills); // 统计没有成功扫描记录的订单 var successfulWaybills = scans .Where(s => s.Result == ScanResult.ReturnedLabel) .Select(s => s.NeutralWaybillNumber) .ToHashSet(); int count = pastOrders.Count(o => !successfulWaybills.Contains(o.NeutralWaybillNumber)); return count; } ``` **标签率计算方法**: ```csharp private async Task CalculateLabelRateAtFirstScanAsync(string handoverNumber) { // 获取该交接单关联的所有订单 var orders = await _labelReplaceRepository.GetOrdersByHandoverNumberAsync(handoverNumber); if (orders.Count == 0) return 0; // 获取该交接单所有订单的扫描记录 var waybills = orders.Select(o => o.NeutralWaybillNumber).ToList(); var allScans = await _labelScanService.GetScanRecordsByNeutralWaybillNumbersAsync(waybills); // 情况1:交接单不存在任何扫描记录 if (allScans == null || allScans.Count == 0) { // 返回当前标签率 int labeledCount = orders.Count(o => !string.IsNullOrEmpty(o.Label)); return (double)labeledCount / orders.Count; } // 情况2:交接单存在扫描记录,找到最早的扫描时间 var earliestScanTime = allScans.Min(s => s.CreatedAt); // 统计在最早扫描时间之前有标签的订单数 int labeledAtScanTime = 0; foreach (var order in orders) { if (!string.IsNullOrEmpty(order.Label)) { // LabelRetrievedAt 可能为 null,如果为 null 则使用 CreatedAt var labelRetrievedAt = order.LabelRetrievedAt ?? order.CreatedAt; if (labelRetrievedAt <= earliestScanTime) { labeledAtScanTime++; } } } return (double)labeledAtScanTime / orders.Count; } ``` **当天应该换单数计算**: ```csharp public async Task GetDailyShouldReplaceCountAsync(DateTime date) { // date 是 UTC-5 格式的日期 var dateStart = GetUtc5DateStart(date); var dateEnd = GetUtc5DateEnd(date); // 获取该日期内到仓的所有交接单 var arrivalForms = await _arrivalHandoverFormService .GetArrivalHandoverFormsByDateRangeAsync(dateStart, dateEnd); if (arrivalForms.Count == 0) return 0; int totalCount = 0; // 计算每个交接单的标签率 foreach (var form in arrivalForms) { var labelRate = await CalculateLabelRateAtFirstScanAsync(form.HandoverNumber); // 只统计标签率 >= 80% 的交接单关联的订单 if (labelRate >= 0.80) { var orders = await _labelReplaceRepository .GetOrdersByHandoverNumberAsync(form.HandoverNumber); totalCount += orders.Count; } } return totalCount; } **当日换单完成数计算**: ```csharp public async Task GetDailyCompletionCountAsync(DateTime date) { // date 是 UTC-5 格式的日期 var dateStart = GetUtc5DateStart(date); var dateEnd = GetUtc5DateEnd(date); // 获取该日期内有成功扫描记录的订单 var successfulScans = await _labelScanService .GetScanRecordsByDateRangeAsync(dateStart, dateEnd, ScanResult.ReturnedLabel); // 去重订单,统计数量 var uniqueWaybills = successfulScans .Select(s => s.NeutralWaybillNumber) .Distinct() .Count(); return uniqueWaybills; } ``` **当日STOP数计算**: ```csharp public async Task GetDailyStopCountAsync(DateTime date) { // date 是 UTC-5 格式的日期 var dateStart = GetUtc5DateStart(date); var dateEnd = GetUtc5DateEnd(date); // 获取该日期内所有成功扫描记录 var successfulScans = await _labelScanService .GetScanRecordsByDateRangeAsync(dateStart, dateEnd, ScanResult.ReturnedLabel); // 筛选 Description 包含 "STOP" 的记录 var stopScans = successfulScans .Where(s => s.Description != null && s.Description.Contains("STOP")) .Select(s => s.NeutralWaybillNumber) .Distinct() .Count(); return stopScans; } ``` **当日标签推送数计算**: ```csharp public async Task GetDailyLabelPushCountAsync(DateTime date) { // date 是 UTC-5 格式的日期 var dateStart = GetUtc5DateStart(date); var dateEnd = GetUtc5DateEnd(date); // 获取所有有标签推送时间的订单 var allOrders = await _labelReplaceRepository.GetAllOrdersWithLabelsAsync(); // 筛选推送时间在该日期内的订单 int count = allOrders.Count(o => o.LabelRetrievedAt.HasValue && o.LabelRetrievedAt >= dateStart && o.LabelRetrievedAt <= dateEnd ); return count; } ``` **完整日统计计算**: ```csharp public async Task GetDailySummaryAsync(DateTime date) { // 并行计算所有指标 var newReplaceCountTask = GetDailyNewReplaceCountAsync(date); var cumulativeCountTask = GetCumulativeTotalReplaceCountAsync(date); var completionCountTask = GetDailyCompletionCountAsync(date); var stopCountTask = GetDailyStopCountAsync(date); var labelPushCountTask = GetDailyLabelPushCountAsync(date); var shouldReplaceCountTask = GetDailyShouldReplaceCountAsync(date); var scanCountTask = GetDailyScanCountAsync(date); var beforeNoonCountTask = GetBeforeNoonArrivedCountAsync(date); var afternoonCountTask = GetAfternoonArrivedCountAsync(date); var beforeNoonPassedTask = GetBeforeNoonPassedCountAsync(date); var afternoonPassedTask = GetAfternoonPassedCountAsync(date); await Task.WhenAll( newReplaceCountTask, cumulativeCountTask, completionCountTask, stopCountTask, labelPushCountTask, shouldReplaceCountTask, scanCountTask, beforeNoonCountTask, afternoonCountTask, beforeNoonPassedTask, afternoonPassedTask ); int newReplaceCount = await newReplaceCountTask; int cumulativeCount = await cumulativeCountTask; int completionCount = await completionCountTask; int stopCount = await stopCountTask; int labelPushCount = await labelPushCountTask; int shouldReplaceCount = await shouldReplaceCountTask; int scanCount = await scanCountTask; int beforeNoonCount = await beforeNoonCountTask; int afternoonCount = await afternoonCountTask; int beforeNoonPassed = await beforeNoonPassedTask; int afternoonPassed = await afternoonPassedTask; // 计算完成率 double dailyCompletionRate = shouldReplaceCount > 0 ? (double)completionCount / shouldReplaceCount * 100 : 0; double rate24Hour = shouldReplaceCount > 0 ? (double)completionCount / shouldReplaceCount * 100 : 0; int unfinishedFailureCount = cumulativeCount; int dailyFailureCount = shouldReplaceCount - completionCount; int dailySuccessCount = completionCount; return new Daily24HCompletionRateDto { Date = date, DailyNewReplaceCount = newReplaceCount, CumulativeTotalReplaceCount = cumulativeCount, UnfinishedFailureCount = unfinishedFailureCount, DailyFailureCount = dailyFailureCount, DailySuccessCount = dailySuccessCount, DailyStopCount = stopCount, DailyShouldReplaceCount = shouldReplaceCount, DailyCompletionRate = $"{dailyCompletionRate:F2}%", Rate24Hour = $"{rate24Hour:F2}%", DailyLabelPushCount = labelPushCount, DailyScanCount = scanCount, BeforeNoonArrivedCount = beforeNoonCount, AfternoonArrivedCount = afternoonCount, BeforeNoonPassedCount = beforeNoonPassed, AfternoonPassedCount = afternoonPassed, DataFetchTime = ConvertUtcToUtc5(DateTime.UtcNow) }; } ``` --- ### 第四阶段:Controller层扩展 新增 `MetricsController` 端点: ```csharp [ApiController] [Route("api/[controller]")] public class MetricsController : ControllerBase { private readonly IMetricsCalculationService _metricsService; [HttpGet("label-rate")] public async Task GetLabelRate([FromQuery] string handoverNumber) [HttpGet("order-assessment")] public async Task GetOrderAssessmentMetrics([FromQuery] string neutralWaybillNumber) [HttpGet("daily-summary")] public async Task GetDailySummary([FromQuery] string date = null) [HttpGet("daily-summaries")] public async Task GetDailySummaries([FromQuery] string startDate, [FromQuery] string endDate) [HttpPost("recalculate-label-rate")] public async Task RecalculateLabelRate([FromBody] RecalculateLabelRateRequest request) } ``` 或扩展现有 `DashboardController`: ```csharp [HttpGet("daily-metrics")] public async Task GetDailyMetrics([FromQuery] string date = null) [HttpGet("date-range-metrics")] public async Task GetDateRangeMetrics([FromQuery] string startDate, [FromQuery] string endDate) ``` --- ## 第五阶段:测试与验证 ### 5.1 单元测试 - 测试标签率计算(边界情况:0个订单、全有标签、全无标签、未扫描状态) - 测试各项指标计算(多种时区场景) - 测试边界时间(UTC vs UTC-5 转换) ### 5.2 集成测试 - 端到端的完整流程测试 - 使用真实数据验证结果的准确性 - 性能测试(大数据量的计算速度) --- ## 第六阶段:异步处理与优化 ### 6.1 异步任务处理 - 大批量指标计算可使用后台任务 - 定期(如每小时)更新一次统计数据 ### 6.2 查询优化 - 优化 Repository 层的 SQL 查询,避免 N+1 问题 - 使用预加载(Include)优化关联查询性能 --- ## 实现步骤(优先级排序) 1. **第一步**: 创建DTO和接口定义 2. **第二步**: Repository层方法实现 3. **第三步**: Service层核心算法实现 4. **第四步**: Controller端点实现 5. **第五步**: 测试与验证 6. **第六步**: 异步处理与性能优化 7. **第七步**: 文档完善和代码审查 --- ## 关键注意事项 1. **时区处理**: 确保所有时间比较都使用 UTC,再根据需要转换为特定时区 2. **Null处理**: ReceiptTime可能为NULL,需要特殊处理 3. **多次重复**: 同一个订单可能有多条扫描记录,需要去重和排序 4. **性能**: 涉及多表JOIN和复杂计算,需要优化SQL查询 5. **数据一致性**: 计算过程中数据可能变化,需要事务保证 --- ## 依赖关系 - 现有的 `LabelReplaceService` - 现有的 `LabelScanService` - 现有的 `ArrivalHandoverFormService` - 可能需要新增 `ILabelScanRepository` 的扫描查询方法