添加动态目标
All checks were successful
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 10s

This commit is contained in:
孙诚
2026-01-15 20:00:41 +08:00
parent caf6f3fe60
commit f4f1600782
7 changed files with 237 additions and 12 deletions

View File

@@ -23,6 +23,16 @@ public class BudgetArchive : BaseEntity
/// </summary> /// </summary>
public DateTime ArchiveDate { get; set; } = DateTime.Now; public DateTime ArchiveDate { get; set; } = DateTime.Now;
/// <summary>
/// 支出结余(预算 - 实际,正数表示省钱,负数表示超支)
/// </summary>
public decimal ExpenseSurplus { get; set; }
/// <summary>
/// 收入结余(实际 - 预算,正数表示超额收入,负数表示未达预期)
/// </summary>
public decimal IncomeSurplus { get; set; }
public string? Summary { get; set; } public string? Summary { get; set; }
} }

View File

@@ -5,6 +5,8 @@ public interface IBudgetArchiveRepository : IBaseRepository<BudgetArchive>
Task<BudgetArchive?> GetArchiveAsync(int year, int month); Task<BudgetArchive?> GetArchiveAsync(int year, int month);
Task<List<BudgetArchive>> GetListAsync(int year, int month); Task<List<BudgetArchive>> GetListAsync(int year, int month);
Task<List<BudgetArchive>> GetArchivesByYearAsync(int year);
} }
public class BudgetArchiveRepository( public class BudgetArchiveRepository(
@@ -25,4 +27,12 @@ public class BudgetArchiveRepository(
.Where(a => a.Year == year && a.Month == month) .Where(a => a.Year == year && a.Month == month)
.ToListAsync(); .ToListAsync();
} }
public async Task<List<BudgetArchive>> GetArchivesByYearAsync(int year)
{
return await FreeSql.Select<BudgetArchive>()
.Where(a => a.Year == year)
.OrderBy(a => a.Month)
.ToListAsync();
}
} }

View File

@@ -19,6 +19,11 @@ public interface IBudgetService
Task<string?> GetArchiveSummaryAsync(int year, int month); Task<string?> GetArchiveSummaryAsync(int year, int month);
Task UpdateArchiveSummaryAsync(int year, int month, string? summary); Task UpdateArchiveSummaryAsync(int year, int month, string? summary);
/// <summary>
/// 获取指定周期的存款预算信息
/// </summary>
Task<BudgetResult?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type);
} }
public class BudgetService( public class BudgetService(
@@ -86,6 +91,12 @@ public class BudgetService(
return dtos.Where(dto => dto != null).Cast<BudgetResult>().ToList(); return dtos.Where(dto => dto != null).Cast<BudgetResult>().ToList();
} }
public async Task<BudgetResult?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type)
{
var referenceDate = new DateTime(year, month, 1);
return await GetVirtualSavingsDtoAsync(type, referenceDate);
}
public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate) public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
{ {
var budgets = await GetListAsync(referenceDate); var budgets = await GetListAsync(referenceDate);
@@ -204,13 +215,14 @@ public class BudgetService(
totalLimit += itemLimit; totalLimit += itemLimit;
// 当前值累加 // 当前值累加
var selectedCategories = budget.SelectedCategories != null ? string.Join(',', budget.SelectedCategories) : string.Empty;
var currentAmount = await CalculateCurrentAmountAsync(new() var currentAmount = await CalculateCurrentAmountAsync(new()
{ {
Name = budget.Name, Name = budget.Name,
Type = budget.Type, Type = budget.Type,
Limit = budget.Limit, Limit = budget.Limit,
Category = budget.Category, Category = budget.Category,
SelectedCategories = string.Join(',', budget.SelectedCategories), SelectedCategories = selectedCategories,
StartDate = new DateTime(referenceDate.Year, referenceDate.Month, 1) StartDate = new DateTime(referenceDate.Year, referenceDate.Month, 1)
}, referenceDate); }, referenceDate);
if (budget.Type == statType) if (budget.Type == statType)
@@ -242,6 +254,14 @@ public class BudgetService(
var budgets = await GetListAsync(referenceDate); var budgets = await GetListAsync(referenceDate);
var expenseSurplus = budgets
.Where(b => b.Category == BudgetCategory.Expense && !b.NoLimit && b.Type == BudgetPeriodType.Month)
.Sum(b => b.Limit - b.Current);
var incomeSurplus = budgets
.Where(b => b.Category == BudgetCategory.Income && !b.NoLimit && b.Type == BudgetPeriodType.Month)
.Sum(b => b.Current - b.Limit);
var content = budgets.Select(b => new BudgetArchiveContent var content = budgets.Select(b => new BudgetArchiveContent
{ {
Name = b.Name, Name = b.Name,
@@ -260,6 +280,8 @@ public class BudgetService(
{ {
archive.Content = content; archive.Content = content;
archive.ArchiveDate = DateTime.Now; archive.ArchiveDate = DateTime.Now;
archive.ExpenseSurplus = expenseSurplus;
archive.IncomeSurplus = incomeSurplus;
if (!await budgetArchiveRepository.UpdateAsync(archive)) if (!await budgetArchiveRepository.UpdateAsync(archive))
{ {
return "更新预算归档失败"; return "更新预算归档失败";
@@ -272,7 +294,9 @@ public class BudgetService(
Year = year, Year = year,
Month = month, Month = month,
Content = content, Content = content,
ArchiveDate = DateTime.Now ArchiveDate = DateTime.Now,
ExpenseSurplus = expenseSurplus,
IncomeSurplus = incomeSurplus
}; };
if (!await budgetArchiveRepository.AddAsync(archive)) if (!await budgetArchiveRepository.AddAsync(archive))
@@ -578,10 +602,8 @@ public class BudgetService(
} }
description.Append($"<p>收入合计: <span class='income-value'><strong>{incomeLimitAtPeriod:N0}</strong></span></p>"); description.Append($"<p>收入合计: <span class='income-value'><strong>{incomeLimitAtPeriod:N0}</strong></span></p>");
// 新增:显示不记额收入明细
description.Append("<h3>不记额收入明细</h3>"); description.Append("<h3>不记额收入明细</h3>");
if (noLimitIncomeItems.Count == 0) description.Append("<p>无不记额收入</p>"); if (noLimitIncomeItems.Count > 0)
else
{ {
description.Append(""" description.Append("""
<table> <table>
@@ -637,11 +659,10 @@ public class BudgetService(
} }
description.Append($"<p>支出合计: <span class='expense-value'><strong>{expenseLimitAtPeriod:N0}</strong></span></p>"); description.Append($"<p>支出合计: <span class='expense-value'><strong>{expenseLimitAtPeriod:N0}</strong></span></p>");
// 新增:显示不记额支出明细 if (noLimitExpenseItems.Count > 0)
description.Append("<h3>不记额支出明细</h3>");
if (noLimitExpenseItems.Count == 0) description.Append("<p>无不记额支出</p>");
else
{ {
description.Append("<h3>不记额支出明细</h3>");
description.Append(""" description.Append("""
<table> <table>
<thead> <thead>
@@ -663,6 +684,7 @@ public class BudgetService(
} }
description.Append("</tbody></table>"); description.Append("</tbody></table>");
} }
description.Append($"<p>不记额支出合计: <span class='expense-value'><strong>{noLimitExpenseAtPeriod:N0}</strong></span></p>"); description.Append($"<p>不记额支出合计: <span class='expense-value'><strong>{noLimitExpenseAtPeriod:N0}</strong></span></p>");
description.Append("<h3>存款计划结论</h3>"); description.Append("<h3>存款计划结论</h3>");
@@ -671,12 +693,70 @@ public class BudgetService(
var totalExpense = expenseLimitAtPeriod + noLimitExpenseAtPeriod; var totalExpense = expenseLimitAtPeriod + noLimitExpenseAtPeriod;
description.Append($"<p>计划收入 = 预算 <span class='income-value'>{incomeLimitAtPeriod:N0}</span> + 不记额 <span class='income-value'>{noLimitIncomeAtPeriod:N0}</span> = <span class='income-value'><strong>{totalIncome:N0}</strong></span></p>"); description.Append($"<p>计划收入 = 预算 <span class='income-value'>{incomeLimitAtPeriod:N0}</span> + 不记额 <span class='income-value'>{noLimitIncomeAtPeriod:N0}</span> = <span class='income-value'><strong>{totalIncome:N0}</strong></span></p>");
description.Append($"<p>计划支出 = 预算 <span class='expense-value'>{expenseLimitAtPeriod:N0}</span> + 不记额 <span class='expense-value'>{noLimitExpenseAtPeriod:N0}</span> = <span class='expense-value'><strong>{totalExpense:N0}</strong></span></p>"); description.Append($"<p>计划支出 = 预算 <span class='expense-value'>{expenseLimitAtPeriod:N0}</span> + 不记额 <span class='expense-value'>{noLimitExpenseAtPeriod:N0}</span> = <span class='expense-value'><strong>{totalExpense:N0}</strong></span></p>");
description.Append($"<p>最终目标:<span class='highlight'><strong>{totalIncome - totalExpense:N0}</strong></span></p>");
decimal historicalSurplus = 0;
if (periodType == BudgetPeriodType.Year)
{
var archives = await budgetArchiveRepository.GetArchivesByYearAsync(date.Year);
if (archives.Count > 0)
{
var expenseSurplus = archives.Sum(a => a.ExpenseSurplus);
var incomeSurplus = archives.Sum(a => a.IncomeSurplus);
historicalSurplus = expenseSurplus + incomeSurplus;
description.Append("<h3>历史月份盈亏</h3>");
description.Append("""
<table>
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
""");
foreach (var archive in archives)
{
var monthlyTotal = archive.ExpenseSurplus + archive.IncomeSurplus;
var monthlyClass = monthlyTotal >= 0 ? "income-value" : "expense-value";
description.Append($"""
<tr>
<td>{archive.Month}月</td>
<td><span class='{(archive.ExpenseSurplus >= 0 ? "income-value" : "expense-value")}'>{archive.ExpenseSurplus:N0}</span></td>
<td><span class='{(archive.IncomeSurplus >= 0 ? "income-value" : "expense-value")}'>{archive.IncomeSurplus:N0}</span></td>
<td><span class='{monthlyClass}'>{monthlyTotal:N0}</span></td>
</tr>
""");
}
var totalClass = historicalSurplus >= 0 ? "income-value" : "expense-value";
description.Append($"""
<tr>
<td><strong>汇总</strong></td>
<td><span class='{(expenseSurplus >= 0 ? "income-value" : "expense-value")}'><strong>{expenseSurplus:N0}</strong></span></td>
<td><span class='{(incomeSurplus >= 0 ? "income-value" : "expense-value")}'><strong>{incomeSurplus:N0}</strong></span></td>
<td><span class='{totalClass}'><strong>{historicalSurplus:N0}</strong></span></td>
</tr>
</tbody></table>
""");
}
var finalGoal = totalIncome - totalExpense + historicalSurplus;
description.Append($"<p>动态目标 = 计划盈余 <span class='highlight'>{totalIncome - totalExpense:N0}</span> + 历史盈亏 <span class='highlight'>{historicalSurplus:N0}</span> = <span class='highlight'><strong>{finalGoal:N0}</strong></span></p>");
}
else
{
description.Append($"<p>最终目标:<span class='highlight'><strong>{totalIncome - totalExpense:N0}</strong></span></p>");
}
var finalLimit = periodType == BudgetPeriodType.Year ? (totalIncome - totalExpense + historicalSurplus) : (totalIncome - totalExpense);
var virtualBudget = await BuildVirtualSavingsBudgetRecordAsync( var virtualBudget = await BuildVirtualSavingsBudgetRecordAsync(
periodType == BudgetPeriodType.Year ? -1 : -2, periodType == BudgetPeriodType.Year ? -1 : -2,
date, date,
totalIncome - totalExpense); // 修改:使用总金额 finalLimit);
// 计算实际发生的 收入 - 支出 // 计算实际发生的 收入 - 支出
var current = await CalculateCurrentAmountAsync(new BudgetRecord var current = await CalculateCurrentAmountAsync(new BudgetRecord

View File

@@ -95,3 +95,17 @@ export function updateArchiveSummary(data) {
data data
}) })
} }
/**
* 获取指定周期的存款预算信息
* @param {number} year 年份
* @param {number} month 月份
* @param {number} type 周期类型 (1:Month, 2:Year)
*/
export function getSavingsBudget(year, month, type) {
return request({
url: '/Budget/GetSavingsBudget',
method: 'get',
params: { year, month, type }
})
}

View File

@@ -133,6 +133,10 @@
</div> </div>
</van-collapse-transition> </van-collapse-transition>
</div> </div>
<div class="card-footer">
<slot name="footer"></slot>
</div>
</div> </div>
</div> </div>

View File

@@ -170,6 +170,33 @@
</div> </div>
</div> </div>
</template> </template>
<template #footer>
<div class="card-footer-actions">
<van-button
size="small"
icon="arrow-left"
plain
type="primary"
@click.stop="handleSavingsNav(budget, -1)"
>
{{ budget.type === BudgetPeriodType.Year ? '上一年' : '上一月' }}
</van-button>
<span class="current-date-label">
{{ getSavingsDateLabel(budget) }}
</span>
<van-button
size="small"
icon="arrow"
plain
type="primary"
icon-position="right"
@click.stop="handleSavingsNav(budget, 1)"
>
{{ budget.type === BudgetPeriodType.Year ? '下一年' : '下一月' }}
</van-button>
</div>
</template>
</BudgetCard> </BudgetCard>
</template> </template>
<van-empty v-else description="暂无存款计划" /> <van-empty v-else description="暂无存款计划" />
@@ -251,7 +278,7 @@
<script setup> <script setup>
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { showToast, showConfirmDialog } from 'vant' import { showToast, showConfirmDialog } from 'vant'
import { getBudgetList, deleteBudget, getCategoryStats, getUncoveredCategories, getArchiveSummary, updateArchiveSummary } from '@/api/budget' import { getBudgetList, deleteBudget, getCategoryStats, getUncoveredCategories, getArchiveSummary, updateArchiveSummary, getSavingsBudget } from '@/api/budget'
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums' import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
import BudgetCard from '@/components/Budget/BudgetCard.vue' import BudgetCard from '@/components/Budget/BudgetCard.vue'
import BudgetSummary from '@/components/Budget/BudgetSummary.vue' import BudgetSummary from '@/components/Budget/BudgetSummary.vue'
@@ -539,9 +566,71 @@ const handleDelete = async (budget) => {
} }
} }
} }
const getSavingsDateLabel = (budget) => {
if (!budget.periodStart) return ''
const date = new Date(budget.periodStart)
if (budget.type === BudgetPeriodType.Year) {
return `${date.getFullYear()}`
} else {
return `${date.getFullYear()}${date.getMonth() + 1}`
}
}
const handleSavingsNav = async (budget, offset) => {
if (!budget.periodStart) return
const date = new Date(budget.periodStart)
let year = date.getFullYear()
let month = date.getMonth() + 1
if (budget.type === BudgetPeriodType.Year) {
year += offset
} else {
month += offset
if (month > 12) {
month = 1
year++
} else if (month < 1) {
month = 12
year--
}
}
try {
const res = await getSavingsBudget(year, month, budget.type)
if (res.success && res.data) {
// 找到并更新对应的 budget 对象
const index = savingsBudgets.value.findIndex(b => b.id === budget.id)
if (index !== -1) {
savingsBudgets.value[index] = res.data
}
} else {
showToast('获取数据失败')
}
} catch (err) {
console.error('切换日期失败', err)
showToast('切换日期失败')
}
}
</script> </script>
<style scoped> <style scoped>
.card-footer-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed var(--van-gray-3);
}
.current-date-label {
font-size: 14px;
font-weight: bold;
color: var(--van-text-color);
}
.budget-tabs { .budget-tabs {
flex: 1; flex: 1;
display: flex; display: flex;

View File

@@ -102,6 +102,24 @@ public class BudgetController(
} }
} }
/// <summary>
/// 获取指定周期的存款预算信息
/// </summary>
[HttpGet]
public async Task<BaseResponse<BudgetResult?>> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type)
{
try
{
var result = await budgetService.GetSavingsBudgetAsync(year, month, type);
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取存款预算信息失败");
return $"获取存款预算信息失败: {ex.Message}".Fail<BudgetResult?>();
}
}
/// <summary> /// <summary>
/// 删除预算 /// 删除预算
/// </summary> /// </summary>