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, string? classify = null, TransactionType? type = null, int? year = null, int? month = null); /// /// 获取总数 /// Task GetTotalCountAsync(); /// /// 获取所有不同的交易分类 /// Task> GetDistinctClassifyAsync(); /// /// 获取指定月份每天的消费统计 /// /// 年份 /// 月份 /// 每天的消费笔数和金额 Task> GetDailyStatisticsAsync(int year, int month); /// /// 获取指定日期范围内的交易记录 /// /// 开始日期 /// 结束日期 /// 交易记录列表 Task> GetByDateRangeAsync(DateTime startDate, DateTime endDate); /// /// 获取指定邮件的交易记录数量 /// /// 邮件ID /// 交易记录数量 Task GetCountByEmailIdAsync(long emailMessageId); /// /// 获取月度统计数据 /// /// 年份 /// 月份 /// 月度统计数据 Task GetMonthlyStatisticsAsync(int year, int month); /// /// 获取分类统计数据 /// /// 年份 /// 月份 /// 交易类型(0:支出, 1:收入) /// 分类统计列表 Task> GetCategoryStatisticsAsync(int year, int month, TransactionType type); /// /// 获取多个月的趋势统计数据 /// /// 开始年份 /// 开始月份 /// 月份数量 /// 趋势统计列表 Task> GetTrendStatisticsAsync(int startYear, int startMonth, int monthCount); /// /// 获取指定邮件的交易记录列表 /// /// 邮件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> QueryByWhereAsync(string sql); /// /// 执行完整的SQL查询 /// /// 完整的SELECT SQL语句 /// 查询结果列表 Task> ExecuteRawSqlAsync(string completeSql); /// /// 执行动态SQL查询,返回动态对象 /// /// 完整的SELECT SQL语句 /// 动态查询结果列表 Task> ExecuteDynamicSqlAsync(string completeSql); } 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, string? classify = null, TransactionType? type = null, int? year = null, int? month = 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 (!string.IsNullOrWhiteSpace(classify)) { query = query.Where(t => t.Classify == classify); } // 按交易类型筛选 if (type.HasValue) { query = query.Where(t => t.Type == type.Value); } // 按年月筛选 if (year.HasValue && month.HasValue) { var startDate = new DateTime(year.Value, month.Value, 1); var endDate = startDate.AddMonths(1); query = query.Where(t => t.OccurredAt >= startDate && t.OccurredAt < endDate); } // 如果提供了游标,则获取小于游标位置的记录 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) .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> QueryByWhereAsync(string sql) { return await FreeSql.Select() .Where(sql) .OrderByDescending(t => t.OccurredAt) .ToListAsync(); } public async Task> ExecuteRawSqlAsync(string completeSql) { return await FreeSql.Ado.QueryAsync(completeSql); } public async Task> ExecuteDynamicSqlAsync(string completeSql) { var dt = await FreeSql.Ado.ExecuteDataTableAsync(completeSql); var result = new List(); foreach (System.Data.DataRow row in dt.Rows) { var expando = new System.Dynamic.ExpandoObject() as IDictionary; foreach (System.Data.DataColumn column in dt.Columns) { expando[column.ColumnName] = row[column]; } result.Add(expando); } return result; } public async Task GetMonthlyStatisticsAsync(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) .ToListAsync(); var statistics = new MonthlyStatistics { Year = year, Month = month }; foreach (var record in records) { var amount = Math.Abs(record.Amount); if (record.Type == TransactionType.Expense) { statistics.TotalExpense += amount; statistics.ExpenseCount++; if (amount > statistics.MaxExpense) { statistics.MaxExpense = amount; } } else if (record.Type == TransactionType.Income) { statistics.TotalIncome += amount; statistics.IncomeCount++; if (amount > statistics.MaxIncome) { statistics.MaxIncome = amount; } } } statistics.Balance = statistics.TotalIncome - statistics.TotalExpense; statistics.TotalCount = records.Count; return statistics; } public async Task> GetCategoryStatisticsAsync(int year, int month, TransactionType type) { 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 && t.Type == type) .ToListAsync(); var categoryGroups = records .GroupBy(t => t.Classify ?? "未分类") .Select(g => new CategoryStatistics { Classify = g.Key, Amount = g.Sum(t => Math.Abs(t.Amount)), Count = g.Count() }) .OrderByDescending(c => c.Amount) .ToList(); // 计算百分比 var total = categoryGroups.Sum(c => c.Amount); if (total > 0) { foreach (var category in categoryGroups) { category.Percent = Math.Round((category.Amount / total) * 100, 1); } } return categoryGroups; } public async Task> GetTrendStatisticsAsync(int startYear, int startMonth, int monthCount) { var trends = new List(); for (int i = 0; i < monthCount; i++) { var targetYear = startYear; var targetMonth = startMonth + i; // 处理月份溢出 while (targetMonth > 12) { targetMonth -= 12; targetYear++; } var startDate = new DateTime(targetYear, targetMonth, 1); var endDate = startDate.AddMonths(1); var records = await FreeSql.Select() .Where(t => t.OccurredAt >= startDate && t.OccurredAt < endDate) .ToListAsync(); var expense = records.Where(t => t.Type == TransactionType.Expense).Sum(t => Math.Abs(t.Amount)); var income = records.Where(t => t.Type == TransactionType.Income).Sum(t => Math.Abs(t.Amount)); trends.Add(new TrendStatistics { Year = targetYear, Month = targetMonth, Expense = expense, Income = income, Balance = income - expense }); } return trends; } } /// /// 按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; } /// /// 月度统计数据 /// public class MonthlyStatistics { public int Year { get; set; } public int Month { get; set; } public decimal TotalExpense { get; set; } public decimal TotalIncome { get; set; } public decimal Balance { get; set; } public int ExpenseCount { get; set; } public int IncomeCount { get; set; } public int TotalCount { get; set; } public decimal MaxExpense { get; set; } public decimal MaxIncome { get; set; } } /// /// 分类统计数据 /// public class CategoryStatistics { public string Classify { get; set; } = string.Empty; public decimal Amount { get; set; } public int Count { get; set; } public decimal Percent { get; set; } } /// /// 趋势统计数据 /// public class TrendStatistics { public int Year { get; set; } public int Month { get; set; } public decimal Expense { get; set; } public decimal Income { get; set; } public decimal Balance { get; set; } }