diff --git a/.sisyphus/notepads/statistics-year-selection/decisions.md b/.sisyphus/notepads/statistics-year-selection/decisions.md new file mode 100644 index 0000000..7513747 --- /dev/null +++ b/.sisyphus/notepads/statistics-year-selection/decisions.md @@ -0,0 +1,80 @@ +# Decisions - Statistics Year Selection Enhancement + +## [2026-01-28] Architecture Decisions + +### Frontend Implementation Strategy + +#### 1. Date Picker Mode Toggle +- Add a toggle switch in the date picker popup to switch between "按月" (month) and "按年" (year) modes +- When "按年" selected: use `columns-type="['year']"` +- When "按月" selected: use `columns-type="['year', 'month']` (current behavior) + +#### 2. State Management +- Add `dateSelectionMode` ref: `'month'` | `'year'` +- When year-only mode: set `currentMonth = 0` to indicate full year +- Keep `currentYear` as integer (unchanged) +- Update `selectedDate` array dynamically based on mode: + - Year mode: `['YYYY']` + - Month mode: `['YYYY', 'MM']` + +#### 3. Display Logic +- Nav bar title: `currentYear年` when `currentMonth === 0`, else `currentYear年currentMonth月` +- Chart titles: Update to reflect year or year-month scope + +#### 4. API Calls +- Pass `month: currentMonth.value || 0` to all API calls +- Backend will handle month=0 as year-only query + +### Backend Implementation Strategy + +#### 1. Repository Layer Change +**File**: `Repository/TransactionRecordRepository.cs` +**Method**: `BuildQuery()` lines 81-86 + +```csharp +if (year.HasValue) +{ + if (month.HasValue && month.Value > 0) + { + // Specific month + var dateStart = new DateTime(year.Value, month.Value, 1); + var dateEnd = dateStart.AddMonths(1); + query = query.Where(t => t.OccurredAt >= dateStart && t.OccurredAt < dateEnd); + } + else + { + // Entire year + var dateStart = new DateTime(year.Value, 1, 1); + var dateEnd = new DateTime(year.Value + 1, 1, 1); + query = query.Where(t => t.OccurredAt >= dateStart && t.OccurredAt < dateEnd); + } +} +``` + +#### 2. Service Layer +- No changes needed - services already pass month parameter to repository +- Services will receive month=0 for year-only queries + +#### 3. API Controller +- No changes needed - already accepts year/month parameters + +### Testing Strategy + +#### Backend Tests +- Test year-only query returns all transactions for that year +- Test month-specific query still works +- Test edge cases: year boundaries, leap years + +#### Frontend Tests +- Test toggle switches picker mode correctly +- Test year selection updates state and fetches data +- Test display updates correctly for year vs year-month + +### User Experience Flow + +1. User clicks date picker in nav bar +2. Popup opens with toggle: "按月 | 按年" +3. User selects mode (default: 按月 for backward compatibility) +4. User selects date(s) and confirms +5. Statistics refresh with new scope +6. Display updates to show scope (year or year-month) \ No newline at end of file diff --git a/.sisyphus/notepads/statistics-year-selection/issues.md b/.sisyphus/notepads/statistics-year-selection/issues.md new file mode 100644 index 0000000..8b2461a --- /dev/null +++ b/.sisyphus/notepads/statistics-year-selection/issues.md @@ -0,0 +1,27 @@ +# Issues - Statistics Year Selection Enhancement + +## [2026-01-28] Backend Repository Limitation + +### Issue +`TransactionRecordRepository.BuildQuery()` requires both year AND month parameters to be present. Year-only queries (month=null or month=0) are not supported. + +### Impact +- Cannot query full-year statistics from the frontend +- Current implementation only supports month-level granularity +- All statistics endpoints rely on `QueryAsync(year, month, ...)` + +### Solution +Modify `BuildQuery()` method in `Repository/TransactionRecordRepository.cs` to support: +1. Year-only queries (when year provided, month is null or 0) +2. Month-specific queries (when both year and month provided - current behavior) + +### Implementation Location +- File: `Repository/TransactionRecordRepository.cs` +- Method: `BuildQuery()` lines 81-86 +- Also need to update service layer to handle month=0 or null + +### Testing Requirements +- Test year-only query returns all transactions for that year +- Test month-specific query still works as before +- Test edge cases: leap years, year boundaries +- Verify all statistics endpoints work with year-only mode \ No newline at end of file diff --git a/.sisyphus/notepads/statistics-year-selection/learnings.md b/.sisyphus/notepads/statistics-year-selection/learnings.md new file mode 100644 index 0000000..9c34f0c --- /dev/null +++ b/.sisyphus/notepads/statistics-year-selection/learnings.md @@ -0,0 +1,181 @@ +# Learnings - Statistics Year Selection Enhancement + +## [2026-01-28] Initial Analysis + +### Current Implementation +- **File**: `Web/src/views/StatisticsView.vue` +- **Current picker**: `columns-type="['year', 'month']` (year-month only) +- **State variables**: + - `currentYear` - integer year + - `currentMonth` - integer month (1-12) + - `selectedDate` - array `['YYYY', 'MM']` for picker +- **API calls**: All endpoints use `{ year, month }` parameters + +### Vant UI Year-Only Pattern +- **Key prop**: `columns-type="['year']"` +- **Picker value**: Single-element array `['YYYY']` +- **Confirmation**: `selectedValues[0]` contains year string + +### Implementation Strategy +1. Add UI toggle to switch between year-month and year-only modes +2. When year-only selected, set `currentMonth = 0` or null to indicate full year +3. Backend API already supports year-only queries (when month=0 or null) +4. Update display logic to show "YYYY年" vs "YYYY年MM月" + +### API Compatibility - CRITICAL FINDING +- **Backend limitation**: `TransactionRecordRepository.BuildQuery()` (lines 81-86) requires BOTH year AND month +- Current logic: `if (year.HasValue && month.HasValue)` - year-only queries are NOT supported +- **Must modify repository** to support year-only queries: + - When year provided but month is null/0: query entire year (Jan 1 to Dec 31) + - When both year and month provided: query specific month (current behavior) +- All statistics endpoints use `QueryAsync(year, month, ...)` pattern + +### Required Backend Changes +**File**: `Repository/TransactionRecordRepository.cs` +**Method**: `BuildQuery()` lines 81-86 +**Change**: Modify year/month filtering logic to support year-only queries + +```csharp +// Current (line 81-86): +if (year.HasValue && month.HasValue) +{ + var dateStart = new DateTime(year.Value, month.Value, 1); + var dateEnd = dateStart.AddMonths(1); + query = query.Where(t => t.OccurredAt >= dateStart && t.OccurredAt < dateEnd); +} + +// Needed: +if (year.HasValue) +{ + if (month.HasValue && month.Value > 0) + { + // Specific month + var dateStart = new DateTime(year.Value, month.Value, 1); + var dateEnd = dateStart.AddMonths(1); + query = query.Where(t => t.OccurredAt >= dateStart && t.OccurredAt < dateEnd); + } + else + { + // Entire year + var dateStart = new DateTime(year.Value, 1, 1); + var dateEnd = new DateTime(year.Value + 1, 1, 1); + query = query.Where(t => t.OccurredAt >= dateStart && t.OccurredAt < dateEnd); + } +} +``` + +### Existing Patterns +- BudgetView.vue uses same year-month picker pattern +- Dayjs used for all date formatting: `dayjs().format('YYYY-MM-DD')` +- Date picker values always arrays for Vant UI +## [2026-01-28] Repository BuildQuery() Enhancement + +### Implementation Completed +- **File Modified**: `Repository/TransactionRecordRepository.cs` lines 81-94 +- **Change**: Updated year/month filtering logic to support year-only queries + +### Logic Changes +```csharp +// Old: Required both year AND month +if (year.HasValue && month.HasValue) { ... } + +// New: Support year-only queries +if (year.HasValue) +{ + if (month.HasValue && month.Value > 0) + { + // 查询指定年月 + var dateStart = new DateTime(year.Value, month.Value, 1); + var dateEnd = dateStart.AddMonths(1); + query = query.Where(t => t.OccurredAt >= dateStart && t.OccurredAt < dateEnd); + } + else + { + // 查询整年数据(1月1日到下年1月1日) + var dateStart = new DateTime(year.Value, 1, 1); + var dateEnd = new DateTime(year.Value + 1, 1, 1); + query = query.Where(t => t.OccurredAt >= dateStart && t.OccurredAt < dateEnd); + } +} +``` + +### Behavior +- **Month-specific** (month.HasValue && month.Value > 0): Query from 1st of month to 1st of next month +- **Year-only** (month is null or 0): Query from Jan 1 to Jan 1 of next year +- **No year provided**: No date filtering applied + +### Verification +- All 14 tests pass: `dotnet test WebApi.Test/WebApi.Test.csproj` +- No breaking changes to existing functionality +- Chinese comments added for business logic clarity + +### Key Pattern +- Use `month.Value > 0` check to distinguish year-only (0/null) from month-specific (1-12) +- Date range is exclusive on upper bound (`< dateEnd`) to avoid including boundary dates + +## [2026-01-28] Frontend Year-Only Selection Implementation + +### Changes Made +**File**: `Web/src/views/StatisticsView.vue` + +#### 1. Nav Bar Title Display (Line 12) +- Updated to show "YYYY年" when `currentMonth === 0` +- Shows "YYYY年MM月" when month is selected +- Template: `{{ currentMonth === 0 ? \`${currentYear}年\` : \`${currentYear}年${currentMonth}月\` }}` + +#### 2. Date Picker Popup (Lines 268-289) +- Added toggle switch using `van-tabs` component +- Two modes: "按月" (month) and "按年" (year) +- Tabs positioned above the date picker +- Dynamic `columns-type` based on selection mode: + - Year mode: `['year']` + - Month mode: `['year', 'month']` + +#### 3. State Management (Line 347) +- Added `dateSelectionMode` ref: `'month'` | `'year'` +- Default: `'month'` for backward compatibility +- `currentMonth` set to `0` when year-only selected + +#### 4. Confirmation Handler (Lines 532-544) +- Updated to handle both year-only and year-month modes +- When year mode: `newMonth = 0` +- When month mode: `newMonth = parseInt(selectedValues[1])` + +#### 5. API Calls (All Statistics Endpoints) +- Updated all API calls to use `month: currentMonth.value || 0` +- Ensures backend receives `0` for year-only queries +- Modified functions: + - `fetchMonthlyData()` (line 574) + - `fetchCategoryData()` (lines 592, 610, 626) + - `fetchDailyData()` (line 649) + - `fetchBalanceData()` (line 672) + - `loadCategoryBills()` (line 1146) + +#### 6. Mode Switching Watcher (Lines 1355-1366) +- Added `watch(dateSelectionMode)` to update `selectedDate` array +- When switching to year mode: `selectedDate = [year.toString()]` +- When switching to month mode: `selectedDate = [year, month]` + +#### 7. Styling (Lines 1690-1705) +- Added `.date-picker-header` styles for tabs +- Clean, minimal design matching Vant UI conventions +- Proper spacing and background colors + +### Vant UI Patterns Used +- **van-tabs**: For mode switching toggle +- **van-date-picker**: Dynamic `columns-type` prop +- **van-popup**: Container for picker and tabs +- Composition API with `watch` for reactive updates + +### User Experience +1. Click nav bar date → popup opens with "按月" default +2. Switch to "按年" → picker shows only year column +3. Select year and confirm → `currentMonth = 0` +4. Nav bar shows "2025年" instead of "2025年1月" +5. All statistics refresh with year-only data + +### Verification +- Build succeeds: `cd Web && pnpm build` +- No TypeScript errors +- No breaking changes to existing functionality +- Backward compatible with month-only selection diff --git a/Repository/BudgetRepository.cs b/Repository/BudgetRepository.cs index e9f4324..3c16a60 100644 --- a/Repository/BudgetRepository.cs +++ b/Repository/BudgetRepository.cs @@ -43,7 +43,7 @@ public class BudgetRepository(IFreeSql freeSql) : BaseRepository(f foreach (var record in records) { var categories = record.SelectedCategories.Split(',').ToList(); - for (int i = 0; i < categories.Count; i++) + for (var i = 0; i < categories.Count; i++) { if (categories[i] == oldName) { diff --git a/Repository/TransactionRecordRepository.cs b/Repository/TransactionRecordRepository.cs index d5919cf..508baac 100644 --- a/Repository/TransactionRecordRepository.cs +++ b/Repository/TransactionRecordRepository.cs @@ -78,11 +78,22 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository t.Type == type!.Value); - if (year.HasValue && month.HasValue) + if (year.HasValue) { - var dateStart = new DateTime(year.Value, month.Value, 1); - var dateEnd = dateStart.AddMonths(1); - query = query.Where(t => t.OccurredAt >= dateStart && t.OccurredAt < dateEnd); + if (month.HasValue && month.Value > 0) + { + // 查询指定年月 + var dateStart = new DateTime(year.Value, month.Value, 1); + var dateEnd = dateStart.AddMonths(1); + query = query.Where(t => t.OccurredAt >= dateStart && t.OccurredAt < dateEnd); + } + else + { + // 查询整年数据(1月1日到下年1月1日) + var dateStart = new DateTime(year.Value, 1, 1); + var dateEnd = new DateTime(year.Value + 1, 1, 1); + query = query.Where(t => t.OccurredAt >= dateStart && t.OccurredAt < dateEnd); + } } query = query.WhereIf(startDate.HasValue, t => t.OccurredAt >= startDate!.Value) diff --git a/Service/AI/SmartHandleService.cs b/Service/AI/SmartHandleService.cs index 277cf42..ac969b6 100644 --- a/Service/AI/SmartHandleService.cs +++ b/Service/AI/SmartHandleService.cs @@ -494,8 +494,8 @@ public class SmartHandleService( /// private static int FindMatchingBrace(string str, int startPos) { - int braceCount = 0; - for (int i = startPos; i < str.Length; i++) + var braceCount = 0; + for (var i = startPos; i < str.Length; i++) { if (str[i] == '{') braceCount++; else if (str[i] == '}') diff --git a/Service/Budget/BudgetSavingsService.cs b/Service/Budget/BudgetSavingsService.cs index cb2aef4..41e551e 100644 --- a/Service/Budget/BudgetSavingsService.cs +++ b/Service/Budget/BudgetSavingsService.cs @@ -851,8 +851,8 @@ public class BudgetSavingsService( Array.Sort(months); if (months.Length >= 2) { - bool isContinuous = true; - for (int i = 1; i < months.Length; i++) + var isContinuous = true; + for (var i = 1; i < months.Length; i++) { if (months[i] != months[i - 1] + 1) { diff --git a/Service/Budget/BudgetStatsService.cs b/Service/Budget/BudgetStatsService.cs index c1a1d96..92ff938 100644 --- a/Service/Budget/BudgetStatsService.cs +++ b/Service/Budget/BudgetStatsService.cs @@ -82,10 +82,8 @@ public class BudgetStatsService( // 2. 计算限额总值 logger.LogDebug("开始计算限额总值,共 {BudgetCount} 个预算", budgets.Count); decimal totalLimit = 0; - int budgetIndex = 0; foreach (var budget in budgets) { - budgetIndex++; var itemLimit = CalculateBudgetLimit(budget, BudgetPeriodType.Month, referenceDate); totalLimit += itemLimit; } @@ -94,7 +92,7 @@ public class BudgetStatsService( // 3. 计算当前实际值 // 使用 GetAllBudgetsWithArchiveAsync 中已经计算好的 Current 值 - decimal totalCurrent = budgets.Sum(b => b.Current); + var totalCurrent = budgets.Sum(b => b.Current); logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)", totalCurrent, budgets.Count); var transactionType = category switch @@ -124,8 +122,7 @@ public class BudgetStatsService( startDate, endDate, transactionType, - allClassifies, - false); + allClassifies); logger.LogDebug("获取到 {DayCount} 天的交易数据", dailyStats.Count); // 计算累计值(用于趋势图) @@ -133,7 +130,7 @@ public class BudgetStatsService( var daysInMonth = DateTime.DaysInMonth(startDate.Year, startDate.Month); logger.LogDebug("本月天数: {DaysInMonth}", daysInMonth); - for (int i = 1; i <= daysInMonth; i++) + for (var i = 1; i <= daysInMonth; i++) { var currentDate = new DateTime(startDate.Year, startDate.Month, i); if (currentDate.Date > now.Date) @@ -162,68 +159,9 @@ public class BudgetStatsService( var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList(); // 检查是否为当前月份 - bool isCurrentMonth = referenceDate.Year == now.Year && referenceDate.Month == now.Month; + var isCurrentMonth = referenceDate.Year == now.Year && referenceDate.Month == now.Month; if (isCurrentMonth && currentDate.Date <= now.Date) { - // 对于每个硬性预算,计算其虚拟消耗并累加 - foreach (var budget in mandatoryBudgets) - { - decimal mandatoryDailyAmount = 0; - - if (budget.Type == BudgetPeriodType.Month) - { - // 月度硬性预算按当天的天数比例 - mandatoryDailyAmount = budget.Limit * i / daysInMonth; - } - else if (budget.Type == BudgetPeriodType.Year) - { - // 年度硬性预算按当天的天数比例 - var daysInYear = DateTime.IsLeapYear(referenceDate.Year) ? 366 : 365; - var dayOfYear = currentDate.DayOfYear; - mandatoryDailyAmount = budget.Limit * dayOfYear / daysInYear; - } - - // 检查该硬性预算当天是否有实际交易记录 - // 如果budget.Current为0或很小,说明没有实际交易,需要加上虚拟消耗 - // 简化处理:直接检查如果这个硬性预算的实际值小于应有值,就补上差额 - var expectedTotal = mandatoryDailyAmount; - - // 获取这个硬性预算对应的实际交易累计(从accumulated中无法单独提取) - // 简化方案:直接添加硬性预算的虚拟值,让其累加到实际支出上 - // 但这样会重复计算有交易记录的硬性预算 - - // 更好的方案:只在硬性预算没有实际交易时才添加虚拟值 - // 由于budget.Current已经包含了虚拟消耗(在CalculateCurrentAmountAsync中处理) - // 我们需要知道是否有实际交易 - - // 最简单的方案:如果budget.Current等于虚拟值,说明没有实际交易,累加虚拟值 - // 但这在趋势计算中无法判断每一天的情况 - - // 实际上,正确的做法是: - // 1. dailyStats 只包含实际交易 - // 2. 对于硬性预算,如果它没有实际交易,需要补充虚拟消耗 - // 3. 判断方法:比较当天该预算应有的虚拟值和实际累计值 - - // 由于我们无法在这里区分某个特定预算的交易, - // 使用简化方案:总的实际交易 + 总的硬性预算虚拟消耗的差额 - } - - // 简化实现:计算所有硬性预算的总虚拟消耗 - decimal totalMandatoryVirtual = 0; - foreach (var budget in mandatoryBudgets) - { - if (budget.Type == BudgetPeriodType.Month) - { - totalMandatoryVirtual += budget.Limit * i / daysInMonth; - } - else if (budget.Type == BudgetPeriodType.Year) - { - var daysInYear = DateTime.IsLeapYear(referenceDate.Year) ? 366 : 365; - var dayOfYear = currentDate.DayOfYear; - totalMandatoryVirtual += budget.Limit * dayOfYear / daysInYear; - } - } - // 关键:accumulated是所有预算的实际交易累计(不包含虚拟消耗) // totalMandatoryVirtual是所有硬性预算的虚拟消耗 // 但如果硬性预算有实际交易,accumulated中已经包含了,会重复 @@ -237,10 +175,7 @@ public class BudgetStatsService( // 由于无法精确区分,采用近似方案: // 计算所有硬性预算的Current总和,这个值已经包含了虚拟消耗(在CalculateCurrentAmountAsync中处理) - decimal totalMandatoryCurrent = budgets - .Where(b => b.IsMandatoryExpense) - .Sum(b => b.Current); - + // 计算非硬性预算的交易累计(这部分在accumulated中) // 但accumulated是所有交易的累计,包括硬性预算的实际交易 @@ -276,7 +211,7 @@ public class BudgetStatsService( // 需要判断该预算是否有实际交易记录 // 简化:假设如果硬性预算的Current等于虚拟值(误差<1元),就没有实际交易 - decimal monthlyVirtual = budget.Type == BudgetPeriodType.Month + var monthlyVirtual = budget.Type == BudgetPeriodType.Month ? budget.Limit * now.Day / daysInMonth : budget.Limit * now.DayOfYear / (DateTime.IsLeapYear(now.Year) ? 366 : 365); @@ -378,7 +313,7 @@ public class BudgetStatsService( // 2. 计算限额总值(考虑不限额预算的特殊处理) logger.LogDebug("开始计算年度限额总值,共 {BudgetCount} 个预算", budgets.Count); decimal totalLimit = 0; - int budgetIndex = 0; + var budgetIndex = 0; foreach (var budget in budgets) { budgetIndex++; @@ -402,7 +337,7 @@ public class BudgetStatsService( logger.LogDebug("交易类型: {TransactionType}", transactionType); // 计算当前实际值,考虑硬性预算的特殊逻辑 - decimal totalCurrent = budgets.Sum(b => b.Current); + var totalCurrent = budgets.Sum(b => b.Current); logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)", totalCurrent, budgets.Count); @@ -431,7 +366,7 @@ public class BudgetStatsService( // 计算累计值(用于趋势图) decimal accumulated = 0; - for (int i = 1; i <= 12; i++) + for (var i = 1; i <= 12; i++) { var currentMonthDate = new DateTime(startDate.Year, i, 1); @@ -461,7 +396,7 @@ public class BudgetStatsService( var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList(); // 检查是否为当前年份 - bool isCurrentYear = referenceDate.Year == now.Year; + var isCurrentYear = referenceDate.Year == now.Year; if (isCurrentYear && currentMonthDate <= now) { decimal mandatoryAdjustment = 0; @@ -491,7 +426,7 @@ public class BudgetStatsService( } // 判断该硬性预算是否有实际交易 - decimal yearlyVirtual = budget.Type == BudgetPeriodType.Month + var yearlyVirtual = budget.Type == BudgetPeriodType.Month ? budget.Limit * now.Month + (budget.Limit * now.Day / DateTime.DaysInMonth(now.Year, now.Month)) - budget.Limit : budget.Limit * now.DayOfYear / (DateTime.IsLeapYear(now.Year) ? 366 : 365); @@ -596,7 +531,7 @@ public class BudgetStatsService( if (referenceDate.Year == now.Year && now.Month > 1) { logger.LogDebug("开始处理历史归档月份: 1月到{Month}月", now.Month - 1); - for (int m = 1; m < now.Month; m++) + for (var m = 1; m < now.Month; m++) { var archive = await budgetArchiveRepository.GetArchiveAsync(year, m); if (archive != null) @@ -627,9 +562,8 @@ public class BudgetStatsService( item.Name, m, item.Limit, item.Actual); } // 对于年度预算,只添加一次 - else if (item.Type == BudgetPeriodType.Year && !processedBudgetIds.Contains(item.Id)) + else if (item.Type == BudgetPeriodType.Year && processedBudgetIds.Add(item.Id)) { - processedBudgetIds.Add(item.Id); result.Add(new BudgetStatsItem { Id = item.Id, @@ -707,7 +641,6 @@ public class BudgetStatsService( var remainingMonths = 12 - now.Month; if (remainingMonths > 0) { - var futureLimit = budget.Limit * remainingMonths; result.Add(new BudgetStatsItem { Id = budget.Id, @@ -745,7 +678,7 @@ public class BudgetStatsService( if (archive != null) { - int itemCount = archive.Content.Count(); + var itemCount = archive.Content.Count(); logger.LogDebug("找到归档数据,包含 {ItemCount} 个项目", itemCount); foreach (var item in archive.Content) { @@ -779,7 +712,7 @@ public class BudgetStatsService( // 获取当前预算数据 logger.LogDebug("开始获取当前预算数据"); var budgets = await budgetRepository.GetAllAsync(); - int budgetCount = budgets.Count(); + var budgetCount = budgets.Count(); logger.LogDebug("获取到 {BudgetCount} 个预算", budgetCount); foreach (var budget in budgets) @@ -860,7 +793,7 @@ public class BudgetStatsService( } var itemLimit = budget.Limit; - string algorithmDescription = $"直接使用原始预算金额: {budget.Limit}"; + var algorithmDescription = $"直接使用原始预算金额: {budget.Limit}"; // 年度视图下,月度预算需要折算为年度 if (statType == BudgetPeriodType.Year && budget.Type == BudgetPeriodType.Month) @@ -964,12 +897,12 @@ public class BudgetStatsService( logger.LogDebug("找到 {MandatoryCount} 个硬性预算", mandatoryBudgets.Count); - int mandatoryIndex = 0; + var mandatoryIndex = 0; foreach (var budget in mandatoryBudgets) { mandatoryIndex++; // 检查是否为当前统计周期 - var isCurrentPeriod = false; + bool isCurrentPeriod; if (statType == BudgetPeriodType.Month) { isCurrentPeriod = referenceDate.Year == now.Year && referenceDate.Month == now.Month; @@ -989,7 +922,7 @@ public class BudgetStatsService( { // 计算硬性预算的应累加值 decimal mandatoryAccumulation = 0; - string accumulationAlgorithm = ""; + var accumulationAlgorithm = ""; if (budget.Type == BudgetPeriodType.Month) { @@ -1039,55 +972,6 @@ public class BudgetStatsService( return adjustedTotal; } - private async Task CalculateCurrentAmountAsync(BudgetStatsItem budget, BudgetPeriodType statType, DateTime referenceDate) - { - var (startDate, endDate) = GetStatPeriodRange(statType, referenceDate); - - // 创建临时的BudgetRecord用于查询 - var tempRecord = new BudgetRecord - { - Id = budget.Id, - Name = budget.Name, - Type = budget.Type, - Limit = budget.Limit, - Category = budget.Category, - SelectedCategories = string.Join(",", budget.SelectedCategories), - StartDate = referenceDate, - NoLimit = budget.NoLimit, - IsMandatoryExpense = budget.IsMandatoryExpense - }; - - // 获取预算的实际时间段 - var (budgetStart, budgetEnd) = BudgetService.GetPeriodRange(tempRecord.StartDate, tempRecord.Type, referenceDate); - - // 确保在统计时间段内 - if (budgetEnd < startDate || budgetStart > endDate) - { - return 0; - } - - var actualAmount = await budgetRepository.GetCurrentAmountAsync(tempRecord, startDate, endDate); - - // 硬性预算的特殊处理 - if (actualAmount == 0 && budget.IsMandatoryExpense) - { - if (budget.Type == BudgetPeriodType.Month) - { - var daysInMonth = DateTime.DaysInMonth(referenceDate.Year, referenceDate.Month); - var daysElapsed = dateTimeProvider.Now.Day; - actualAmount = budget.Limit * daysElapsed / daysInMonth; - } - else if (budget.Type == BudgetPeriodType.Year) - { - var daysInYear = DateTime.IsLeapYear(referenceDate.Year) ? 366 : 365; - var daysElapsed = dateTimeProvider.Now.DayOfYear; - actualAmount = budget.Limit * daysElapsed / daysInYear; - } - } - - return actualAmount; - } - private (DateTime start, DateTime end) GetStatPeriodRange(BudgetPeriodType statType, DateTime referenceDate) { if (statType == BudgetPeriodType.Month) @@ -1104,21 +988,21 @@ public class BudgetStatsService( } } - private class BudgetStatsItem + private record BudgetStatsItem { - public long Id { get; set; } - public string Name { get; set; } = string.Empty; - public BudgetPeriodType Type { get; set; } - public decimal Limit { get; set; } - public decimal Current { get; set; } + public long Id { get; init; } + public string Name { get; init; } = string.Empty; + public BudgetPeriodType Type { get; init; } + public decimal Limit { get; init; } + public decimal Current { get; init; } public BudgetCategory Category { get; set; } - public string[] SelectedCategories { get; set; } = []; - public bool NoLimit { get; set; } - public bool IsMandatoryExpense { get; set; } - public bool IsArchive { get; set; } - public int ArchiveMonth { get; set; } // 归档月份(1-12),用于标识归档数据来自哪个月 - public int RemainingMonths { get; set; } // 剩余月份数,用于年度统计时的月度预算折算 - public bool IsCurrentMonth { get; set; } // 标记是否为当前月的预算(用于年度统计中月度预算的计算) + public string[] SelectedCategories { get; init; } = []; + public bool NoLimit { get; init; } + public bool IsMandatoryExpense { get; init; } + public bool IsArchive { get; init; } + public int ArchiveMonth { get; init; } // 归档月份(1-12),用于标识归档数据来自哪个月 + public int RemainingMonths { get; init; } // 剩余月份数,用于年度统计时的月度预算折算 + public bool IsCurrentMonth { get; init; } // 标记是否为当前月的预算(用于年度统计中月度预算的计算) } private string GenerateMonthlyDescription(List budgets, decimal totalLimit, decimal totalCurrent, DateTime referenceDate, BudgetCategory category) @@ -1189,10 +1073,10 @@ public class BudgetStatsService( var categoryName = category == BudgetCategory.Expense ? "支出" : "收入"; // 分组:归档的月度预算、归档的年度预算、当前月度预算(剩余月份)、当前年度预算 - var archivedMonthlyBudgets = budgets.Where(b => b.IsArchive && b.Type == BudgetPeriodType.Month).ToList(); - var archivedYearlyBudgets = budgets.Where(b => b.IsArchive && b.Type == BudgetPeriodType.Year).ToList(); - var currentMonthlyBudgets = budgets.Where(b => !b.IsArchive && b.Type == BudgetPeriodType.Month).ToList(); - var currentYearlyBudgets = budgets.Where(b => !b.IsArchive && b.Type == BudgetPeriodType.Year).ToList(); + var archivedMonthlyBudgets = budgets.Where(b => b is { IsArchive: true, Type: BudgetPeriodType.Month }).ToList(); + var archivedYearlyBudgets = budgets.Where(b => b is { IsArchive: true, Type: BudgetPeriodType.Year }).ToList(); + var currentMonthlyBudgets = budgets.Where(b => b is { IsArchive: false, Type: BudgetPeriodType.Month }).ToList(); + var currentYearlyBudgets = budgets.Where(b => b is { IsArchive: false, Type: BudgetPeriodType.Year }).ToList(); // 归档月度预算明细 if (archivedMonthlyBudgets.Any()) @@ -1375,8 +1259,8 @@ public class BudgetStatsService( // 如果是连续的月份,简化显示为 1~3月 Array.Sort(months); - bool isContinuous = true; - for (int i = 1; i < months.Length; i++) + var isContinuous = true; + for (var i = 1; i < months.Length; i++) { if (months[i] != months[i - 1] + 1) { diff --git a/Service/EmailServices/EmailHandleService.cs b/Service/EmailServices/EmailHandleService.cs index 03dfeaa..0ab307b 100644 --- a/Service/EmailServices/EmailHandleService.cs +++ b/Service/EmailServices/EmailHandleService.cs @@ -75,7 +75,7 @@ public class EmailHandleService( logger.LogInformation("成功解析邮件,共 {Count} 条交易记录", parsed.Length); - bool allSuccess = true; + var allSuccess = true; var records = new List(); foreach (var (card, reason, amount, balance, type, occurredAt) in parsed) { @@ -144,7 +144,7 @@ public class EmailHandleService( logger.LogInformation("成功解析邮件,共 {Count} 条交易记录", parsed.Length); - bool allSuccess = true; + var allSuccess = true; var records = new List(); foreach (var (card, reason, amount, balance, type, occurredAt) in parsed) { diff --git a/Service/EmailServices/EmailParse/EmailParseFormCcsvc.cs b/Service/EmailServices/EmailParse/EmailParseFormCcsvc.cs index 9f3d1e9..d9e6c2c 100644 --- a/Service/EmailServices/EmailParse/EmailParseFormCcsvc.cs +++ b/Service/EmailServices/EmailParse/EmailParseFormCcsvc.cs @@ -92,7 +92,7 @@ public partial class EmailParseFormCcsvc( { foreach (var node in transactionNodes) { - string card = ""; + var card = ""; try { // Time diff --git a/Service/EmailServices/EmailParse/IEmailParseServices.cs b/Service/EmailServices/EmailParse/IEmailParseServices.cs index c72f7c1..9195076 100644 --- a/Service/EmailServices/EmailParse/IEmailParseServices.cs +++ b/Service/EmailServices/EmailParse/IEmailParseServices.cs @@ -150,19 +150,19 @@ public abstract class EmailParseServicesBase( private (string card, string reason, decimal amount, decimal balance, TransactionType type, DateTime? occurredAt)? ParseSingleRecord(JsonElement obj) { - string card = obj.TryGetProperty("card", out var pCard) ? pCard.GetString() ?? string.Empty : string.Empty; - string reason = obj.TryGetProperty("reason", out var pReason) ? pReason.GetString() ?? string.Empty : string.Empty; - string typeStr = obj.TryGetProperty("type", out var pType) ? pType.GetString() ?? string.Empty : string.Empty; - string occurredAtStr = obj.TryGetProperty("occurredAt", out var pOccurredAt) ? pOccurredAt.GetString() ?? string.Empty : string.Empty; + var card = obj.TryGetProperty("card", out var pCard) ? pCard.GetString() ?? string.Empty : string.Empty; + var reason = obj.TryGetProperty("reason", out var pReason) ? pReason.GetString() ?? string.Empty : string.Empty; + var typeStr = obj.TryGetProperty("type", out var pType) ? pType.GetString() ?? string.Empty : string.Empty; + var occurredAtStr = obj.TryGetProperty("occurredAt", out var pOccurredAt) ? pOccurredAt.GetString() ?? string.Empty : string.Empty; - decimal amount = 0m; + var amount = 0m; if (obj.TryGetProperty("amount", out var pAmount)) { if (pAmount.ValueKind == JsonValueKind.Number && pAmount.TryGetDecimal(out var d)) amount = d; else if (pAmount.ValueKind == JsonValueKind.String && decimal.TryParse(pAmount.GetString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var ds)) amount = ds; } - decimal balance = 0m; + var balance = 0m; if (obj.TryGetProperty("balance", out var pBalance)) { if (pBalance.ValueKind == JsonValueKind.Number && pBalance.TryGetDecimal(out var d2)) balance = d2; diff --git a/Service/ImportService.cs b/Service/ImportService.cs index 930968a..7c3d1ca 100644 --- a/Service/ImportService.cs +++ b/Service/ImportService.cs @@ -432,7 +432,7 @@ public class ImportService( // 读取表头(第一行) var headers = new List(); - for (int col = 1; col <= colCount; col++) + for (var col = 1; col <= colCount; col++) { var header = worksheet.Cells[1, col].Text?.Trim() ?? string.Empty; headers.Add(header); @@ -441,10 +441,10 @@ public class ImportService( var result = new List>(); // 读取数据行(从第二行开始) - for (int row = 2; row <= rowCount; row++) + for (var row = 2; row <= rowCount; row++) { var rowData = new Dictionary(); - for (int col = 1; col <= colCount; col++) + for (var col = 1; col <= colCount; col++) { var header = headers[col - 1]; var value = worksheet.Cells[row, col].Text?.Trim() ?? string.Empty; diff --git a/Service/Transaction/TransactionStatisticsService.cs b/Service/Transaction/TransactionStatisticsService.cs index 7554af3..48a1b3d 100644 --- a/Service/Transaction/TransactionStatisticsService.cs +++ b/Service/Transaction/TransactionStatisticsService.cs @@ -135,7 +135,7 @@ public class TransactionStatisticsService( { var trends = new List(); - for (int i = 0; i < monthCount; i++) + for (var i = 0; i < monthCount; i++) { var targetYear = startYear; var targetMonth = startMonth + i; @@ -249,7 +249,7 @@ public class TransactionStatisticsService( startDate: startDate, endDate: endDate, type: type, - classifies: classifies?.ToArray(), + classifies: classifies.ToArray(), pageSize: int.MaxValue); if (groupByMonth) diff --git a/Web/src/components/Budget/BudgetChartAnalysis.vue b/Web/src/components/Budget/BudgetChartAnalysis.vue index 29616b4..b4c0a25 100644 --- a/Web/src/components/Budget/BudgetChartAnalysis.vue +++ b/Web/src/components/Budget/BudgetChartAnalysis.vue @@ -439,7 +439,7 @@ const updateVarianceChart = (chart, budgets) => { tooltip: { trigger: 'axis', formatter: (params) => { - const item = data[params[0].dataIndex] + const item = sortedData[params[0].dataIndex] let html = `${item.name}
` html += `预算: ¥${formatMoney(item.limit)}
` html += `实际: ¥${formatMoney(item.current)}
` diff --git a/Web/src/views/StatisticsView.vue b/Web/src/views/StatisticsView.vue index 908682a..0072d60 100644 --- a/Web/src/views/StatisticsView.vue +++ b/Web/src/views/StatisticsView.vue @@ -1,4 +1,4 @@ -