上传源代码版本
This commit is contained in:
951
.trae/documents/metrics_calculation_plan.md
Normal file
951
.trae/documents/metrics_calculation_plan.md
Normal file
@@ -0,0 +1,951 @@
|
||||
# 订单指标系统计算实现计划
|
||||
|
||||
## 项目目标
|
||||
实现复杂的业务指标系统,包括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 方法中,对 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<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. 将 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<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 关键实现提示
|
||||
|
||||
**时区转换注意事项**:
|
||||
- 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<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` 的扫描查询方法
|
||||
Reference in New Issue
Block a user