diff --git a/Entity/BudgetRecord.cs b/Entity/BudgetRecord.cs new file mode 100644 index 0000000..b71fea5 --- /dev/null +++ b/Entity/BudgetRecord.cs @@ -0,0 +1,83 @@ +namespace Entity; + +/// +/// 预算管理 +/// +public class BudgetRecord : BaseEntity +{ + /// + /// 预算名称 + /// + 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; } = string.Empty; + + /// + /// 是否停止 + /// + public bool IsStopped { get; set; } = false; + + /// + /// 开始日期 + /// + public DateTime StartDate { get; set; } = DateTime.Now; + + /// + /// 上次同步时间 + /// + public DateTime? LastSync { get; set; } +} + +public enum BudgetPeriodType +{ + /// + /// 周 + /// + Week, + /// + /// 月 + /// + Month, + /// + /// 年 + /// + Year, + /// + /// 长期 + /// + Longterm +} + +public enum BudgetCategory +{ + /// + /// 支出 + /// + Expense = 0, + /// + /// 收入 + /// + Income = 1, + /// + /// 存款 + /// + Savings = 2 +} diff --git a/Repository/BudgetRepository.cs b/Repository/BudgetRepository.cs new file mode 100644 index 0000000..377ce2a --- /dev/null +++ b/Repository/BudgetRepository.cs @@ -0,0 +1,36 @@ +namespace Repository; + +public interface IBudgetRepository : IBaseRepository +{ + Task GetCurrentAmountAsync(BudgetRecord budget, DateTime startDate, DateTime endDate); +} + +public class BudgetRepository(IFreeSql freeSql) : BaseRepository(freeSql), IBudgetRepository +{ + public async Task GetCurrentAmountAsync(BudgetRecord budget, DateTime startDate, DateTime endDate) + { + var query = FreeSql.Select() + .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); + } +} diff --git a/Service/BudgetService.cs b/Service/BudgetService.cs new file mode 100644 index 0000000..d9a56f9 --- /dev/null +++ b/Service/BudgetService.cs @@ -0,0 +1,94 @@ +namespace Service; + +public interface IBudgetService +{ + Task> GetAllAsync(); + Task GetByIdAsync(long id); + Task AddAsync(BudgetRecord budget); + Task DeleteAsync(long id); + Task UpdateAsync(BudgetRecord budget); + Task CalculateCurrentAmountAsync(BudgetRecord budget, DateTime? now = null); + Task ToggleStopAsync(long id); +} + +public class BudgetService( + IBudgetRepository budgetRepository, + ILogger logger) : IBudgetService +{ + public async Task> GetAllAsync() + { + var list = await budgetRepository.GetAllAsync(); + return list.ToList(); + } + + public async Task GetByIdAsync(long id) + { + return await budgetRepository.GetByIdAsync(id); + } + + public async Task AddAsync(BudgetRecord budget) + { + return await budgetRepository.AddAsync(budget); + } + + public async Task DeleteAsync(long id) + { + return await budgetRepository.DeleteAsync(id); + } + + public async Task UpdateAsync(BudgetRecord budget) + { + return await budgetRepository.UpdateAsync(budget); + } + + public async Task 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 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); + } +} diff --git a/Web/src/App.vue b/Web/src/App.vue index 35a8cd2..b0af6ac 100644 --- a/Web/src/App.vue +++ b/Web/src/App.vue @@ -9,9 +9,6 @@ 统计 - - 预算 - 账单 + + 预算 + 设置 diff --git a/Web/src/api/budget.js b/Web/src/api/budget.js new file mode 100644 index 0000000..21920ab --- /dev/null +++ b/Web/src/api/budget.js @@ -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 } + }) +} diff --git a/Web/src/constants/enums.js b/Web/src/constants/enums.js new file mode 100644 index 0000000..6d152a0 --- /dev/null +++ b/Web/src/constants/enums.js @@ -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 +} diff --git a/Web/src/views/BudgetView.vue b/Web/src/views/BudgetView.vue index b26cdd3..33e10b3 100644 --- a/Web/src/views/BudgetView.vue +++ b/Web/src/views/BudgetView.vue @@ -8,7 +8,7 @@
- +