2026-01-18 22:04:56 +08:00
|
|
|
|
namespace WebApi.Controllers;
|
2025-12-26 17:13:57 +08:00
|
|
|
|
|
2025-12-25 11:20:56 +08:00
|
|
|
|
[ApiController]
|
|
|
|
|
|
[Route("api/[controller]/[action]")]
|
|
|
|
|
|
public class TransactionRecordController(
|
|
|
|
|
|
ITransactionRecordRepository transactionRepository,
|
2025-12-30 18:49:46 +08:00
|
|
|
|
ISmartHandleService smartHandleService,
|
2026-01-16 11:15:44 +08:00
|
|
|
|
ILogger<TransactionRecordController> logger,
|
|
|
|
|
|
IConfigService configService
|
2025-12-25 11:20:56 +08:00
|
|
|
|
) : ControllerBase
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取交易记录列表(分页)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
2025-12-27 11:50:12 +08:00
|
|
|
|
public async Task<PagedResponse<TransactionRecord>> GetListAsync(
|
2025-12-27 22:05:50 +08:00
|
|
|
|
[FromQuery] int pageIndex = 1,
|
|
|
|
|
|
[FromQuery] int pageSize = 20,
|
2025-12-26 17:56:08 +08:00
|
|
|
|
[FromQuery] string? searchKeyword = null,
|
|
|
|
|
|
[FromQuery] string? classify = null,
|
|
|
|
|
|
[FromQuery] int? type = null,
|
|
|
|
|
|
[FromQuery] int? year = null,
|
2025-12-27 22:05:50 +08:00
|
|
|
|
[FromQuery] int? month = null,
|
2026-01-09 16:21:03 +08:00
|
|
|
|
[FromQuery] DateTime? startDate = null,
|
|
|
|
|
|
[FromQuery] DateTime? endDate = null,
|
2025-12-30 17:02:30 +08:00
|
|
|
|
[FromQuery] string? reason = null,
|
2025-12-27 22:05:50 +08:00
|
|
|
|
[FromQuery] bool sortByAmount = false
|
2025-12-25 11:20:56 +08:00
|
|
|
|
)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-01-11 11:21:13 +08:00
|
|
|
|
string[]? classifies = string.IsNullOrWhiteSpace(classify)
|
|
|
|
|
|
? null
|
2026-01-09 16:21:03 +08:00
|
|
|
|
: classify.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
|
2025-12-26 17:56:08 +08:00
|
|
|
|
TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null;
|
2025-12-27 22:05:50 +08:00
|
|
|
|
var list = await transactionRepository.GetPagedListAsync(
|
|
|
|
|
|
pageIndex,
|
2025-12-29 20:30:15 +08:00
|
|
|
|
pageSize,
|
|
|
|
|
|
searchKeyword,
|
2026-01-09 16:21:03 +08:00
|
|
|
|
classifies,
|
2025-12-29 20:30:15 +08:00
|
|
|
|
transactionType,
|
|
|
|
|
|
year,
|
2025-12-27 22:05:50 +08:00
|
|
|
|
month,
|
2026-01-09 16:21:03 +08:00
|
|
|
|
startDate,
|
|
|
|
|
|
endDate,
|
2025-12-30 17:02:30 +08:00
|
|
|
|
reason,
|
2025-12-27 22:05:50 +08:00
|
|
|
|
sortByAmount);
|
|
|
|
|
|
var total = await transactionRepository.GetTotalCountAsync(
|
2025-12-29 20:30:15 +08:00
|
|
|
|
searchKeyword,
|
2026-01-09 16:21:03 +08:00
|
|
|
|
classifies,
|
2025-12-29 20:30:15 +08:00
|
|
|
|
transactionType,
|
|
|
|
|
|
year,
|
2025-12-30 17:02:30 +08:00
|
|
|
|
month,
|
2026-01-09 16:21:03 +08:00
|
|
|
|
startDate,
|
|
|
|
|
|
endDate,
|
2025-12-30 17:02:30 +08:00
|
|
|
|
reason);
|
2025-12-25 11:20:56 +08:00
|
|
|
|
|
2025-12-27 11:50:12 +08:00
|
|
|
|
return new PagedResponse<TransactionRecord>
|
2025-12-25 11:20:56 +08:00
|
|
|
|
{
|
|
|
|
|
|
Success = true,
|
|
|
|
|
|
Data = list.ToArray(),
|
2025-12-27 22:05:50 +08:00
|
|
|
|
Total = (int)total
|
2025-12-25 11:20:56 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2025-12-27 22:05:50 +08:00
|
|
|
|
logger.LogError(ex, "获取交易记录列表失败,页码: {PageIndex}, 页大小: {PageSize}", pageIndex, pageSize);
|
2025-12-27 11:50:12 +08:00
|
|
|
|
return PagedResponse<TransactionRecord>.Fail($"获取交易记录列表失败: {ex.Message}");
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 12:22:37 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取待确认分类的交易记录列表
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
|
public async Task<BaseResponse<List<TransactionRecord>>> GetUnconfirmedListAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var list = await transactionRepository.GetUnconfirmedRecordsAsync();
|
|
|
|
|
|
return list.Ok();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取待确认分类交易列表失败");
|
|
|
|
|
|
return $"获取待确认分类交易列表失败: {ex.Message}".Fail<List<TransactionRecord>>();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 全部确认待确认的交易分类
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpPost]
|
2026-01-11 12:02:20 +08:00
|
|
|
|
public async Task<BaseResponse<int>> ConfirmAllUnconfirmedAsync([FromBody] ConfirmAllUnconfirmedRequestDto request)
|
2026-01-10 12:22:37 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-01-11 12:02:20 +08:00
|
|
|
|
var count = await transactionRepository.ConfirmAllUnconfirmedAsync(request.Ids);
|
2026-01-10 12:22:37 +08:00
|
|
|
|
return count.Ok();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "全部确认待确认分类失败");
|
|
|
|
|
|
return $"全部确认待确认分类失败: {ex.Message}".Fail<int>();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 11:20:56 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据ID获取交易记录详情
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet("{id}")]
|
2025-12-27 11:50:12 +08:00
|
|
|
|
public async Task<BaseResponse<TransactionRecord>> GetByIdAsync(long id)
|
2025-12-25 11:20:56 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var transaction = await transactionRepository.GetByIdAsync(id);
|
|
|
|
|
|
if (transaction == null)
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return "交易记录不存在".Fail<TransactionRecord>();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return transaction.Ok();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取交易记录详情失败,交易ID: {TransactionId}", id);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"获取交易记录详情失败: {ex.Message}".Fail<TransactionRecord>();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据邮件ID获取交易记录列表
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet("{emailId}")]
|
2025-12-27 11:50:12 +08:00
|
|
|
|
public async Task<BaseResponse<List<TransactionRecord>>> GetByEmailIdAsync(long emailId)
|
2025-12-25 11:20:56 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var transactions = await transactionRepository.GetByEmailIdAsync(emailId);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return transactions.Ok();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取邮件交易记录失败,邮件ID: {EmailId}", emailId);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"获取邮件交易记录失败: {ex.Message}".Fail<List<TransactionRecord>>();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建交易记录
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpPost]
|
|
|
|
|
|
public async Task<BaseResponse> CreateAsync([FromBody] CreateTransactionDto dto)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 解析日期字符串
|
|
|
|
|
|
if (!DateTime.TryParse(dto.OccurredAt, out var occurredAt))
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return "交易时间格式不正确".Fail();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-27 11:50:12 +08:00
|
|
|
|
var transaction = new TransactionRecord
|
2025-12-25 11:20:56 +08:00
|
|
|
|
{
|
|
|
|
|
|
OccurredAt = occurredAt,
|
|
|
|
|
|
Reason = dto.Reason ?? string.Empty,
|
|
|
|
|
|
Amount = dto.Amount,
|
|
|
|
|
|
Type = dto.Type,
|
|
|
|
|
|
Classify = dto.Classify ?? string.Empty,
|
|
|
|
|
|
ImportFrom = "手动录入",
|
2026-01-01 14:43:43 +08:00
|
|
|
|
ImportNo = Guid.NewGuid().ToString("N"),
|
|
|
|
|
|
Card = "手动",
|
2025-12-25 11:20:56 +08:00
|
|
|
|
EmailMessageId = 0 // 手动录入的记录,EmailMessageId 设为 0
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var result = await transactionRepository.AddAsync(transaction);
|
|
|
|
|
|
if (result)
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return BaseResponse.Done();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
2026-01-18 22:04:56 +08:00
|
|
|
|
|
|
|
|
|
|
return "创建交易记录失败".Fail();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "创建交易记录失败,交易信息: {@TransactionDto}", dto);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"创建交易记录失败: {ex.Message}".Fail();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 更新交易记录
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpPost]
|
|
|
|
|
|
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateTransactionDto dto)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var transaction = await transactionRepository.GetByIdAsync(dto.Id);
|
|
|
|
|
|
if (transaction == null)
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return "交易记录不存在".Fail();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新可编辑字段
|
|
|
|
|
|
transaction.Reason = dto.Reason ?? string.Empty;
|
|
|
|
|
|
transaction.Amount = dto.Amount;
|
|
|
|
|
|
transaction.Balance = dto.Balance;
|
|
|
|
|
|
transaction.Type = dto.Type;
|
|
|
|
|
|
transaction.Classify = dto.Classify ?? string.Empty;
|
2026-01-11 11:21:13 +08:00
|
|
|
|
|
2026-01-10 12:22:37 +08:00
|
|
|
|
// 清除待确认状态
|
|
|
|
|
|
transaction.UnconfirmedClassify = null;
|
|
|
|
|
|
transaction.UnconfirmedType = null;
|
2025-12-25 11:20:56 +08:00
|
|
|
|
|
|
|
|
|
|
var success = await transactionRepository.UpdateAsync(transaction);
|
|
|
|
|
|
if (success)
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return BaseResponse.Done();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
2026-01-18 22:04:56 +08:00
|
|
|
|
|
|
|
|
|
|
return "更新交易记录失败".Fail();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "更新交易记录失败,交易ID: {TransactionId}, 交易信息: {@TransactionDto}", dto.Id, dto);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"更新交易记录失败: {ex.Message}".Fail();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 删除交易记录
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpPost]
|
|
|
|
|
|
public async Task<BaseResponse> DeleteByIdAsync([FromQuery] long id)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var success = await transactionRepository.DeleteAsync(id);
|
|
|
|
|
|
if (success)
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return BaseResponse.Done();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
2026-01-18 22:04:56 +08:00
|
|
|
|
|
|
|
|
|
|
return "删除交易记录失败,记录不存在".Fail();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "删除交易记录失败,交易ID: {TransactionId}", id);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"删除交易记录失败: {ex.Message}".Fail();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 16:09:38 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取累积余额统计数据(用于余额卡片图表)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
|
public async Task<BaseResponse<List<BalanceStatisticsDto>>> GetBalanceStatisticsAsync(
|
|
|
|
|
|
[FromQuery] int year,
|
|
|
|
|
|
[FromQuery] int month
|
|
|
|
|
|
)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 获取存款分类
|
|
|
|
|
|
var savingClassify = await configService.GetConfigByKeyAsync<string>("SavingsCategories");
|
|
|
|
|
|
|
|
|
|
|
|
// 获取每日统计数据
|
|
|
|
|
|
var statistics = await transactionRepository.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)
|
|
|
|
|
|
{
|
|
|
|
|
|
decimal 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>>();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 11:20:56 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取指定月份每天的消费统计
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
|
public async Task<BaseResponse<List<DailyStatisticsDto>>> GetDailyStatisticsAsync(
|
|
|
|
|
|
[FromQuery] int year,
|
|
|
|
|
|
[FromQuery] int month
|
|
|
|
|
|
)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-01-16 11:15:44 +08:00
|
|
|
|
// 获取存款分类
|
|
|
|
|
|
var savingClassify = await configService.GetConfigByKeyAsync<string>("SavingsCategories");
|
|
|
|
|
|
|
|
|
|
|
|
var statistics = await transactionRepository.GetDailyStatisticsAsync(year, month, savingClassify);
|
2026-01-15 22:10:57 +08:00
|
|
|
|
var result = statistics.Select(s => new DailyStatisticsDto(
|
2026-01-18 22:25:59 +08:00
|
|
|
|
s.Key,
|
|
|
|
|
|
s.Value.count,
|
|
|
|
|
|
s.Value.expense,
|
2026-01-15 22:10:57 +08:00
|
|
|
|
s.Value.income,
|
2026-01-16 11:15:44 +08:00
|
|
|
|
s.Value.saving
|
2026-01-15 22:10:57 +08:00
|
|
|
|
)).ToList();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return result.Ok();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取日历统计数据失败,年份: {Year}, 月份: {Month}", year, month);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"获取日历统计数据失败: {ex.Message}".Fail<List<DailyStatisticsDto>>();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 17:48:17 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取指定日期范围内的每日统计
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
|
public async Task<BaseResponse<List<DailyStatisticsDto>>> GetDailyStatisticsRangeAsync(
|
|
|
|
|
|
[FromQuery] DateTime startDate,
|
|
|
|
|
|
[FromQuery] DateTime endDate
|
|
|
|
|
|
)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 确保包含结束日期当天
|
|
|
|
|
|
var effectiveEndDate = endDate.Date.AddDays(1);
|
|
|
|
|
|
var effectiveStartDate = startDate.Date;
|
|
|
|
|
|
|
2026-01-16 11:15:44 +08:00
|
|
|
|
// 获取存款分类
|
|
|
|
|
|
var savingClassify = await configService.GetConfigByKeyAsync<string>("SavingsCategories");
|
|
|
|
|
|
|
|
|
|
|
|
var statistics = await transactionRepository.GetDailyStatisticsByRangeAsync(
|
2026-01-18 22:25:59 +08:00
|
|
|
|
effectiveStartDate,
|
2026-01-16 11:15:44 +08:00
|
|
|
|
effectiveEndDate,
|
|
|
|
|
|
savingClassify);
|
2026-01-15 22:10:57 +08:00
|
|
|
|
var result = statistics.Select(s => new DailyStatisticsDto(
|
2026-01-18 22:25:59 +08:00
|
|
|
|
s.Key,
|
|
|
|
|
|
s.Value.count,
|
|
|
|
|
|
s.Value.expense,
|
2026-01-15 22:10:57 +08:00
|
|
|
|
s.Value.income,
|
2026-01-21 16:09:38 +08:00
|
|
|
|
s.Value.saving
|
2026-01-15 22:10:57 +08:00
|
|
|
|
)).ToList();
|
2026-01-15 17:48:17 +08:00
|
|
|
|
|
|
|
|
|
|
return result.Ok();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取日历统计数据失败,开始: {StartDate}, 结束: {EndDate}", startDate, endDate);
|
|
|
|
|
|
return $"获取日历统计数据失败: {ex.Message}".Fail<List<DailyStatisticsDto>>();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 11:20:56 +08:00
|
|
|
|
/// <summary>
|
2025-12-26 17:13:57 +08:00
|
|
|
|
/// 获取月度统计数据
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
|
public async Task<BaseResponse<MonthlyStatistics>> GetMonthlyStatisticsAsync(
|
|
|
|
|
|
[FromQuery] int year,
|
|
|
|
|
|
[FromQuery] int month
|
|
|
|
|
|
)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var statistics = await transactionRepository.GetMonthlyStatisticsAsync(year, month);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return statistics.Ok();
|
2025-12-26 17:13:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取月度统计数据失败,年份: {Year}, 月份: {Month}", year, month);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"获取月度统计数据失败: {ex.Message}".Fail<MonthlyStatistics>();
|
2025-12-26 17:13:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取分类统计数据
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
|
public async Task<BaseResponse<List<CategoryStatistics>>> GetCategoryStatisticsAsync(
|
|
|
|
|
|
[FromQuery] int year,
|
|
|
|
|
|
[FromQuery] int month,
|
|
|
|
|
|
[FromQuery] TransactionType type
|
|
|
|
|
|
)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var statistics = await transactionRepository.GetCategoryStatisticsAsync(year, month, type);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return statistics.Ok();
|
2025-12-26 17:13:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取分类统计数据失败,年份: {Year}, 月份: {Month}, 类型: {Type}", year, month, type);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"获取分类统计数据失败: {ex.Message}".Fail<List<CategoryStatistics>>();
|
2025-12-26 17:13:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取趋势统计数据
|
2025-12-25 11:20:56 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
2025-12-26 17:13:57 +08:00
|
|
|
|
public async Task<BaseResponse<List<TrendStatistics>>> GetTrendStatisticsAsync(
|
|
|
|
|
|
[FromQuery] int startYear,
|
|
|
|
|
|
[FromQuery] int startMonth,
|
|
|
|
|
|
[FromQuery] int monthCount = 6
|
2025-12-25 11:20:56 +08:00
|
|
|
|
)
|
2025-12-26 17:13:57 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var statistics = await transactionRepository.GetTrendStatisticsAsync(startYear, startMonth, monthCount);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return statistics.Ok();
|
2025-12-26 17:13:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-01-18 22:25:59 +08:00
|
|
|
|
logger.LogError(ex, "获取趋势统计数据失败,开始年份: {Year}, 开始月份: {Month}, 月份数: {Count}", startYear, startMonth,
|
|
|
|
|
|
monthCount);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"获取趋势统计数据失败: {ex.Message}".Fail<List<TrendStatistics>>();
|
2025-12-26 17:13:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 智能分析账单(流式输出)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task AnalyzeBillAsync([FromBody] BillAnalysisRequest request)
|
|
|
|
|
|
{
|
|
|
|
|
|
Response.ContentType = "text/event-stream";
|
|
|
|
|
|
Response.Headers.Append("Cache-Control", "no-cache");
|
|
|
|
|
|
Response.Headers.Append("Connection", "keep-alive");
|
|
|
|
|
|
|
2025-12-31 11:10:10 +08:00
|
|
|
|
if (string.IsNullOrWhiteSpace(request.UserInput))
|
2025-12-26 17:13:57 +08:00
|
|
|
|
{
|
2025-12-31 11:10:10 +08:00
|
|
|
|
await WriteEventAsync("<div class='error-message'>请输入分析内容</div>");
|
|
|
|
|
|
return;
|
2025-12-26 17:13:57 +08:00
|
|
|
|
}
|
2025-12-31 11:10:10 +08:00
|
|
|
|
|
2026-01-18 22:25:59 +08:00
|
|
|
|
await smartHandleService.AnalyzeBillAsync(request.UserInput, async void (chunk) =>
|
2025-12-26 17:13:57 +08:00
|
|
|
|
{
|
2026-01-18 22:25:59 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await WriteEventAsync(chunk);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(e, "流式写入账单分析结果失败");
|
|
|
|
|
|
}
|
2025-12-31 11:10:10 +08:00
|
|
|
|
});
|
2025-12-26 17:13:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取指定日期的交易记录
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
2025-12-27 11:50:12 +08:00
|
|
|
|
public async Task<BaseResponse<List<TransactionRecord>>> GetByDateAsync([FromQuery] string date)
|
2025-12-25 11:20:56 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!DateTime.TryParse(date, out var targetDate))
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return "日期格式不正确".Fail<List<TransactionRecord>>();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当天的开始和结束时间
|
|
|
|
|
|
var startDate = targetDate.Date;
|
|
|
|
|
|
var endDate = startDate.AddDays(1);
|
|
|
|
|
|
|
|
|
|
|
|
var records = await transactionRepository.GetByDateRangeAsync(startDate, endDate);
|
|
|
|
|
|
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return records.Ok();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取指定日期的交易记录失败,日期: {Date}", date);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"获取指定日期的交易记录失败: {ex.Message}".Fail<List<TransactionRecord>>();
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 15:40:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取未分类的账单数量
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
|
public async Task<BaseResponse<int>> GetUnclassifiedCountAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var count = await transactionRepository.GetUnclassifiedCountAsync();
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return count.Ok();
|
2025-12-25 15:40:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取未分类账单数量失败");
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"获取未分类账单数量失败: {ex.Message}".Fail<int>();
|
2025-12-25 15:40:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取未分类的账单列表
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
2025-12-27 11:50:12 +08:00
|
|
|
|
public async Task<BaseResponse<List<TransactionRecord>>> GetUnclassifiedAsync([FromQuery] int pageSize = 10)
|
2025-12-25 15:40:50 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var records = await transactionRepository.GetUnclassifiedAsync(pageSize);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return records.Ok();
|
2025-12-25 15:40:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取未分类账单列表失败");
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"获取未分类账单列表失败: {ex.Message}".Fail<List<TransactionRecord>>();
|
2025-12-25 15:40:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 智能分类 - 使用AI对账单进行分类(流式响应)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpPost]
|
|
|
|
|
|
public async Task SmartClassifyAsync([FromBody] SmartClassifyRequest request)
|
|
|
|
|
|
{
|
|
|
|
|
|
Response.ContentType = "text/event-stream";
|
|
|
|
|
|
Response.Headers.Append("Cache-Control", "no-cache");
|
|
|
|
|
|
Response.Headers.Append("Connection", "keep-alive");
|
|
|
|
|
|
|
2025-12-30 18:49:46 +08:00
|
|
|
|
// 验证账单ID列表
|
|
|
|
|
|
if (request.TransactionIds == null || request.TransactionIds.Count == 0)
|
2025-12-25 15:40:50 +08:00
|
|
|
|
{
|
2025-12-30 18:49:46 +08:00
|
|
|
|
await WriteEventAsync("error", "请提供要分类的账单ID");
|
|
|
|
|
|
return;
|
2025-12-25 15:40:50 +08:00
|
|
|
|
}
|
2025-12-30 18:49:46 +08:00
|
|
|
|
|
2026-01-18 22:25:59 +08:00
|
|
|
|
await smartHandleService.SmartClassifyAsync(request.TransactionIds.ToArray(), async void (chunk) =>
|
2025-12-25 15:40:50 +08:00
|
|
|
|
{
|
2026-01-18 22:25:59 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var (eventType, content) = chunk;
|
|
|
|
|
|
await TrySetUnconfirmedAsync(eventType, content);
|
|
|
|
|
|
await WriteEventAsync(eventType, content);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(e, "流式写入智能分类结果失败");
|
|
|
|
|
|
}
|
2025-12-30 18:49:46 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await Response.Body.FlushAsync();
|
2025-12-25 15:40:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-11 11:21:13 +08:00
|
|
|
|
private async Task TrySetUnconfirmedAsync(string eventType, string content)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (eventType != "data")
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var jsonObject = JsonSerializer.Deserialize<JsonObject>(content);
|
|
|
|
|
|
var id = jsonObject?["id"]?.GetValue<long>() ?? 0;
|
|
|
|
|
|
var classify = jsonObject?["Classify"]?.GetValue<string>() ?? string.Empty;
|
|
|
|
|
|
var typeValue = jsonObject?["Type"]?.GetValue<int>() ?? -1;
|
|
|
|
|
|
|
2026-01-18 22:25:59 +08:00
|
|
|
|
if (id == 0 || typeValue == -1 || string.IsNullOrEmpty(classify))
|
2026-01-11 11:21:13 +08:00
|
|
|
|
{
|
|
|
|
|
|
logger.LogWarning("解析智能分类结果时,发现无效数据,内容: {Content}", content);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var record = await transactionRepository.GetByIdAsync(id);
|
|
|
|
|
|
if (record == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogWarning("解析智能分类结果时,未找到对应的交易记录,ID: {Id}", id);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
record.UnconfirmedClassify = classify;
|
|
|
|
|
|
record.UnconfirmedType = (TransactionType)typeValue;
|
|
|
|
|
|
|
|
|
|
|
|
var success = await transactionRepository.UpdateAsync(record);
|
|
|
|
|
|
|
|
|
|
|
|
if (!success)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogWarning("解析智能分类结果时,更新交易记录失败,ID: {Id}", id);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "解析智能分类结果失败,内容: {Content}", content);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 15:40:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 批量更新账单分类
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[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;
|
2025-12-26 15:21:31 +08:00
|
|
|
|
// 如果提供了Type,也更新Type
|
|
|
|
|
|
if (item.Type.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
record.Type = item.Type.Value;
|
|
|
|
|
|
}
|
2026-01-18 22:25:59 +08:00
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(record.Classify))
|
2026-01-11 12:02:20 +08:00
|
|
|
|
{
|
|
|
|
|
|
record.UnconfirmedClassify = null;
|
|
|
|
|
|
}
|
2026-01-18 22:25:59 +08:00
|
|
|
|
|
|
|
|
|
|
if (record.Type == item.Type)
|
2026-01-11 12:02:20 +08:00
|
|
|
|
{
|
|
|
|
|
|
record.UnconfirmedType = TransactionType.None;
|
|
|
|
|
|
}
|
2026-01-18 22:25:59 +08:00
|
|
|
|
|
2025-12-25 15:40:50 +08:00
|
|
|
|
var success = await transactionRepository.UpdateAsync(record);
|
|
|
|
|
|
if (success)
|
|
|
|
|
|
successCount++;
|
|
|
|
|
|
else
|
|
|
|
|
|
failCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
failCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"批量更新完成,成功 {successCount} 条,失败 {failCount} 条".Ok();
|
2025-12-25 15:40:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "批量更新分类失败");
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"批量更新分类失败: {ex.Message}".Fail();
|
2025-12-25 15:40:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:21:31 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取按交易摘要分组的统计信息(支持分页)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
2025-12-27 11:50:12 +08:00
|
|
|
|
public async Task<PagedResponse<ReasonGroupDto>> GetReasonGroupsAsync(
|
2025-12-26 15:21:31 +08:00
|
|
|
|
[FromQuery] int pageIndex = 1,
|
|
|
|
|
|
[FromQuery] int pageSize = 20)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var (list, total) = await transactionRepository.GetReasonGroupsAsync(pageIndex, pageSize);
|
2025-12-27 11:50:12 +08:00
|
|
|
|
return new PagedResponse<ReasonGroupDto>
|
2025-12-26 15:21:31 +08:00
|
|
|
|
{
|
|
|
|
|
|
Success = true,
|
|
|
|
|
|
Data = list.ToArray(),
|
|
|
|
|
|
Total = total
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取交易摘要分组失败");
|
2025-12-27 11:50:12 +08:00
|
|
|
|
return PagedResponse<ReasonGroupDto>.Fail($"获取交易摘要分组失败: {ex.Message}");
|
2025-12-26 15:21:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 按摘要批量更新分类
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpPost]
|
|
|
|
|
|
public async Task<BaseResponse<int>> BatchUpdateByReasonAsync([FromBody] BatchUpdateByReasonDto dto)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var count = await transactionRepository.BatchUpdateByReasonAsync(dto.Reason, dto.Type, dto.Classify);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return count.Ok($"成功更新 {count} 条记录");
|
2025-12-26 15:21:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "按摘要批量更新分类失败,摘要: {Reason}", dto.Reason);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"按摘要批量更新分类失败: {ex.Message}".Fail<int>();
|
2025-12-26 15:21:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-01 14:43:43 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 一句话录账解析
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpPost]
|
|
|
|
|
|
public async Task<BaseResponse<TransactionParseResult>> ParseOneLine([FromBody] ParseOneLineRequestDto request)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(request.Text))
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return "请求参数缺失:text".Fail<TransactionParseResult>();
|
2026-01-01 14:43:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = await smartHandleService.ParseOneLineBillAsync(request.Text);
|
|
|
|
|
|
|
|
|
|
|
|
if (result == null)
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return "AI解析失败".Fail<TransactionParseResult>();
|
2026-01-01 14:43:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return result.Ok();
|
2026-01-01 14:43:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "一句话录账解析失败,文本: {Text}", request.Text);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return ("AI解析失败: " + ex.Message).Fail<TransactionParseResult>();
|
2026-01-01 14:43:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取抵账候选列表
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet("{id}")]
|
|
|
|
|
|
public async Task<BaseResponse<TransactionRecord[]>> GetCandidatesForOffset(long id)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var current = await transactionRepository.GetByIdAsync(id);
|
|
|
|
|
|
if (current == null)
|
|
|
|
|
|
{
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return ((TransactionRecord[])[]).Ok();
|
2026-01-01 14:43:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var list = await transactionRepository.GetCandidatesForOffsetAsync(id, current.Amount, current.Type);
|
|
|
|
|
|
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return list.ToArray().Ok();
|
2026-01-01 14:43:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
logger.LogError(ex, "获取抵账候选列表失败,交易ID: {TransactionId}", id);
|
2026-01-04 16:43:32 +08:00
|
|
|
|
return $"获取抵账候选列表失败: {ex.Message}".Fail<TransactionRecord[]>();
|
2026-01-01 14:43:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 抵账(删除两笔交易)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpPost]
|
|
|
|
|
|
public async Task<IActionResult> OffsetTransactions([FromBody] OffsetTransactionDto dto)
|
|
|
|
|
|
{
|
|
|
|
|
|
var t1 = await transactionRepository.GetByIdAsync(dto.Id1);
|
|
|
|
|
|
var t2 = await transactionRepository.GetByIdAsync(dto.Id2);
|
|
|
|
|
|
|
|
|
|
|
|
if (t1 == null || t2 == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return NotFound("交易记录不存在");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await transactionRepository.DeleteAsync(dto.Id1);
|
|
|
|
|
|
await transactionRepository.DeleteAsync(dto.Id2);
|
|
|
|
|
|
|
|
|
|
|
|
return Ok(new { success = true, message = "抵账成功" });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 15:40:50 +08:00
|
|
|
|
private async Task WriteEventAsync(string eventType, string data)
|
|
|
|
|
|
{
|
|
|
|
|
|
var message = $"event: {eventType}\ndata: {data}\n\n";
|
|
|
|
|
|
await Response.WriteAsync(message);
|
|
|
|
|
|
await Response.Body.FlushAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-31 11:10:10 +08:00
|
|
|
|
private async Task WriteEventAsync(string data)
|
2025-12-29 20:30:15 +08:00
|
|
|
|
{
|
2025-12-31 11:10:10 +08:00
|
|
|
|
var message = $"data: {data}\n\n";
|
|
|
|
|
|
await Response.WriteAsync(message);
|
|
|
|
|
|
await Response.Body.FlushAsync();
|
2025-12-29 20:30:15 +08:00
|
|
|
|
}
|
2025-12-25 11:20:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建交易记录DTO
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public record CreateTransactionDto(
|
|
|
|
|
|
string OccurredAt,
|
|
|
|
|
|
string? Reason,
|
|
|
|
|
|
decimal Amount,
|
|
|
|
|
|
TransactionType Type,
|
2025-12-26 15:21:31 +08:00
|
|
|
|
string? Classify
|
2026-01-18 22:25:59 +08:00
|
|
|
|
);
|
2025-12-25 11:20:56 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 更新交易记录DTO
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public record UpdateTransactionDto(
|
|
|
|
|
|
long Id,
|
|
|
|
|
|
string? Reason,
|
|
|
|
|
|
decimal Amount,
|
|
|
|
|
|
decimal Balance,
|
|
|
|
|
|
TransactionType Type,
|
2025-12-26 15:21:31 +08:00
|
|
|
|
string? Classify
|
2026-01-18 22:25:59 +08:00
|
|
|
|
);
|
2025-12-25 11:20:56 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 日历统计响应DTO
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public record DailyStatisticsDto(
|
|
|
|
|
|
string Date,
|
|
|
|
|
|
int Count,
|
2026-01-15 22:10:57 +08:00
|
|
|
|
decimal Expense,
|
|
|
|
|
|
decimal Income,
|
|
|
|
|
|
decimal Balance
|
2026-01-18 22:25:59 +08:00
|
|
|
|
);
|
2025-12-25 15:40:50 +08:00
|
|
|
|
|
2026-01-21 16:09:38 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 累积余额统计DTO
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public record BalanceStatisticsDto(
|
|
|
|
|
|
string Date,
|
|
|
|
|
|
decimal CumulativeBalance
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-12-25 15:40:50 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 智能分类请求DTO
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public record SmartClassifyRequest(
|
2025-12-26 15:21:31 +08:00
|
|
|
|
List<long>? TransactionIds = null
|
2026-01-18 22:25:59 +08:00
|
|
|
|
);
|
2025-12-25 15:40:50 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 批量更新分类项DTO
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public record BatchUpdateClassifyItem(
|
|
|
|
|
|
long Id,
|
|
|
|
|
|
string? Classify,
|
2025-12-26 15:21:31 +08:00
|
|
|
|
TransactionType? Type = null
|
2026-01-18 22:25:59 +08:00
|
|
|
|
);
|
2025-12-26 15:21:31 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 按摘要批量更新DTO
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public record BatchUpdateByReasonDto(
|
|
|
|
|
|
string Reason,
|
|
|
|
|
|
TransactionType Type,
|
|
|
|
|
|
string Classify
|
2026-01-18 22:25:59 +08:00
|
|
|
|
);
|
2025-12-26 15:21:31 +08:00
|
|
|
|
|
2025-12-26 17:13:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 账单分析请求DTO
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public record BillAnalysisRequest(
|
|
|
|
|
|
string UserInput
|
2026-01-18 22:25:59 +08:00
|
|
|
|
);
|
2026-01-01 14:43:43 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 抵账请求DTO
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public record OffsetTransactionDto(
|
|
|
|
|
|
long Id1,
|
|
|
|
|
|
long Id2
|
2026-01-18 22:25:59 +08:00
|
|
|
|
);
|
2026-01-01 14:43:43 +08:00
|
|
|
|
|
|
|
|
|
|
public record ParseOneLineRequestDto(
|
|
|
|
|
|
string Text
|
2026-01-18 22:25:59 +08:00
|
|
|
|
);
|
2026-01-11 12:02:20 +08:00
|
|
|
|
|
|
|
|
|
|
public record ConfirmAllUnconfirmedRequestDto(
|
|
|
|
|
|
long[] Ids
|
2026-01-18 22:25:59 +08:00
|
|
|
|
);
|