namespace Repository; public interface ITransactionRecordRepository : IBaseRepository { Task ExistsByEmailMessageIdAsync(long emailMessageId, DateTime occurredAt); Task ExistsByImportNoAsync(string importNo, string importFrom); /// /// 分页获取交易记录列表(游标分页) /// /// 上一页最后一条记录的发生时间 /// 上一页最后一条记录的ID /// 每页数量 /// 搜索关键词(搜索交易摘要和分类) /// 交易记录列表、最后发生时间和最后ID Task<(List list, DateTime? lastOccurredAt, long lastId)> GetPagedListAsync(DateTime? lastOccurredAt, long? lastId, int pageSize = 20, string? searchKeyword = null); /// /// 获取总数 /// Task GetTotalCountAsync(); /// /// 获取所有不同的交易分类 /// Task> GetDistinctClassifyAsync(); /// /// 获取指定月份每天的消费统计 /// /// 年份 /// 月份 /// 每天的消费笔数和金额 Task> GetDailyStatisticsAsync(int year, int month); /// /// 获取指定日期范围内的交易记录 /// /// 开始日期 /// 结束日期 /// 交易记录列表 Task> GetByDateRangeAsync(DateTime startDate, DateTime endDate); /// /// 获取指定邮件的交易记录数量 /// /// 邮件ID /// 交易记录数量 Task GetCountByEmailIdAsync(long emailMessageId); /// /// 获取指定邮件的交易记录列表 /// /// 邮件ID /// 交易记录列表 Task> GetByEmailIdAsync(long emailMessageId); /// /// 获取未分类的账单数量 /// /// 未分类账单数量 Task GetUnclassifiedCountAsync(); /// /// 获取未分类的账单列表 /// /// 每页数量 /// 未分类账单列表 Task> GetUnclassifiedAsync(int pageSize = 10); /// /// 获取按交易摘要(Reason)分组的统计信息(支持分页) /// /// 页码,从1开始 /// 每页数量 /// 分组统计列表和总数 Task<(List list, int total)> GetReasonGroupsAsync(int pageIndex = 1, int pageSize = 20); /// /// 按摘要批量更新交易记录的分类 /// /// 交易摘要 /// 交易类型 /// 分类名称 /// 更新的记录数量 Task BatchUpdateByReasonAsync(string reason, TransactionType type, string classify); /// /// 根据关键词查询交易记录(模糊匹配Reason字段) /// /// 关键词 /// 匹配的交易记录列表 Task> QueryBySqlAsync(string sql); } public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository(freeSql), ITransactionRecordRepository { public async Task ExistsByEmailMessageIdAsync(long emailMessageId, DateTime occurredAt) { return await FreeSql.Select() .Where(t => t.EmailMessageId == emailMessageId && t.OccurredAt == occurredAt) .FirstAsync(); } public async Task ExistsByImportNoAsync(string importNo, string importFrom) { return await FreeSql.Select() .Where(t => t.ImportNo == importNo && t.ImportFrom == importFrom) .FirstAsync(); } public async Task<(List list, DateTime? lastOccurredAt, long lastId)> GetPagedListAsync(DateTime? lastOccurredAt, long? lastId, int pageSize = 20, string? searchKeyword = null) { var query = FreeSql.Select(); // 如果提供了搜索关键词,则添加搜索条件 if (!string.IsNullOrWhiteSpace(searchKeyword)) { query = query.Where(t => t.Reason.Contains(searchKeyword) || t.Classify.Contains(searchKeyword) || t.Card.Contains(searchKeyword) || t.ImportFrom.Contains(searchKeyword)); } // 如果提供了游标,则获取小于游标位置的记录 if (lastOccurredAt.HasValue && lastId.HasValue) { query = query.Where(t => t.OccurredAt < lastOccurredAt.Value || (t.OccurredAt == lastOccurredAt.Value && t.Id < lastId.Value)); } var list = await query .OrderByDescending(t => t.OccurredAt) .OrderByDescending(t => t.Id) .Page(1, pageSize) .ToListAsync(); var lastRecord = list.Count > 0 ? list.Last() : null; return (list, lastRecord?.OccurredAt, lastRecord?.Id ?? 0); } public async Task GetTotalCountAsync() { return await FreeSql.Select().CountAsync(); } public async Task> GetDistinctClassifyAsync() { return await FreeSql.Select() .Where(t => !string.IsNullOrEmpty(t.Classify)) .Distinct() .ToListAsync(t => t.Classify); } public async Task> GetDailyStatisticsAsync(int year, int month) { var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1); var records = await FreeSql.Select() .Where(t => t.OccurredAt >= startDate && t.OccurredAt < endDate) .Where(t => t.Type == TransactionType.Expense || t.Type == TransactionType.Income) // 统计消费和收入 .ToListAsync(); var statistics = records .GroupBy(t => t.OccurredAt.ToString("yyyy-MM-dd")) .ToDictionary( g => g.Key, g => { // 分别统计收入和支出 var income = g.Where(t => t.Type == TransactionType.Income).Sum(t => t.Amount); var expense = g.Where(t => t.Type == TransactionType.Expense).Sum(t => t.Amount); // 净额 = 收入 - 支出(消费大于收入时为负数) var netAmount = income - expense; return (count: g.Count(), amount: netAmount); } ); return statistics; } public async Task> GetByDateRangeAsync(DateTime startDate, DateTime endDate) { return await FreeSql.Select() .Where(t => t.OccurredAt >= startDate && t.OccurredAt <= endDate) .OrderBy(t => t.OccurredAt) .ToListAsync(); } public async Task GetCountByEmailIdAsync(long emailMessageId) { return (int)await FreeSql.Select() .Where(t => t.EmailMessageId == emailMessageId) .CountAsync(); } public async Task> GetByEmailIdAsync(long emailMessageId) { return await FreeSql.Select() .Where(t => t.EmailMessageId == emailMessageId) .OrderBy(t => t.OccurredAt) .ToListAsync(); } public async Task GetUnclassifiedCountAsync() { return (int)await FreeSql.Select() .Where(t => string.IsNullOrEmpty(t.Classify)) .CountAsync(); } public async Task> GetUnclassifiedAsync(int pageSize = 10) { return await FreeSql.Select() .Where(t => string.IsNullOrEmpty(t.Classify)) .OrderByDescending(t => t.OccurredAt) .Page(1, pageSize) .ToListAsync(); } public async Task<(List list, int total)> GetReasonGroupsAsync(int pageIndex = 1, int pageSize = 20) { // 先按照Reason分组,统计每个Reason的数量 var groups = await FreeSql.Select() .Where(t => !string.IsNullOrEmpty(t.Reason)) .Where(t => string.IsNullOrEmpty(t.Classify)) // 只统计未分类的 .GroupBy(t => t.Reason) .ToListAsync(g => new { Reason = g.Key, Count = g.Count() }); // 按数量降序排序 var sortedGroups = groups.OrderByDescending(g => g.Count).ToList(); var total = sortedGroups.Count; // 分页 var pagedGroups = sortedGroups .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToList(); // 为每个分组获取示例记录 var result = new List(); foreach (var group in pagedGroups) { var sample = await FreeSql.Select() .Where(t => t.Reason == group.Reason) .FirstAsync(); if (sample != null) { result.Add(new ReasonGroupDto { Reason = group.Reason, Count = (int)group.Count, SampleType = sample.Type, SampleClassify = sample.Classify ?? string.Empty }); } } return (result, total); } public async Task BatchUpdateByReasonAsync(string reason, TransactionType type, string classify) { return await FreeSql.Update() .Set(t => t.Type, type) .Set(t => t.Classify, classify) .Where(t => t.Reason == reason) .ExecuteAffrowsAsync(); } public async Task> QueryBySqlAsync(string sql) { return await FreeSql.Select() .Where(sql) .OrderByDescending(t => t.OccurredAt) .ToListAsync(); } } /// /// 按Reason分组统计DTO /// public class ReasonGroupDto { /// /// 交易摘要 /// public string Reason { get; set; } = string.Empty; /// /// 该摘要的记录数量 /// public int Count { get; set; } /// /// 示例交易类型(该分组中第一条记录的类型) /// public TransactionType SampleType { get; set; } /// /// 示例分类(该分组中第一条记录的分类) /// public string SampleClassify { get; set; } = string.Empty; }