This commit is contained in:
SunCheng
2026-02-10 17:49:19 +08:00
parent 3e18283e52
commit d052ae5197
104 changed files with 10369 additions and 3000 deletions

View File

@@ -1,77 +1,23 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Application;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Service.AppSettingModel;
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class AuthController : ControllerBase
public class AuthController(
IAuthApplication authApplication,
ILogger<AuthController> logger) : ControllerBase
{
private readonly AuthSettings _authSettings;
private readonly JwtSettings _jwtSettings;
private readonly ILogger<AuthController> _logger;
public AuthController(
IOptions<AuthSettings> authSettings,
IOptions<JwtSettings> jwtSettings,
ILogger<AuthController> logger)
{
_authSettings = authSettings.Value;
_jwtSettings = jwtSettings.Value;
_logger = logger;
}
/// <summary>
/// 用户登录
/// </summary>
[AllowAnonymous]
[HttpPost]
public BaseResponse<LoginResponse> Login([FromBody] LoginRequest request)
public BaseResponse<Application.Dto.LoginResponse> Login([FromBody] Application.Dto.LoginRequest request)
{
// 验证密码
if (string.IsNullOrEmpty(request.Password) || request.Password != _authSettings.Password)
{
_logger.LogWarning("登录失败: 密码错误");
return "密码错误".Fail<LoginResponse>();
}
// 生成JWT Token
var token = GenerateJwtToken();
var expiresAt = DateTime.UtcNow.AddHours(_jwtSettings.ExpirationHours);
_logger.LogInformation("用户登录成功");
return new LoginResponse
{
Token = token,
ExpiresAt = expiresAt
}.Ok();
}
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);
var response = authApplication.Login(request);
logger.LogInformation("用户登录成功");
return response.Ok();
}
}

View File

@@ -1,4 +1,7 @@
namespace WebApi.Controllers;
using Application;
using Application.Dto;
namespace WebApi.Controllers;
/// <summary>
/// 账单导入控制器
@@ -6,8 +9,7 @@
[ApiController]
[Route("api/[controller]/[action]")]
public class BillImportController(
ILogger<BillImportController> logger,
IImportService importService
IImportApplication importApplication
) : ControllerBase
{
/// <summary>
@@ -22,61 +24,34 @@ public class BillImportController(
[FromForm] string type
)
{
try
// 将IFormFile转换为ImportRequest
var stream = new MemoryStream();
await file.CopyToAsync(stream);
stream.Position = 0;
var request = new ImportRequest
{
// 验证参数
if (file.Length == 0)
{
return "请选择要上传的文件".Fail();
}
FileStream = stream,
FileExtension = Path.GetExtension(file.FileName).ToLowerInvariant(),
FileName = file.FileName,
FileSize = file.Length
};
if (string.IsNullOrWhiteSpace(type) || (type != "Alipay" && type != "WeChat"))
{
return "账单类型参数错误,必须是 Alipay 或 WeChat".Fail();
}
ImportResponse result;
// 验证文件类型
var allowedExtensions = new[] { ".csv", ".xlsx", ".xls" };
var fileExtension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(fileExtension))
{
return "只支持 CSV 或 Excel 文件格式".Fail();
}
// 验证文件大小10MB限制
const long maxFileSize = 10 * 1024 * 1024;
if (file.Length > maxFileSize)
{
return "文件大小不能超过 10MB".Fail();
}
// 保存文件
var ok = false;
var message = string.Empty;
await using (var stream = new MemoryStream())
{
await file.CopyToAsync(stream);
if (type == "Alipay")
{
(ok, message) = await importService.ImportAlipayAsync(stream, fileExtension);
}
else if (type == "WeChat")
{
(ok, message) = await importService.ImportWeChatAsync(stream, fileExtension);
}
}
if (!ok)
{
return message.Fail();
}
return message.Ok();
}
catch (Exception ex)
if (type == "Alipay")
{
logger.LogError(ex, "文件上传失败,类型: {Type}", type);
return $"文件上传失败: {ex.Message}".Fail();
result = await importApplication.ImportAlipayAsync(request);
}
else if (type == "WeChat")
{
result = await importApplication.ImportWeChatAsync(request);
}
else
{
return "账单类型参数错误,必须是 Alipay 或 WeChat".Fail();
}
return result.Message.Ok();
}
}

View File

@@ -1,72 +1,42 @@
using Service.Budget;
using Application;
using Application.Dto;
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class BudgetController(
IBudgetService budgetService,
IBudgetRepository budgetRepository,
ILogger<BudgetController> logger) : ControllerBase
IBudgetApplication budgetApplication
) : ControllerBase
{
/// <summary>
/// 获取预算列表
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<BudgetResult>>> GetListAsync([FromQuery] DateTime referenceDate)
public async Task<BaseResponse<List<BudgetResponse>>> GetListAsync([FromQuery] DateTime referenceDate)
{
try
{
return (await budgetService.GetListAsync(referenceDate))
.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)
.ToList()
.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取预算列表失败");
return $"获取预算列表失败: {ex.Message}".Fail<List<BudgetResult>>();
}
var result = await budgetApplication.GetListAsync(referenceDate);
return result.Ok();
}
/// <summary>
/// 获取分类统计信息(月度和年度)
/// </summary>
[HttpGet]
public async Task<BaseResponse<BudgetCategoryStats>> GetCategoryStatsAsync([FromQuery] BudgetCategory category, [FromQuery] DateTime referenceDate)
public async Task<BaseResponse<BudgetCategoryStatsResponse>> GetCategoryStatsAsync([FromQuery] BudgetCategory category, [FromQuery] DateTime referenceDate)
{
try
{
var result = await budgetService.GetCategoryStatsAsync(category, referenceDate);
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取分类统计失败, Category: {Category}", category);
return $"获取分类统计失败: {ex.Message}".Fail<BudgetCategoryStats>();
}
var result = await budgetApplication.GetCategoryStatsAsync(category, referenceDate);
return result.Ok();
}
/// <summary>
/// 获取未被预算覆盖的分类统计信息
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<UncoveredCategoryDetail>>> GetUncoveredCategoriesAsync([FromQuery] BudgetCategory category, [FromQuery] DateTime? referenceDate = null)
public async Task<BaseResponse<List<UncoveredCategoryResponse>>> GetUncoveredCategoriesAsync([FromQuery] BudgetCategory category, [FromQuery] DateTime? referenceDate = null)
{
try
{
var result = await budgetService.GetUncoveredCategoriesAsync(category, referenceDate);
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取未覆盖分类统计失败, Category: {Category}", category);
return $"获取未覆盖分类统计失败: {ex.Message}".Fail<List<UncoveredCategoryDetail>>();
}
var result = await budgetApplication.GetUncoveredCategoriesAsync(category, referenceDate);
return result.Ok();
}
/// <summary>
@@ -75,34 +45,18 @@ public class BudgetController(
[HttpGet]
public async Task<BaseResponse<string?>> GetArchiveSummaryAsync([FromQuery] DateTime referenceDate)
{
try
{
var result = await budgetService.GetArchiveSummaryAsync(referenceDate.Year, referenceDate.Month);
return result.Ok<string?>();
}
catch (Exception ex)
{
logger.LogError(ex, "获取归档总结失败");
return $"获取归档总结失败: {ex.Message}".Fail<string?>();
}
var result = await budgetApplication.GetArchiveSummaryAsync(referenceDate);
return result.Ok<string?>();
}
/// <summary>
/// 获取指定周期的存款预算信息
/// </summary>
[HttpGet]
public async Task<BaseResponse<BudgetResult?>> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type)
public async Task<BaseResponse<BudgetResponse?>> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type)
{
try
{
var result = await budgetService.GetSavingsBudgetAsync(year, month, type);
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取存款预算信息失败");
return $"获取存款预算信息失败: {ex.Message}".Fail<BudgetResult?>();
}
var result = await budgetApplication.GetSavingsBudgetAsync(year, month, type);
return result.Ok();
}
/// <summary>
@@ -111,127 +65,27 @@ public class BudgetController(
[HttpDelete("{id}")]
public async Task<BaseResponse> DeleteByIdAsync(long id)
{
try
{
var success = await budgetRepository.DeleteAsync(id);
return success ? BaseResponse.Done() : "删除预算失败".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "删除预算失败, Id: {Id}", id);
return $"删除预算失败: {ex.Message}".Fail();
}
await budgetApplication.DeleteByIdAsync(id);
return BaseResponse.Done();
}
/// <summary>
/// 创建预算
/// </summary>
[HttpPost]
public async Task<BaseResponse<long>> CreateAsync([FromBody] CreateBudgetDto dto)
public async Task<BaseResponse<long>> CreateAsync([FromBody] CreateBudgetRequest request)
{
try
{
// 不记额预算的金额强制设为0
var limit = dto.NoLimit ? 0 : dto.Limit;
var budget = new BudgetRecord
{
Name = dto.Name,
Type = dto.Type,
Limit = limit,
Category = dto.Category,
SelectedCategories = string.Join(",", dto.SelectedCategories),
StartDate = dto.StartDate ?? DateTime.Now,
NoLimit = dto.NoLimit,
IsMandatoryExpense = dto.IsMandatoryExpense
};
var varidationError = await ValidateBudgetSelectedCategoriesAsync(budget);
if (!string.IsNullOrEmpty(varidationError))
{
return varidationError.Fail<long>();
}
var success = await budgetRepository.AddAsync(budget);
if (success)
{
return budget.Id.Ok();
}
return "创建预算失败".Fail<long>();
}
catch (Exception ex)
{
logger.LogError(ex, "创建预算失败");
return $"创建预算失败: {ex.Message}".Fail<long>();
}
var budgetId = await budgetApplication.CreateAsync(request);
return budgetId.Ok();
}
/// <summary>
/// 更新预算
/// </summary>
[HttpPost]
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateBudgetDto dto)
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateBudgetRequest request)
{
try
{
var budget = await budgetRepository.GetByIdAsync(dto.Id);
if (budget == null) return "预算不存在".Fail();
// 不记额预算的金额强制设为0
var limit = dto.NoLimit ? 0 : dto.Limit;
budget.Name = dto.Name;
budget.Type = dto.Type;
budget.Limit = limit;
budget.Category = dto.Category;
budget.SelectedCategories = string.Join(",", dto.SelectedCategories);
budget.NoLimit = dto.NoLimit;
budget.IsMandatoryExpense = dto.IsMandatoryExpense;
if (dto.StartDate.HasValue)
{
budget.StartDate = dto.StartDate.Value;
}
var varidationError = await ValidateBudgetSelectedCategoriesAsync(budget);
if (!string.IsNullOrEmpty(varidationError))
{
return varidationError.Fail();
}
var success = await budgetRepository.UpdateAsync(budget);
return success ? BaseResponse.Done() : "更新预算失败".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "更新预算失败, Id: {Id}", dto.Id);
return $"更新预算失败: {ex.Message}".Fail();
}
}
private async Task<string> ValidateBudgetSelectedCategoriesAsync(BudgetRecord record)
{
// 验证不记额预算必须是年度预算
if (record.NoLimit && record.Type != BudgetPeriodType.Year)
{
return "不记额预算只能设置为年度预算。";
}
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())
{
return $"和 {budget.Name} 存在分类冲突,请调整相关分类。";
}
}
}
return string.Empty;
await budgetApplication.UpdateAsync(request);
return BaseResponse.Done();
}
}

View File

@@ -1,9 +1,11 @@
namespace WebApi.Controllers;
using Application;
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class ConfigController(
IConfigService configService
IConfigApplication configApplication
) : ControllerBase
{
/// <summary>
@@ -11,16 +13,8 @@ public class ConfigController(
/// </summary>
public async Task<BaseResponse<string>> GetConfig(string key)
{
try
{
var config = await configService.GetConfigByKeyAsync<string>(key);
var value = config ?? string.Empty;
return value.Ok("配置获取成功");
}
catch (Exception ex)
{
return $"获取{key}配置失败: {ex.Message}".Fail<string>();
}
var value = await configApplication.GetConfigAsync(key);
return value.Ok("配置获取成功");
}
/// <summary>
@@ -28,14 +22,7 @@ public class ConfigController(
/// </summary>
public async Task<BaseResponse> SetConfig(string key, string value)
{
try
{
await configService.SetConfigByKeyAsync(key, value);
return "配置设置成功".Ok();
}
catch (Exception ex)
{
return $"设置{key}配置失败: {ex.Message}".Fail();
}
await configApplication.SetConfigAsync(key, value);
return "配置设置成功".Ok();
}
}

View File

@@ -1,25 +0,0 @@
namespace WebApi.Controllers.Dto;
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; } = [];
public DateTime? StartDate { get; set; }
public bool NoLimit { get; set; } = false;
public bool IsMandatoryExpense { get; set; } = false;
}
public class UpdateBudgetDto : CreateBudgetDto
{
public long Id { get; set; }
}
public class UpdateArchiveSummaryDto
{
public DateTime ReferenceDate { get; set; }
public string? Summary { get; set; }
}

View File

@@ -1,50 +0,0 @@
namespace WebApi.Controllers.Dto;
/// <summary>
/// 邮件消息DTO包含额外的统计信息
/// </summary>
public class EmailMessageDto
{
public long Id { get; set; }
public string Subject { get; set; } = string.Empty;
public string From { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
public string HtmlBody { get; set; } = string.Empty;
public DateTime ReceivedDate { get; set; }
public DateTime CreateTime { get; set; }
public DateTime? UpdateTime { get; set; }
/// <summary>
/// 已解析的账单数量
/// </summary>
public int TransactionCount { get; set; }
public string ToName { get; set; } = string.Empty;
/// <summary>
/// 从实体转换为DTO
/// </summary>
public static EmailMessageDto FromEntity(EmailMessage entity, int transactionCount = 0)
{
return new EmailMessageDto
{
Id = entity.Id,
Subject = entity.Subject,
From = entity.From,
Body = entity.Body,
HtmlBody = entity.HtmlBody,
ReceivedDate = entity.ReceivedDate,
CreateTime = entity.CreateTime,
UpdateTime = entity.UpdateTime,
TransactionCount = transactionCount,
ToName = entity.To.Split('<').FirstOrDefault()?.Trim() ?? "未知"
};
}
}

View File

@@ -1,6 +0,0 @@
namespace WebApi.Controllers.Dto;
public class LoginRequest
{
public string Password { get; set; } = string.Empty;
}

View File

@@ -1,7 +0,0 @@
namespace WebApi.Controllers.Dto;
public class LoginResponse
{
public string Token { get; set; } = string.Empty;
public DateTime ExpiresAt { get; set; }
}

View File

@@ -1,99 +1,46 @@
using Service.EmailServices;
using Application.Dto.Email;
using Application;
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class EmailMessageController(
IEmailMessageRepository emailRepository,
ITransactionRecordRepository transactionRepository,
ILogger<EmailMessageController> logger,
IEmailHandleService emailHandleService,
IEmailSyncService emailBackgroundService
IEmailMessageApplication emailApplication
) : ControllerBase
{
/// <summary>
/// 获取邮件列表(分页)
/// </summary>
[HttpGet]
public async Task<PagedResponse<EmailMessageDto>> GetListAsync(
public async Task<BaseResponse<EmailPagedResult>> GetListAsync(
[FromQuery] DateTime? lastReceivedDate = null,
[FromQuery] long? lastId = null
)
{
try
var request = new EmailQueryRequest
{
var (list, lastTime, lastIdResult) = await emailRepository.GetPagedListAsync(lastReceivedDate, lastId);
var total = await emailRepository.GetTotalCountAsync();
// 为每个邮件获取账单数量
var emailDtos = new List<EmailMessageDto>();
foreach (var email in list)
{
var transactionCount = await transactionRepository.GetCountByEmailIdAsync(email.Id);
emailDtos.Add(EmailMessageDto.FromEntity(email, transactionCount));
}
return new PagedResponse<EmailMessageDto>
{
Success = true,
Data = emailDtos.ToArray(),
Total = (int)total,
LastId = lastIdResult,
LastTime = lastTime
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取邮件列表失败,时间: {LastTime}, ID: {LastId}", lastReceivedDate, lastId);
return PagedResponse<EmailMessageDto>.Fail($"获取邮件列表失败: {ex.Message}");
}
LastReceivedDate = lastReceivedDate,
LastId = lastId
};
var result = await emailApplication.GetListAsync(request);
return result.Ok();
}
/// <summary>
/// 根据ID获取邮件详情
/// </summary>
[HttpGet("{id}")]
public async Task<BaseResponse<EmailMessageDto>> GetByIdAsync(long id)
public async Task<BaseResponse<EmailMessageResponse>> GetByIdAsync(long id)
{
try
{
var email = await emailRepository.GetByIdAsync(id);
if (email == null)
{
return "邮件不存在".Fail<EmailMessageDto>();
}
// 获取账单数量
var transactionCount = await transactionRepository.GetCountByEmailIdAsync(id);
var emailDto = EmailMessageDto.FromEntity(email, transactionCount);
return emailDto.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取邮件详情失败邮件ID: {EmailId}", id);
return $"获取邮件详情失败: {ex.Message}".Fail<EmailMessageDto>();
}
var email = await emailApplication.GetByIdAsync(id);
return email.Ok();
}
public async Task<BaseResponse> DeleteByIdAsync(long id)
{
try
{
var success = await emailRepository.DeleteAsync(id);
if (success)
{
return BaseResponse.Done();
}
return "删除邮件失败,邮件不存在".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "删除邮件失败邮件ID: {EmailId}", id);
return $"删除邮件失败: {ex.Message}".Fail();
}
await emailApplication.DeleteByIdAsync(id);
return BaseResponse.Done();
}
/// <summary>
@@ -102,27 +49,8 @@ public class EmailMessageController(
[HttpPost]
public async Task<BaseResponse> RefreshTransactionRecordsAsync([FromQuery] long id)
{
try
{
var email = await emailRepository.GetByIdAsync(id);
if (email == null)
{
return "邮件不存在".Fail();
}
var success = await emailHandleService.RefreshTransactionRecordsAsync(id);
if (success)
{
return BaseResponse.Done();
}
return "重新分析失败".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "重新分析邮件失败邮件ID: {EmailId}", id);
return $"重新分析失败: {ex.Message}".Fail();
}
await emailApplication.RefreshTransactionRecordsAsync(id);
return BaseResponse.Done();
}
/// <summary>
@@ -131,15 +59,7 @@ public class EmailMessageController(
[HttpPost]
public async Task<BaseResponse> SyncEmailsAsync()
{
try
{
await emailBackgroundService.SyncEmailsAsync();
return "邮件同步成功".Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "同步邮件失败");
return $"同步邮件失败: {ex.Message}".Fail();
}
await emailApplication.SyncEmailsAsync();
return "邮件同步成功".Ok();
}
}

View File

@@ -1,115 +1,39 @@
using Quartz;
using Quartz.Impl.Matchers;
using Application;
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class JobController(ISchedulerFactory schedulerFactory, ILogger<JobController> logger) : ControllerBase
public class JobController(
IJobApplication jobApplication
) : ControllerBase
{
[HttpGet]
public async Task<BaseResponse<List<JobStatus>>> GetJobsAsync()
{
try
{
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.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取任务列表失败");
return $"获取任务列表失败: {ex.Message}".Fail<List<JobStatus>>();
}
var jobs = await jobApplication.GetJobsAsync();
return jobs.Ok();
}
[HttpPost]
public async Task<BaseResponse<bool>> ExecuteAsync([FromBody] JobRequest request)
{
try
{
var scheduler = await schedulerFactory.GetScheduler();
await scheduler.TriggerJob(new JobKey(request.JobName));
return true.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "执行任务失败: {JobName}", request.JobName);
return $"执行任务失败: {ex.Message}".Fail<bool>();
}
await jobApplication.ExecuteAsync(request.JobName);
return true.Ok();
}
[HttpPost]
public async Task<BaseResponse<bool>> PauseAsync([FromBody] JobRequest request)
{
try
{
var scheduler = await schedulerFactory.GetScheduler();
await scheduler.PauseJob(new JobKey(request.JobName));
return true.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "暂停任务失败: {JobName}", request.JobName);
return $"暂停任务失败: {ex.Message}".Fail<bool>();
}
await jobApplication.PauseAsync(request.JobName);
return true.Ok();
}
[HttpPost]
public async Task<BaseResponse<bool>> ResumeAsync([FromBody] JobRequest request)
{
try
{
var scheduler = await schedulerFactory.GetScheduler();
await scheduler.ResumeJob(new JobKey(request.JobName));
return true.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "恢复任务失败: {JobName}", request.JobName);
return $"恢复任务失败: {ex.Message}".Fail<bool>();
}
}
public class JobStatus
{
public string Name { get; set; } = string.Empty;
public string JobDescription { get; set; } = string.Empty;
public string TriggerDescription { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string NextRunTime { get; set; } = string.Empty;
await jobApplication.ResumeAsync(request.JobName);
return true.Ok();
}
public class JobRequest

View File

@@ -1,29 +1,24 @@
using Microsoft.AspNetCore.Authorization;
using Service.Message;
using Application.Dto.Message;
using Microsoft.AspNetCore.Authorization;
using Application;
namespace WebApi.Controllers;
[Authorize]
[ApiController]
[Route("api/[controller]/[action]")]
public class MessageRecordController(IMessageService messageService, ILogger<MessageRecordController> logger) : ControllerBase
public class MessageRecordController(
IMessageRecordApplication messageApplication
) : ControllerBase
{
/// <summary>
/// 获取消息列表
/// </summary>
[HttpGet]
public async Task<PagedResponse<MessageRecord>> GetList([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20)
public async Task<PagedResponse<MessageRecordResponse>> GetList([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20)
{
try
{
var (list, total) = await messageService.GetPagedListAsync(pageIndex, pageSize);
return PagedResponse<MessageRecord>.Done(list.ToArray(), (int)total);
}
catch (Exception ex)
{
logger.LogError(ex, "获取消息列表失败");
return PagedResponse<MessageRecord>.Fail($"获取消息列表失败: {ex.Message}");
}
var result = await messageApplication.GetListAsync(pageIndex, pageSize);
return PagedResponse<MessageRecordResponse>.Done(result.Data, result.Total);
}
/// <summary>
@@ -32,16 +27,8 @@ public class MessageRecordController(IMessageService messageService, ILogger<Mes
[HttpGet]
public async Task<BaseResponse<long>> GetUnreadCount()
{
try
{
var count = await messageService.GetUnreadCountAsync();
return count.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取未读消息数量失败");
return $"获取未读消息数量失败: {ex.Message}".Fail<long>();
}
var count = await messageApplication.GetUnreadCountAsync();
return count.Ok();
}
/// <summary>
@@ -50,16 +37,8 @@ public class MessageRecordController(IMessageService messageService, ILogger<Mes
[HttpPost]
public async Task<BaseResponse<bool>> MarkAsRead([FromQuery] long id)
{
try
{
var result = await messageService.MarkAsReadAsync(id);
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "标记消息已读失败ID: {Id}", id);
return $"标记消息已读失败: {ex.Message}".Fail<bool>();
}
var result = await messageApplication.MarkAsReadAsync(id);
return result.Ok();
}
/// <summary>
@@ -68,16 +47,8 @@ public class MessageRecordController(IMessageService messageService, ILogger<Mes
[HttpPost]
public async Task<BaseResponse<bool>> MarkAllAsRead()
{
try
{
var result = await messageService.MarkAllAsReadAsync();
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "全部标记已读失败");
return $"全部标记已读失败: {ex.Message}".Fail<bool>();
}
var result = await messageApplication.MarkAllAsReadAsync();
return result.Ok();
}
/// <summary>
@@ -86,16 +57,8 @@ public class MessageRecordController(IMessageService messageService, ILogger<Mes
[HttpPost]
public async Task<BaseResponse<bool>> Delete([FromQuery] long id)
{
try
{
var result = await messageService.DeleteAsync(id);
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "删除消息失败ID: {Id}", id);
return $"删除消息失败: {ex.Message}".Fail<bool>();
}
var result = await messageApplication.DeleteAsync(id);
return result.Ok();
}
/// <summary>
@@ -104,15 +67,7 @@ public class MessageRecordController(IMessageService messageService, ILogger<Mes
[HttpPost]
public async Task<BaseResponse<bool>> Add([FromBody] MessageRecord message)
{
try
{
var result = await messageService.AddAsync(message);
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "新增消息失败");
return $"新增消息失败: {ex.Message}".Fail<bool>();
}
var result = await messageApplication.AddAsync(message);
return result.Ok();
}
}

View File

@@ -1,48 +1,29 @@
using Service.Message;
using Application;
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class NotificationController(INotificationService notificationService) : ControllerBase
public class NotificationController(
INotificationApplication notificationApplication
) : ControllerBase
{
[HttpGet]
public async Task<BaseResponse<string>> GetVapidPublicKey()
{
try
{
var key = await notificationService.GetVapidPublicKeyAsync();
return key.Ok<string>();
}
catch (Exception ex)
{
return ex.Message.Fail<string>();
}
var key = await notificationApplication.GetVapidPublicKeyAsync();
return key.Ok<string>();
}
public async Task<BaseResponse> Subscribe([FromBody] PushSubscription subscription)
{
try
{
await notificationService.SubscribeAsync(subscription);
return BaseResponse.Done();
}
catch (Exception ex)
{
return ex.Message.Fail();
}
await notificationApplication.SubscribeAsync(subscription);
return BaseResponse.Done();
}
public async Task<BaseResponse> TestNotification([FromQuery] string message)
{
try
{
await notificationService.SendNotificationAsync(message);
return BaseResponse.Done();
}
catch (Exception ex)
{
return ex.Message.Fail();
}
await notificationApplication.SendNotificationAsync(message);
return BaseResponse.Done();
}
}

View File

@@ -1,147 +1,52 @@
using Service.AI;
using Application;
using Application.Dto.Category;
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class TransactionCategoryController(
ITransactionCategoryRepository categoryRepository,
ITransactionRecordRepository transactionRecordRepository,
ILogger<TransactionCategoryController> logger,
IBudgetRepository budgetRepository,
IOpenAiService openAiService
ITransactionCategoryApplication categoryApplication
) : ControllerBase
{
/// <summary>
/// 获取分类列表(支持按类型筛选)
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TransactionCategory>>> GetListAsync([FromQuery] TransactionType? type = null)
public async Task<BaseResponse<List<CategoryResponse>>> GetListAsync([FromQuery] TransactionType? type = null)
{
try
{
List<TransactionCategory> categories;
if (type.HasValue)
{
categories = await categoryRepository.GetCategoriesByTypeAsync(type.Value);
}
else
{
categories = (await categoryRepository.GetAllAsync()).ToList();
}
return categories.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取分类列表失败");
return $"获取分类列表失败: {ex.Message}".Fail<List<TransactionCategory>>();
}
var categories = await categoryApplication.GetListAsync(type);
return categories.Ok();
}
/// <summary>
/// 根据ID获取分类详情
/// </summary>
[HttpGet("{id}")]
public async Task<BaseResponse<TransactionCategory>> GetByIdAsync(long id)
public async Task<BaseResponse<CategoryResponse>> GetByIdAsync(long id)
{
try
{
var category = await categoryRepository.GetByIdAsync(id);
if (category == null)
{
return "分类不存在".Fail<TransactionCategory>();
}
return category.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取分类详情失败, Id: {Id}", id);
return $"获取分类详情失败: {ex.Message}".Fail<TransactionCategory>();
}
var category = await categoryApplication.GetByIdAsync(id);
return category.Ok();
}
/// <summary>
/// 创建分类
/// </summary>
[HttpPost]
public async Task<BaseResponse<long>> CreateAsync([FromBody] CreateCategoryDto dto)
public async Task<BaseResponse<long>> CreateAsync([FromBody] CreateCategoryRequest request)
{
try
{
// 检查同名分类
var existing = await categoryRepository.GetByNameAndTypeAsync(dto.Name, dto.Type);
if (existing != null)
{
return "已存在相同名称的分类".Fail<long>();
}
var category = new TransactionCategory
{
Name = dto.Name,
Type = dto.Type
};
var result = await categoryRepository.AddAsync(category);
if (result)
{
return category.Id.Ok();
}
return "创建分类失败".Fail<long>();
}
catch (Exception ex)
{
logger.LogError(ex, "创建分类失败, Dto: {@Dto}", dto);
return $"创建分类失败: {ex.Message}".Fail<long>();
}
var categoryId = await categoryApplication.CreateAsync(request);
return categoryId.Ok();
}
/// <summary>
/// 更新分类
/// </summary>
[HttpPost]
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateCategoryDto dto)
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateCategoryRequest request)
{
try
{
var category = await categoryRepository.GetByIdAsync(dto.Id);
if (category == null)
{
return "分类不存在".Fail();
}
// 如果修改了名称,检查同名
if (category.Name != dto.Name)
{
var existing = await categoryRepository.GetByNameAndTypeAsync(dto.Name, category.Type);
if (existing != null && existing.Id != dto.Id)
{
return "已存在相同名称的分类".Fail();
}
// 同步更新交易记录中的分类名称
await transactionRecordRepository.UpdateCategoryNameAsync(category.Name, dto.Name, category.Type);
await budgetRepository.UpdateBudgetCategoryNameAsync(category.Name, dto.Name, category.Type);
}
category.Name = dto.Name;
category.UpdateTime = DateTime.Now;
var success = await categoryRepository.UpdateAsync(category);
if (success)
{
return "更新分类成功".Ok();
}
return "更新分类失败".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "更新分类失败, Dto: {@Dto}", dto);
return $"更新分类失败: {ex.Message}".Fail();
}
await categoryApplication.UpdateAsync(request);
return "更新分类成功".Ok();
}
/// <summary>
@@ -150,263 +55,37 @@ public class TransactionCategoryController(
[HttpPost]
public async Task<BaseResponse> DeleteAsync([FromQuery] long id)
{
try
{
// 检查是否被使用
var inUse = await categoryRepository.IsCategoryInUseAsync(id);
if (inUse)
{
return "该分类已被使用,无法删除".Fail();
}
var success = await categoryRepository.DeleteAsync(id);
if (success)
{
return BaseResponse.Done();
}
return "删除分类失败,分类不存在".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "删除分类失败, Id: {Id}", id);
return $"删除分类失败: {ex.Message}".Fail();
}
await categoryApplication.DeleteAsync(id);
return BaseResponse.Done();
}
/// <summary>
/// 批量创建分类(用于初始化)
/// </summary>
[HttpPost]
public async Task<BaseResponse<int>> BatchCreateAsync([FromBody] List<CreateCategoryDto> dtoList)
public async Task<BaseResponse<int>> BatchCreateAsync([FromBody] List<CreateCategoryRequest> requests)
{
try
{
var categories = dtoList.Select(dto => new TransactionCategory
{
Name = dto.Name,
Type = dto.Type
}).ToList();
var result = await categoryRepository.AddRangeAsync(categories);
if (result)
{
return categories.Count.Ok();
}
return "批量创建分类失败".Fail<int>();
}
catch (Exception ex)
{
logger.LogError(ex, "批量创建分类失败, Count: {Count}", dtoList.Count);
return $"批量创建分类失败: {ex.Message}".Fail<int>();
}
var count = await categoryApplication.BatchCreateAsync(requests);
return count.Ok();
}
/// <summary>
/// 为指定分类生成新的SVG图标
/// </summary>
[HttpPost]
public async Task<BaseResponse<string>> GenerateIconAsync([FromBody] GenerateIconDto dto)
public async Task<BaseResponse<string>> GenerateIconAsync([FromBody] GenerateIconRequest request)
{
try
{
var category = await categoryRepository.GetByIdAsync(dto.CategoryId);
if (category == null)
{
return "分类不存在".Fail<string>();
}
// 使用AI生成简洁、风格鲜明的SVG图标
var systemPrompt = @"你是一个专业的SVG图标设计师。你的任务是为预算分类生成极简风格、视觉识别度高的SVG图标。
## 核心设计原则
1. **语义相关性**:图标必须直观反映分类本质。例如:
- 「餐饮」→ 餐具、碗筷或热腾腾的食物
- 「交通」→ 汽车、地铁或公交车
- 「购物」→ 购物袋或购物车
- 「娱乐」→ 电影票、游戏手柄或麦克风
- 「医疗」→ 十字架或药丸
- 「工资」→ 钱袋或上升箭头
2. **极简风格**
- 线条简洁流畅,避免复杂细节
- 使用几何图形和圆润的边角
- 2-4个主要形状元素即可
- 笔画粗细统一stroke-width: 2
3. **视觉识别**
- 轮廓清晰,一眼能认出是什么
- 避免抽象符号,优先具象图形
- 留白合理,图标不要过于密集
## 技术规范
- viewBox=""0 0 24 24""
- 尺寸为 24×24
- 使用单色fill=""currentColor"" 或 stroke=""currentColor""
- 优先使用 stroke描边而非 fill填充更显轻盈
- stroke-width=""2"" stroke-linecap=""round"" stroke-linejoin=""round""
- 只返回 <svg> 标签及其内容,不要其他说明
## 回退方案
如果该分类实在无法用具象图形表达(如「其他」「杂项」等),则生成包含该分类**首字**的文字图标:
```xml
<svg viewBox=""0 0 24 24"" fill=""none"" xmlns=""http://www.w3.org/2000/svg"">
<circle cx=""12"" cy=""12"" r=""10"" stroke=""currentColor"" stroke-width=""2""/>
<text x=""12"" y=""16"" font-size=""12"" font-weight=""bold"" text-anchor=""middle"" fill=""currentColor"">{首字}</text>
</svg>
```
## 示例
**好的图标**
- 「咖啡」→ 咖啡杯+热气
- 「房租」→ 房子外轮廓
- 「健身」→ 哑铃
**差的图标**
- 过于复杂的写实风格
- 无法识别的抽象符号
- 图形过小或过密";
var transactionTypeDesc = category.Type switch
{
TransactionType.Expense => "支出",
TransactionType.Income => "收入",
_ => "不计收支"
};
var userPrompt = $@"请为「{category.Name}」分类生成图标({transactionTypeDesc}类别)。
要求:
1. 分析这个分类的核心含义
2. 选择最具代表性的视觉元素
3. 用极简线条勾勒出图标(优先使用 stroke 描边风格)
4. 如果实在无法用图形表达,则生成包含「{(category.Name.Length > 0 ? category.Name[0] : '?')}」的文字图标
直接返回SVG代码无需解释。";
var svgContent = await openAiService.ChatAsync(systemPrompt, userPrompt, timeoutSeconds: 30);
if (string.IsNullOrWhiteSpace(svgContent))
{
return "AI生成图标失败".Fail<string>();
}
// 提取SVG标签内容
var svgMatch = System.Text.RegularExpressions.Regex.Match(svgContent, @"<svg[^>]*>.*?</svg>",
System.Text.RegularExpressions.RegexOptions.Singleline);
if (!svgMatch.Success)
{
return "生成的内容不包含有效的SVG标签".Fail<string>();
}
var svg = svgMatch.Value;
// 解析现有图标数组
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)
{
return "更新分类图标失败".Fail<string>();
}
return svg.Ok<string>();
}
catch (Exception ex)
{
logger.LogError(ex, "生成图标失败, CategoryId: {CategoryId}", dto.CategoryId);
return $"生成图标失败: {ex.Message}".Fail<string>();
}
var svg = await categoryApplication.GenerateIconAsync(request);
return svg.Ok<string>();
}
/// <summary>
/// 更新分类的选中图标索引
/// </summary>
[HttpPost]
public async Task<BaseResponse> UpdateSelectedIconAsync([FromBody] UpdateSelectedIconDto dto)
public async Task<BaseResponse> UpdateSelectedIconAsync([FromBody] UpdateSelectedIconRequest request)
{
try
{
var category = await categoryRepository.GetByIdAsync(dto.CategoryId);
if (category == null)
{
return "分类不存在".Fail();
}
// 验证索引有效性
if (string.IsNullOrWhiteSpace(category.Icon))
{
return "该分类没有可用图标".Fail();
}
var icons = JsonSerializer.Deserialize<List<string>>(category.Icon);
if (icons == null || dto.SelectedIndex < 0 || dto.SelectedIndex >= icons.Count)
{
return "无效的图标索引".Fail();
}
// 这里可以添加一个SelectedIconIndex字段到实体中或者将选中的图标移到数组第一位
// 暂时采用移动到第一位的方式
var selectedIcon = icons[dto.SelectedIndex];
icons.RemoveAt(dto.SelectedIndex);
icons.Insert(0, selectedIcon);
category.Icon = JsonSerializer.Serialize(icons);
category.UpdateTime = DateTime.Now;
var success = await categoryRepository.UpdateAsync(category);
if (success)
{
return "更新图标成功".Ok();
}
return "更新图标失败".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "更新选中图标失败, Dto: {@Dto}", dto);
return $"更新选中图标失败: {ex.Message}".Fail();
}
await categoryApplication.UpdateSelectedIconAsync(request);
return "更新图标成功".Ok();
}
}
/// <summary>
/// 创建分类DTO
/// </summary>
public record CreateCategoryDto(
string Name,
TransactionType Type
);
/// <summary>
/// 更新分类DTO
/// </summary>
public record UpdateCategoryDto(
long Id,
string Name
);
/// <summary>
/// 生成图标DTO
/// </summary>
public record GenerateIconDto(
long CategoryId
);
/// <summary>
/// 更新选中图标DTO
/// </summary>
public record UpdateSelectedIconDto(
long CategoryId,
int SelectedIndex
);

View File

@@ -1,4 +1,5 @@
using Service.Transaction;
using Application.Dto.Periodic;
using Application;
namespace WebApi.Controllers;
@@ -8,98 +9,46 @@ namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class TransactionPeriodicController(
ITransactionPeriodicRepository periodicRepository,
ITransactionPeriodicService periodicService,
ILogger<TransactionPeriodicController> logger
ITransactionPeriodicApplication periodicApplication
) : ControllerBase
{
/// <summary>
/// 获取周期性账单列表(分页)
/// </summary>
[HttpGet]
public async Task<PagedResponse<TransactionPeriodic>> GetListAsync(
public async Task<PagedResponse<PeriodicResponse>> GetListAsync(
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? searchKeyword = null
)
{
try
var result = await periodicApplication.GetListAsync(pageIndex, pageSize, searchKeyword);
return new PagedResponse<PeriodicResponse>
{
var list = await periodicRepository.GetPagedListAsync(pageIndex, pageSize, searchKeyword);
var total = await periodicRepository.GetTotalCountAsync(searchKeyword);
return new PagedResponse<TransactionPeriodic>
{
Success = true,
Data = list.ToArray(),
Total = (int)total
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取周期性账单列表失败");
return PagedResponse<TransactionPeriodic>.Fail($"获取列表失败: {ex.Message}");
}
Success = true,
Data = result.Data,
Total = result.Total
};
}
/// <summary>
/// 根据ID获取周期性账单详情
/// </summary>
[HttpGet("{id}")]
public async Task<BaseResponse<TransactionPeriodic>> GetByIdAsync(long id)
public async Task<BaseResponse<PeriodicResponse>> GetByIdAsync(long id)
{
try
{
var periodic = await periodicRepository.GetByIdAsync(id);
if (periodic == null)
{
return "周期性账单不存在".Fail<TransactionPeriodic>();
}
return periodic.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取周期性账单详情失败ID: {Id}", id);
return $"获取详情失败: {ex.Message}".Fail<TransactionPeriodic>();
}
var periodic = await periodicApplication.GetByIdAsync(id);
return periodic.Ok();
}
/// <summary>
/// 创建周期性账单
/// </summary>
[HttpPost]
public async Task<BaseResponse<TransactionPeriodic>> CreateAsync([FromBody] CreatePeriodicRequest request)
public async Task<BaseResponse<PeriodicResponse>> CreateAsync([FromBody] CreatePeriodicRequest request)
{
try
{
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)
{
return "创建周期性账单失败".Fail<TransactionPeriodic>();
}
return periodic.Ok("创建成功");
}
catch (Exception ex)
{
logger.LogError(ex, "创建周期性账单失败");
return $"创建失败: {ex.Message}".Fail<TransactionPeriodic>();
}
var periodic = await periodicApplication.CreateAsync(request);
return periodic.Ok("创建成功");
}
/// <summary>
@@ -108,39 +57,8 @@ public class TransactionPeriodicController(
[HttpPost]
public async Task<BaseResponse> UpdateAsync([FromBody] UpdatePeriodicRequest request)
{
try
{
var periodic = await periodicRepository.GetByIdAsync(request.Id);
if (periodic == null)
{
return "周期性账单不存在".Fail();
}
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)
{
return "更新周期性账单失败".Fail();
}
return "更新成功".Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "更新周期性账单失败ID: {Id}", request.Id);
return $"更新失败: {ex.Message}".Fail();
}
await periodicApplication.UpdateAsync(request);
return "更新成功".Ok();
}
/// <summary>
@@ -149,21 +67,8 @@ public class TransactionPeriodicController(
[HttpPost]
public async Task<BaseResponse> DeleteByIdAsync([FromQuery] long id)
{
try
{
var success = await periodicRepository.DeleteAsync(id);
if (!success)
{
return "删除周期性账单失败".Fail();
}
return "删除成功".Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "删除周期性账单失败ID: {Id}", id);
return $"删除失败: {ex.Message}".Fail();
}
await periodicApplication.DeleteByIdAsync(id);
return "删除成功".Ok();
}
/// <summary>
@@ -172,57 +77,7 @@ public class TransactionPeriodicController(
[HttpPost]
public async Task<BaseResponse> ToggleEnabledAsync([FromQuery] long id, [FromQuery] bool enabled)
{
try
{
var periodic = await periodicRepository.GetByIdAsync(id);
if (periodic == null)
{
return "周期性账单不存在".Fail();
}
periodic.IsEnabled = enabled;
periodic.UpdateTime = DateTime.Now;
var success = await periodicRepository.UpdateAsync(periodic);
if (!success)
{
return "操作失败".Fail();
}
return (enabled ? "已启用" : "已禁用").Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "启用/禁用周期性账单失败ID: {Id}", id);
return $"操作失败: {ex.Message}".Fail();
}
await periodicApplication.ToggleEnabledAsync(id, enabled);
return (enabled ? "已启用" : "已禁用").Ok();
}
}
/// <summary>
/// 创建周期性账单请求
/// </summary>
public class CreatePeriodicRequest
{
public PeriodicType PeriodicType { get; set; }
public string? PeriodicConfig { get; set; }
public decimal Amount { get; set; }
public TransactionType Type { get; set; }
public string? Classify { get; set; }
public string? Reason { get; set; }
}
/// <summary>
/// 更新周期性账单请求
/// </summary>
public class UpdatePeriodicRequest
{
public long Id { get; set; }
public PeriodicType PeriodicType { get; set; }
public string? PeriodicConfig { get; set; }
public decimal Amount { get; set; }
public TransactionType Type { get; set; }
public string? Classify { get; set; }
public string? Reason { get; set; }
public bool IsEnabled { get; set; }
}

View File

@@ -1,13 +1,14 @@
using Application.Dto.Transaction;
using Service.AI;
using Service.Transaction;
using Application;
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class TransactionRecordController(
ITransactionApplication transactionApplication,
ITransactionRecordRepository transactionRepository,
ISmartHandleService smartHandleService,
ILogger<TransactionRecordController> logger
) : ControllerBase
{
@@ -15,7 +16,7 @@ public class TransactionRecordController(
/// 获取交易记录列表(分页)
/// </summary>
[HttpGet]
public async Task<PagedResponse<TransactionRecord>> GetListAsync(
public async Task<PagedResponse<TransactionResponse>> GetListAsync(
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? searchKeyword = null,
@@ -29,212 +30,88 @@ public class TransactionRecordController(
[FromQuery] bool sortByAmount = false
)
{
try
var request = new TransactionQueryRequest
{
var classifies = string.IsNullOrWhiteSpace(classify)
? null
: classify.Split(',', StringSplitOptions.RemoveEmptyEntries);
PageIndex = pageIndex,
PageSize = pageSize,
SearchKeyword = searchKeyword,
Classify = classify,
Type = type,
Year = year,
Month = month,
StartDate = startDate,
EndDate = endDate,
Reason = reason,
SortByAmount = sortByAmount
};
TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null;
var list = await transactionRepository.QueryAsync(
year: year,
month: month,
startDate: startDate,
endDate: endDate,
type: transactionType,
classifies: classifies,
searchKeyword: searchKeyword,
reason: reason,
pageIndex: pageIndex,
pageSize: pageSize,
sortByAmount: sortByAmount);
var total = await transactionRepository.CountAsync(
year: year,
month: month,
startDate: startDate,
endDate: endDate,
type: transactionType,
classifies: classifies,
searchKeyword: searchKeyword,
reason: reason);
return new PagedResponse<TransactionRecord>
{
Success = true,
Data = list.ToArray(),
Total = (int)total
};
}
catch (Exception ex)
var result = await transactionApplication.GetListAsync(request);
return new PagedResponse<TransactionResponse>
{
logger.LogError(ex, "获取交易记录列表失败,页码: {PageIndex}, 页大小: {PageSize}", pageIndex, pageSize);
return PagedResponse<TransactionRecord>.Fail($"获取交易记录列表失败: {ex.Message}");
}
Success = true,
Data = result.Data,
Total = result.Total
};
}
/// <summary>
/// 获取待确认分类的交易记录列表
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TransactionRecord>>> GetUnconfirmedListAsync()
public async Task<BaseResponse<List<TransactionResponse>>> GetUnconfirmedListAsync()
{
try
{
var list = await transactionRepository.GetUnconfirmedRecordsAsync();
return list.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取待确认分类交易列表失败");
return $"获取待确认分类交易列表失败: {ex.Message}".Fail<List<TransactionRecord>>();
}
var list = await transactionApplication.GetUnconfirmedListAsync();
return list.Ok();
}
/// <summary>
/// 全部确认待确认的交易分类
/// </summary>
[HttpPost]
public async Task<BaseResponse<int>> ConfirmAllUnconfirmedAsync([FromBody] ConfirmAllUnconfirmedRequestDto request)
public async Task<BaseResponse<int>> ConfirmAllUnconfirmedAsync([FromBody] ConfirmAllUnconfirmedRequest request)
{
try
{
var count = await transactionRepository.ConfirmAllUnconfirmedAsync(request.Ids);
return count.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "全部确认待确认分类失败");
return $"全部确认待确认分类失败: {ex.Message}".Fail<int>();
}
var count = await transactionApplication.ConfirmAllUnconfirmedAsync(request.Ids);
return count.Ok();
}
/// <summary>
/// 根据ID获取交易记录详情
/// </summary>
[HttpGet("{id}")]
public async Task<BaseResponse<TransactionRecord>> GetByIdAsync(long id)
public async Task<BaseResponse<TransactionResponse>> GetByIdAsync(long id)
{
try
{
var transaction = await transactionRepository.GetByIdAsync(id);
if (transaction == null)
{
return "交易记录不存在".Fail<TransactionRecord>();
}
return transaction.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取交易记录详情失败交易ID: {TransactionId}", id);
return $"获取交易记录详情失败: {ex.Message}".Fail<TransactionRecord>();
}
var transaction = await transactionApplication.GetByIdAsync(id);
return transaction.Ok();
}
/// <summary>
/// 根据邮件ID获取交易记录列表
/// </summary>
[HttpGet("{emailId}")]
public async Task<BaseResponse<List<TransactionRecord>>> GetByEmailIdAsync(long emailId)
public async Task<BaseResponse<List<TransactionResponse>>> GetByEmailIdAsync(long emailId)
{
try
{
var transactions = await transactionRepository.GetByEmailIdAsync(emailId);
return transactions.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取邮件交易记录失败邮件ID: {EmailId}", emailId);
return $"获取邮件交易记录失败: {ex.Message}".Fail<List<TransactionRecord>>();
}
var transactions = await transactionApplication.GetByEmailIdAsync(emailId);
return transactions.Ok();
}
/// <summary>
/// 创建交易记录
/// </summary>
[HttpPost]
public async Task<BaseResponse> CreateAsync([FromBody] CreateTransactionDto dto)
public async Task<BaseResponse> CreateAsync([FromBody] CreateTransactionRequest request)
{
try
{
// 解析日期字符串
if (!DateTime.TryParse(dto.OccurredAt, out var occurredAt))
{
return "交易时间格式不正确".Fail();
}
var transaction = new TransactionRecord
{
OccurredAt = occurredAt,
Reason = dto.Reason ?? string.Empty,
Amount = dto.Amount,
Type = dto.Type,
Classify = dto.Classify ?? string.Empty,
ImportFrom = "手动录入",
ImportNo = Guid.NewGuid().ToString("N"),
Card = "手动",
EmailMessageId = 0 // 手动录入的记录EmailMessageId 设为 0
};
var result = await transactionRepository.AddAsync(transaction);
if (result)
{
return BaseResponse.Done();
}
return "创建交易记录失败".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "创建交易记录失败,交易信息: {@TransactionDto}", dto);
return $"创建交易记录失败: {ex.Message}".Fail();
}
await transactionApplication.CreateAsync(request);
return BaseResponse.Done();
}
/// <summary>
/// 更新交易记录
/// </summary>
[HttpPost]
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateTransactionDto dto)
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateTransactionRequest request)
{
try
{
var transaction = await transactionRepository.GetByIdAsync(dto.Id);
if (transaction == null)
{
return "交易记录不存在".Fail();
}
// 更新可编辑字段
transaction.Reason = dto.Reason ?? string.Empty;
transaction.Amount = dto.Amount;
transaction.Balance = dto.Balance;
transaction.Type = dto.Type;
transaction.Classify = dto.Classify ?? string.Empty;
// 更新交易时间
if (!string.IsNullOrEmpty(dto.OccurredAt) && DateTime.TryParse(dto.OccurredAt, out var occurredAt))
{
transaction.OccurredAt = occurredAt;
}
// 清除待确认状态
transaction.UnconfirmedClassify = null;
transaction.UnconfirmedType = null;
var success = await transactionRepository.UpdateAsync(transaction);
if (success)
{
return BaseResponse.Done();
}
return "更新交易记录失败".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "更新交易记录失败交易ID: {TransactionId}, 交易信息: {@TransactionDto}", dto.Id, dto);
return $"更新交易记录失败: {ex.Message}".Fail();
}
await transactionApplication.UpdateAsync(request);
return BaseResponse.Done();
}
/// <summary>
@@ -243,29 +120,17 @@ public class TransactionRecordController(
[HttpPost]
public async Task<BaseResponse> DeleteByIdAsync([FromQuery] long id)
{
try
{
var success = await transactionRepository.DeleteAsync(id);
if (success)
{
return BaseResponse.Done();
}
return "删除交易记录失败,记录不存在".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "删除交易记录失败交易ID: {TransactionId}", id);
return $"删除交易记录失败: {ex.Message}".Fail();
}
await transactionApplication.DeleteByIdAsync(id);
return BaseResponse.Done();
}
/// <summary>
/// 智能分析账单(流式输出)
/// </summary>
[HttpPost]
public async Task AnalyzeBillAsync([FromBody] BillAnalysisRequest request)
{
// SSE响应头设置保留在Controller
Response.ContentType = "text/event-stream";
Response.Headers.Append("Cache-Control", "no-cache");
Response.Headers.Append("Connection", "keep-alive");
@@ -276,45 +141,31 @@ public class TransactionRecordController(
return;
}
await smartHandleService.AnalyzeBillAsync(request.UserInput, async void (chunk) =>
{
try
// 调用Application传递回调
await transactionApplication.AnalyzeBillAsync(
request.UserInput,
async chunk =>
{
await WriteEventAsync(chunk);
}
catch (Exception e)
{
logger.LogError(e, "流式写入账单分析结果失败");
}
});
try
{
await WriteEventAsync(chunk);
}
catch (Exception e)
{
logger.LogError(e, "流式写入账单分析结果失败");
}
});
}
/// <summary>
/// 获取指定日期的交易记录
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TransactionRecord>>> GetByDateAsync([FromQuery] string date)
public async Task<BaseResponse<List<TransactionResponse>>> GetByDateAsync([FromQuery] string date)
{
try
{
if (!DateTime.TryParse(date, out var targetDate))
{
return "日期格式不正确".Fail<List<TransactionRecord>>();
}
// 获取当天的开始和结束时间
var startDate = targetDate.Date;
var endDate = startDate.AddDays(1);
var records = await transactionRepository.QueryAsync(startDate: startDate, endDate: endDate);
return records.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取指定日期的交易记录失败,日期: {Date}", date);
return $"获取指定日期的交易记录失败: {ex.Message}".Fail<List<TransactionRecord>>();
}
var dateTime = DateTime.Parse(date);
var records = await transactionApplication.GetByDateAsync(dateTime);
return records.Ok();
}
/// <summary>
@@ -323,34 +174,18 @@ public class TransactionRecordController(
[HttpGet]
public async Task<BaseResponse<int>> GetUnclassifiedCountAsync()
{
try
{
var count = (int)await transactionRepository.CountAsync();
return count.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取未分类账单数量失败");
return $"获取未分类账单数量失败: {ex.Message}".Fail<int>();
}
var count = await transactionApplication.GetUnclassifiedCountAsync();
return count.Ok();
}
/// <summary>
/// 获取未分类的账单列表
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TransactionRecord>>> GetUnclassifiedAsync([FromQuery] int pageSize = 10)
public async Task<BaseResponse<List<TransactionResponse>>> GetUnclassifiedAsync([FromQuery] int pageSize = 10)
{
try
{
var records = await transactionRepository.GetUnclassifiedAsync(pageSize);
return records.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取未分类账单列表失败");
return $"获取未分类账单列表失败: {ex.Message}".Fail<List<TransactionRecord>>();
}
var records = await transactionApplication.GetUnclassifiedAsync(pageSize);
return records.Ok();
}
/// <summary>
@@ -359,6 +194,7 @@ public class TransactionRecordController(
[HttpPost]
public async Task SmartClassifyAsync([FromBody] SmartClassifyRequest request)
{
// SSE响应头设置保留在Controller
Response.ContentType = "text/event-stream";
Response.Headers.Append("Cache-Control", "no-cache");
Response.Headers.Append("Connection", "keep-alive");
@@ -370,23 +206,29 @@ public class TransactionRecordController(
return;
}
await smartHandleService.SmartClassifyAsync(request.TransactionIds.ToArray(), async void (chunk) =>
{
try
// 调用Application传递回调
await transactionApplication.SmartClassifyAsync(
request.TransactionIds.ToArray(),
async chunk =>
{
var (eventType, content) = chunk;
await TrySetUnconfirmedAsync(eventType, content);
await WriteEventAsync(eventType, content);
}
catch (Exception e)
{
logger.LogError(e, "流式写入智能分类结果失败");
}
});
try
{
var (eventType, content) = chunk;
await TrySetUnconfirmedAsync(eventType, content);
await WriteEventAsync(eventType, content);
}
catch (Exception e)
{
logger.LogError(e, "流式写入智能分类结果失败");
}
});
await Response.Body.FlushAsync();
}
/// <summary>
/// Controller专属逻辑解析AI返回的JSON并更新交易记录的UnconfirmedClassify字段
/// </summary>
private async Task TrySetUnconfirmedAsync(string eventType, string content)
{
if (eventType != "data")
@@ -436,102 +278,33 @@ public class TransactionRecordController(
[HttpPost]
public async Task<BaseResponse> BatchUpdateClassifyAsync([FromBody] List<BatchUpdateClassifyItem> items)
{
try
{
var successCount = 0;
var failCount = 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 (record.Type == item.Type)
{
record.UnconfirmedType = TransactionType.None;
}
var success = await transactionRepository.UpdateAsync(record);
if (success)
successCount++;
else
failCount++;
}
else
{
failCount++;
}
}
return $"批量更新完成,成功 {successCount} 条,失败 {failCount} 条".Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "批量更新分类失败");
return $"批量更新分类失败: {ex.Message}".Fail();
}
var count = await transactionApplication.BatchUpdateClassifyAsync(items);
return $"批量更新完成,成功 {count} 条".Ok();
}
/// <summary>
/// 按摘要批量更新分类
/// </summary>
[HttpPost]
public async Task<BaseResponse<int>> BatchUpdateByReasonAsync([FromBody] BatchUpdateByReasonDto dto)
public async Task<BaseResponse<int>> BatchUpdateByReasonAsync([FromBody] BatchUpdateByReasonRequest request)
{
try
{
var count = await transactionRepository.BatchUpdateByReasonAsync(dto.Reason, dto.Type, dto.Classify);
return count.Ok($"成功更新 {count} 条记录");
}
catch (Exception ex)
{
logger.LogError(ex, "按摘要批量更新分类失败,摘要: {Reason}", dto.Reason);
return $"按摘要批量更新分类失败: {ex.Message}".Fail<int>();
}
var count = await transactionApplication.BatchUpdateByReasonAsync(request);
return count.Ok($"成功更新 {count} 条记录");
}
/// <summary>
/// 一句话录账解析
/// </summary>
[HttpPost]
public async Task<BaseResponse<TransactionParseResult>> ParseOneLine([FromBody] ParseOneLineRequestDto request)
public async Task<BaseResponse<TransactionParseResult?>> ParseOneLine([FromBody] ParseOneLineRequest request)
{
if (string.IsNullOrEmpty(request.Text))
{
return "请求参数缺失text".Fail<TransactionParseResult>();
}
try
{
var result = await smartHandleService.ParseOneLineBillAsync(request.Text);
if (result == null)
{
return "AI解析失败".Fail<TransactionParseResult>();
}
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "一句话录账解析失败,文本: {Text}", request.Text);
return ("AI解析失败: " + ex.Message).Fail<TransactionParseResult>();
}
var result = await transactionApplication.ParseOneLineAsync(request.Text);
return result.Ok();
}
/// <summary>
/// SSE辅助方法写入事件带类型
/// </summary>
private async Task WriteEventAsync(string eventType, string data)
{
var message = $"event: {eventType}\ndata: {data}\n\n";
@@ -539,6 +312,9 @@ public class TransactionRecordController(
await Response.Body.FlushAsync();
}
/// <summary>
/// SSE辅助方法写入数据
/// </summary>
private async Task WriteEventAsync(string data)
{
var message = $"data: {data}\n\n";
@@ -547,32 +323,6 @@ public class TransactionRecordController(
}
}
/// <summary>
/// 创建交易记录DTO
/// </summary>
public record CreateTransactionDto(
string OccurredAt,
string? Reason,
decimal Amount,
TransactionType Type,
string? Classify
);
/// <summary>
/// 更新交易记录DTO
/// </summary>
public record UpdateTransactionDto(
long Id,
string? Reason,
decimal Amount,
decimal Balance,
TransactionType Type,
string? Classify,
string? OccurredAt = null
);
/// <summary>
/// 智能分类请求DTO
/// </summary>
@@ -580,35 +330,9 @@ public record SmartClassifyRequest(
List<long>? TransactionIds = null
);
/// <summary>
/// 批量更新分类项DTO
/// </summary>
public record BatchUpdateClassifyItem(
long Id,
string? Classify,
TransactionType? Type = null
);
/// <summary>
/// 按摘要批量更新DTO
/// </summary>
public record BatchUpdateByReasonDto(
string Reason,
TransactionType Type,
string Classify
);
/// <summary>
/// 账单分析请求DTO
/// </summary>
public record BillAnalysisRequest(
string UserInput
);
public record ParseOneLineRequestDto(
string Text
);
public record ConfirmAllUnconfirmedRequestDto(
long[] Ids
);

View File

@@ -1,4 +1,5 @@
using Service.Transaction;
using Application.Dto.Statistics;
using Application;
namespace WebApi.Controllers;
@@ -8,308 +9,173 @@ namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class TransactionStatisticsController(
ITransactionRecordRepository transactionRepository,
ITransactionStatisticsService transactionStatisticsService,
ILogger<TransactionStatisticsController> logger,
IConfigService configService
ITransactionStatisticsApplication statisticsApplication
) : ControllerBase
{
// ===== 新统一接口(推荐使用) =====
/// <summary>
/// 获取累积余额统计数据(用于余额卡片图表
/// 按日期范围获取每日统计(新统一接口
/// </summary>
/// <param name="startDate">开始日期(包含)</param>
/// <param name="endDate">结束日期(不包含)</param>
/// <param name="savingClassify">储蓄分类(可选,不传则使用系统配置)</param>
[HttpGet]
public async Task<BaseResponse<List<BalanceStatisticsDto>>> GetBalanceStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month
public async Task<BaseResponse<List<DailyStatisticsDto>>> GetDailyStatisticsByRangeAsync(
[FromQuery] DateTime startDate,
[FromQuery] DateTime endDate,
[FromQuery] string? savingClassify = null
)
{
try
{
// 获取存款分类
var savingClassify = await configService.GetConfigByKeyAsync<string>("SavingsCategories");
// 获取每日统计数据
var statistics = await transactionStatisticsService.GetDailyStatisticsAsync(year, month, savingClassify);
// 按日期排序并计算累积余额
var sortedStats = statistics.OrderBy(s => 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(
item.Key,
cumulativeBalance
));
}
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取累积余额统计失败,年份: {Year}, 月份: {Month}", year, month);
return $"获取累积余额统计失败: {ex.Message}".Fail<List<BalanceStatisticsDto>>();
}
var result = await statisticsApplication.GetDailyStatisticsByRangeAsync(startDate, endDate, savingClassify);
return result.Ok();
}
/// <summary>
/// 获取指定月份每天的消费统计
/// 按日期范围获取汇总统计(新统一接口)
/// </summary>
/// <param name="startDate">开始日期(包含)</param>
/// <param name="endDate">结束日期(不包含)</param>
[HttpGet]
public async Task<BaseResponse<List<DailyStatisticsDto>>> GetDailyStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month
)
{
try
{
// 获取存款分类
var savingClassify = await configService.GetConfigByKeyAsync<string>("SavingsCategories");
var statistics = await transactionStatisticsService.GetDailyStatisticsAsync(year, month, savingClassify);
var result = statistics.Select(s => new DailyStatisticsDto(
s.Key,
s.Value.count,
s.Value.expense,
s.Value.income,
s.Value.saving
)).ToList();
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取日历统计数据失败,年份: {Year}, 月份: {Month}", year, month);
return $"获取日历统计数据失败: {ex.Message}".Fail<List<DailyStatisticsDto>>();
}
}
/// <summary>
/// 获取周统计数据
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<DailyStatisticsDto>>> GetWeeklyStatisticsAsync(
public async Task<BaseResponse<Service.Transaction.MonthlyStatistics>> GetSummaryByRangeAsync(
[FromQuery] DateTime startDate,
[FromQuery] DateTime endDate
)
{
try
{
// 获取存款分类
var savingClassify = await configService.GetConfigByKeyAsync<string>("SavingsCategories");
var statistics = await transactionStatisticsService.GetDailyStatisticsByRangeAsync(startDate, endDate, savingClassify);
var result = statistics.Select(s => new DailyStatisticsDto(
s.Key,
s.Value.count,
s.Value.expense,
s.Value.income,
s.Value.saving
)).ToList();
return result.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取周统计数据失败,开始日期: {StartDate}, 结束日期: {EndDate}", startDate, endDate);
return $"获取周统计数据失败: {ex.Message}".Fail<List<DailyStatisticsDto>>();
}
var result = await statisticsApplication.GetSummaryByRangeAsync(startDate, endDate);
return result.Ok();
}
/// <summary>
/// 获取指定日期范围的统计汇总数据
/// 按日期范围获取分类统计(新统一接口)
/// </summary>
/// <param name="startDate">开始日期(包含)</param>
/// <param name="endDate">结束日期(不包含)</param>
/// <param name="type">交易类型</param>
[HttpGet]
public async Task<BaseResponse<MonthlyStatistics>> GetRangeStatisticsAsync(
public async Task<BaseResponse<List<Service.Transaction.CategoryStatistics>>> GetCategoryStatisticsByRangeAsync(
[FromQuery] DateTime startDate,
[FromQuery] DateTime endDate
)
{
try
{
// 通过日期范围查询数据
var records = await transactionRepository.QueryAsync(
startDate: startDate,
endDate: endDate,
pageSize: int.MaxValue);
var statistics = new MonthlyStatistics
{
Year = startDate.Year,
Month = startDate.Month
};
foreach (var record in records)
{
var amount = Math.Abs(record.Amount);
if (record.Type == TransactionType.Expense)
{
statistics.TotalExpense += amount;
statistics.ExpenseCount++;
}
else if (record.Type == TransactionType.Income)
{
statistics.TotalIncome += amount;
statistics.IncomeCount++;
}
}
statistics.Balance = statistics.TotalIncome - statistics.TotalExpense;
statistics.TotalCount = records.Count;
return statistics.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取时间范围统计数据失败,开始日期: {StartDate}, 结束日期: {EndDate}", startDate, endDate);
return $"获取时间范围统计数据失败: {ex.Message}".Fail<MonthlyStatistics>();
}
}
/// <summary>
/// 获取月度统计数据
/// </summary>
[HttpGet]
public async Task<BaseResponse<MonthlyStatistics>> GetMonthlyStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month
)
{
try
{
var statistics = await transactionStatisticsService.GetMonthlyStatisticsAsync(year, month);
return statistics.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取月度统计数据失败,年份: {Year}, 月份: {Month}", year, month);
return $"获取月度统计数据失败: {ex.Message}".Fail<MonthlyStatistics>();
}
}
/// <summary>
/// 获取分类统计数据
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<CategoryStatistics>>> GetCategoryStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month,
[FromQuery] DateTime endDate,
[FromQuery] TransactionType type
)
{
try
{
var statistics = await transactionStatisticsService.GetCategoryStatisticsAsync(year, month, type);
return statistics.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取分类统计数据失败,年份: {Year}, 月份: {Month}, 类型: {Type}", year, month, type);
return $"获取分类统计数据失败: {ex.Message}".Fail<List<CategoryStatistics>>();
}
}
/// <summary>
/// 按日期范围获取分类统计数据
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<CategoryStatistics>>> GetCategoryStatisticsByDateRangeAsync(
[FromQuery] string startDate,
[FromQuery] string endDate,
[FromQuery] TransactionType type
)
{
try
{
if (!DateTime.TryParse(startDate, out var start))
{
return "开始日期格式错误".Fail<List<CategoryStatistics>>();
}
if (!DateTime.TryParse(endDate, out var end))
{
return "结束日期格式错误".Fail<List<CategoryStatistics>>();
}
var statistics = await transactionStatisticsService.GetCategoryStatisticsByDateRangeAsync(start, end, type);
return statistics.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取分类统计数据失败,开始日期: {StartDate}, 结束日期: {EndDate}, 类型: {Type}", startDate, endDate, type);
return $"获取分类统计数据失败: {ex.Message}".Fail<List<CategoryStatistics>>();
}
var result = await statisticsApplication.GetCategoryStatisticsByRangeAsync(startDate, endDate, type);
return result.Ok();
}
/// <summary>
/// 获取趋势统计数据
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TrendStatistics>>> GetTrendStatisticsAsync(
public async Task<BaseResponse<List<Service.Transaction.TrendStatistics>>> GetTrendStatisticsAsync(
[FromQuery] int startYear,
[FromQuery] int startMonth,
[FromQuery] int monthCount = 6
)
{
try
{
var statistics = await transactionStatisticsService.GetTrendStatisticsAsync(startYear, startMonth, monthCount);
return statistics.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取趋势统计数据失败,开始年份: {Year}, 开始月份: {Month}, 月份数: {Count}", startYear, startMonth,
monthCount);
return $"获取趋势统计数据失败: {ex.Message}".Fail<List<TrendStatistics>>();
}
var result = await statisticsApplication.GetTrendStatisticsAsync(startYear, startMonth, monthCount);
return result.Ok();
}
// ===== 旧接口(保留用于向后兼容,已标记为过时) =====
/// <summary>
/// 获取累积余额统计数据(用于余额卡片图表)
/// </summary>
[Obsolete("请使用 GetDailyStatisticsByRangeAsync 并在前端计算累积余额")]
[HttpGet]
public async Task<BaseResponse<List<BalanceStatisticsDto>>> GetBalanceStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month
)
{
var result = await statisticsApplication.GetBalanceStatisticsAsync(year, month);
return result.Ok();
}
/// <summary>
/// 获取按交易摘要分组的统计信息(支持分页)
/// 获取指定月份每天的消费统计
/// </summary>
[Obsolete("请使用 GetDailyStatisticsByRangeAsync")]
[HttpGet]
public async Task<PagedResponse<ReasonGroupDto>> GetReasonGroupsAsync(
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 20)
public async Task<BaseResponse<List<DailyStatisticsDto>>> GetDailyStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month
)
{
try
{
var (list, total) = await transactionStatisticsService.GetReasonGroupsAsync(pageIndex, pageSize);
return new PagedResponse<ReasonGroupDto>
{
Success = true,
Data = list.ToArray(),
Total = total
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取交易摘要分组失败");
return PagedResponse<ReasonGroupDto>.Fail($"获取交易摘要分组失败: {ex.Message}");
}
var result = await statisticsApplication.GetDailyStatisticsAsync(year, month);
return result.Ok();
}
/// <summary>
/// 获取周统计数据
/// </summary>
[Obsolete("请使用 GetDailyStatisticsByRangeAsync")]
[HttpGet]
public async Task<BaseResponse<List<DailyStatisticsDto>>> GetWeeklyStatisticsAsync(
[FromQuery] DateTime startDate,
[FromQuery] DateTime endDate
)
{
var result = await statisticsApplication.GetWeeklyStatisticsAsync(startDate, endDate);
return result.Ok();
}
/// <summary>
/// 获取指定日期范围的统计汇总数据
/// </summary>
[Obsolete("请使用 GetSummaryByRangeAsync")]
[HttpGet]
public async Task<BaseResponse<Service.Transaction.MonthlyStatistics>> GetRangeStatisticsAsync(
[FromQuery] DateTime startDate,
[FromQuery] DateTime endDate
)
{
var result = await statisticsApplication.GetRangeStatisticsAsync(startDate, endDate);
return result.Ok();
}
/// <summary>
/// 获取月度统计数据
/// </summary>
[Obsolete("请使用 GetSummaryByRangeAsync")]
[HttpGet]
public async Task<BaseResponse<Service.Transaction.MonthlyStatistics>> GetMonthlyStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month
)
{
var result = await statisticsApplication.GetMonthlyStatisticsAsync(year, month);
return result.Ok();
}
/// <summary>
/// 获取分类统计数据
/// </summary>
[Obsolete("请使用 GetCategoryStatisticsByRangeAsync")]
[HttpGet]
public async Task<BaseResponse<List<Service.Transaction.CategoryStatistics>>> GetCategoryStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month,
[FromQuery] TransactionType type
)
{
var result = await statisticsApplication.GetCategoryStatisticsAsync(year, month, type);
return result.Ok();
}
/// <summary>
/// 按日期范围获取分类统计数据
/// </summary>
[Obsolete("请使用 GetCategoryStatisticsByRangeAsyncDateTime 参数版本)")]
[HttpGet]
public async Task<BaseResponse<List<Service.Transaction.CategoryStatistics>>> GetCategoryStatisticsByDateRangeAsync(
[FromQuery] string startDate,
[FromQuery] string endDate,
[FromQuery] TransactionType type
)
{
var result = await statisticsApplication.GetCategoryStatisticsByDateRangeAsync(startDate, endDate, type);
return result.Ok();
}
}
/// <summary>
/// 日历统计响应DTO
/// </summary>
public record DailyStatisticsDto(
string Date,
int Count,
decimal Expense,
decimal Income,
decimal Balance
);
/// <summary>
/// 累积余额统计DTO
/// </summary>
public record BalanceStatisticsDto(
string Date,
decimal CumulativeBalance
);

View File

@@ -0,0 +1,63 @@
using Application.Exceptions;
using Microsoft.AspNetCore.Mvc.Filters;
namespace WebApi.Filters;
/// <summary>
/// 全局异常过滤器
/// </summary>
/// <remarks>
/// 统一处理Application层抛出的异常转换为标准的BaseResponse格式
/// </remarks>
public class GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger) : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
BaseResponse response;
var statusCode = 500;
switch (context.Exception)
{
case ValidationException ex:
// 业务验证失败 - 400 Bad Request
logger.LogWarning(ex, "业务验证失败: {Message}", ex.Message);
response = ex.Message.Fail();
statusCode = 400;
break;
case NotFoundException ex:
// 资源未找到 - 404 Not Found
logger.LogWarning(ex, "资源未找到: {Message}", ex.Message);
response = ex.Message.Fail();
statusCode = 404;
break;
case BusinessException ex:
// 业务逻辑异常 - 500 Internal Server Error
logger.LogError(ex, "业务逻辑错误: {Message}", ex.Message);
response = ex.Message.Fail();
statusCode = 500;
break;
case Application.Exceptions.ApplicationException ex:
// 应用层一般异常 - 500 Internal Server Error
logger.LogError(ex, "应用层错误: {Message}", ex.Message);
response = ex.Message.Fail();
statusCode = 500;
break;
default:
// 未处理的异常 - 500 Internal Server Error
logger.LogError(context.Exception, "未处理的异常: {Message}", context.Exception.Message);
response = "操作失败,请稍后重试".Fail();
statusCode = 500;
break;
}
context.Result = new ObjectResult(response)
{
StatusCode = statusCode
};
context.ExceptionHandled = true;
}
}

View File

@@ -1,5 +1,4 @@
global using Service;
global using Common;
global using Common;
global using Microsoft.AspNetCore.Mvc;
global using WebApi.Controllers.Dto;
global using Repository;

View File

@@ -2,15 +2,8 @@ using Serilog.Context;
namespace WebApi.Middleware;
public class RequestIdMiddleware
public class RequestIdMiddleware(RequestDelegate next)
{
private readonly RequestDelegate _next;
public RequestIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var requestId = context.Request.Headers["X-Request-ID"].FirstOrDefault() ?? Guid.NewGuid().ToString("N");
@@ -19,7 +12,7 @@ public class RequestIdMiddleware
using (LogContext.PushProperty("RequestId", requestId))
{
await _next(context);
await next(context);
}
}
}

View File

@@ -8,7 +8,9 @@ using Serilog;
using Service.AppSettingModel;
using WebApi;
using WebApi.Middleware;
using WebApi.Filters;
using Yitter.IdGenerator;
using Application.Extensions;
// 初始化雪花算法ID生成器
var options = new IdGeneratorOptions(1); // WorkerId 为 1可根据实际部署情况调整
@@ -30,6 +32,7 @@ builder.Services.AddControllers(mvcOptions =>
.Build();
mvcOptions.Filters.Add(new AuthorizeFilter(policy));
mvcOptions.Filters.Add<GlobalExceptionFilter>(); // 添加全局异常过滤器
});
builder.Services.AddOpenApi();
builder.Services.AddHttpClient();
@@ -124,6 +127,9 @@ builder.Services.AddSingleton(fsql);
// 自动扫描注册服务和仓储
builder.Services.AddServices();
// 注册Application层服务
builder.Services.AddApplicationServices();
// 配置 Quartz.NET 定时任务
builder.AddScheduler();

View File

@@ -10,6 +10,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Application\Application.csproj" />
<ProjectReference Include="..\Service\Service.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\Repository\Repository.csproj" />