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 @@ - + @@ -77,7 +77,7 @@ - + @@ -146,7 +146,7 @@ - + @@ -222,7 +222,7 @@ - + - 周 - 月 - 年 - 长期 + 周 + 月 + 年 + 长期 @@ -256,9 +256,9 @@ - 支出 - 收入 - 存款 + 支出 + 收入 + 存款 @@ -296,137 +296,41 @@ - - - 保 存 - - + + 保存预算 + diff --git a/WebApi/Controllers/BudgetController.cs b/WebApi/Controllers/BudgetController.cs new file mode 100644 index 0000000..f0da66d --- /dev/null +++ b/WebApi/Controllers/BudgetController.cs @@ -0,0 +1,151 @@ +namespace WebApi.Controllers; + +[ApiController] +[Route("api/[controller]/[action]")] +public class BudgetController( + IBudgetService budgetService, + ILogger logger) : ControllerBase +{ + /// + /// 获取预算列表 + /// + [HttpGet] + public async Task>> GetListAsync([FromQuery] DateTime? referenceDate = null) + { + try + { + var budgets = await budgetService.GetAllAsync(); + var dtos = new List(); + + 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>(); + } + } + + /// + /// 获取单个预算统计信息 + /// + [HttpGet] + public async Task> GetStatisticsAsync([FromQuery] long id, [FromQuery] DateTime? referenceDate = null) + { + try + { + var budget = await budgetService.GetByIdAsync(id); + if (budget == null) return "预算不存在".Fail(); + + 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(); + } + } + + /// + /// 创建预算 + /// + [HttpPost] + public async Task> 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(); + } + catch (Exception ex) + { + logger.LogError(ex, "创建预算失败"); + return $"创建预算失败: {ex.Message}".Fail(); + } + } + + /// + /// 删除预算 + /// + [HttpDelete("{id}")] + public async Task 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(); + } + } + + /// + /// 切换预算暂停状态 + /// + [HttpPost] + public async Task 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(); + } + } + + /// + /// 同步预算数据 + /// + [HttpPost] + public async Task> SyncAsync([FromQuery] long id, [FromQuery] DateTime? referenceDate = null) + { + try + { + var budget = await budgetService.GetByIdAsync(id); + if (budget == null) + { + return "预算不存在".Fail(); + } + + 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(); + } + } +} diff --git a/WebApi/Controllers/Dto/BudgetDto.cs b/WebApi/Controllers/Dto/BudgetDto.cs new file mode 100644 index 0000000..552121a --- /dev/null +++ b/WebApi/Controllers/Dto/BudgetDto.cs @@ -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(); + 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() + : 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(); + public DateTime? StartDate { get; set; } +} diff --git a/WebApi/Program.cs b/WebApi/Program.cs index 7e06f8c..17f336d 100644 --- a/WebApi/Program.cs +++ b/WebApi/Program.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using FreeSql; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens;