重构预算管理模块,添加预算记录和服务,更新相关API,优化预算统计逻辑
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 25s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s

This commit is contained in:
孙诚
2026-01-06 21:15:02 +08:00
parent 0ca7f44e37
commit 343c754431
10 changed files with 654 additions and 221 deletions

83
Entity/BudgetRecord.cs Normal file
View File

@@ -0,0 +1,83 @@
namespace Entity;
/// <summary>
/// 预算管理
/// </summary>
public class BudgetRecord : BaseEntity
{
/// <summary>
/// 预算名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 统计周期
/// </summary>
public BudgetPeriodType Type { get; set; } = BudgetPeriodType.Month;
/// <summary>
/// 预算金额
/// </summary>
public decimal Limit { get; set; }
/// <summary>
/// 预算类别
/// </summary>
public BudgetCategory Category { get; set; }
/// <summary>
/// 相关分类 (逗号分隔的分类名称)
/// </summary>
public string SelectedCategories { get; set; } = string.Empty;
/// <summary>
/// 是否停止
/// </summary>
public bool IsStopped { get; set; } = false;
/// <summary>
/// 开始日期
/// </summary>
public DateTime StartDate { get; set; } = DateTime.Now;
/// <summary>
/// 上次同步时间
/// </summary>
public DateTime? LastSync { get; set; }
}
public enum BudgetPeriodType
{
/// <summary>
/// 周
/// </summary>
Week,
/// <summary>
/// 月
/// </summary>
Month,
/// <summary>
/// 年
/// </summary>
Year,
/// <summary>
/// 长期
/// </summary>
Longterm
}
public enum BudgetCategory
{
/// <summary>
/// 支出
/// </summary>
Expense = 0,
/// <summary>
/// 收入
/// </summary>
Income = 1,
/// <summary>
/// 存款
/// </summary>
Savings = 2
}

View File

@@ -0,0 +1,36 @@
namespace Repository;
public interface IBudgetRepository : IBaseRepository<BudgetRecord>
{
Task<decimal> GetCurrentAmountAsync(BudgetRecord budget, DateTime startDate, DateTime endDate);
}
public class BudgetRepository(IFreeSql freeSql) : BaseRepository<BudgetRecord>(freeSql), IBudgetRepository
{
public async Task<decimal> GetCurrentAmountAsync(BudgetRecord budget, DateTime startDate, DateTime endDate)
{
var query = FreeSql.Select<TransactionRecord>()
.Where(t => t.OccurredAt >= startDate && t.OccurredAt <= endDate);
if (!string.IsNullOrEmpty(budget.SelectedCategories))
{
var categoryList = budget.SelectedCategories.Split(',');
query = query.Where(t => categoryList.Contains(t.Classify));
}
if (budget.Category == BudgetCategory.Expense)
{
query = query.Where(t => t.Type == TransactionType.Expense);
}
else if (budget.Category == BudgetCategory.Income)
{
query = query.Where(t => t.Type == TransactionType.Income);
}
else if (budget.Category == BudgetCategory.Savings)
{
query = query.Where(t => t.Type == TransactionType.None);
}
return await query.SumAsync(t => t.Amount);
}
}

94
Service/BudgetService.cs Normal file
View File

@@ -0,0 +1,94 @@
namespace Service;
public interface IBudgetService
{
Task<List<BudgetRecord>> GetAllAsync();
Task<BudgetRecord?> GetByIdAsync(long id);
Task<bool> AddAsync(BudgetRecord budget);
Task<bool> DeleteAsync(long id);
Task<bool> UpdateAsync(BudgetRecord budget);
Task<decimal> CalculateCurrentAmountAsync(BudgetRecord budget, DateTime? now = null);
Task<bool> ToggleStopAsync(long id);
}
public class BudgetService(
IBudgetRepository budgetRepository,
ILogger<BudgetService> logger) : IBudgetService
{
public async Task<List<BudgetRecord>> GetAllAsync()
{
var list = await budgetRepository.GetAllAsync();
return list.ToList();
}
public async Task<BudgetRecord?> GetByIdAsync(long id)
{
return await budgetRepository.GetByIdAsync(id);
}
public async Task<bool> AddAsync(BudgetRecord budget)
{
return await budgetRepository.AddAsync(budget);
}
public async Task<bool> DeleteAsync(long id)
{
return await budgetRepository.DeleteAsync(id);
}
public async Task<bool> UpdateAsync(BudgetRecord budget)
{
return await budgetRepository.UpdateAsync(budget);
}
public async Task<bool> ToggleStopAsync(long id)
{
var budget = await budgetRepository.GetByIdAsync(id);
if (budget == null) return false;
budget.IsStopped = !budget.IsStopped;
return await budgetRepository.UpdateAsync(budget);
}
public async Task<decimal> CalculateCurrentAmountAsync(BudgetRecord budget, DateTime? now = null)
{
if (budget.IsStopped) return 0;
var referenceDate = now ?? DateTime.Now;
var (startDate, endDate) = GetPeriodRange(budget.StartDate, budget.Type, referenceDate);
return await budgetRepository.GetCurrentAmountAsync(budget, startDate, endDate);
}
public static (DateTime start, DateTime end) GetPeriodRange(DateTime startDate, BudgetPeriodType type, DateTime referenceDate)
{
if (type == BudgetPeriodType.Longterm) return (startDate, DateTime.MaxValue);
DateTime start;
DateTime end;
if (type == BudgetPeriodType.Week)
{
var daysFromStart = (referenceDate.Date - startDate.Date).Days;
var weeksFromStart = daysFromStart / 7;
start = startDate.Date.AddDays(weeksFromStart * 7);
end = start.AddDays(6).AddHours(23).AddMinutes(59).AddSeconds(59);
}
else if (type == BudgetPeriodType.Month)
{
start = new DateTime(referenceDate.Year, referenceDate.Month, 1);
end = start.AddMonths(1).AddDays(-1).AddHours(23).AddMinutes(59).AddSeconds(59);
}
else if (type == BudgetPeriodType.Year)
{
start = new DateTime(referenceDate.Year, 1, 1);
end = new DateTime(referenceDate.Year, 12, 31).AddHours(23).AddMinutes(59).AddSeconds(59);
}
else
{
start = startDate;
end = DateTime.MaxValue;
}
return (start, end);
}
}

View File

@@ -9,9 +9,6 @@
<van-tabbar-item name="statistics" icon="chart-trending-o" to="/" @click="handleTabClick('/statistics')">
统计
</van-tabbar-item>
<van-tabbar-item name="budget" icon="bill-o" to="/budget" @click="handleTabClick('/budget')">
预算
</van-tabbar-item>
<van-tabbar-item
name="balance"
icon="balance-list"
@@ -21,6 +18,9 @@
>
账单
</van-tabbar-item>
<van-tabbar-item name="budget" icon="bill-o" to="/budget" @click="handleTabClick('/budget')">
预算
</van-tabbar-item>
<van-tabbar-item name="setting" icon="setting" to="/setting">
设置
</van-tabbar-item>

74
Web/src/api/budget.js Normal file
View File

@@ -0,0 +1,74 @@
import request from './request'
/**
* 获取预算列表
* @param {string} referenceDate 参考日期 (可选)
*/
export function getBudgetList(referenceDate) {
return request({
url: '/Budget/GetList',
method: 'get',
params: { referenceDate }
})
}
/**
* 获取单个预算统计
* @param {number} id 预算ID
* @param {string} referenceDate 参考日期
*/
export function getBudgetStatistics(id, referenceDate) {
return request({
url: '/Budget/GetStatistics',
method: 'get',
params: { id, referenceDate }
})
}
/**
* 创建预算
* @param {object} data 预算数据
*/
export function createBudget(data) {
return request({
url: '/Budget/Create',
method: 'post',
data
})
}
/**
* 删除预算
* @param {number} id 预算ID
*/
export function deleteBudget(id) {
return request({
url: `/Budget/DeleteById/${id}`,
method: 'delete'
})
}
/**
* 切换预算状态 (停止/恢复)
* @param {number} id 预算ID
*/
export function toggleStopBudget(id) {
return request({
url: '/Budget/ToggleStop',
method: 'post',
params: { id }
})
}
/**
* 同步预算进度
* @param {number} id 预算ID
* @param {string} referenceDate 参考日期 (可选)
*/
export function syncBudget(id, referenceDate) {
return request({
url: '/Budget/Sync',
method: 'post',
params: { id, referenceDate }
})
}

View File

@@ -0,0 +1,27 @@
/**
* 预算周期类型
*/
export const BudgetPeriodType = {
Week: 0,
Month: 1,
Year: 2,
Longterm: 3
}
/**
* 预算类别
*/
export const BudgetCategory = {
Expense: 0,
Income: 1,
Savings: 2
}
/**
* 交易类型 (与后端 TransactionType 对应)
*/
export const TransactionType = {
Expense: 0,
Income: 1,
None: 2
}

View File

@@ -8,7 +8,7 @@
<div class="page-content">
<van-tabs v-model:active="activeTab" sticky offset-top="46" type="card">
<van-tab title="支出" name="expense">
<van-tab title="支出" :name="BudgetCategory.Expense">
<div class="budget-list">
<template v-if="expenseBudgets?.length > 0">
<van-swipe-cell v-for="budget in expenseBudgets" :key="budget.id">
@@ -77,7 +77,7 @@
</div>
</van-tab>
<van-tab title="收入" name="income">
<van-tab title="收入" :name="BudgetCategory.Income">
<div class="budget-list">
<template v-if="incomeBudgets?.length > 0">
<van-swipe-cell v-for="budget in incomeBudgets" :key="budget.id">
@@ -146,7 +146,7 @@
</div>
</van-tab>
<van-tab title="存款" name="savings">
<van-tab title="存款" :name="BudgetCategory.Savings">
<div class="budget-list">
<template v-if="savingsBudgets?.length > 0">
<van-swipe-cell v-for="budget in savingsBudgets" :key="budget.id">
@@ -222,7 +222,7 @@
<!-- 添加预算弹窗 -->
<PopupContainer v-model="showAddPopup" title="新增预算" height="70%">
<div class="add-budget-form">
<van-form @submit="onSubmit">
<van-form>
<van-cell-group inset>
<van-field
v-model="form.name"
@@ -234,10 +234,10 @@
<van-field name="type" label="统计周期">
<template #input>
<van-radio-group v-model="form.type" direction="horizontal">
<van-radio name="week"></van-radio>
<van-radio name="month"></van-radio>
<van-radio name="year"></van-radio>
<van-radio name="longterm">长期</van-radio>
<van-radio :name="BudgetPeriodType.Week"></van-radio>
<van-radio :name="BudgetPeriodType.Month"></van-radio>
<van-radio :name="BudgetPeriodType.Year"></van-radio>
<van-radio :name="BudgetPeriodType.Longterm">长期</van-radio>
</van-radio-group>
</template>
</van-field>
@@ -256,9 +256,9 @@
<van-field name="category" label="类型">
<template #input>
<van-radio-group v-model="form.category" direction="horizontal">
<van-radio name="expense">支出</van-radio>
<van-radio name="income">收入</van-radio>
<van-radio name="savings">存款</van-radio>
<van-radio :name="BudgetCategory.Expense">支出</van-radio>
<van-radio :name="BudgetCategory.Income">收入</van-radio>
<van-radio :name="BudgetCategory.Savings">存款</van-radio>
</van-radio-group>
</template>
</van-field>
@@ -296,137 +296,41 @@
</div>
</van-cell-group>
<div style="margin: 32px 16px;">
<van-button round block type="primary" native-type="submit">
</van-button>
</div>
</van-form>
</div>
<template #footer>
<van-button block type="primary" @click="onSubmit">保存预算</van-button>
</template>
</PopupContainer>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, watch } from 'vue'
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { showToast, showConfirmDialog } from 'vant'
import PopupContainer from '@/components/PopupContainer.vue'
import { getCategoryList } from '@/api/transactionCategory'
import { getBudgetList, createBudget, deleteBudget, toggleStopBudget, syncBudget, getBudgetStatistics } from '@/api/budget'
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
import PopupContainer from '@/components/PopupContainer.vue'
const activeTab = ref('expense')
const activeTab = ref(BudgetCategory.Expense)
const showAddPopup = ref(false)
const categories = ref([])
// 模拟数据 (提前定义以防止模板渲染时 undefined)
const expenseBudgets = ref([
{
id: 1,
name: '总支出预算',
type: 'month',
limit: 5000,
current: 3250.5,
isStopped: false,
startDate: '2026-01-01',
period: '2026-01-01 ~ 2026-01-31',
lastSync: '2026-01-06 10:00',
syncing: false
},
{
id: 2,
name: '餐饮美食',
type: 'month',
limit: 1500,
current: 1420,
isStopped: false,
startDate: '2026-01-01',
period: '2026-01-01 ~ 2026-01-31',
lastSync: '2026-01-06 09:30',
syncing: false
},
{
id: 3,
name: '年度旅游',
type: 'year',
limit: 10000,
current: 0,
isStopped: true,
startDate: '2026-01-01',
period: '2026-01-01 ~ 2026-12-31',
lastSync: '2026-01-01 00:00',
syncing: false
}
])
const incomeBudgets = ref([
{
id: 101,
name: '月度薪资',
type: 'month',
limit: 10000,
current: 10000,
isStopped: false,
startDate: '2026-01-01',
period: '2026-01-01 ~ 2026-01-31',
lastSync: '2026-01-06 10:05',
syncing: false
},
{
id: 102,
name: '理财收益',
type: 'year',
limit: 2000,
current: 450.8,
isStopped: false,
startDate: '2026-01-01',
period: '2026-01-01 ~ 2026-12-31',
lastSync: '2026-01-06 10:05',
syncing: false
}
])
const savingsBudgets = ref([
{
id: 201,
name: '买房基金',
type: 'year',
limit: 500000,
current: 125000,
isStopped: false,
startDate: '2025-01-01',
period: '2025-01-01 ~ 2030-12-31',
lastSync: '2026-01-06 11:00',
syncing: false
},
{
id: 202,
name: '养老金',
type: 'year',
limit: 1000000,
current: 50000,
isStopped: false,
startDate: '2026-01-01',
period: '长期',
lastSync: '2026-01-06 11:00',
syncing: false
}
])
const expenseBudgets = ref([])
const incomeBudgets = ref([])
const savingsBudgets = ref([])
const form = reactive({
name: '',
type: 'month',
category: 'expense',
type: BudgetPeriodType.Month,
category: BudgetCategory.Expense,
limit: '',
selectedCategories: []
})
const categoryTypeMap = {
expense: 0,
income: 1,
savings: 2
}
const filteredCategories = computed(() => {
const targetType = categoryTypeMap[form.category]
const targetType = form.category === BudgetCategory.Expense ? 0 : (form.category === BudgetCategory.Income ? 1 : 2)
return categories.value.filter(c => c.type === targetType)
})
@@ -452,14 +356,31 @@ const toggleAll = () => {
}
}
onMounted(async () => {
const fetchBudgetList = async () => {
try {
const res = await getCategoryList()
const res = await getBudgetList()
if (res.success) {
categories.value = res.data || []
const data = res.data || []
expenseBudgets.value = data.filter(b => b.category === BudgetCategory.Expense)
incomeBudgets.value = data.filter(b => b.category === BudgetCategory.Income)
savingsBudgets.value = data.filter(b => b.category === BudgetCategory.Savings)
}
} catch (err) {
console.error('加载分类失败', err)
console.error('加载预算列表失败', err)
}
}
onMounted(async () => {
try {
const [catRes] = await Promise.all([
getCategoryList(),
fetchBudgetList()
])
if (catRes.success) {
categories.value = catRes.data || []
}
} catch (err) {
console.error('获取初始化数据失败', err)
}
})
@@ -468,14 +389,15 @@ watch(() => form.category, () => {
})
const formatMoney = (val) => {
return parseFloat(val).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
return parseFloat(val || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
const getPeriodLabel = (type) => {
const map = {
week: '本周',
month: '本月',
year: '本年'
[BudgetPeriodType.Week]: '本周',
[BudgetPeriodType.Month]: '本月',
[BudgetPeriodType.Year]: '本年',
[BudgetPeriodType.Longterm]: '长期'
}
return map[type] || '周期'
}
@@ -493,113 +415,109 @@ const getIncomeProgressColor = (budget) => {
return '#1989fa' // 蓝色
}
const getPeriodRange = (startDate, type) => {
if (!startDate || startDate === '长期') return startDate
const start = new Date(startDate)
let end = new Date(startDate)
if (type === 'week') {
end.setDate(start.getDate() + 6)
} else if (type === 'month') {
end = new Date(start.getFullYear(), start.getMonth() + 1, 0)
} else if (type === 'year') {
end = new Date(start.getFullYear(), 11, 31)
}
const format = (d) => d.toISOString().split('T')[0]
return `${format(start)} ~ ${format(end)}`
}
const refDateMap = {}
const handleSwitchPeriod = (budget, direction) => {
if (!budget.startDate || budget.period === '长期') return
const currentStart = new Date(budget.startDate)
if (budget.type === 'week') {
currentStart.setDate(currentStart.getDate() + direction * 7)
} else if (budget.type === 'month') {
currentStart.setMonth(currentStart.getMonth() + direction)
currentStart.setDate(1)
} else if (budget.type === 'year') {
currentStart.setFullYear(currentStart.getFullYear() + direction)
currentStart.setMonth(0)
currentStart.setDate(1)
const handleSwitchPeriod = async (budget, direction) => {
if (budget.type === BudgetPeriodType.Longterm) {
showToast('长期预算不支持切换周期')
return
}
// 获取或初始化该预算的参考日期
let currentRefDate = refDateMap[budget.id] || new Date()
const date = new Date(currentRefDate)
if (budget.type === BudgetPeriodType.Week) {
date.setDate(date.getDate() + direction * 7)
} else if (budget.type === BudgetPeriodType.Month) {
date.setMonth(date.getMonth() + direction)
} else if (budget.type === BudgetPeriodType.Year) {
date.setFullYear(date.getFullYear() + direction)
}
try {
const res = await getBudgetStatistics(budget.id, date.toISOString())
if (res.success) {
refDateMap[budget.id] = date
// 更新当前列表中的预算对象信息
Object.assign(budget, res.data)
}
} catch (err) {
showToast('加载历史统计失败')
console.error('加载预算历史统计失败', err)
}
budget.startDate = currentStart.toISOString().split('T')[0]
budget.period = getPeriodRange(budget.startDate, budget.type)
// 模拟数据更新
budget.current = Math.floor(Math.random() * budget.limit * 1.2 * 100) / 100
budget.lastSync = new Date().toLocaleString()
}
const handleDelete = (budget) => {
showConfirmDialog({
title: '确认删除',
message: `确定要删除预算 "${budget.name}" 吗?`,
}).then(() => {
expenseBudgets.value = expenseBudgets.value.filter(b => b.id !== budget.id)
incomeBudgets.value = incomeBudgets.value.filter(b => b.id !== budget.id)
savingsBudgets.value = savingsBudgets.value.filter(b => b.id !== budget.id)
showToast('已删除')
}).then(async () => {
try {
const res = await deleteBudget(budget.id)
if (res.success) {
showToast('已删除')
delete refDateMap[budget.id]
fetchBudgetList()
}
} catch (err) {
showToast('删除失败')
console.error('删除预算失败', err)
}
}).catch(() => {})
}
const handleSync = (budget) => {
const handleSync = async (budget) => {
budget.syncing = true
setTimeout(() => {
try {
const refDate = refDateMap[budget.id] ? refDateMap[budget.id].toISOString() : null
const res = await syncBudget(budget.id, refDate)
if (res.success) {
showToast('同步成功')
Object.assign(budget, res.data)
}
} catch (err) {
showToast('同步失败')
console.error('同步预算失败', err)
} finally {
budget.syncing = false
budget.lastSync = new Date().toLocaleString()
showToast('同步成功')
}, 1000)
}
}
const handleToggleStop = (budget) => {
budget.isStopped = !budget.isStopped
showToast(budget.isStopped ? '已停止' : '已恢复')
const handleToggleStop = async (budget) => {
try {
const res = await toggleStopBudget(budget.id)
if (res.success) {
showToast(budget.isStopped ? '已恢复' : '已停止')
// 切换停止状态后刷新列表
fetchBudgetList()
}
} catch (err) {
showToast('操作失败')
console.error('切换预算状态失败', err)
}
}
const onSubmit = () => {
const startDate = new Date()
if (form.type === 'month') startDate.setDate(1)
if (form.type === 'year') {
startDate.setMonth(0)
startDate.setDate(1)
const onSubmit = async () => {
try {
const res = await createBudget({
...form,
limit: parseFloat(form.limit),
categoryNames: form.selectedCategories
})
if (res.success) {
showToast('保存成功')
showAddPopup.value = false
fetchBudgetList()
// 重置表单
form.name = ''
form.limit = ''
form.selectedCategories = []
}
} catch (err) {
showToast('保存失败')
console.error('保存预算失败', err)
}
const startDateStr = startDate.toISOString().split('T')[0]
const newBudget = {
id: Date.now(),
name: form.name,
type: form.type,
limit: parseFloat(form.limit),
categories: [...form.selectedCategories],
current: 0,
isStopped: false,
startDate: startDateStr,
period: getPeriodRange(startDateStr, form.type),
lastSync: '刚刚',
syncing: false
}
if (form.category === 'expense') {
expenseBudgets.value.unshift(newBudget)
activeTab.value = 'expense'
} else if (form.category === 'income') {
incomeBudgets.value.unshift(newBudget)
activeTab.value = 'income'
} else {
savingsBudgets.value.unshift(newBudget)
activeTab.value = 'savings'
}
showAddPopup.value = false
showToast('添加成功')
// 重置表单
form.name = ''
form.limit = ''
form.selectedCategories = []
}
</script>

View File

@@ -0,0 +1,151 @@
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class BudgetController(
IBudgetService budgetService,
ILogger<BudgetController> logger) : ControllerBase
{
/// <summary>
/// 获取预算列表
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<BudgetDto>>> GetListAsync([FromQuery] DateTime? referenceDate = null)
{
try
{
var budgets = await budgetService.GetAllAsync();
var dtos = new List<BudgetDto>();
foreach (var budget in budgets)
{
var currentAmount = await budgetService.CalculateCurrentAmountAsync(budget, referenceDate);
dtos.Add(BudgetDto.FromEntity(budget, currentAmount, referenceDate));
}
return dtos.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取预算列表失败");
return $"获取预算列表失败: {ex.Message}".Fail<List<BudgetDto>>();
}
}
/// <summary>
/// 获取单个预算统计信息
/// </summary>
[HttpGet]
public async Task<BaseResponse<BudgetDto>> GetStatisticsAsync([FromQuery] long id, [FromQuery] DateTime? referenceDate = null)
{
try
{
var budget = await budgetService.GetByIdAsync(id);
if (budget == null) return "预算不存在".Fail<BudgetDto>();
var currentAmount = await budgetService.CalculateCurrentAmountAsync(budget, referenceDate);
return BudgetDto.FromEntity(budget, currentAmount, referenceDate).Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取预算统计失败, Id: {Id}", id);
return $"获取预算统计失败: {ex.Message}".Fail<BudgetDto>();
}
}
/// <summary>
/// 创建预算
/// </summary>
[HttpPost]
public async Task<BaseResponse<long>> CreateAsync([FromBody] CreateBudgetDto dto)
{
try
{
var budget = new BudgetRecord
{
Name = dto.Name,
Type = dto.Type,
Limit = dto.Limit,
Category = dto.Category,
SelectedCategories = dto.SelectedCategories != null ? string.Join(",", dto.SelectedCategories) : string.Empty,
StartDate = dto.StartDate ?? DateTime.Now,
LastSync = DateTime.Now
};
var success = await budgetService.AddAsync(budget);
if (success)
{
return budget.Id.Ok();
}
return "创建预算失败".Fail<long>();
}
catch (Exception ex)
{
logger.LogError(ex, "创建预算失败");
return $"创建预算失败: {ex.Message}".Fail<long>();
}
}
/// <summary>
/// 删除预算
/// </summary>
[HttpDelete("{id}")]
public async Task<BaseResponse> DeleteByIdAsync(long id)
{
try
{
var success = await budgetService.DeleteAsync(id);
return success ? BaseResponse.Done() : "删除预算失败".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "删除预算失败, Id: {Id}", id);
return $"删除预算失败: {ex.Message}".Fail();
}
}
/// <summary>
/// 切换预算暂停状态
/// </summary>
[HttpPost]
public async Task<BaseResponse> ToggleStopAsync([FromQuery] long id)
{
try
{
var success = await budgetService.ToggleStopAsync(id);
return success ? BaseResponse.Done() : "操作失败".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "切换预算状态失败, Id: {Id}", id);
return $"操作失败: {ex.Message}".Fail();
}
}
/// <summary>
/// 同步预算数据
/// </summary>
[HttpPost]
public async Task<BaseResponse<BudgetDto>> SyncAsync([FromQuery] long id, [FromQuery] DateTime? referenceDate = null)
{
try
{
var budget = await budgetService.GetByIdAsync(id);
if (budget == null)
{
return "预算不存在".Fail<BudgetDto>();
}
budget.LastSync = DateTime.Now;
await budgetService.UpdateAsync(budget);
var currentAmount = await budgetService.CalculateCurrentAmountAsync(budget, referenceDate);
return BudgetDto.FromEntity(budget, currentAmount, referenceDate).Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "同步预算失败, Id: {Id}", id);
return $"同步失败: {ex.Message}".Fail<BudgetDto>();
}
}
}

View File

@@ -0,0 +1,49 @@
namespace WebApi.Controllers.Dto;
public class BudgetDto
{
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 BudgetCategory Category { get; set; }
public string[] SelectedCategories { get; set; } = Array.Empty<string>();
public bool IsStopped { get; set; }
public string StartDate { get; set; } = string.Empty;
public string Period { get; set; } = string.Empty;
public string LastSync { get; set; } = string.Empty;
public static BudgetDto FromEntity(BudgetRecord entity, decimal currentAmount = 0, DateTime? referenceDate = null)
{
var date = referenceDate ?? DateTime.Now;
var (start, end) = BudgetService.GetPeriodRange(entity.StartDate, entity.Type, date);
return new BudgetDto
{
Id = entity.Id,
Name = entity.Name,
Type = entity.Type,
Limit = entity.Limit,
Current = currentAmount,
Category = entity.Category,
SelectedCategories = string.IsNullOrEmpty(entity.SelectedCategories)
? Array.Empty<string>()
: entity.SelectedCategories.Split(','),
IsStopped = entity.IsStopped,
StartDate = entity.StartDate.ToString("yyyy-MM-dd"),
Period = entity.Type == BudgetPeriodType.Longterm ? "长期" : $"{start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}",
LastSync = entity.LastSync?.ToString("yyyy-MM-dd HH:mm") ?? "未同步"
};
}
}
public class CreateBudgetDto
{
public string Name { get; set; } = string.Empty;
public BudgetPeriodType Type { get; set; } = BudgetPeriodType.Month;
public decimal Limit { get; set; }
public BudgetCategory Category { get; set; }
public string[] SelectedCategories { get; set; } = Array.Empty<string>();
public DateTime? StartDate { get; set; }
}

View File

@@ -1,3 +1,4 @@
using System.Text.Json.Serialization;
using FreeSql;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;