fix
This commit is contained in:
21
Application/Application.csproj
Normal file
21
Application/Application.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Service\Service.csproj" />
|
||||
<ProjectReference Include="..\Repository\Repository.csproj" />
|
||||
<ProjectReference Include="..\Entity\Entity.csproj" />
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
85
Application/AuthApplication.cs
Normal file
85
Application/AuthApplication.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Service.AppSettingModel;
|
||||
|
||||
namespace Application;
|
||||
|
||||
/// <summary>
|
||||
/// 认证应用服务接口
|
||||
/// </summary>
|
||||
public interface IAuthApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户登录
|
||||
/// </summary>
|
||||
LoginResponse Login(LoginRequest request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 认证应用服务实现
|
||||
/// </summary>
|
||||
public class AuthApplication(
|
||||
IOptions<AuthSettings> authSettings,
|
||||
IOptions<JwtSettings> jwtSettings,
|
||||
ILogger<AuthApplication> logger) : IAuthApplication
|
||||
{
|
||||
private readonly AuthSettings _authSettings = authSettings.Value;
|
||||
private readonly JwtSettings _jwtSettings = jwtSettings.Value;
|
||||
private readonly ILogger<AuthApplication> _logger = logger;
|
||||
|
||||
public LoginResponse Login(LoginRequest request)
|
||||
{
|
||||
// 验证密码
|
||||
if (string.IsNullOrEmpty(request.Password))
|
||||
{
|
||||
throw new ValidationException("密码不能为空");
|
||||
}
|
||||
|
||||
if (request.Password != _authSettings.Password)
|
||||
{
|
||||
_logger.LogWarning("登录失败: 密码错误");
|
||||
throw new ValidationException("密码错误");
|
||||
}
|
||||
|
||||
// 生成JWT Token
|
||||
var token = GenerateJwtToken();
|
||||
var expiresAt = DateTime.UtcNow.AddHours(_jwtSettings.ExpirationHours);
|
||||
|
||||
_logger.LogInformation("用户登录成功");
|
||||
|
||||
return new LoginResponse
|
||||
{
|
||||
Token = token,
|
||||
ExpiresAt = expiresAt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成JWT Token
|
||||
/// </summary>
|
||||
private string GenerateJwtToken()
|
||||
{
|
||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey));
|
||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()),
|
||||
new Claim("auth", "password-auth")
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _jwtSettings.Issuer,
|
||||
audience: _jwtSettings.Audience,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddHours(_jwtSettings.ExpirationHours),
|
||||
signingCredentials: credentials
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
}
|
||||
302
Application/BudgetApplication.cs
Normal file
302
Application/BudgetApplication.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
using Service.Budget;
|
||||
|
||||
namespace Application;
|
||||
|
||||
/// <summary>
|
||||
/// 预算应用服务接口
|
||||
/// </summary>
|
||||
public interface IBudgetApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取预算列表
|
||||
/// </summary>
|
||||
Task<List<BudgetResponse>> GetListAsync(DateTime referenceDate);
|
||||
|
||||
/// <summary>
|
||||
/// 获取分类统计信息(月度和年度)
|
||||
/// </summary>
|
||||
Task<BudgetCategoryStatsResponse> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate);
|
||||
|
||||
/// <summary>
|
||||
/// 获取未被预算覆盖的分类统计信息
|
||||
/// </summary>
|
||||
Task<List<UncoveredCategoryResponse>> GetUncoveredCategoriesAsync(BudgetCategory category, DateTime? referenceDate = null);
|
||||
|
||||
/// <summary>
|
||||
/// 获取归档总结
|
||||
/// </summary>
|
||||
Task<string?> GetArchiveSummaryAsync(DateTime referenceDate);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定周期的存款预算信息
|
||||
/// </summary>
|
||||
Task<BudgetResponse?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type);
|
||||
|
||||
/// <summary>
|
||||
/// 删除预算
|
||||
/// </summary>
|
||||
Task DeleteByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 创建预算
|
||||
/// </summary>
|
||||
Task<long> CreateAsync(CreateBudgetRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 更新预算
|
||||
/// </summary>
|
||||
Task UpdateAsync(UpdateBudgetRequest request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预算应用服务实现
|
||||
/// </summary>
|
||||
public class BudgetApplication(
|
||||
IBudgetService budgetService,
|
||||
IBudgetRepository budgetRepository
|
||||
) : IBudgetApplication
|
||||
{
|
||||
public async Task<List<BudgetResponse>> GetListAsync(DateTime referenceDate)
|
||||
{
|
||||
var results = await budgetService.GetListAsync(referenceDate);
|
||||
|
||||
// 排序: 刚性支出优先 → 按分类 → 按类型 → 按使用率 → 按名称
|
||||
return results
|
||||
.OrderByDescending(b => b.IsMandatoryExpense)
|
||||
.ThenBy(b => b.Category)
|
||||
.ThenBy(b => b.Type)
|
||||
.ThenByDescending(b => b.Limit > 0 ? b.Current / b.Limit : 0)
|
||||
.ThenBy(b => b.Name)
|
||||
.Select(MapToResponse)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<BudgetCategoryStatsResponse> GetCategoryStatsAsync(
|
||||
BudgetCategory category,
|
||||
DateTime referenceDate)
|
||||
{
|
||||
var result = await budgetService.GetCategoryStatsAsync(category, referenceDate);
|
||||
|
||||
return new BudgetCategoryStatsResponse
|
||||
{
|
||||
Month = new BudgetStatsDetail
|
||||
{
|
||||
Limit = result.Month.Limit,
|
||||
Current = result.Month.Current,
|
||||
Remaining = result.Month.Limit - result.Month.Current,
|
||||
UsagePercentage = result.Month.Rate
|
||||
},
|
||||
Year = new BudgetStatsDetail
|
||||
{
|
||||
Limit = result.Year.Limit,
|
||||
Current = result.Year.Current,
|
||||
Remaining = result.Year.Limit - result.Year.Current,
|
||||
UsagePercentage = result.Year.Rate
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<UncoveredCategoryResponse>> GetUncoveredCategoriesAsync(
|
||||
BudgetCategory category,
|
||||
DateTime? referenceDate = null)
|
||||
{
|
||||
var results = await budgetService.GetUncoveredCategoriesAsync(category, referenceDate);
|
||||
|
||||
return results.Select(r => new UncoveredCategoryResponse
|
||||
{
|
||||
Category = r.Category,
|
||||
Amount = r.TotalAmount,
|
||||
Count = r.TransactionCount
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<string?> GetArchiveSummaryAsync(DateTime referenceDate)
|
||||
{
|
||||
return await budgetService.GetArchiveSummaryAsync(referenceDate.Year, referenceDate.Month);
|
||||
}
|
||||
|
||||
public async Task<BudgetResponse?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type)
|
||||
{
|
||||
var result = await budgetService.GetSavingsBudgetAsync(year, month, type);
|
||||
return result == null ? null : MapToResponse(result);
|
||||
}
|
||||
|
||||
public async Task DeleteByIdAsync(long id)
|
||||
{
|
||||
var success = await budgetRepository.DeleteAsync(id);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("删除预算失败,记录不存在");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<long> CreateAsync(CreateBudgetRequest request)
|
||||
{
|
||||
// 业务验证
|
||||
await ValidateCreateRequestAsync(request);
|
||||
|
||||
// 不记额预算的金额强制设为0
|
||||
var limit = request.NoLimit ? 0 : request.Limit;
|
||||
|
||||
var budget = new BudgetRecord
|
||||
{
|
||||
Name = request.Name,
|
||||
Type = request.Type,
|
||||
Limit = limit,
|
||||
Category = request.Category,
|
||||
SelectedCategories = string.Join(",", request.SelectedCategories),
|
||||
StartDate = request.StartDate ?? DateTime.Now,
|
||||
NoLimit = request.NoLimit,
|
||||
IsMandatoryExpense = request.IsMandatoryExpense
|
||||
};
|
||||
|
||||
// 验证分类冲突
|
||||
await ValidateBudgetCategoriesAsync(budget);
|
||||
|
||||
var success = await budgetRepository.AddAsync(budget);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("创建预算失败");
|
||||
}
|
||||
|
||||
return budget.Id;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(UpdateBudgetRequest request)
|
||||
{
|
||||
var budget = await budgetRepository.GetByIdAsync(request.Id);
|
||||
if (budget == null)
|
||||
{
|
||||
throw new NotFoundException("预算不存在");
|
||||
}
|
||||
|
||||
// 业务验证
|
||||
await ValidateUpdateRequestAsync(request);
|
||||
|
||||
// 不记额预算的金额强制设为0
|
||||
var limit = request.NoLimit ? 0 : request.Limit;
|
||||
|
||||
budget.Name = request.Name;
|
||||
budget.Type = request.Type;
|
||||
budget.Limit = limit;
|
||||
budget.Category = request.Category;
|
||||
budget.SelectedCategories = string.Join(",", request.SelectedCategories);
|
||||
budget.NoLimit = request.NoLimit;
|
||||
budget.IsMandatoryExpense = request.IsMandatoryExpense;
|
||||
if (request.StartDate.HasValue)
|
||||
{
|
||||
budget.StartDate = request.StartDate.Value;
|
||||
}
|
||||
|
||||
// 验证分类冲突
|
||||
await ValidateBudgetCategoriesAsync(budget);
|
||||
|
||||
var success = await budgetRepository.UpdateAsync(budget);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("更新预算失败");
|
||||
}
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// 映射到响应DTO
|
||||
/// </summary>
|
||||
private static BudgetResponse MapToResponse(BudgetResult result)
|
||||
{
|
||||
// 解析StartDate字符串为DateTime
|
||||
DateTime.TryParse(result.StartDate, out var startDate);
|
||||
|
||||
return new BudgetResponse
|
||||
{
|
||||
Id = result.Id,
|
||||
Name = result.Name,
|
||||
Type = result.Type,
|
||||
Limit = result.Limit,
|
||||
Current = result.Current,
|
||||
Category = result.Category,
|
||||
SelectedCategories = result.SelectedCategories,
|
||||
StartDate = startDate,
|
||||
NoLimit = result.NoLimit,
|
||||
IsMandatoryExpense = result.IsMandatoryExpense,
|
||||
UsagePercentage = result.Limit > 0 ? result.Current / result.Limit * 100 : 0
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证创建请求
|
||||
/// </summary>
|
||||
private static Task ValidateCreateRequestAsync(CreateBudgetRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new ValidationException("预算名称不能为空");
|
||||
}
|
||||
|
||||
if (!request.NoLimit && request.Limit <= 0)
|
||||
{
|
||||
throw new ValidationException("预算金额必须大于0");
|
||||
}
|
||||
|
||||
if (request.SelectedCategories.Length == 0)
|
||||
{
|
||||
throw new ValidationException("请至少选择一个分类");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证更新请求
|
||||
/// </summary>
|
||||
private static Task ValidateUpdateRequestAsync(UpdateBudgetRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Name))
|
||||
{
|
||||
throw new ValidationException("预算名称不能为空");
|
||||
}
|
||||
|
||||
if (!request.NoLimit && request.Limit <= 0)
|
||||
{
|
||||
throw new ValidationException("预算金额必须大于0");
|
||||
}
|
||||
|
||||
if (request.SelectedCategories.Length == 0)
|
||||
{
|
||||
throw new ValidationException("请至少选择一个分类");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证预算分类(从Controller迁移的业务逻辑)
|
||||
/// </summary>
|
||||
private async Task ValidateBudgetCategoriesAsync(BudgetRecord record)
|
||||
{
|
||||
// 验证不记额预算必须是年度预算
|
||||
if (record.NoLimit && record.Type != BudgetPeriodType.Year)
|
||||
{
|
||||
throw new ValidationException("不记额预算只能设置为年度预算。");
|
||||
}
|
||||
|
||||
var allBudgets = await budgetRepository.GetAllAsync();
|
||||
var recordSelectedCategories = record.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var budget in allBudgets)
|
||||
{
|
||||
var selectedCategories = budget.SelectedCategories.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (budget.Id != record.Id)
|
||||
{
|
||||
if (budget.Category == record.Category &&
|
||||
recordSelectedCategories.Intersect(selectedCategories).Any())
|
||||
{
|
||||
throw new ValidationException($"和 {budget.Name} 存在分类冲突,请调整相关分类。");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
53
Application/ConfigApplication.cs
Normal file
53
Application/ConfigApplication.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace Application;
|
||||
|
||||
/// <summary>
|
||||
/// 配置应用服务接口
|
||||
/// </summary>
|
||||
public interface IConfigApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取配置值
|
||||
/// </summary>
|
||||
Task<string> GetConfigAsync(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 设置配置值
|
||||
/// </summary>
|
||||
Task SetConfigAsync(string key, string value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置应用服务实现
|
||||
/// </summary>
|
||||
public class ConfigApplication(
|
||||
IConfigService configService,
|
||||
ILogger<ConfigApplication> logger
|
||||
) : IConfigApplication
|
||||
{
|
||||
public async Task<string> GetConfigAsync(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
throw new ValidationException("配置键不能为空");
|
||||
}
|
||||
|
||||
var value = await configService.GetConfigByKeyAsync<string>(key);
|
||||
return value ?? string.Empty;
|
||||
}
|
||||
|
||||
public async Task SetConfigAsync(string key, string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
throw new ValidationException("配置键不能为空");
|
||||
}
|
||||
|
||||
var success = await configService.SetConfigByKeyAsync(key, value);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException($"设置配置 {key} 失败");
|
||||
}
|
||||
|
||||
logger.LogInformation("配置 {Key} 已更新", key);
|
||||
}
|
||||
}
|
||||
89
Application/Dto/BudgetDto.cs
Normal file
89
Application/Dto/BudgetDto.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
namespace Application.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 预算响应
|
||||
/// </summary>
|
||||
public record BudgetResponse
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public BudgetPeriodType Type { get; init; }
|
||||
public decimal Limit { get; init; }
|
||||
public decimal Current { get; init; }
|
||||
public BudgetCategory Category { get; init; }
|
||||
public string[] SelectedCategories { get; init; } = [];
|
||||
public DateTime StartDate { get; init; }
|
||||
public bool NoLimit { get; init; }
|
||||
public bool IsMandatoryExpense { get; init; }
|
||||
public decimal UsagePercentage { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建预算请求
|
||||
/// </summary>
|
||||
public record CreateBudgetRequest
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public BudgetPeriodType Type { get; init; } = BudgetPeriodType.Month;
|
||||
public decimal Limit { get; init; }
|
||||
public BudgetCategory Category { get; init; }
|
||||
public string[] SelectedCategories { get; init; } = [];
|
||||
public DateTime? StartDate { get; init; }
|
||||
public bool NoLimit { get; init; }
|
||||
public bool IsMandatoryExpense { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新预算请求
|
||||
/// </summary>
|
||||
public record UpdateBudgetRequest
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public BudgetPeriodType Type { get; init; } = BudgetPeriodType.Month;
|
||||
public decimal Limit { get; init; }
|
||||
public BudgetCategory Category { get; init; }
|
||||
public string[] SelectedCategories { get; init; } = [];
|
||||
public DateTime? StartDate { get; init; }
|
||||
public bool NoLimit { get; init; }
|
||||
public bool IsMandatoryExpense { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分类统计响应
|
||||
/// </summary>
|
||||
public record BudgetCategoryStatsResponse
|
||||
{
|
||||
public BudgetStatsDetail Month { get; init; } = new();
|
||||
public BudgetStatsDetail Year { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统计详情
|
||||
/// </summary>
|
||||
public record BudgetStatsDetail
|
||||
{
|
||||
public decimal Limit { get; init; }
|
||||
public decimal Current { get; init; }
|
||||
public decimal Remaining { get; init; }
|
||||
public decimal UsagePercentage { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 未覆盖分类响应
|
||||
/// </summary>
|
||||
public record UncoveredCategoryResponse
|
||||
{
|
||||
public string Category { get; init; } = string.Empty;
|
||||
public decimal Amount { get; init; }
|
||||
public int Count { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新归档总结请求
|
||||
/// </summary>
|
||||
public record UpdateArchiveSummaryRequest
|
||||
{
|
||||
public DateTime ReferenceDate { get; init; }
|
||||
public string? Summary { get; init; }
|
||||
}
|
||||
49
Application/Dto/Category/CategoryDto.cs
Normal file
49
Application/Dto/Category/CategoryDto.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace Application.Dto.Category;
|
||||
|
||||
/// <summary>
|
||||
/// 分类响应
|
||||
/// </summary>
|
||||
public record CategoryResponse
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public TransactionType Type { get; init; }
|
||||
public string? Icon { get; init; }
|
||||
public DateTime CreateTime { get; init; }
|
||||
public DateTime? UpdateTime { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建分类请求
|
||||
/// </summary>
|
||||
public record CreateCategoryRequest
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public TransactionType Type { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新分类请求
|
||||
/// </summary>
|
||||
public record UpdateCategoryRequest
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string Name { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成图标请求
|
||||
/// </summary>
|
||||
public record GenerateIconRequest
|
||||
{
|
||||
public long CategoryId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新选中图标请求
|
||||
/// </summary>
|
||||
public record UpdateSelectedIconRequest
|
||||
{
|
||||
public long CategoryId { get; init; }
|
||||
public int SelectedIndex { get; init; }
|
||||
}
|
||||
28
Application/Dto/ConfigDto.cs
Normal file
28
Application/Dto/ConfigDto.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace Application.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置请求
|
||||
/// </summary>
|
||||
public record GetConfigRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置键
|
||||
/// </summary>
|
||||
public string Key { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置配置请求
|
||||
/// </summary>
|
||||
public record SetConfigRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置键
|
||||
/// </summary>
|
||||
public string Key { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 配置值
|
||||
/// </summary>
|
||||
public string Value { get; init; } = string.Empty;
|
||||
}
|
||||
38
Application/Dto/Email/EmailMessageDto.cs
Normal file
38
Application/Dto/Email/EmailMessageDto.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
namespace Application.Dto.Email;
|
||||
|
||||
/// <summary>
|
||||
/// 邮件消息响应
|
||||
/// </summary>
|
||||
public record EmailMessageResponse
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string Subject { get; init; } = string.Empty;
|
||||
public string From { get; init; } = string.Empty;
|
||||
public string Body { get; init; } = string.Empty;
|
||||
public string HtmlBody { get; init; } = string.Empty;
|
||||
public DateTime ReceivedDate { get; init; }
|
||||
public DateTime CreateTime { get; init; }
|
||||
public DateTime? UpdateTime { get; init; }
|
||||
public int TransactionCount { get; init; }
|
||||
public string ToName { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 邮件查询请求
|
||||
/// </summary>
|
||||
public record EmailQueryRequest
|
||||
{
|
||||
public DateTime? LastReceivedDate { get; init; }
|
||||
public long? LastId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 邮件分页结果
|
||||
/// </summary>
|
||||
public record EmailPagedResult
|
||||
{
|
||||
public EmailMessageResponse[] Data { get; init; } = [];
|
||||
public int Total { get; init; }
|
||||
public long? LastId { get; init; }
|
||||
public DateTime? LastTime { get; init; }
|
||||
}
|
||||
38
Application/Dto/ImportDto.cs
Normal file
38
Application/Dto/ImportDto.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
namespace Application.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 导入请求
|
||||
/// </summary>
|
||||
public record ImportRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件流
|
||||
/// </summary>
|
||||
public required Stream FileStream { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件扩展名(.csv, .xlsx, .xls)
|
||||
/// </summary>
|
||||
public required string FileExtension { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名
|
||||
/// </summary>
|
||||
public string FileName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 文件大小(字节)
|
||||
/// </summary>
|
||||
public long FileSize { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入响应
|
||||
/// </summary>
|
||||
public record ImportResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 导入结果消息
|
||||
/// </summary>
|
||||
public string Message { get; init; } = string.Empty;
|
||||
}
|
||||
12
Application/Dto/LoginRequest.cs
Normal file
12
Application/Dto/LoginRequest.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Application.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 登录请求
|
||||
/// </summary>
|
||||
public record LoginRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 密码
|
||||
/// </summary>
|
||||
public string Password { get; init; } = string.Empty;
|
||||
}
|
||||
17
Application/Dto/LoginResponse.cs
Normal file
17
Application/Dto/LoginResponse.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Application.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 登录响应
|
||||
/// </summary>
|
||||
public record LoginResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// JWT Token
|
||||
/// </summary>
|
||||
public string Token { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Token过期时间(UTC)
|
||||
/// </summary>
|
||||
public DateTime ExpiresAt { get; init; }
|
||||
}
|
||||
22
Application/Dto/Message/MessageDto.cs
Normal file
22
Application/Dto/Message/MessageDto.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Application.Dto.Message;
|
||||
|
||||
/// <summary>
|
||||
/// 消息记录响应
|
||||
/// </summary>
|
||||
public record MessageRecordResponse
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Content { get; init; } = string.Empty;
|
||||
public bool IsRead { get; init; }
|
||||
public DateTime CreateTime { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 消息分页结果
|
||||
/// </summary>
|
||||
public record MessagePagedResult
|
||||
{
|
||||
public MessageRecordResponse[] Data { get; init; } = [];
|
||||
public int Total { get; init; }
|
||||
}
|
||||
56
Application/Dto/Periodic/PeriodicDto.cs
Normal file
56
Application/Dto/Periodic/PeriodicDto.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
namespace Application.Dto.Periodic;
|
||||
|
||||
/// <summary>
|
||||
/// 周期性账单响应
|
||||
/// </summary>
|
||||
public record PeriodicResponse
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public PeriodicType PeriodicType { get; init; }
|
||||
public string PeriodicConfig { get; init; } = string.Empty;
|
||||
public decimal Amount { get; init; }
|
||||
public TransactionType Type { get; init; }
|
||||
public string Classify { get; init; } = string.Empty;
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
public bool IsEnabled { get; init; }
|
||||
public DateTime? NextExecuteTime { get; init; }
|
||||
public DateTime CreateTime { get; init; }
|
||||
public DateTime? UpdateTime { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建周期性账单请求
|
||||
/// </summary>
|
||||
public record CreatePeriodicRequest
|
||||
{
|
||||
public PeriodicType PeriodicType { get; init; }
|
||||
public string? PeriodicConfig { get; init; }
|
||||
public decimal Amount { get; init; }
|
||||
public TransactionType Type { get; init; }
|
||||
public string? Classify { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新周期性账单请求
|
||||
/// </summary>
|
||||
public record UpdatePeriodicRequest
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public PeriodicType PeriodicType { get; init; }
|
||||
public string? PeriodicConfig { get; init; }
|
||||
public decimal Amount { get; init; }
|
||||
public TransactionType Type { get; init; }
|
||||
public string? Classify { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
public bool IsEnabled { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 周期性账单分页结果
|
||||
/// </summary>
|
||||
public record PeriodicPagedResult
|
||||
{
|
||||
public PeriodicResponse[] Data { get; init; } = [];
|
||||
public int Total { get; init; }
|
||||
}
|
||||
20
Application/Dto/Statistics/StatisticsDto.cs
Normal file
20
Application/Dto/Statistics/StatisticsDto.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Application.Dto.Statistics;
|
||||
|
||||
/// <summary>
|
||||
/// 余额统计DTO
|
||||
/// </summary>
|
||||
public record BalanceStatisticsDto(
|
||||
int Day,
|
||||
decimal CumulativeBalance
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 每日统计DTO
|
||||
/// </summary>
|
||||
public record DailyStatisticsDto(
|
||||
int Day,
|
||||
int Count,
|
||||
decimal Expense,
|
||||
decimal Income,
|
||||
decimal Saving
|
||||
);
|
||||
106
Application/Dto/Transaction/TransactionDto.cs
Normal file
106
Application/Dto/Transaction/TransactionDto.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
namespace Application.Dto.Transaction;
|
||||
|
||||
/// <summary>
|
||||
/// 交易响应
|
||||
/// </summary>
|
||||
public record TransactionResponse
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public DateTime OccurredAt { get; init; }
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
public decimal Amount { get; init; }
|
||||
public decimal Balance { get; init; }
|
||||
public TransactionType Type { get; init; }
|
||||
public string Classify { get; init; } = string.Empty;
|
||||
public string? UnconfirmedClassify { get; init; }
|
||||
public TransactionType? UnconfirmedType { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建交易请求
|
||||
/// </summary>
|
||||
public record CreateTransactionRequest
|
||||
{
|
||||
public string OccurredAt { get; init; } = string.Empty;
|
||||
public string? Reason { get; init; }
|
||||
public decimal Amount { get; init; }
|
||||
public TransactionType Type { get; init; }
|
||||
public string? Classify { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新交易请求
|
||||
/// </summary>
|
||||
public record UpdateTransactionRequest
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
public decimal Amount { get; init; }
|
||||
public decimal Balance { get; init; }
|
||||
public TransactionType Type { get; init; }
|
||||
public string? Classify { get; init; }
|
||||
public string? OccurredAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 交易查询请求
|
||||
/// </summary>
|
||||
public record TransactionQueryRequest
|
||||
{
|
||||
public int PageIndex { get; init; } = 1;
|
||||
public int PageSize { get; init; } = 20;
|
||||
public string? SearchKeyword { get; init; }
|
||||
public string? Classify { get; init; }
|
||||
public int? Type { get; init; }
|
||||
public int? Year { get; init; }
|
||||
public int? Month { get; init; }
|
||||
public DateTime? StartDate { get; init; }
|
||||
public DateTime? EndDate { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
public bool SortByAmount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页结果
|
||||
/// </summary>
|
||||
public record PagedResult<T>
|
||||
{
|
||||
public T[] Data { get; init; } = [];
|
||||
public int Total { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量更新分类项
|
||||
/// </summary>
|
||||
public record BatchUpdateClassifyItem
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public string? Classify { get; init; }
|
||||
public TransactionType? Type { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按摘要批量更新请求
|
||||
/// </summary>
|
||||
public record BatchUpdateByReasonRequest
|
||||
{
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
public TransactionType Type { get; init; }
|
||||
public string Classify { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一句话录账解析请求
|
||||
/// </summary>
|
||||
public record ParseOneLineRequest
|
||||
{
|
||||
public string Text { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确认所有未确认记录请求
|
||||
/// </summary>
|
||||
public record ConfirmAllUnconfirmedRequest
|
||||
{
|
||||
public long[] Ids { get; init; } = [];
|
||||
}
|
||||
131
Application/EmailMessageApplication.cs
Normal file
131
Application/EmailMessageApplication.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using Application.Dto.Email;
|
||||
using Service.EmailServices;
|
||||
|
||||
namespace Application;
|
||||
|
||||
/// <summary>
|
||||
/// 邮件消息应用服务接口
|
||||
/// </summary>
|
||||
public interface IEmailMessageApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取邮件列表(分页)
|
||||
/// </summary>
|
||||
Task<EmailPagedResult> GetListAsync(EmailQueryRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取邮件详情
|
||||
/// </summary>
|
||||
Task<EmailMessageResponse> GetByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 删除邮件
|
||||
/// </summary>
|
||||
Task DeleteByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 重新分析邮件并刷新交易记录
|
||||
/// </summary>
|
||||
Task RefreshTransactionRecordsAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 立即同步邮件
|
||||
/// </summary>
|
||||
Task SyncEmailsAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 邮件消息应用服务实现
|
||||
/// </summary>
|
||||
public class EmailMessageApplication(
|
||||
IEmailMessageRepository emailRepository,
|
||||
ITransactionRecordRepository transactionRepository,
|
||||
IEmailHandleService emailHandleService,
|
||||
IEmailSyncService emailSyncService,
|
||||
ILogger<EmailMessageApplication> logger
|
||||
) : IEmailMessageApplication
|
||||
{
|
||||
public async Task<EmailPagedResult> GetListAsync(EmailQueryRequest request)
|
||||
{
|
||||
var (list, lastTime, lastId) = await emailRepository.GetPagedListAsync(
|
||||
request.LastReceivedDate,
|
||||
request.LastId);
|
||||
|
||||
var total = await emailRepository.GetTotalCountAsync();
|
||||
|
||||
// 为每个邮件获取账单数量
|
||||
var emailResponses = new List<EmailMessageResponse>();
|
||||
foreach (var email in list)
|
||||
{
|
||||
var transactionCount = await transactionRepository.GetCountByEmailIdAsync(email.Id);
|
||||
emailResponses.Add(MapToResponse(email, transactionCount));
|
||||
}
|
||||
|
||||
return new EmailPagedResult
|
||||
{
|
||||
Data = emailResponses.ToArray(),
|
||||
Total = (int)total,
|
||||
LastId = lastId,
|
||||
LastTime = lastTime
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<EmailMessageResponse> GetByIdAsync(long id)
|
||||
{
|
||||
var email = await emailRepository.GetByIdAsync(id);
|
||||
if (email == null)
|
||||
{
|
||||
throw new NotFoundException("邮件不存在");
|
||||
}
|
||||
|
||||
// 获取账单数量
|
||||
var transactionCount = await transactionRepository.GetCountByEmailIdAsync(id);
|
||||
return MapToResponse(email, transactionCount);
|
||||
}
|
||||
|
||||
public async Task DeleteByIdAsync(long id)
|
||||
{
|
||||
var success = await emailRepository.DeleteAsync(id);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("删除邮件失败,邮件不存在");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshTransactionRecordsAsync(long id)
|
||||
{
|
||||
var email = await emailRepository.GetByIdAsync(id);
|
||||
if (email == null)
|
||||
{
|
||||
throw new NotFoundException("邮件不存在");
|
||||
}
|
||||
|
||||
var success = await emailHandleService.RefreshTransactionRecordsAsync(id);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("重新分析失败");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SyncEmailsAsync()
|
||||
{
|
||||
await emailSyncService.SyncEmailsAsync();
|
||||
}
|
||||
|
||||
private static EmailMessageResponse MapToResponse(EmailMessage email, int transactionCount)
|
||||
{
|
||||
return new EmailMessageResponse
|
||||
{
|
||||
Id = email.Id,
|
||||
Subject = email.Subject,
|
||||
From = email.From,
|
||||
Body = email.Body,
|
||||
HtmlBody = email.HtmlBody,
|
||||
ReceivedDate = email.ReceivedDate,
|
||||
CreateTime = email.CreateTime,
|
||||
UpdateTime = email.UpdateTime,
|
||||
TransactionCount = transactionCount,
|
||||
ToName = email.To.Split('<').FirstOrDefault()?.Trim() ?? "未知"
|
||||
};
|
||||
}
|
||||
}
|
||||
16
Application/Exceptions/ApplicationException.cs
Normal file
16
Application/Exceptions/ApplicationException.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Application.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// 应用层异常基类
|
||||
/// </summary>
|
||||
public class ApplicationException : Exception
|
||||
{
|
||||
public ApplicationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ApplicationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
19
Application/Exceptions/BusinessException.cs
Normal file
19
Application/Exceptions/BusinessException.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Application.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// 业务逻辑异常(对应HTTP 500 Internal Server Error)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 用于业务操作失败、数据状态不一致等场景
|
||||
/// </remarks>
|
||||
public class BusinessException : ApplicationException
|
||||
{
|
||||
public BusinessException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public BusinessException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
11
Application/Exceptions/NotFoundException.cs
Normal file
11
Application/Exceptions/NotFoundException.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Application.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// 资源未找到异常(对应HTTP 404 Not Found)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 用于查询的资源不存在等场景
|
||||
/// </remarks>
|
||||
public class NotFoundException(string message) : ApplicationException(message)
|
||||
{
|
||||
}
|
||||
11
Application/Exceptions/ValidationException.cs
Normal file
11
Application/Exceptions/ValidationException.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Application.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// 业务验证异常(对应HTTP 400 Bad Request)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 用于参数验证失败、业务规则不满足等场景
|
||||
/// </remarks>
|
||||
public class ValidationException(string message) : ApplicationException(message)
|
||||
{
|
||||
}
|
||||
36
Application/Extensions/ServiceCollectionExtensions.cs
Normal file
36
Application/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace Application.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Application层服务注册扩展
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册所有Application层服务
|
||||
/// </summary>
|
||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
|
||||
{
|
||||
// 获取Application层程序集
|
||||
var assembly = typeof(ServiceCollectionExtensions).Assembly;
|
||||
|
||||
// 自动注册所有以"Application"结尾的类
|
||||
// 匹配接口规则: IXxxApplication -> XxxApplication
|
||||
var applicationTypes = assembly.GetTypes()
|
||||
.Where(t => t.IsClass && !t.IsAbstract && t.Name.EndsWith("Application"))
|
||||
.ToList();
|
||||
|
||||
foreach (var implementationType in applicationTypes)
|
||||
{
|
||||
// 查找对应的接口 IXxxApplication
|
||||
var interfaceType = implementationType.GetInterfaces()
|
||||
.FirstOrDefault(i => i.Name == $"I{implementationType.Name}");
|
||||
|
||||
if (interfaceType != null)
|
||||
{
|
||||
services.AddScoped(interfaceType, implementationType);
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
13
Application/GlobalUsings.cs
Normal file
13
Application/GlobalUsings.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// 全局引用 - Application层
|
||||
global using Microsoft.Extensions.Logging;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using System.Text;
|
||||
|
||||
// 项目引用
|
||||
global using Entity;
|
||||
global using Repository;
|
||||
global using Service;
|
||||
|
||||
// Application层内部引用
|
||||
global using Application.Exceptions;
|
||||
global using Application.Dto;
|
||||
112
Application/ImportApplication.cs
Normal file
112
Application/ImportApplication.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
namespace Application;
|
||||
|
||||
/// <summary>
|
||||
/// 导入类型
|
||||
/// </summary>
|
||||
public enum ImportType
|
||||
{
|
||||
/// <summary>
|
||||
/// 支付宝
|
||||
/// </summary>
|
||||
Alipay,
|
||||
|
||||
/// <summary>
|
||||
/// 微信
|
||||
/// </summary>
|
||||
WeChat
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入应用服务接口
|
||||
/// </summary>
|
||||
public interface IImportApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// 导入支付宝账单
|
||||
/// </summary>
|
||||
Task<ImportResponse> ImportAlipayAsync(ImportRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 导入微信账单
|
||||
/// </summary>
|
||||
Task<ImportResponse> ImportWeChatAsync(ImportRequest request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入应用服务实现
|
||||
/// </summary>
|
||||
public class ImportApplication(
|
||||
IImportService importService,
|
||||
ILogger<ImportApplication> logger
|
||||
) : IImportApplication
|
||||
{
|
||||
private static readonly string[] AllowedExtensions = { ".csv", ".xlsx", ".xls" };
|
||||
private const long MaxFileSize = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
|
||||
public async Task<ImportResponse> ImportAlipayAsync(ImportRequest request)
|
||||
{
|
||||
ValidateRequest(request);
|
||||
|
||||
var (ok, message) = await importService.ImportAlipayAsync(
|
||||
(MemoryStream)request.FileStream,
|
||||
request.FileExtension
|
||||
);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
throw new BusinessException(message);
|
||||
}
|
||||
|
||||
logger.LogInformation("支付宝账单导入成功: {Message}", message);
|
||||
return new ImportResponse { Message = message };
|
||||
}
|
||||
|
||||
public async Task<ImportResponse> ImportWeChatAsync(ImportRequest request)
|
||||
{
|
||||
ValidateRequest(request);
|
||||
|
||||
var (ok, message) = await importService.ImportWeChatAsync(
|
||||
(MemoryStream)request.FileStream,
|
||||
request.FileExtension
|
||||
);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
throw new BusinessException(message);
|
||||
}
|
||||
|
||||
logger.LogInformation("微信账单导入成功: {Message}", message);
|
||||
return new ImportResponse { Message = message };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证导入请求
|
||||
/// </summary>
|
||||
private static void ValidateRequest(ImportRequest request)
|
||||
{
|
||||
// 验证文件流
|
||||
if (request.FileStream == null || request.FileStream.Length == 0)
|
||||
{
|
||||
throw new ValidationException("请选择要上传的文件");
|
||||
}
|
||||
|
||||
// 验证文件扩展名
|
||||
if (string.IsNullOrWhiteSpace(request.FileExtension))
|
||||
{
|
||||
throw new ValidationException("文件扩展名不能为空");
|
||||
}
|
||||
|
||||
var extension = request.FileExtension.ToLowerInvariant();
|
||||
if (!AllowedExtensions.Contains(extension))
|
||||
{
|
||||
throw new ValidationException("只支持 CSV 或 Excel 文件格式");
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
if (request.FileSize > MaxFileSize)
|
||||
{
|
||||
throw new ValidationException("文件大小不能超过 10MB");
|
||||
}
|
||||
}
|
||||
}
|
||||
107
Application/JobApplication.cs
Normal file
107
Application/JobApplication.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using Quartz;
|
||||
using Quartz.Impl.Matchers;
|
||||
|
||||
namespace Application;
|
||||
|
||||
/// <summary>
|
||||
/// 任务状态
|
||||
/// </summary>
|
||||
public record JobStatus
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string JobDescription { get; init; } = string.Empty;
|
||||
public string TriggerDescription { get; init; } = string.Empty;
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public string NextRunTime { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务应用服务接口
|
||||
/// </summary>
|
||||
public interface IJobApplication
|
||||
{
|
||||
Task<List<JobStatus>> GetJobsAsync();
|
||||
Task<bool> ExecuteAsync(string jobName);
|
||||
Task<bool> PauseAsync(string jobName);
|
||||
Task<bool> ResumeAsync(string jobName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务应用服务实现
|
||||
/// </summary>
|
||||
public class JobApplication(
|
||||
ISchedulerFactory schedulerFactory,
|
||||
ILogger<JobApplication> logger
|
||||
) : IJobApplication
|
||||
{
|
||||
public async Task<List<JobStatus>> GetJobsAsync()
|
||||
{
|
||||
var scheduler = await schedulerFactory.GetScheduler();
|
||||
var jobKeys = await scheduler.GetJobKeys(GroupMatcher<JobKey>.AnyGroup());
|
||||
var jobStatuses = new List<JobStatus>();
|
||||
|
||||
foreach (var jobKey in jobKeys)
|
||||
{
|
||||
var jobDetail = await scheduler.GetJobDetail(jobKey);
|
||||
var triggers = await scheduler.GetTriggersOfJob(jobKey);
|
||||
var trigger = triggers.FirstOrDefault();
|
||||
|
||||
var status = "Unknown";
|
||||
DateTime? nextFireTime = null;
|
||||
|
||||
if (trigger != null)
|
||||
{
|
||||
var triggerState = await scheduler.GetTriggerState(trigger.Key);
|
||||
status = triggerState.ToString();
|
||||
nextFireTime = trigger.GetNextFireTimeUtc()?.ToLocalTime().DateTime;
|
||||
}
|
||||
|
||||
jobStatuses.Add(new JobStatus
|
||||
{
|
||||
Name = jobKey.Name,
|
||||
JobDescription = jobDetail?.Description ?? jobKey.Name,
|
||||
TriggerDescription = trigger?.Description ?? string.Empty,
|
||||
Status = status,
|
||||
NextRunTime = nextFireTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "无"
|
||||
});
|
||||
}
|
||||
|
||||
return jobStatuses;
|
||||
}
|
||||
|
||||
public async Task<bool> ExecuteAsync(string jobName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(jobName))
|
||||
{
|
||||
throw new ValidationException("任务名称不能为空");
|
||||
}
|
||||
|
||||
var scheduler = await schedulerFactory.GetScheduler();
|
||||
await scheduler.TriggerJob(new JobKey(jobName));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> PauseAsync(string jobName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(jobName))
|
||||
{
|
||||
throw new ValidationException("任务名称不能为空");
|
||||
}
|
||||
|
||||
var scheduler = await schedulerFactory.GetScheduler();
|
||||
await scheduler.PauseJob(new JobKey(jobName));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ResumeAsync(string jobName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(jobName))
|
||||
{
|
||||
throw new ValidationException("任务名称不能为空");
|
||||
}
|
||||
|
||||
var scheduler = await schedulerFactory.GetScheduler();
|
||||
await scheduler.ResumeJob(new JobKey(jobName));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
97
Application/MessageRecordApplication.cs
Normal file
97
Application/MessageRecordApplication.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using Application.Dto.Message;
|
||||
using Service.Message;
|
||||
|
||||
namespace Application;
|
||||
|
||||
/// <summary>
|
||||
/// 消息记录应用服务接口
|
||||
/// </summary>
|
||||
public interface IMessageRecordApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取消息列表(分页)
|
||||
/// </summary>
|
||||
Task<MessagePagedResult> GetListAsync(int pageIndex = 1, int pageSize = 20);
|
||||
|
||||
/// <summary>
|
||||
/// 获取未读消息数量
|
||||
/// </summary>
|
||||
Task<long> GetUnreadCountAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 标记为已读
|
||||
/// </summary>
|
||||
Task<bool> MarkAsReadAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 全部标记为已读
|
||||
/// </summary>
|
||||
Task<bool> MarkAllAsReadAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 删除消息
|
||||
/// </summary>
|
||||
Task<bool> DeleteAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 新增消息
|
||||
/// </summary>
|
||||
Task<bool> AddAsync(MessageRecord message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 消息记录应用服务实现
|
||||
/// </summary>
|
||||
public class MessageRecordApplication(
|
||||
IMessageService messageService,
|
||||
ILogger<MessageRecordApplication> logger
|
||||
) : IMessageRecordApplication
|
||||
{
|
||||
public async Task<MessagePagedResult> GetListAsync(int pageIndex = 1, int pageSize = 20)
|
||||
{
|
||||
var (list, total) = await messageService.GetPagedListAsync(pageIndex, pageSize);
|
||||
|
||||
return new MessagePagedResult
|
||||
{
|
||||
Data = list.Select(MapToResponse).ToArray(),
|
||||
Total = (int)total
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<long> GetUnreadCountAsync()
|
||||
{
|
||||
return await messageService.GetUnreadCountAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> MarkAsReadAsync(long id)
|
||||
{
|
||||
return await messageService.MarkAsReadAsync(id);
|
||||
}
|
||||
|
||||
public async Task<bool> MarkAllAsReadAsync()
|
||||
{
|
||||
return await messageService.MarkAllAsReadAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(long id)
|
||||
{
|
||||
return await messageService.DeleteAsync(id);
|
||||
}
|
||||
|
||||
public async Task<bool> AddAsync(MessageRecord message)
|
||||
{
|
||||
return await messageService.AddAsync(message);
|
||||
}
|
||||
|
||||
private static MessageRecordResponse MapToResponse(MessageRecord record)
|
||||
{
|
||||
return new MessageRecordResponse
|
||||
{
|
||||
Id = record.Id,
|
||||
Title = record.Title,
|
||||
Content = record.Content,
|
||||
IsRead = record.IsRead,
|
||||
CreateTime = record.CreateTime
|
||||
};
|
||||
}
|
||||
}
|
||||
37
Application/NotificationApplication.cs
Normal file
37
Application/NotificationApplication.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Service.Message;
|
||||
|
||||
namespace Application;
|
||||
|
||||
/// <summary>
|
||||
/// 通知应用服务接口
|
||||
/// </summary>
|
||||
public interface INotificationApplication
|
||||
{
|
||||
Task<string> GetVapidPublicKeyAsync();
|
||||
Task SubscribeAsync(PushSubscription subscription);
|
||||
Task SendNotificationAsync(string message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知应用服务实现
|
||||
/// </summary>
|
||||
public class NotificationApplication(
|
||||
INotificationService notificationService,
|
||||
ILogger<NotificationApplication> logger
|
||||
) : INotificationApplication
|
||||
{
|
||||
public async Task<string> GetVapidPublicKeyAsync()
|
||||
{
|
||||
return await notificationService.GetVapidPublicKeyAsync();
|
||||
}
|
||||
|
||||
public async Task SubscribeAsync(PushSubscription subscription)
|
||||
{
|
||||
await notificationService.SubscribeAsync(subscription);
|
||||
}
|
||||
|
||||
public async Task SendNotificationAsync(string message)
|
||||
{
|
||||
await notificationService.SendNotificationAsync(message);
|
||||
}
|
||||
}
|
||||
388
Application/TransactionApplication.cs
Normal file
388
Application/TransactionApplication.cs
Normal file
@@ -0,0 +1,388 @@
|
||||
using Application.Dto.Transaction;
|
||||
using Service.AI;
|
||||
|
||||
namespace Application;
|
||||
|
||||
/// <summary>
|
||||
/// 交易应用服务接口
|
||||
/// </summary>
|
||||
public interface ITransactionApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取交易记录列表(分页)
|
||||
/// </summary>
|
||||
Task<PagedResult<TransactionResponse>> GetListAsync(TransactionQueryRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取交易记录详情
|
||||
/// </summary>
|
||||
Task<TransactionResponse> GetByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 创建交易记录
|
||||
/// </summary>
|
||||
Task CreateAsync(CreateTransactionRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 更新交易记录
|
||||
/// </summary>
|
||||
Task UpdateAsync(UpdateTransactionRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 删除交易记录
|
||||
/// </summary>
|
||||
Task DeleteByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 根据邮件ID获取交易记录列表
|
||||
/// </summary>
|
||||
Task<List<TransactionResponse>> GetByEmailIdAsync(long emailId);
|
||||
|
||||
/// <summary>
|
||||
/// 根据日期获取交易记录列表
|
||||
/// </summary>
|
||||
Task<List<TransactionResponse>> GetByDateAsync(DateTime date);
|
||||
|
||||
/// <summary>
|
||||
/// 获取未确认的交易记录列表
|
||||
/// </summary>
|
||||
Task<List<TransactionResponse>> GetUnconfirmedListAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取未确认的交易记录数量
|
||||
/// </summary>
|
||||
Task<int> GetUnconfirmedCountAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取未分类的账单数量
|
||||
/// </summary>
|
||||
Task<int> GetUnclassifiedCountAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取未分类的账单列表
|
||||
/// </summary>
|
||||
Task<List<TransactionResponse>> GetUnclassifiedAsync(int pageSize);
|
||||
|
||||
/// <summary>
|
||||
/// 确认所有未确认的记录
|
||||
/// </summary>
|
||||
Task<int> ConfirmAllUnconfirmedAsync(long[] ids);
|
||||
|
||||
/// <summary>
|
||||
/// 智能分类(AI分类,支持回调)
|
||||
/// </summary>
|
||||
Task SmartClassifyAsync(long[] transactionIds, Action<(string type, string data)> onChunk);
|
||||
|
||||
/// <summary>
|
||||
/// 一句话录账解析
|
||||
/// </summary>
|
||||
Task<TransactionParseResult?> ParseOneLineAsync(string text);
|
||||
|
||||
/// <summary>
|
||||
/// 账单分析(AI分析,支持回调)
|
||||
/// </summary>
|
||||
Task AnalyzeBillAsync(string userInput, Action<string> onChunk);
|
||||
|
||||
/// <summary>
|
||||
/// 批量更新分类
|
||||
/// </summary>
|
||||
Task<int> BatchUpdateClassifyAsync(List<BatchUpdateClassifyItem> items);
|
||||
|
||||
/// <summary>
|
||||
/// 按摘要批量更新分类
|
||||
/// </summary>
|
||||
Task<int> BatchUpdateByReasonAsync(BatchUpdateByReasonRequest request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 交易应用服务实现
|
||||
/// </summary>
|
||||
public class TransactionApplication(
|
||||
ITransactionRecordRepository transactionRepository,
|
||||
ISmartHandleService smartHandleService,
|
||||
ILogger<TransactionApplication> logger
|
||||
) : ITransactionApplication
|
||||
{
|
||||
public async Task<PagedResult<TransactionResponse>> GetListAsync(TransactionQueryRequest request)
|
||||
{
|
||||
var classifies = string.IsNullOrWhiteSpace(request.Classify)
|
||||
? null
|
||||
: request.Classify.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
TransactionType? transactionType = request.Type.HasValue ? (TransactionType)request.Type.Value : null;
|
||||
|
||||
var list = await transactionRepository.QueryAsync(
|
||||
year: request.Year,
|
||||
month: request.Month,
|
||||
startDate: request.StartDate,
|
||||
endDate: request.EndDate,
|
||||
type: transactionType,
|
||||
classifies: classifies,
|
||||
searchKeyword: request.SearchKeyword,
|
||||
reason: request.Reason,
|
||||
pageIndex: request.PageIndex,
|
||||
pageSize: request.PageSize,
|
||||
sortByAmount: request.SortByAmount);
|
||||
|
||||
var total = await transactionRepository.CountAsync(
|
||||
year: request.Year,
|
||||
month: request.Month,
|
||||
startDate: request.StartDate,
|
||||
endDate: request.EndDate,
|
||||
type: transactionType,
|
||||
classifies: classifies,
|
||||
searchKeyword: request.SearchKeyword,
|
||||
reason: request.Reason);
|
||||
|
||||
return new PagedResult<TransactionResponse>
|
||||
{
|
||||
Data = list.Select(MapToResponse).ToArray(),
|
||||
Total = (int)total
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<TransactionResponse> GetByIdAsync(long id)
|
||||
{
|
||||
var transaction = await transactionRepository.GetByIdAsync(id);
|
||||
if (transaction == null)
|
||||
{
|
||||
throw new NotFoundException("交易记录不存在");
|
||||
}
|
||||
|
||||
return MapToResponse(transaction);
|
||||
}
|
||||
|
||||
public async Task CreateAsync(CreateTransactionRequest request)
|
||||
{
|
||||
// 解析日期字符串
|
||||
if (!DateTime.TryParse(request.OccurredAt, out var occurredAt))
|
||||
{
|
||||
throw new ValidationException("交易时间格式不正确");
|
||||
}
|
||||
|
||||
var transaction = new TransactionRecord
|
||||
{
|
||||
OccurredAt = occurredAt,
|
||||
Reason = request.Reason ?? string.Empty,
|
||||
Amount = request.Amount,
|
||||
Type = request.Type,
|
||||
Classify = request.Classify ?? string.Empty,
|
||||
ImportFrom = "手动录入",
|
||||
ImportNo = Guid.NewGuid().ToString("N"),
|
||||
Card = "手动",
|
||||
EmailMessageId = 0
|
||||
};
|
||||
|
||||
var result = await transactionRepository.AddAsync(transaction);
|
||||
if (!result)
|
||||
{
|
||||
throw new BusinessException("创建交易记录失败");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(UpdateTransactionRequest request)
|
||||
{
|
||||
var transaction = await transactionRepository.GetByIdAsync(request.Id);
|
||||
if (transaction == null)
|
||||
{
|
||||
throw new NotFoundException("交易记录不存在");
|
||||
}
|
||||
|
||||
// 更新可编辑字段
|
||||
transaction.Reason = request.Reason ?? string.Empty;
|
||||
transaction.Amount = request.Amount;
|
||||
transaction.Balance = request.Balance;
|
||||
transaction.Type = request.Type;
|
||||
transaction.Classify = request.Classify ?? string.Empty;
|
||||
|
||||
// 更新交易时间
|
||||
if (!string.IsNullOrEmpty(request.OccurredAt) && DateTime.TryParse(request.OccurredAt, out var occurredAt))
|
||||
{
|
||||
transaction.OccurredAt = occurredAt;
|
||||
}
|
||||
|
||||
// 清除待确认状态
|
||||
transaction.UnconfirmedClassify = null;
|
||||
transaction.UnconfirmedType = null;
|
||||
|
||||
var success = await transactionRepository.UpdateAsync(transaction);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("更新交易记录失败");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteByIdAsync(long id)
|
||||
{
|
||||
var success = await transactionRepository.DeleteAsync(id);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("删除交易记录失败,记录不存在");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<TransactionResponse>> GetByEmailIdAsync(long emailId)
|
||||
{
|
||||
var transactions = await transactionRepository.GetByEmailIdAsync(emailId);
|
||||
return transactions.Select(MapToResponse).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<TransactionResponse>> GetByDateAsync(DateTime date)
|
||||
{
|
||||
// 获取当天的开始和结束时间
|
||||
var startDate = date.Date;
|
||||
var endDate = startDate.AddDays(1);
|
||||
|
||||
var records = await transactionRepository.QueryAsync(startDate: startDate, endDate: endDate);
|
||||
return records.Select(MapToResponse).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<TransactionResponse>> GetUnconfirmedListAsync()
|
||||
{
|
||||
var records = await transactionRepository.GetUnconfirmedRecordsAsync();
|
||||
return records.Select(MapToResponse).ToList();
|
||||
}
|
||||
|
||||
public async Task<int> GetUnconfirmedCountAsync()
|
||||
{
|
||||
var records = await transactionRepository.GetUnconfirmedRecordsAsync();
|
||||
return records.Count;
|
||||
}
|
||||
|
||||
public async Task<int> GetUnclassifiedCountAsync()
|
||||
{
|
||||
return (int)await transactionRepository.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<List<TransactionResponse>> GetUnclassifiedAsync(int pageSize)
|
||||
{
|
||||
var records = await transactionRepository.GetUnclassifiedAsync(pageSize);
|
||||
return records.Select(MapToResponse).ToList();
|
||||
}
|
||||
|
||||
public async Task<int> ConfirmAllUnconfirmedAsync(long[] ids)
|
||||
{
|
||||
if (ids == null || ids.Length == 0)
|
||||
{
|
||||
throw new ValidationException("请提供要确认的交易ID列表");
|
||||
}
|
||||
|
||||
return await transactionRepository.ConfirmAllUnconfirmedAsync(ids);
|
||||
}
|
||||
|
||||
public async Task SmartClassifyAsync(long[] transactionIds, Action<(string type, string data)> onChunk)
|
||||
{
|
||||
// 验证
|
||||
if (transactionIds == null || transactionIds.Length == 0)
|
||||
{
|
||||
throw new ValidationException("请提供要分类的账单ID");
|
||||
}
|
||||
|
||||
// 调用Service进行智能分类
|
||||
await smartHandleService.SmartClassifyAsync(transactionIds, onChunk);
|
||||
}
|
||||
|
||||
public async Task<TransactionParseResult?> ParseOneLineAsync(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
throw new ValidationException("解析文本不能为空");
|
||||
}
|
||||
|
||||
var result = await smartHandleService.ParseOneLineBillAsync(text);
|
||||
if (result == null)
|
||||
{
|
||||
throw new BusinessException("AI解析失败");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task AnalyzeBillAsync(string userInput, Action<string> onChunk)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userInput))
|
||||
{
|
||||
throw new ValidationException("请输入分析内容");
|
||||
}
|
||||
|
||||
await smartHandleService.AnalyzeBillAsync(userInput, onChunk);
|
||||
}
|
||||
|
||||
public async Task<int> BatchUpdateClassifyAsync(List<BatchUpdateClassifyItem> items)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
{
|
||||
throw new ValidationException("请提供要更新的记录");
|
||||
}
|
||||
|
||||
var successCount = 0;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var record = await transactionRepository.GetByIdAsync(item.Id);
|
||||
if (record != null)
|
||||
{
|
||||
record.Classify = item.Classify ?? string.Empty;
|
||||
|
||||
// 如果提供了Type,也更新Type
|
||||
if (item.Type.HasValue)
|
||||
{
|
||||
record.Type = item.Type.Value;
|
||||
}
|
||||
|
||||
// 清除待确认状态
|
||||
if (!string.IsNullOrEmpty(record.Classify))
|
||||
{
|
||||
record.UnconfirmedClassify = null;
|
||||
}
|
||||
|
||||
if (item.Type.HasValue && record.Type == item.Type.Value)
|
||||
{
|
||||
record.UnconfirmedType = null;
|
||||
}
|
||||
|
||||
var success = await transactionRepository.UpdateAsync(record);
|
||||
if (success)
|
||||
{
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return successCount;
|
||||
}
|
||||
|
||||
public async Task<int> BatchUpdateByReasonAsync(BatchUpdateByReasonRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Reason))
|
||||
{
|
||||
throw new ValidationException("摘要不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Classify))
|
||||
{
|
||||
throw new ValidationException("分类不能为空");
|
||||
}
|
||||
|
||||
return await transactionRepository.BatchUpdateByReasonAsync(
|
||||
request.Reason,
|
||||
request.Type,
|
||||
request.Classify);
|
||||
}
|
||||
|
||||
private static TransactionResponse MapToResponse(TransactionRecord record)
|
||||
{
|
||||
return new TransactionResponse
|
||||
{
|
||||
Id = record.Id,
|
||||
OccurredAt = record.OccurredAt,
|
||||
Reason = record.Reason,
|
||||
Amount = record.Amount,
|
||||
Balance = record.Balance,
|
||||
Type = record.Type,
|
||||
Classify = record.Classify,
|
||||
UnconfirmedClassify = record.UnconfirmedClassify,
|
||||
UnconfirmedType = record.UnconfirmedType
|
||||
};
|
||||
}
|
||||
}
|
||||
231
Application/TransactionCategoryApplication.cs
Normal file
231
Application/TransactionCategoryApplication.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
using Application.Dto.Category;
|
||||
using Service.AI;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Application;
|
||||
|
||||
/// <summary>
|
||||
/// 交易分类应用服务接口
|
||||
/// </summary>
|
||||
public interface ITransactionCategoryApplication
|
||||
{
|
||||
Task<List<CategoryResponse>> GetListAsync(TransactionType? type = null);
|
||||
Task<CategoryResponse> GetByIdAsync(long id);
|
||||
Task<long> CreateAsync(CreateCategoryRequest request);
|
||||
Task UpdateAsync(UpdateCategoryRequest request);
|
||||
Task DeleteAsync(long id);
|
||||
Task<int> BatchCreateAsync(List<CreateCategoryRequest> requests);
|
||||
Task<string> GenerateIconAsync(GenerateIconRequest request);
|
||||
Task UpdateSelectedIconAsync(UpdateSelectedIconRequest request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 交易分类应用服务实现
|
||||
/// </summary>
|
||||
public class TransactionCategoryApplication(
|
||||
ITransactionCategoryRepository categoryRepository,
|
||||
ITransactionRecordRepository transactionRepository,
|
||||
IBudgetRepository budgetRepository,
|
||||
ISmartHandleService smartHandleService,
|
||||
ILogger<TransactionCategoryApplication> logger
|
||||
) : ITransactionCategoryApplication
|
||||
{
|
||||
public async Task<List<CategoryResponse>> GetListAsync(TransactionType? type = null)
|
||||
{
|
||||
List<TransactionCategory> categories;
|
||||
if (type.HasValue)
|
||||
{
|
||||
categories = await categoryRepository.GetCategoriesByTypeAsync(type.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
categories = (await categoryRepository.GetAllAsync()).ToList();
|
||||
}
|
||||
|
||||
return categories.Select(MapToResponse).ToList();
|
||||
}
|
||||
|
||||
public async Task<CategoryResponse> GetByIdAsync(long id)
|
||||
{
|
||||
var category = await categoryRepository.GetByIdAsync(id);
|
||||
if (category == null)
|
||||
{
|
||||
throw new NotFoundException("分类不存在");
|
||||
}
|
||||
|
||||
return MapToResponse(category);
|
||||
}
|
||||
|
||||
public async Task<long> CreateAsync(CreateCategoryRequest request)
|
||||
{
|
||||
// 检查同名分类
|
||||
var existing = await categoryRepository.GetByNameAndTypeAsync(request.Name, request.Type);
|
||||
if (existing != null)
|
||||
{
|
||||
throw new ValidationException("已存在相同名称的分类");
|
||||
}
|
||||
|
||||
var category = new TransactionCategory
|
||||
{
|
||||
Name = request.Name,
|
||||
Type = request.Type
|
||||
};
|
||||
|
||||
var result = await categoryRepository.AddAsync(category);
|
||||
if (!result)
|
||||
{
|
||||
throw new BusinessException("创建分类失败");
|
||||
}
|
||||
|
||||
return category.Id;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(UpdateCategoryRequest request)
|
||||
{
|
||||
var category = await categoryRepository.GetByIdAsync(request.Id);
|
||||
if (category == null)
|
||||
{
|
||||
throw new NotFoundException("分类不存在");
|
||||
}
|
||||
|
||||
// 如果修改了名称,检查同名
|
||||
if (category.Name != request.Name)
|
||||
{
|
||||
var existing = await categoryRepository.GetByNameAndTypeAsync(request.Name, category.Type);
|
||||
if (existing != null && existing.Id != request.Id)
|
||||
{
|
||||
throw new ValidationException("已存在相同名称的分类");
|
||||
}
|
||||
|
||||
// 同步更新交易记录中的分类名称
|
||||
await transactionRepository.UpdateCategoryNameAsync(category.Name, request.Name, category.Type);
|
||||
await budgetRepository.UpdateBudgetCategoryNameAsync(category.Name, request.Name, category.Type);
|
||||
}
|
||||
|
||||
category.Name = request.Name;
|
||||
category.UpdateTime = DateTime.Now;
|
||||
|
||||
var success = await categoryRepository.UpdateAsync(category);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("更新分类失败");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(long id)
|
||||
{
|
||||
// 检查是否被使用
|
||||
var inUse = await categoryRepository.IsCategoryInUseAsync(id);
|
||||
if (inUse)
|
||||
{
|
||||
throw new ValidationException("该分类已被使用,无法删除");
|
||||
}
|
||||
|
||||
var success = await categoryRepository.DeleteAsync(id);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("删除分类失败,分类不存在");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> BatchCreateAsync(List<CreateCategoryRequest> requests)
|
||||
{
|
||||
var categories = requests.Select(r => new TransactionCategory
|
||||
{
|
||||
Name = r.Name,
|
||||
Type = r.Type
|
||||
}).ToList();
|
||||
|
||||
var result = await categoryRepository.AddRangeAsync(categories);
|
||||
if (!result)
|
||||
{
|
||||
throw new BusinessException("批量创建分类失败");
|
||||
}
|
||||
|
||||
return categories.Count;
|
||||
}
|
||||
|
||||
public async Task<string> GenerateIconAsync(GenerateIconRequest request)
|
||||
{
|
||||
var category = await categoryRepository.GetByIdAsync(request.CategoryId);
|
||||
if (category == null)
|
||||
{
|
||||
throw new NotFoundException("分类不存在");
|
||||
}
|
||||
|
||||
// 使用 SmartHandleService 统一封装的图标生成方法
|
||||
var svg = await smartHandleService.GenerateSingleCategoryIconAsync(category.Name, category.Type);
|
||||
if (string.IsNullOrWhiteSpace(svg))
|
||||
{
|
||||
throw new BusinessException("AI生成图标失败");
|
||||
}
|
||||
|
||||
// 解析现有图标数组
|
||||
var icons = string.IsNullOrWhiteSpace(category.Icon)
|
||||
? new List<string>()
|
||||
: JsonSerializer.Deserialize<List<string>>(category.Icon) ?? new List<string>();
|
||||
|
||||
// 添加新图标
|
||||
icons.Add(svg);
|
||||
|
||||
// 更新数据库
|
||||
category.Icon = JsonSerializer.Serialize(icons);
|
||||
category.UpdateTime = DateTime.Now;
|
||||
|
||||
var success = await categoryRepository.UpdateAsync(category);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("更新分类图标失败");
|
||||
}
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
public async Task UpdateSelectedIconAsync(UpdateSelectedIconRequest request)
|
||||
{
|
||||
var category = await categoryRepository.GetByIdAsync(request.CategoryId);
|
||||
if (category == null)
|
||||
{
|
||||
throw new NotFoundException("分类不存在");
|
||||
}
|
||||
|
||||
// 验证索引有效性
|
||||
if (string.IsNullOrWhiteSpace(category.Icon))
|
||||
{
|
||||
throw new ValidationException("该分类没有可用图标");
|
||||
}
|
||||
|
||||
var icons = JsonSerializer.Deserialize<List<string>>(category.Icon);
|
||||
if (icons == null || request.SelectedIndex < 0 || request.SelectedIndex >= icons.Count)
|
||||
{
|
||||
throw new ValidationException("无效的图标索引");
|
||||
}
|
||||
|
||||
// 将选中的图标移到数组第一位
|
||||
var selectedIcon = icons[request.SelectedIndex];
|
||||
icons.RemoveAt(request.SelectedIndex);
|
||||
icons.Insert(0, selectedIcon);
|
||||
|
||||
category.Icon = JsonSerializer.Serialize(icons);
|
||||
category.UpdateTime = DateTime.Now;
|
||||
|
||||
var success = await categoryRepository.UpdateAsync(category);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("更新图标失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static CategoryResponse MapToResponse(TransactionCategory category)
|
||||
{
|
||||
return new CategoryResponse
|
||||
{
|
||||
Id = category.Id,
|
||||
Name = category.Name,
|
||||
Type = category.Type,
|
||||
Icon = category.Icon,
|
||||
CreateTime = category.CreateTime,
|
||||
UpdateTime = category.UpdateTime
|
||||
};
|
||||
}
|
||||
}
|
||||
170
Application/TransactionPeriodicApplication.cs
Normal file
170
Application/TransactionPeriodicApplication.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using Application.Dto.Periodic;
|
||||
using Service.Transaction;
|
||||
|
||||
namespace Application;
|
||||
|
||||
/// <summary>
|
||||
/// 周期性账单应用服务接口
|
||||
/// </summary>
|
||||
public interface ITransactionPeriodicApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取周期性账单列表(分页)
|
||||
/// </summary>
|
||||
Task<PeriodicPagedResult> GetListAsync(int pageIndex = 1, int pageSize = 20, string? searchKeyword = null);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取周期性账单详情
|
||||
/// </summary>
|
||||
Task<PeriodicResponse> GetByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 创建周期性账单
|
||||
/// </summary>
|
||||
Task<PeriodicResponse> CreateAsync(CreatePeriodicRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 更新周期性账单
|
||||
/// </summary>
|
||||
Task UpdateAsync(UpdatePeriodicRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 删除周期性账单
|
||||
/// </summary>
|
||||
Task DeleteByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 启用/禁用周期性账单
|
||||
/// </summary>
|
||||
Task ToggleEnabledAsync(long id, bool enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 周期性账单应用服务实现
|
||||
/// </summary>
|
||||
public class TransactionPeriodicApplication(
|
||||
ITransactionPeriodicRepository periodicRepository,
|
||||
ITransactionPeriodicService periodicService,
|
||||
ILogger<TransactionPeriodicApplication> logger
|
||||
) : ITransactionPeriodicApplication
|
||||
{
|
||||
public async Task<PeriodicPagedResult> GetListAsync(int pageIndex = 1, int pageSize = 20, string? searchKeyword = null)
|
||||
{
|
||||
var list = await periodicRepository.GetPagedListAsync(pageIndex, pageSize, searchKeyword);
|
||||
var total = await periodicRepository.GetTotalCountAsync(searchKeyword);
|
||||
|
||||
return new PeriodicPagedResult
|
||||
{
|
||||
Data = list.Select(MapToResponse).ToArray(),
|
||||
Total = (int)total
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<PeriodicResponse> GetByIdAsync(long id)
|
||||
{
|
||||
var periodic = await periodicRepository.GetByIdAsync(id);
|
||||
if (periodic == null)
|
||||
{
|
||||
throw new NotFoundException("周期性账单不存在");
|
||||
}
|
||||
|
||||
return MapToResponse(periodic);
|
||||
}
|
||||
|
||||
public async Task<PeriodicResponse> CreateAsync(CreatePeriodicRequest request)
|
||||
{
|
||||
var periodic = new TransactionPeriodic
|
||||
{
|
||||
PeriodicType = request.PeriodicType,
|
||||
PeriodicConfig = request.PeriodicConfig ?? string.Empty,
|
||||
Amount = request.Amount,
|
||||
Type = request.Type,
|
||||
Classify = request.Classify ?? string.Empty,
|
||||
Reason = request.Reason ?? string.Empty,
|
||||
IsEnabled = true
|
||||
};
|
||||
|
||||
// 计算下次执行时间
|
||||
periodic.NextExecuteTime = periodicService.CalculateNextExecuteTime(periodic, DateTime.Now);
|
||||
|
||||
var success = await periodicRepository.AddAsync(periodic);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("创建周期性账单失败");
|
||||
}
|
||||
|
||||
return MapToResponse(periodic);
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(UpdatePeriodicRequest request)
|
||||
{
|
||||
var periodic = await periodicRepository.GetByIdAsync(request.Id);
|
||||
if (periodic == null)
|
||||
{
|
||||
throw new NotFoundException("周期性账单不存在");
|
||||
}
|
||||
|
||||
periodic.PeriodicType = request.PeriodicType;
|
||||
periodic.PeriodicConfig = request.PeriodicConfig ?? string.Empty;
|
||||
periodic.Amount = request.Amount;
|
||||
periodic.Type = request.Type;
|
||||
periodic.Classify = request.Classify ?? string.Empty;
|
||||
periodic.Reason = request.Reason ?? string.Empty;
|
||||
periodic.IsEnabled = request.IsEnabled;
|
||||
periodic.UpdateTime = DateTime.Now;
|
||||
|
||||
// 重新计算下次执行时间
|
||||
periodic.NextExecuteTime = periodicService.CalculateNextExecuteTime(periodic, DateTime.Now);
|
||||
|
||||
var success = await periodicRepository.UpdateAsync(periodic);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("更新周期性账单失败");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteByIdAsync(long id)
|
||||
{
|
||||
var success = await periodicRepository.DeleteAsync(id);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("删除周期性账单失败");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ToggleEnabledAsync(long id, bool enabled)
|
||||
{
|
||||
var periodic = await periodicRepository.GetByIdAsync(id);
|
||||
if (periodic == null)
|
||||
{
|
||||
throw new NotFoundException("周期性账单不存在");
|
||||
}
|
||||
|
||||
periodic.IsEnabled = enabled;
|
||||
periodic.UpdateTime = DateTime.Now;
|
||||
|
||||
var success = await periodicRepository.UpdateAsync(periodic);
|
||||
if (!success)
|
||||
{
|
||||
throw new BusinessException("操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static PeriodicResponse MapToResponse(TransactionPeriodic periodic)
|
||||
{
|
||||
return new PeriodicResponse
|
||||
{
|
||||
Id = periodic.Id,
|
||||
PeriodicType = periodic.PeriodicType,
|
||||
PeriodicConfig = periodic.PeriodicConfig,
|
||||
Amount = periodic.Amount,
|
||||
Type = periodic.Type,
|
||||
Classify = periodic.Classify,
|
||||
Reason = periodic.Reason,
|
||||
IsEnabled = periodic.IsEnabled,
|
||||
NextExecuteTime = periodic.NextExecuteTime,
|
||||
CreateTime = periodic.CreateTime,
|
||||
UpdateTime = periodic.UpdateTime
|
||||
};
|
||||
}
|
||||
}
|
||||
203
Application/TransactionStatisticsApplication.cs
Normal file
203
Application/TransactionStatisticsApplication.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
using Application.Dto.Statistics;
|
||||
using Service.Transaction;
|
||||
|
||||
namespace Application;
|
||||
|
||||
/// <summary>
|
||||
/// 交易统计应用服务接口
|
||||
/// </summary>
|
||||
public interface ITransactionStatisticsApplication
|
||||
{
|
||||
// === 新统一接口(推荐使用) ===
|
||||
|
||||
/// <summary>
|
||||
/// 按日期范围获取每日统计(新统一接口)
|
||||
/// </summary>
|
||||
Task<List<DailyStatisticsDto>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate, string? savingClassify = null);
|
||||
|
||||
/// <summary>
|
||||
/// 按日期范围获取汇总统计(新统一接口)
|
||||
/// </summary>
|
||||
Task<MonthlyStatistics> GetSummaryByRangeAsync(DateTime startDate, DateTime endDate);
|
||||
|
||||
/// <summary>
|
||||
/// 按日期范围获取分类统计(新统一接口)
|
||||
/// </summary>
|
||||
Task<List<CategoryStatistics>> GetCategoryStatisticsByRangeAsync(DateTime startDate, DateTime endDate, TransactionType type);
|
||||
|
||||
/// <summary>
|
||||
/// 获取趋势统计数据
|
||||
/// </summary>
|
||||
Task<List<TrendStatistics>> GetTrendStatisticsAsync(int startYear, int startMonth, int monthCount);
|
||||
|
||||
// === 旧接口(保留用于向后兼容,建议迁移到新接口) ===
|
||||
|
||||
[Obsolete("请使用 GetDailyStatisticsByRangeAsync")]
|
||||
Task<List<BalanceStatisticsDto>> GetBalanceStatisticsAsync(int year, int month);
|
||||
|
||||
[Obsolete("请使用 GetDailyStatisticsByRangeAsync")]
|
||||
Task<List<DailyStatisticsDto>> GetDailyStatisticsAsync(int year, int month);
|
||||
|
||||
[Obsolete("请使用 GetDailyStatisticsByRangeAsync")]
|
||||
Task<List<DailyStatisticsDto>> GetWeeklyStatisticsAsync(DateTime startDate, DateTime endDate);
|
||||
|
||||
[Obsolete("请使用 GetSummaryByRangeAsync")]
|
||||
Task<MonthlyStatistics> GetRangeStatisticsAsync(DateTime startDate, DateTime endDate);
|
||||
|
||||
[Obsolete("请使用 GetSummaryByRangeAsync")]
|
||||
Task<MonthlyStatistics> GetMonthlyStatisticsAsync(int year, int month);
|
||||
|
||||
[Obsolete("请使用 GetCategoryStatisticsByRangeAsync")]
|
||||
Task<List<CategoryStatistics>> GetCategoryStatisticsAsync(int year, int month, TransactionType type);
|
||||
|
||||
[Obsolete("请使用 GetCategoryStatisticsByRangeAsync")]
|
||||
Task<List<CategoryStatistics>> GetCategoryStatisticsByDateRangeAsync(string startDate, string endDate, TransactionType type);
|
||||
|
||||
Task<(List<ReasonGroupDto> list, int total)> GetReasonGroupsAsync(int pageIndex, int pageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 交易统计应用服务实现
|
||||
/// </summary>
|
||||
public class TransactionStatisticsApplication(
|
||||
ITransactionStatisticsService statisticsService,
|
||||
IConfigService configService,
|
||||
ILogger<TransactionStatisticsApplication> logger
|
||||
) : ITransactionStatisticsApplication
|
||||
{
|
||||
// === 新统一接口实现 ===
|
||||
|
||||
/// <summary>
|
||||
/// 按日期范围获取每日统计(新统一接口)
|
||||
/// </summary>
|
||||
public async Task<List<DailyStatisticsDto>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate, string? savingClassify = null)
|
||||
{
|
||||
// 如果未指定 savingClassify,从配置读取
|
||||
savingClassify ??= await configService.GetConfigByKeyAsync<string>("SavingsCategories");
|
||||
|
||||
var statistics = await statisticsService.GetDailyStatisticsByRangeAsync(startDate, endDate, savingClassify);
|
||||
|
||||
return statistics.Select(s => new DailyStatisticsDto(
|
||||
DateTime.Parse(s.Key).Day,
|
||||
s.Value.count,
|
||||
s.Value.expense,
|
||||
s.Value.income,
|
||||
s.Value.saving
|
||||
)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按日期范围获取汇总统计(新统一接口)
|
||||
/// </summary>
|
||||
public async Task<MonthlyStatistics> GetSummaryByRangeAsync(DateTime startDate, DateTime endDate)
|
||||
{
|
||||
return await statisticsService.GetSummaryByRangeAsync(startDate, endDate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按日期范围获取分类统计(新统一接口)
|
||||
/// </summary>
|
||||
public async Task<List<CategoryStatistics>> GetCategoryStatisticsByRangeAsync(DateTime startDate, DateTime endDate, TransactionType type)
|
||||
{
|
||||
return await statisticsService.GetCategoryStatisticsByDateRangeAsync(startDate, endDate, type);
|
||||
}
|
||||
|
||||
// === 旧接口实现(保留用于向后兼容) ===
|
||||
|
||||
public async Task<List<BalanceStatisticsDto>> GetBalanceStatisticsAsync(int year, int month)
|
||||
{
|
||||
var savingClassify = await configService.GetConfigByKeyAsync<string>("SavingsCategories");
|
||||
var statistics = await statisticsService.GetDailyStatisticsAsync(year, month, savingClassify);
|
||||
|
||||
var sortedStats = statistics.OrderBy(s => DateTime.Parse(s.Key)).ToList();
|
||||
var result = new List<BalanceStatisticsDto>();
|
||||
decimal cumulativeBalance = 0;
|
||||
|
||||
foreach (var item in sortedStats)
|
||||
{
|
||||
var dailyBalance = item.Value.income - item.Value.expense;
|
||||
cumulativeBalance += dailyBalance;
|
||||
result.Add(new BalanceStatisticsDto(DateTime.Parse(item.Key).Day, cumulativeBalance));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<DailyStatisticsDto>> GetDailyStatisticsAsync(int year, int month)
|
||||
{
|
||||
var savingClassify = await configService.GetConfigByKeyAsync<string>("SavingsCategories");
|
||||
var statistics = await statisticsService.GetDailyStatisticsAsync(year, month, savingClassify);
|
||||
|
||||
return statistics.Select(s => new DailyStatisticsDto(
|
||||
DateTime.Parse(s.Key).Day, // 从完整日期字符串 "yyyy-MM-dd" 中提取 day
|
||||
s.Value.count,
|
||||
s.Value.expense,
|
||||
s.Value.income,
|
||||
s.Value.saving
|
||||
)).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<DailyStatisticsDto>> GetWeeklyStatisticsAsync(DateTime startDate, DateTime endDate)
|
||||
{
|
||||
var savingClassify = await configService.GetConfigByKeyAsync<string>("SavingsCategories");
|
||||
var statistics = await statisticsService.GetDailyStatisticsByRangeAsync(startDate, endDate, savingClassify);
|
||||
|
||||
return statistics.Select(s => new DailyStatisticsDto(
|
||||
DateTime.Parse(s.Key).Day, // 从完整日期字符串 "yyyy-MM-dd" 中提取 day
|
||||
s.Value.count,
|
||||
s.Value.expense,
|
||||
s.Value.income,
|
||||
s.Value.saving
|
||||
)).ToList();
|
||||
}
|
||||
|
||||
public async Task<MonthlyStatistics> GetRangeStatisticsAsync(DateTime startDate, DateTime endDate)
|
||||
{
|
||||
var records = await statisticsService.GetDailyStatisticsByRangeAsync(startDate, endDate, null);
|
||||
|
||||
var totalExpense = records.Sum(r => r.Value.expense);
|
||||
var totalIncome = records.Sum(r => r.Value.income);
|
||||
var totalCount = records.Sum(r => r.Value.count);
|
||||
var expenseCount = records.Count(r => r.Value.expense > 0);
|
||||
var incomeCount = records.Count(r => r.Value.income > 0);
|
||||
|
||||
return new MonthlyStatistics
|
||||
{
|
||||
Year = startDate.Year,
|
||||
Month = startDate.Month,
|
||||
TotalExpense = totalExpense,
|
||||
TotalIncome = totalIncome,
|
||||
Balance = totalIncome - totalExpense,
|
||||
ExpenseCount = expenseCount,
|
||||
IncomeCount = incomeCount,
|
||||
TotalCount = totalCount
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<MonthlyStatistics> GetMonthlyStatisticsAsync(int year, int month)
|
||||
{
|
||||
return await statisticsService.GetMonthlyStatisticsAsync(year, month);
|
||||
}
|
||||
|
||||
public async Task<List<CategoryStatistics>> GetCategoryStatisticsAsync(int year, int month, TransactionType type)
|
||||
{
|
||||
return await statisticsService.GetCategoryStatisticsAsync(year, month, type);
|
||||
}
|
||||
|
||||
public async Task<List<CategoryStatistics>> GetCategoryStatisticsByDateRangeAsync(string startDate, string endDate, TransactionType type)
|
||||
{
|
||||
var start = DateTime.Parse(startDate);
|
||||
var end = DateTime.Parse(endDate);
|
||||
return await statisticsService.GetCategoryStatisticsByDateRangeAsync(start, end, type);
|
||||
}
|
||||
|
||||
public async Task<List<TrendStatistics>> GetTrendStatisticsAsync(int startYear, int startMonth, int monthCount)
|
||||
{
|
||||
return await statisticsService.GetTrendStatisticsAsync(startYear, startMonth, monthCount);
|
||||
}
|
||||
|
||||
public async Task<(List<ReasonGroupDto> list, int total)> GetReasonGroupsAsync(int pageIndex, int pageSize)
|
||||
{
|
||||
return await statisticsService.GetReasonGroupsAsync(pageIndex, pageSize);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user