398 lines
14 KiB
Markdown
Raw Permalink Normal View History

2026-01-06 22:59:58 +08:00
# 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)