Files
LabelChange-server/.trae/documents/metrics_calculation_plan.md
2026-06-01 16:30:29 +08:00

952 lines
30 KiB
Markdown
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.

# 订单指标系统计算实现计划
## 项目目标
实现复杂的业务指标系统包括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<int> 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<int> 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 方法中,对 CreatedAtUTC+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<int> 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. 将 LabelRetrievedAtUTC+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<int> 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<List<LabelReplaceEntity>> GetOrdersByHandoverNumberAsync(string handoverNumber)
// 获取指定交接单号列表对应的所有订单
Task<List<LabelReplaceEntity>> GetOrdersByHandoverNumbersAsync(List<string> handoverNumbers)
// 获取所有有标签的订单(不分页)
Task<List<LabelReplaceEntity>> GetAllOrdersWithLabelsAsync()
// 获取指定日期范围内有标签的订单
Task<List<LabelReplaceEntity>> GetOrdersWithLabelsByDateRangeAsync(
DateTime startDate, DateTime endDate)
```
#### 2.2 LabelScanRepository 扩展方法
```csharp
// 获取指定日期范围内的扫描记录(指定结果)
Task<List<LabelScanEntity>> GetScanRecordsByDateRangeAsync(
DateTime startDate, DateTime endDate, ScanResult? resultFilter = null)
// 获取指定中性面单号列表的所有扫描记录
Task<List<LabelScanEntity>> GetScanRecordsByNeutralWaybillNumbersAsync(
List<string> neutralWaybillNumbers)
// 获取指定中性面单号的第一条扫描记录
Task<LabelScanEntity> GetFirstScanRecordByWaybillNumberAsync(string neutralWaybillNumber)
// 批量获取多个中性面单号的第一条扫描记录
Task<Dictionary<string, LabelScanEntity>> GetFirstScanRecordsByWaybillNumbersAsync(
List<string> neutralWaybillNumbers)
// 检查指定时间前是否有成功扫描
Task<bool> HasSuccessScanBeforeAsync(string neutralWaybillNumber, DateTime beforeTime)
// 获取指定中性面单号的第一条成功扫描
Task<LabelScanEntity> GetFirstSuccessScanAsync(string neutralWaybillNumber)
```
#### 2.3 ArrivalHandoverFormRepository 扩展方法
```csharp
// 获取指定日期范围内的到货交接单
Task<List<ArrivalHandoverFormEntity>> GetArrivalHandoverFormsByDateRangeAsync(
DateTime startDate, DateTime endDate)
```
---
#### 2.4 关键实现提示
**时区转换注意事项**:
- ReceiptTimeUTC-5进行时间比较前需转换为 UTC 再进行 SQL 比较
- LabelRetrievedAtUTC+0直接比较
- CreatedAtUTC+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<LabelRateMetricsDto> GetLabelRateAsync(string handoverNumber);
Task<MetricsCalculationDto> GetOrderMetricsAsync(string neutralWaybillNumber);
// 当日指标计算
Task<int> GetDailyNewReplaceCountAsync(DateTime date);
Task<int> GetCumulativeTotalReplaceCountAsync(DateTime date);
Task<int> GetDailyCompletionCountAsync(DateTime date);
Task<int> GetDailyStopCountAsync(DateTime date);
Task<int> GetDailyLabelPushCountAsync(DateTime date);
Task<int> GetDailyUnfinishedFailureCountAsync();
Task<int> GetDailyFailureCountAsync(DateTime date);
Task<int> GetDailySuccessCountAsync(DateTime date);
Task<int> GetDailyShouldReplaceCountAsync(DateTime date);
Task<int> GetBeforeNoonArrivedCountAsync(DateTime date);
Task<int> GetAfternoonArrivedCountAsync(DateTime date);
Task<int> GetBeforeNoonPassedCountAsync(DateTime date);
Task<int> GetAfternoonPassedCountAsync(DateTime date);
Task<int> GetDailyScanCountAsync(DateTime date);
// 24小时和每日完成率
Task<double> Calculate24HCompletionRateAsync(DateTime date);
Task<double> CalculateDailyCompletionRateAsync(DateTime date);
// 完整日统计
Task<Daily24HCompletionRateDto> GetDailySummaryAsync(DateTime date);
// 批量计算多个订单的指标
Task<List<MetricsCalculationDto>> GetBatchOrderMetricsAsync(List<string> neutralWaybillNumbers);
// 获取指定日期范围的每日统计
Task<List<Daily24HCompletionRateDto>> GetDailySummariesAsync(
DateTime startDate, DateTime endDate);
// 重新计算并缓存某个交接单的标签率
Task<bool> 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<int> 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<int> 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<double> 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<int> 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<int> 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<int> 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<int> 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<Daily24HCompletionRateDto> 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<IActionResult> GetLabelRate([FromQuery] string handoverNumber)
[HttpGet("order-assessment")]
public async Task<IActionResult> GetOrderAssessmentMetrics([FromQuery] string neutralWaybillNumber)
[HttpGet("daily-summary")]
public async Task<IActionResult> GetDailySummary([FromQuery] string date = null)
[HttpGet("daily-summaries")]
public async Task<IActionResult> GetDailySummaries([FromQuery] string startDate, [FromQuery] string endDate)
[HttpPost("recalculate-label-rate")]
public async Task<IActionResult> RecalculateLabelRate([FromBody] RecalculateLabelRateRequest request)
}
```
或扩展现有 `DashboardController`:
```csharp
[HttpGet("daily-metrics")]
public async Task<IActionResult> GetDailyMetrics([FromQuery] string date = null)
[HttpGet("date-range-metrics")]
public async Task<IActionResult> 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` 的扫描查询方法