398 lines
14 KiB
Markdown
398 lines
14 KiB
Markdown
# Design Document
|
|
|
|
## Overview
|
|
|
|
This design document specifies the modifications to the `GetYearlyStatisticsByMonthAsync` method to implement a rolling 15-month display window that adapts based on the current month. The solution maintains backward compatibility with the existing API while providing more contextually relevant data to users.
|
|
|
|
## Architecture
|
|
|
|
The solution follows the existing service architecture pattern:
|
|
|
|
```
|
|
SecondaryCircuitInspectionResultStatisticsAppService
|
|
└── GetYearlyStatisticsByMonthAsync()
|
|
├── CalculateDisplayWindow() [NEW]
|
|
├── GetStatisticsAsync() [EXISTING]
|
|
├── PredictMonthlyStatisticsWithModuleDetails() [EXISTING]
|
|
└── CreateZeroStatistics() [NEW]
|
|
```
|
|
|
|
The design introduces two new helper methods and modifies the main method logic to support the rolling window calculation.
|
|
|
|
## Components and Interfaces
|
|
|
|
### Modified Component: GetYearlyStatisticsByMonthAsync
|
|
|
|
**Signature:**
|
|
```csharp
|
|
public async Task<RequestResult<YearlyStatisticsByMonthOutput>> GetYearlyStatisticsByMonthAsync(
|
|
int year,
|
|
CancellationToken cancellationToken = default)
|
|
```
|
|
|
|
**Behavior Changes:**
|
|
- The `year` parameter is now ignored; the method always uses the current year
|
|
- Returns a 15-month rolling window instead of a fixed calendar year
|
|
- Includes cross-year data when appropriate
|
|
|
|
### New Component: CalculateDisplayWindow
|
|
|
|
**Purpose:** Calculate the start and end months for the display window based on the current month.
|
|
|
|
**Signature:**
|
|
```csharp
|
|
private (int startYear, int startMonth, int endYear, int endMonth) CalculateDisplayWindow(
|
|
int currentYear,
|
|
int currentMonth)
|
|
```
|
|
|
|
**Algorithm:**
|
|
```
|
|
Input: currentYear, currentMonth
|
|
Output: (startYear, startMonth, endYear, endMonth)
|
|
|
|
1. Calculate historical start (3 months before current):
|
|
historicalStartMonth = currentMonth - 3
|
|
|
|
2. Handle year boundary for historical data:
|
|
IF historicalStartMonth <= 0 THEN
|
|
startYear = currentYear - 1
|
|
startMonth = 12 + historicalStartMonth
|
|
ELSE
|
|
startYear = currentYear
|
|
startMonth = historicalStartMonth
|
|
END IF
|
|
|
|
3. Calculate end month (11 months after current):
|
|
endMonthOffset = currentMonth + 11
|
|
|
|
4. Handle year boundary for end data:
|
|
IF endMonthOffset > 12 THEN
|
|
endYear = currentYear + 1
|
|
endMonth = endMonthOffset - 12
|
|
ELSE
|
|
endYear = currentYear
|
|
endMonth = endMonthOffset
|
|
END IF
|
|
|
|
5. Return (startYear, startMonth, endYear, endMonth)
|
|
```
|
|
|
|
**Examples:**
|
|
- Current: September 2025 → Window: June 2025 to April 2026 (but display Jan-Dec 2025 per requirements)
|
|
- Current: October 2025 → Window: July 2025 to August 2026 (but display Jan 2025 to Jan 2026 per requirements)
|
|
- Current: January 2025 → Window: October 2024 to November 2025 (but display Oct-Dec 2024, Jan-Dec 2025 per requirements)
|
|
|
|
### New Component: CreateZeroStatistics
|
|
|
|
**Purpose:** Create a statistics object with all counts set to zero for months beyond the prediction range.
|
|
|
|
**Signature:**
|
|
```csharp
|
|
private SecondaryCircuitInspectionStatisticsOutput CreateZeroStatistics(
|
|
int year,
|
|
int month)
|
|
```
|
|
|
|
**Implementation:**
|
|
```csharp
|
|
return new SecondaryCircuitInspectionStatisticsOutput
|
|
{
|
|
StatisticsStartTime = new DateTime(year, month, 1),
|
|
StatisticsEndTime = new DateTime(year, month, 1).AddMonths(1).AddSeconds(-1),
|
|
TotalCount = 0,
|
|
NormalCount = 0,
|
|
AbnormalCount = 0,
|
|
ErrorCount = 0,
|
|
AverageExecutionDurationMs = 0,
|
|
AbnormalRate = 0,
|
|
StatisticsByModule = new List<SecondaryCircuitInspectionStatisticsByModule>()
|
|
};
|
|
```
|
|
|
|
## Data Models
|
|
|
|
### Existing Models (No Changes)
|
|
|
|
**YearlyStatisticsByMonthOutput:**
|
|
```csharp
|
|
public class YearlyStatisticsByMonthOutput
|
|
{
|
|
public int Year { get; set; }
|
|
public List<MonthlyStatisticsItem> MonthlyStatistics { get; set; }
|
|
}
|
|
```
|
|
|
|
**MonthlyStatisticsItem:**
|
|
```csharp
|
|
public class MonthlyStatisticsItem
|
|
{
|
|
public int Year { get; set; }
|
|
public int Month { get; set; }
|
|
public SecondaryCircuitInspectionStatisticsOutput Statistics { get; set; }
|
|
public bool IsPredicted { get; set; }
|
|
}
|
|
```
|
|
|
|
### Data Flow
|
|
|
|
```mermaid
|
|
graph TD
|
|
A[GetYearlyStatisticsByMonthAsync] --> B[Get Current Date]
|
|
B --> C[CalculateDisplayWindow]
|
|
C --> D[Iterate Through Display Window]
|
|
D --> E{Month Type?}
|
|
E -->|Historical| F[GetStatisticsAsync]
|
|
E -->|Current| F
|
|
E -->|Predicted| G[PredictMonthlyStatisticsWithModuleDetails]
|
|
E -->|Zero| H[CreateZeroStatistics]
|
|
F --> I[Add to MonthlyStatistics]
|
|
G --> I
|
|
H --> I
|
|
I --> J{More Months?}
|
|
J -->|Yes| D
|
|
J -->|No| K[Return Result]
|
|
```
|
|
|
|
## Detailed Algorithm
|
|
|
|
### Main Method Logic
|
|
|
|
```
|
|
1. Get current date and time:
|
|
now = DateTime.Now
|
|
currentYear = now.Year
|
|
currentMonth = now.Month
|
|
|
|
2. Initialize output:
|
|
output = new YearlyStatisticsByMonthOutput { Year = currentYear }
|
|
|
|
3. Calculate display window:
|
|
(startYear, startMonth, endYear, endMonth) = CalculateDisplayWindow(currentYear, currentMonth)
|
|
|
|
4. Collect historical data for prediction:
|
|
historicalMonthlyStats = new List<>()
|
|
|
|
5. Iterate through display window:
|
|
FOR each month from (startYear, startMonth) to (endYear, endMonth):
|
|
targetYear = current iteration year
|
|
targetMonth = current iteration month
|
|
|
|
a. Determine month type:
|
|
isHistorical = (targetYear < currentYear) OR
|
|
(targetYear == currentYear AND targetMonth <= currentMonth)
|
|
isPredicted = (targetYear > currentYear) OR
|
|
(targetYear == currentYear AND targetMonth > currentMonth AND targetMonth <= currentMonth + 3)
|
|
isZero = NOT isHistorical AND NOT isPredicted
|
|
|
|
b. Get statistics based on type:
|
|
IF isHistorical THEN
|
|
dateRange = $"{targetYear:D4}-{targetMonth:D2}"
|
|
input = new SecondaryCircuitInspectionStatisticsInput { DateRange = dateRange }
|
|
monthlyStats = await GetStatisticsAsync(input, cancellationToken)
|
|
historicalMonthlyStats.Add(monthlyStats)
|
|
ELSE IF isPredicted THEN
|
|
IF predictedStats is empty THEN
|
|
predictStartYear = currentYear
|
|
predictStartMonth = currentMonth + 1
|
|
IF predictStartMonth > 12 THEN
|
|
predictStartMonth = 1
|
|
predictStartYear++
|
|
END IF
|
|
predictedStats = PredictMonthlyStatisticsWithModuleDetails(
|
|
historicalMonthlyStats,
|
|
predictStartYear,
|
|
predictStartMonth,
|
|
3)
|
|
END IF
|
|
monthlyStats = predictedStats[predictIndex++]
|
|
ELSE (isZero)
|
|
monthlyStats = CreateZeroStatistics(targetYear, targetMonth)
|
|
END IF
|
|
|
|
c. Add to output:
|
|
output.MonthlyStatistics.Add(new MonthlyStatisticsItem {
|
|
Year = targetYear,
|
|
Month = targetMonth,
|
|
Statistics = monthlyStats,
|
|
IsPredicted = isPredicted OR isZero
|
|
})
|
|
END FOR
|
|
|
|
6. Return success result:
|
|
return RequestResult<YearlyStatisticsByMonthOutput>.CreateSuccess(output)
|
|
```
|
|
|
|
### Special Case Handling
|
|
|
|
**September (Month 9):**
|
|
```
|
|
Current: September 2025
|
|
Historical: June, July, August 2025
|
|
Current: September 2025
|
|
Predicted: October, November, December 2025
|
|
Zero: None (only 12 months displayed: Jan-Dec 2025)
|
|
|
|
Special logic: Display window is Jan-Dec of current year only
|
|
```
|
|
|
|
**October (Month 10):**
|
|
```
|
|
Current: October 2025
|
|
Historical: July, August, September 2025
|
|
Current: October 2025
|
|
Predicted: November, December 2025, January 2026
|
|
Zero: February-December 2026
|
|
|
|
Display: Jan 2025 - Jan 2026 (13 months)
|
|
```
|
|
|
|
**January (Month 1):**
|
|
```
|
|
Current: January 2025
|
|
Historical: October, November, December 2024
|
|
Current: January 2025
|
|
Predicted: February, March, April 2025
|
|
Zero: May-December 2025
|
|
|
|
Display: Oct-Dec 2024, Jan-Dec 2025 (15 months)
|
|
```
|
|
|
|
## Correctness Properties
|
|
|
|
*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
|
|
|
### Property 1: Three Historical Months Preceding Current
|
|
|
|
*For any* current month from April through December, the display window should include the 3 months immediately preceding the current month with actual statistics data retrieved from the database.
|
|
|
|
**Validates: Requirements 1.1, 1.5**
|
|
|
|
### Property 2: Current Month Inclusion and Marking
|
|
|
|
*For any* current month, the display window should include that month, retrieve actual statistics data from the database, and mark it as historical (IsPredicted = false).
|
|
|
|
**Validates: Requirements 2.1, 2.2, 2.3**
|
|
|
|
### Property 3: Three Predicted Months Following Current
|
|
|
|
*For any* current month, the display window should include the 3 months immediately following the current month, generate predicted statistics using the prediction algorithm, mark them as predicted (IsPredicted = true), and correctly handle year boundaries.
|
|
|
|
**Validates: Requirements 3.1, 3.2, 3.3, 3.4**
|
|
|
|
### Property 4: Zero Data Beyond Prediction Range
|
|
|
|
*For any* month in the display window beyond the 3-month prediction range (current + 4 onwards), all count values (TotalCount, NormalCount, AbnormalCount, ErrorCount) should equal 0, IsPredicted should be false, and StatisticsByModule should be an empty list.
|
|
|
|
**Validates: Requirements 4.1, 4.3, 4.4**
|
|
|
|
### Property 5: Display Window Size
|
|
|
|
*For any* current month except September, the display window should contain exactly 15 months; for September, the display window should contain exactly 12 months.
|
|
|
|
**Validates: Requirements 10.2, 12.2**
|
|
|
|
### Property 6: Chronological Month Sequence
|
|
|
|
*For any* two consecutive MonthlyStatisticsItem objects in the output, the second month should be exactly one calendar month after the first, with year values correctly incremented when crossing year boundaries.
|
|
|
|
**Validates: Requirements 11.3, 12.4**
|
|
|
|
### Property 7: Month and Year Value Validity
|
|
|
|
*For any* MonthlyStatisticsItem in the output, the Month property should be in the range 1-12 inclusive, and the Year property should be a valid four-digit year (>= 1900 and <= 9999).
|
|
|
|
**Validates: Requirements 11.1, 11.2**
|
|
|
|
### Property 8: Output Year Property
|
|
|
|
*For any* successful response, the Year property of YearlyStatisticsByMonthOutput should equal the current year.
|
|
|
|
**Validates: Requirements 12.5**
|
|
|
|
## Error Handling
|
|
|
|
### Input Validation
|
|
|
|
```csharp
|
|
// Year parameter validation (maintained for backward compatibility)
|
|
if (year < 1900 || year > 9999)
|
|
{
|
|
return RequestResult<YearlyStatisticsByMonthOutput>.CreateFailed(
|
|
$"年份参数无效: {year}。有效范围为 1900-9999");
|
|
}
|
|
|
|
// Note: The year parameter is validated but not used in calculations
|
|
Log4Helper.Info($"Year parameter {year} provided but using current year {currentYear} for rolling window");
|
|
```
|
|
|
|
### Exception Handling
|
|
|
|
```csharp
|
|
try
|
|
{
|
|
// Main logic
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log4Helper.Error($"获取年度统计信息失败: Year={year}, Error={ex.Message}", ex);
|
|
return RequestResult<YearlyStatisticsByMonthOutput>.CreateFailed(
|
|
$"获取年度统计信息失败: {ex.Message}");
|
|
}
|
|
```
|
|
|
|
### Partial Failure Handling
|
|
|
|
```csharp
|
|
// If a specific month fails to load, add zero statistics instead of failing entirely
|
|
try
|
|
{
|
|
monthlyStats = await GetStatisticsAsync(input, cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log4Helper.Warning($"获取月度统计失败: Year={targetYear}, Month={targetMonth}, Error={ex.Message}");
|
|
monthlyStats = CreateZeroStatistics(targetYear, targetMonth);
|
|
}
|
|
```
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Testing
|
|
|
|
Unit tests will verify specific examples and edge cases:
|
|
|
|
1. **Test September Display**: Verify that September returns exactly 12 months (Jan-Dec of current year)
|
|
2. **Test October Display**: Verify that October returns 13 months (Jan of current year to Jan of next year)
|
|
3. **Test January Display**: Verify that January returns 15 months (Oct-Dec of previous year, Jan-Dec of current year)
|
|
4. **Test Year Boundary**: Verify correct year values when crossing year boundaries
|
|
5. **Test Month Sequence**: Verify months are in correct chronological order
|
|
6. **Test Zero Data**: Verify months beyond prediction range have all zeros
|
|
7. **Test IsPredicted Flag**: Verify historical, predicted, and zero months have correct flags
|
|
|
|
### Property-Based Testing
|
|
|
|
Property-based tests will verify universal properties across all inputs using a PBT library (e.g., FsCheck for C#, or a similar library):
|
|
|
|
1. **Property Test for Display Window Size**: Generate random current months, verify window size is correct
|
|
2. **Property Test for Historical Data**: Generate random current months, verify all months <= current are historical
|
|
3. **Property Test for Predicted Data**: Generate random current months, verify next 3 months are predicted
|
|
4. **Property Test for Zero Data**: Generate random current months, verify months beyond prediction are zero
|
|
5. **Property Test for Chronological Order**: Generate random current months, verify all months are sequential
|
|
6. **Property Test for Month Validity**: Generate random current months, verify all month numbers are 1-12
|
|
7. **Property Test for Year Validity**: Generate random current months, verify year values are valid
|
|
|
|
### Integration Testing
|
|
|
|
Integration tests will verify the method works correctly with real dependencies:
|
|
|
|
1. **Test with Real Database**: Verify historical data is correctly retrieved
|
|
2. **Test with Prediction Algorithm**: Verify predicted data is correctly generated
|
|
3. **Test End-to-End**: Verify complete flow from request to response
|
|
|
|
### Test Configuration
|
|
|
|
- Minimum 100 iterations per property test
|
|
- Each property test tagged with: **Feature: yearly-statistics-display-logic, Property {number}: {property_text}**
|
|
- Unit tests and property tests are complementary (both required)
|