fix
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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() ?? "未知"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace WebApi.Controllers.Dto;
|
||||
|
||||
public class LoginRequest
|
||||
{
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace WebApi.Controllers.Dto;
|
||||
|
||||
public class LoginResponse
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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("请使用 GetCategoryStatisticsByRangeAsync(DateTime 参数版本)")]
|
||||
[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
|
||||
);
|
||||
63
WebApi/Filters/GlobalExceptionFilter.cs
Normal file
63
WebApi/Filters/GlobalExceptionFilter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user