14 KiB
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:
public async Task<RequestResult<YearlyStatisticsByMonthOutput>> GetYearlyStatisticsByMonthAsync(
int year,
CancellationToken cancellationToken = default)
Behavior Changes:
- The
yearparameter 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:
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:
private SecondaryCircuitInspectionStatisticsOutput CreateZeroStatistics(
int year,
int month)
Implementation:
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:
public class YearlyStatisticsByMonthOutput
{
public int Year { get; set; }
public List<MonthlyStatisticsItem> MonthlyStatistics { get; set; }
}
MonthlyStatisticsItem:
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
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
// 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
try
{
// Main logic
}
catch (Exception ex)
{
Log4Helper.Error($"获取年度统计信息失败: Year={year}, Error={ex.Message}", ex);
return RequestResult<YearlyStatisticsByMonthOutput>.CreateFailed(
$"获取年度统计信息失败: {ex.Message}");
}
Partial Failure Handling
// 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:
- Test September Display: Verify that September returns exactly 12 months (Jan-Dec of current year)
- Test October Display: Verify that October returns 13 months (Jan of current year to Jan of next year)
- Test January Display: Verify that January returns 15 months (Oct-Dec of previous year, Jan-Dec of current year)
- Test Year Boundary: Verify correct year values when crossing year boundaries
- Test Month Sequence: Verify months are in correct chronological order
- Test Zero Data: Verify months beyond prediction range have all zeros
- 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):
- Property Test for Display Window Size: Generate random current months, verify window size is correct
- Property Test for Historical Data: Generate random current months, verify all months <= current are historical
- Property Test for Predicted Data: Generate random current months, verify next 3 months are predicted
- Property Test for Zero Data: Generate random current months, verify months beyond prediction are zero
- Property Test for Chronological Order: Generate random current months, verify all months are sequential
- Property Test for Month Validity: Generate random current months, verify all month numbers are 1-12
- 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:
- Test with Real Database: Verify historical data is correctly retrieved
- Test with Prediction Algorithm: Verify predicted data is correctly generated
- 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)