Files
EmailBill/Service/Budget/BudgetItemCalculator.cs
SunCheng 4cc205fc25 feat(budget): 实现存款明细计算核心逻辑
- 添加 BudgetItemCalculator 辅助类,实现明细项计算规则
  - 收入:实际>0取实际,否则取预算
  - 支出:取MAX(预算, 实际)
  - 硬性支出未发生:按天数折算
  - 归档数据:直接使用实际值

- 实现月度和年度存款核心公式
  - 月度:收入预算 + 本月年度收入 - 支出预算 - 本月年度支出
  - 年度:归档已实收 + 未来收入预算 - 归档已实支 - 未来支出预算

- 定义存款明细数据结构
  - SavingsDetail: 包含收入/支出明细列表和汇总
  - BudgetDetailItem: 预算明细项(含计算用金额、计算说明等)
  - SavingsCalculationSummary: 计算汇总信息

- 新增单元测试
  - BudgetItemCalculatorTest: 11个测试覆盖所有计算规则
  - BudgetSavingsCalculationTest: 6个测试验证核心公式

测试结果:所有测试通过 (366 passed, 0 failed)
2026-02-20 16:26:04 +08:00

122 lines
3.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace Service.Budget;
/// <summary>
/// 预算明细项计算辅助类
/// 用于计算单个预算项的有效金额(计算用金额)
/// </summary>
public static class BudgetItemCalculator
{
/// <summary>
/// 计算预算项的有效金额
/// </summary>
/// <param name="category">预算类别(收入/支出)</param>
/// <param name="budgetLimit">预算金额</param>
/// <param name="actualAmount">实际金额</param>
/// <param name="isMandatory">是否为硬性消费</param>
/// <param name="isArchived">是否为归档数据</param>
/// <param name="referenceDate">参考日期</param>
/// <param name="periodType">预算周期类型(月度/年度)</param>
/// <returns>有效金额(用于计算的金额)</returns>
public static decimal CalculateEffectiveAmount(
BudgetCategory category,
decimal budgetLimit,
decimal actualAmount,
bool isMandatory,
bool isArchived,
DateTime referenceDate,
BudgetPeriodType periodType)
{
// 归档数据直接返回实际值
if (isArchived)
{
return actualAmount;
}
// 收入:实际>0取实际否则取预算
if (category == BudgetCategory.Income)
{
return actualAmount > 0 ? actualAmount : budgetLimit;
}
// 支出(硬性且实际=0按天数折算
if (category == BudgetCategory.Expense && isMandatory && actualAmount == 0)
{
return CalculateMandatoryAmount(budgetLimit, referenceDate, periodType);
}
// 支出普通取MAX
if (category == BudgetCategory.Expense)
{
return Math.Max(budgetLimit, actualAmount);
}
return budgetLimit;
}
/// <summary>
/// 计算硬性消费按天数折算的金额
/// </summary>
private static decimal CalculateMandatoryAmount(
decimal limit,
DateTime date,
BudgetPeriodType periodType)
{
if (periodType == BudgetPeriodType.Month)
{
var daysInMonth = DateTime.DaysInMonth(date.Year, date.Month);
return limit / daysInMonth * date.Day;
}
else // Year
{
var daysInYear = DateTime.IsLeapYear(date.Year) ? 366 : 365;
return limit / daysInYear * date.DayOfYear;
}
}
/// <summary>
/// 生成计算说明
/// </summary>
/// <param name="category">预算类别</param>
/// <param name="budgetLimit">预算金额</param>
/// <param name="actualAmount">实际金额</param>
/// <param name="effectiveAmount">有效金额</param>
/// <param name="isMandatory">是否为硬性消费</param>
/// <param name="isArchived">是否为归档数据</param>
/// <returns>计算说明文本</returns>
public static string GenerateCalculationNote(
BudgetCategory category,
decimal budgetLimit,
decimal actualAmount,
decimal effectiveAmount,
bool isMandatory,
bool isArchived)
{
if (isArchived)
{
return "归档实际";
}
if (category == BudgetCategory.Income)
{
return actualAmount > 0 ? "使用实际" : "使用预算";
}
if (category == BudgetCategory.Expense)
{
if (isMandatory && actualAmount == 0)
{
return "按天折算";
}
if (actualAmount > budgetLimit)
{
return "使用实际(超支)";
}
return effectiveAmount == actualAmount ? "使用实际" : "使用预算";
}
return "使用预算";
}
}