feat(budget): 实现存款明细计算核心逻辑
- 添加 BudgetItemCalculator 辅助类,实现明细项计算规则 - 收入:实际>0取实际,否则取预算 - 支出:取MAX(预算, 实际) - 硬性支出未发生:按天数折算 - 归档数据:直接使用实际值 - 实现月度和年度存款核心公式 - 月度:收入预算 + 本月年度收入 - 支出预算 - 本月年度支出 - 年度:归档已实收 + 未来收入预算 - 归档已实支 - 未来支出预算 - 定义存款明细数据结构 - SavingsDetail: 包含收入/支出明细列表和汇总 - BudgetDetailItem: 预算明细项(含计算用金额、计算说明等) - SavingsCalculationSummary: 计算汇总信息 - 新增单元测试 - BudgetItemCalculatorTest: 11个测试覆盖所有计算规则 - BudgetSavingsCalculationTest: 6个测试验证核心公式 测试结果:所有测试通过 (366 passed, 0 failed)
This commit is contained in:
121
Service/Budget/BudgetItemCalculator.cs
Normal file
121
Service/Budget/BudgetItemCalculator.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
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 "使用预算";
|
||||
}
|
||||
}
|
||||
@@ -935,4 +935,32 @@ public class BudgetSavingsService(
|
||||
return string.Join(", ", months) + "月";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算月度计划存款
|
||||
/// 公式:收入预算 + 发生在本月的年度收入 - 支出预算 - 发生在本月的年度支出
|
||||
/// </summary>
|
||||
public static decimal CalculateMonthlyPlannedSavings(
|
||||
decimal monthlyIncomeBudget,
|
||||
decimal yearlyIncomeInThisMonth,
|
||||
decimal monthlyExpenseBudget,
|
||||
decimal yearlyExpenseInThisMonth)
|
||||
{
|
||||
return monthlyIncomeBudget + yearlyIncomeInThisMonth
|
||||
- monthlyExpenseBudget - yearlyExpenseInThisMonth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算年度计划存款
|
||||
/// 公式:归档月已实收 + 未来月收入预算 - 归档月已实支 - 未来月支出预算
|
||||
/// </summary>
|
||||
public static decimal CalculateYearlyPlannedSavings(
|
||||
decimal archivedIncome,
|
||||
decimal futureIncomeBudget,
|
||||
decimal archivedExpense,
|
||||
decimal futureExpenseBudget)
|
||||
{
|
||||
return archivedIncome + futureIncomeBudget
|
||||
- archivedExpense - futureExpenseBudget;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,6 +448,11 @@ public record BudgetResult
|
||||
public bool NoLimit { get; set; }
|
||||
public bool IsMandatoryExpense { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 存款明细数据(可选,用于存款预算)
|
||||
/// </summary>
|
||||
public SavingsDetail? Details { get; set; }
|
||||
|
||||
public static BudgetResult FromEntity(
|
||||
BudgetRecord entity,
|
||||
@@ -547,3 +552,41 @@ public class UncoveredCategoryDetail
|
||||
public int TransactionCount { get; set; }
|
||||
public decimal TotalAmount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 存款明细数据
|
||||
/// </summary>
|
||||
public record SavingsDetail
|
||||
{
|
||||
public List<BudgetDetailItem> IncomeItems { get; init; } = new();
|
||||
public List<BudgetDetailItem> ExpenseItems { get; init; } = new();
|
||||
public SavingsCalculationSummary Summary { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预算明细项
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 存款计算汇总
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user