127 lines
5.1 KiB
Markdown
127 lines
5.1 KiB
Markdown
|
|
# Spec: 预算存款服务重构
|
|||
|
|
|
|||
|
|
## MODIFIED Requirements
|
|||
|
|
|
|||
|
|
### Requirement: GetForMonthAsync 返回明细数据
|
|||
|
|
`BudgetSavingsService.GetForMonthAsync` 方法 SHALL 返回包含明细数据的 `BudgetResult` 对象,除了原有的 HTML 描述外,还包括结构化的明细数据。
|
|||
|
|
|
|||
|
|
返回对象应包含:
|
|||
|
|
- `Limit`: 计划存款金额(使用新公式计算)
|
|||
|
|
- `Current`: 实际存款金额(从配置的存款分类中累加)
|
|||
|
|
- `Description`: HTML 格式的详细说明(保留兼容性)
|
|||
|
|
- `Details`: 新增的结构化明细数据(`SavingsDetail` 对象)
|
|||
|
|
|
|||
|
|
#### Scenario: 月度查询返回明细
|
|||
|
|
- **WHEN** 调用 `GetForMonthAsync(BudgetPeriodType.Month, new DateTime(2026, 2, 1))`
|
|||
|
|
- **THEN** 返回的 `BudgetResult` 对象包含 `Details` 字段,其中 `IncomeItems` 和 `ExpenseItems` 包含所有月度预算项和本月发生的年度预算项
|
|||
|
|
|
|||
|
|
#### Scenario: 向后兼容 HTML 描述
|
|||
|
|
- **WHEN** 调用 `GetForMonthAsync` 方法
|
|||
|
|
- **THEN** 返回的 `Description` 字段仍包含原有的 HTML 表格格式说明,确保旧版前端不受影响
|
|||
|
|
|
|||
|
|
### Requirement: GetForYearAsync 返回明细数据
|
|||
|
|
`BudgetSavingsService.GetForYearAsync` 方法 SHALL 返回包含归档月份和未来月份明细的完整数据结构。
|
|||
|
|
|
|||
|
|
归档月份明细应标注:
|
|||
|
|
- `IsArchived`: true
|
|||
|
|
- `ArchivedMonths`: 归档月份列表(如 [1, 2])
|
|||
|
|
|
|||
|
|
#### Scenario: 年度查询包含归档明细
|
|||
|
|
- **WHEN** 调用 `GetForYearAsync(BudgetPeriodType.Year, new DateTime(2026, 3, 1))`,且1月、2月已归档
|
|||
|
|
- **THEN** 返回的 `Details.IncomeItems` 和 `Details.ExpenseItems` 中,归档月份的项目标记为 `IsArchived = true`
|
|||
|
|
|
|||
|
|
#### Scenario: 年初无归档数据
|
|||
|
|
- **WHEN** 调用 `GetForYearAsync(BudgetPeriodType.Year, new DateTime(2026, 1, 1))`,无归档数据
|
|||
|
|
- **THEN** 返回的明细中所有项目 `IsArchived = false`,未来月份数 = 12
|
|||
|
|
|
|||
|
|
## ADDED Requirements
|
|||
|
|
|
|||
|
|
### Requirement: BudgetItemCalculator 辅助类
|
|||
|
|
系统 SHALL 提供 `BudgetItemCalculator` 静态类,用于计算单个预算项的计算用金额。
|
|||
|
|
|
|||
|
|
方法签名:
|
|||
|
|
```csharp
|
|||
|
|
public static decimal CalculateEffectiveAmount(
|
|||
|
|
BudgetCategory category,
|
|||
|
|
decimal budgetLimit,
|
|||
|
|
decimal actualAmount,
|
|||
|
|
bool isMandatory,
|
|||
|
|
bool isArchived,
|
|||
|
|
DateTime referenceDate,
|
|||
|
|
BudgetPeriodType periodType)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Scenario: 调用计算器计算收入项
|
|||
|
|
- **WHEN** 调用 `CalculateEffectiveAmount(BudgetCategory.Income, 10000, 9500, false, false, date, Month)`
|
|||
|
|
- **THEN** 返回 9500(使用实际值)
|
|||
|
|
|
|||
|
|
#### Scenario: 调用计算器计算硬性支出
|
|||
|
|
- **WHEN** 调用 `CalculateEffectiveAmount(BudgetCategory.Expense, 3000, 0, true, false, new DateTime(2026, 2, 15), Month)`
|
|||
|
|
- **THEN** 返回 ≈ 1607.14(按天数折算)
|
|||
|
|
|
|||
|
|
### Requirement: SavingsDetail 数据结构定义
|
|||
|
|
系统 SHALL 定义以下 record 类型用于存储明细数据:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public record SavingsDetail
|
|||
|
|
{
|
|||
|
|
public List<BudgetDetailItem> IncomeItems { get; init; } = new();
|
|||
|
|
public List<BudgetDetailItem> ExpenseItems { get; init; } = new();
|
|||
|
|
public SavingsCalculationSummary Summary { get; init; } = new();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public record BudgetDetailItem
|
|||
|
|
{
|
|||
|
|
public long Id { get; init; }
|
|||
|
|
public string Name { get; init; } = string.Empty;
|
|||
|
|
public BudgetPeriodType Type { get; init; }
|
|||
|
|
public decimal BudgetLimit { get; init; }
|
|||
|
|
public decimal ActualAmount { get; init; }
|
|||
|
|
public decimal EffectiveAmount { get; init; }
|
|||
|
|
public string CalculationNote { get; init; } = string.Empty;
|
|||
|
|
public bool IsOverBudget { get; init; }
|
|||
|
|
public bool IsArchived { get; init; }
|
|||
|
|
public int[]? ArchivedMonths { get; init; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public record SavingsCalculationSummary
|
|||
|
|
{
|
|||
|
|
public decimal TotalIncomeBudget { get; init; }
|
|||
|
|
public decimal TotalExpenseBudget { get; init; }
|
|||
|
|
public decimal PlannedSavings { get; init; }
|
|||
|
|
public string CalculationFormula { get; init; } = string.Empty;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Scenario: 创建明细对象
|
|||
|
|
- **WHEN** 系统计算完月度存款明细
|
|||
|
|
- **THEN** 创建 `SavingsDetail` 对象,填充 `IncomeItems`、`ExpenseItems` 和 `Summary`
|
|||
|
|
|
|||
|
|
### Requirement: 核心计算公式方法提取
|
|||
|
|
系统 SHALL 将核心计算公式提取为独立的私有方法:
|
|||
|
|
- `CalculateMonthlyPlannedSavings`: 月度计划存款计算
|
|||
|
|
- `CalculateYearlyPlannedSavings`: 年度计划存款计算
|
|||
|
|
|
|||
|
|
#### Scenario: 单元测试可测试性
|
|||
|
|
- **WHEN** 开发人员编写单元测试
|
|||
|
|
- **THEN** 可以通过反射或测试友好的设计(如 internal 可见性)测试核心计算公式
|
|||
|
|
|
|||
|
|
### Requirement: 计算说明生成
|
|||
|
|
系统 SHALL 为每个明细项生成 `CalculationNote` 字段,说明使用了哪种计算规则:
|
|||
|
|
- "使用预算"
|
|||
|
|
- "使用实际"
|
|||
|
|
- "使用实际(超支)"
|
|||
|
|
- "按天折算"
|
|||
|
|
- "归档实际"
|
|||
|
|
|
|||
|
|
#### Scenario: 生成计算说明
|
|||
|
|
- **WHEN** 餐饮预算2000,实际2500
|
|||
|
|
- **THEN** `CalculationNote = "使用实际(超支)"`,`IsOverBudget = true`
|
|||
|
|
|
|||
|
|
### Requirement: 年度归档月份标注
|
|||
|
|
对于年度查询中的归档月份数据,系统 SHALL 标注 `IsArchived = true` 和 `ArchivedMonths` 字段。
|
|||
|
|
|
|||
|
|
#### Scenario: 标注归档月份
|
|||
|
|
- **WHEN** 工资预算在1月和2月都有归档数据
|
|||
|
|
- **THEN** 返回的明细项中 `IsArchived = true`,`ArchivedMonths = [1, 2]`
|