Files
EmailBill/WebApi/Controllers/TransactionRecordController.cs
孙诚 b5d0524868
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 26s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
feat: 更新交易记录和预算服务,支持按分类和日期范围筛选
2026-01-09 16:21:03 +08:00

682 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace WebApi.Controllers;
using Repository;
[ApiController]
[Route("api/[controller]/[action]")]
public class TransactionRecordController(
ITransactionRecordRepository transactionRepository,
ISmartHandleService smartHandleService,
ILogger<TransactionRecordController> logger
) : ControllerBase
{
/// <summary>
/// 获取交易记录列表(分页)
/// </summary>
[HttpGet]
public async Task<PagedResponse<TransactionRecord>> GetListAsync(
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? searchKeyword = null,
[FromQuery] string? classify = null,
[FromQuery] int? type = null,
[FromQuery] int? year = null,
[FromQuery] int? month = null,
[FromQuery] DateTime? startDate = null,
[FromQuery] DateTime? endDate = null,
[FromQuery] string? reason = null,
[FromQuery] bool sortByAmount = false
)
{
try
{
string[]? classifies = string.IsNullOrWhiteSpace(classify)
? null
: classify.Split(',', StringSplitOptions.RemoveEmptyEntries);
TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null;
var list = await transactionRepository.GetPagedListAsync(
pageIndex,
pageSize,
searchKeyword,
classifies,
transactionType,
year,
month,
startDate,
endDate,
reason,
sortByAmount);
var total = await transactionRepository.GetTotalCountAsync(
searchKeyword,
classifies,
transactionType,
year,
month,
startDate,
endDate,
reason);
return new PagedResponse<TransactionRecord>
{
Success = true,
Data = list.ToArray(),
Total = (int)total
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取交易记录列表失败,页码: {PageIndex}, 页大小: {PageSize}", pageIndex, pageSize);
return PagedResponse<TransactionRecord>.Fail($"获取交易记录列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据ID获取交易记录详情
/// </summary>
[HttpGet("{id}")]
public async Task<BaseResponse<TransactionRecord>> 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>();
}
}
/// <summary>
/// 根据邮件ID获取交易记录列表
/// </summary>
[HttpGet("{emailId}")]
public async Task<BaseResponse<List<TransactionRecord>>> 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>>();
}
}
/// <summary>
/// 创建交易记录
/// </summary>
[HttpPost]
public async Task<BaseResponse> CreateAsync([FromBody] CreateTransactionDto dto)
{
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();
}
else
{
return "创建交易记录失败".Fail();
}
}
catch (Exception ex)
{
logger.LogError(ex, "创建交易记录失败,交易信息: {@TransactionDto}", dto);
return $"创建交易记录失败: {ex.Message}".Fail();
}
}
/// <summary>
/// 更新交易记录
/// </summary>
[HttpPost]
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateTransactionDto dto)
{
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;
var success = await transactionRepository.UpdateAsync(transaction);
if (success)
{
return BaseResponse.Done();
}
else
{
return "更新交易记录失败".Fail();
}
}
catch (Exception ex)
{
logger.LogError(ex, "更新交易记录失败交易ID: {TransactionId}, 交易信息: {@TransactionDto}", dto.Id, dto);
return $"更新交易记录失败: {ex.Message}".Fail();
}
}
/// <summary>
/// 删除交易记录
/// </summary>
[HttpPost]
public async Task<BaseResponse> DeleteByIdAsync([FromQuery] long id)
{
try
{
var success = await transactionRepository.DeleteAsync(id);
if (success)
{
return BaseResponse.Done();
}
else
{
return "删除交易记录失败,记录不存在".Fail();
}
}
catch (Exception ex)
{
logger.LogError(ex, "删除交易记录失败交易ID: {TransactionId}", id);
return $"删除交易记录失败: {ex.Message}".Fail();
}
}
/// <summary>
/// 获取指定月份每天的消费统计
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<DailyStatisticsDto>>> GetDailyStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month
)
{
try
{
var statistics = await transactionRepository.GetDailyStatisticsAsync(year, month);
var result = statistics.Select(s => new DailyStatisticsDto(s.Key, s.Value.count, s.Value.amount)).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<MonthlyStatistics>> GetMonthlyStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month
)
{
try
{
var statistics = await transactionRepository.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] TransactionType type
)
{
try
{
var statistics = await transactionRepository.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<TrendStatistics>>> GetTrendStatisticsAsync(
[FromQuery] int startYear,
[FromQuery] int startMonth,
[FromQuery] int monthCount = 6
)
{
try
{
var statistics = await transactionRepository.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>>();
}
}
/// <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");
if (string.IsNullOrWhiteSpace(request.UserInput))
{
await WriteEventAsync("<div class='error-message'>请输入分析内容</div>");
return;
}
await smartHandleService.AnalyzeBillAsync(request.UserInput, async (chunk) =>
{
await WriteEventAsync(chunk);
});
}
/// <summary>
/// 获取指定日期的交易记录
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TransactionRecord>>> 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.GetByDateRangeAsync(startDate, endDate);
return records.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取指定日期的交易记录失败,日期: {Date}", date);
return $"获取指定日期的交易记录失败: {ex.Message}".Fail<List<TransactionRecord>>();
}
}
/// <summary>
/// 获取未分类的账单数量
/// </summary>
[HttpGet]
public async Task<BaseResponse<int>> GetUnclassifiedCountAsync()
{
try
{
var count = await transactionRepository.GetUnclassifiedCountAsync();
return count.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取未分类账单数量失败");
return $"获取未分类账单数量失败: {ex.Message}".Fail<int>();
}
}
/// <summary>
/// 获取未分类的账单列表
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TransactionRecord>>> 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>>();
}
}
/// <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");
// 验证账单ID列表
if (request.TransactionIds == null || request.TransactionIds.Count == 0)
{
await WriteEventAsync("error", "请提供要分类的账单ID");
return;
}
await smartHandleService.SmartClassifyAsync(request.TransactionIds.ToArray(), async (chunk) =>
{
var (eventType, content) = chunk;
await WriteEventAsync(eventType, content);
});
await Response.Body.FlushAsync();
}
/// <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;
// 如果提供了Type也更新Type
if (item.Type.HasValue)
{
record.Type = item.Type.Value;
}
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();
}
}
/// <summary>
/// 获取按交易摘要分组的统计信息(支持分页)
/// </summary>
[HttpGet]
public async Task<PagedResponse<ReasonGroupDto>> GetReasonGroupsAsync(
[FromQuery] int pageIndex = 1,
[FromQuery] int pageSize = 20)
{
try
{
var (list, total) = await transactionRepository.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}");
}
}
/// <summary>
/// 按摘要批量更新分类
/// </summary>
[HttpPost]
public async Task<BaseResponse<int>> BatchUpdateByReasonAsync([FromBody] BatchUpdateByReasonDto dto)
{
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>();
}
}
/// <summary>
/// 一句话录账解析
/// </summary>
[HttpPost]
public async Task<BaseResponse<TransactionParseResult>> ParseOneLine([FromBody] ParseOneLineRequestDto 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>();
}
}
/// <summary>
/// 获取抵账候选列表
/// </summary>
[HttpGet("{id}")]
public async Task<BaseResponse<TransactionRecord[]>> GetCandidatesForOffset(long id)
{
try
{
var current = await transactionRepository.GetByIdAsync(id);
if (current == null)
{
return ((TransactionRecord[])[]).Ok();
}
var list = await transactionRepository.GetCandidatesForOffsetAsync(id, current.Amount, current.Type);
return list.ToArray().Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取抵账候选列表失败交易ID: {TransactionId}", id);
return $"获取抵账候选列表失败: {ex.Message}".Fail<TransactionRecord[]>();
}
}
/// <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 = "抵账成功" });
}
private async Task WriteEventAsync(string eventType, string data)
{
var message = $"event: {eventType}\ndata: {data}\n\n";
await Response.WriteAsync(message);
await Response.Body.FlushAsync();
}
private async Task WriteEventAsync(string data)
{
var message = $"data: {data}\n\n";
await Response.WriteAsync(message);
await Response.Body.FlushAsync();
}
private static string GetTypeName(TransactionType type)
{
return type switch
{
TransactionType.Expense => "支出",
TransactionType.Income => "收入",
TransactionType.None => "不计入收支",
_ => "未知"
};
}
}
/// <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
);
/// <summary>
/// 日历统计响应DTO
/// </summary>
public record DailyStatisticsDto(
string Date,
int Count,
decimal Amount
);
/// <summary>
/// 智能分类请求DTO
/// </summary>
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
);
/// <summary>
/// 抵账请求DTO
/// </summary>
public record OffsetTransactionDto(
long Id1,
long Id2
);
public record ParseOneLineRequestDto(
string Text
);