Files
EmailBill/Repository/TransactionRecordRepository.cs
孙诚 0c95b6aa6e
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 28s
Docker Build & Deploy / Deploy to Production (push) Successful in 11s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
发布
2026-01-16 15:56:53 +08:00

796 lines
29 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 Repository;
public interface ITransactionRecordRepository : IBaseRepository<TransactionRecord>
{
Task<TransactionRecord?> ExistsByEmailMessageIdAsync(long emailMessageId, DateTime occurredAt);
Task<TransactionRecord?> ExistsByImportNoAsync(string importNo, string importFrom);
/// <summary>
/// 分页获取交易记录列表
/// </summary>
/// <param name="pageIndex">页码从1开始</param>
/// <param name="pageSize">每页数量</param>
/// <param name="searchKeyword">搜索关键词(搜索交易摘要和分类)</param>
/// <param name="classifies">筛选分类列表</param>
/// <param name="type">筛选交易类型</param>
/// <param name="year">筛选年份</param>
/// <param name="month">筛选月份</param>
/// <param name="startDate">筛选开始日期</param>
/// <param name="endDate">筛选结束日期</param>
/// <param name="reason">筛选交易摘要</param>
/// <param name="sortByAmount">是否按金额降序排列默认为false按时间降序</param>
/// <returns>交易记录列表</returns>
Task<List<TransactionRecord>> GetPagedListAsync(
int pageIndex = 1,
int pageSize = 20,
string? searchKeyword = null,
string[]? classifies = null,
TransactionType? type = null,
int? year = null,
int? month = null,
DateTime? startDate = null,
DateTime? endDate = null,
string? reason = null,
bool sortByAmount = false);
/// <summary>
/// 获取总数
/// </summary>
Task<long> GetTotalCountAsync(
string? searchKeyword = null,
string[]? classifies = null,
TransactionType? type = null,
int? year = null,
int? month = null,
DateTime? startDate = null,
DateTime? endDate = null,
string? reason = null);
/// <summary>
/// 获取所有不同的交易分类
/// </summary>
Task<List<string>> GetDistinctClassifyAsync();
/// <summary>
/// 获取指定月份每天的消费统计
/// </summary>
/// <param name="year">年份</param>
/// <param name="month">月份</param>
/// <returns>每天的消费笔数和金额详情</returns>
Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsAsync(int year, int month, string? savingClassify = null);
/// <summary>
/// 获取指定日期范围内的每日统计
/// </summary>
/// <param name="startDate">开始日期</param>
/// <param name="endDate">结束日期</param>
/// <returns>每天的消费笔数和金额详情</returns>
Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate, string? savingClassify = null);
/// <summary>
/// 获取指定日期范围内的交易记录
/// </summary>
/// <param name="startDate">开始日期</param>
/// <param name="endDate">结束日期</param>
/// <returns>交易记录列表</returns>
Task<List<TransactionRecord>> GetByDateRangeAsync(DateTime startDate, DateTime endDate);
/// <summary>
/// 获取指定邮件的交易记录数量
/// </summary>
/// <param name="emailMessageId">邮件ID</param>
/// <returns>交易记录数量</returns>
Task<int> GetCountByEmailIdAsync(long emailMessageId);
/// <summary>
/// 获取月度统计数据
/// </summary>
/// <param name="year">年份</param>
/// <param name="month">月份</param>
/// <returns>月度统计数据</returns>
Task<MonthlyStatistics> GetMonthlyStatisticsAsync(int year, int month);
/// <summary>
/// 获取分类统计数据
/// </summary>
/// <param name="year">年份</param>
/// <param name="month">月份</param>
/// <param name="type">交易类型0:支出, 1:收入)</param>
/// <returns>分类统计列表</returns>
Task<List<CategoryStatistics>> GetCategoryStatisticsAsync(int year, int month, TransactionType type);
/// <summary>
/// 获取多个月的趋势统计数据
/// </summary>
/// <param name="startYear">开始年份</param>
/// <param name="startMonth">开始月份</param>
/// <param name="monthCount">月份数量</param>
/// <returns>趋势统计列表</returns>
Task<List<TrendStatistics>> GetTrendStatisticsAsync(int startYear, int startMonth, int monthCount);
/// <summary>
/// 获取指定邮件的交易记录列表
/// </summary>
/// <param name="emailMessageId">邮件ID</param>
/// <returns>交易记录列表</returns>
Task<List<TransactionRecord>> GetByEmailIdAsync(long emailMessageId);
/// <summary>
/// 获取未分类的账单数量
/// </summary>
/// <returns>未分类账单数量</returns>
Task<int> GetUnclassifiedCountAsync();
/// <summary>
/// 获取未分类的账单列表
/// </summary>
/// <param name="pageSize">每页数量</param>
/// <returns>未分类账单列表</returns>
Task<List<TransactionRecord>> GetUnclassifiedAsync(int pageSize = 10);
/// <summary>
/// 获取按交易摘要(Reason)分组的统计信息(支持分页)
/// </summary>
/// <param name="pageIndex">页码从1开始</param>
/// <param name="pageSize">每页数量</param>
/// <returns>分组统计列表和总数</returns>
Task<(List<ReasonGroupDto> list, int total)> GetReasonGroupsAsync(int pageIndex = 1, int pageSize = 20);
/// <summary>
/// 按摘要批量更新交易记录的分类
/// </summary>
/// <param name="reason">交易摘要</param>
/// <param name="type">交易类型</param>
/// <param name="classify">分类名称</param>
/// <returns>更新的记录数量</returns>
Task<int> BatchUpdateByReasonAsync(string reason, TransactionType type, string classify);
/// <summary>
/// 根据关键词查询交易记录模糊匹配Reason字段
/// </summary>
/// <param name="keyword">关键词</param>
/// <returns>匹配的交易记录列表</returns>
Task<List<TransactionRecord>> QueryByWhereAsync(string sql);
/// <summary>
/// 执行完整的SQL查询
/// </summary>
/// <param name="completeSql">完整的SELECT SQL语句</param>
/// <returns>查询结果列表</returns>
Task<List<TransactionRecord>> ExecuteRawSqlAsync(string completeSql);
/// <summary>
/// 根据关键词查询已分类的账单(用于智能分类参考)
/// </summary>
/// <param name="keywords">关键词列表</param>
/// <param name="limit">返回结果数量限制</param>
/// <returns>已分类的账单列表</returns>
Task<List<TransactionRecord>> GetClassifiedByKeywordsAsync(List<string> keywords, int limit = 10);
/// <summary>
/// 根据关键词查询已分类的账单,并计算相关度分数
/// </summary>
/// <param name="keywords">关键词列表</param>
/// <param name="minMatchRate">最小匹配率0.0-1.0默认0.3表示至少匹配30%的关键词</param>
/// <param name="limit">返回结果数量限制</param>
/// <returns>带相关度分数的已分类账单列表</returns>
Task<List<(TransactionRecord record, double relevanceScore)>> GetClassifiedByKeywordsWithScoreAsync(List<string> keywords, double minMatchRate = 0.3, int limit = 10);
/// <summary>
/// 获取抵账候选列表
/// </summary>
/// <param name="currentId">当前交易ID</param>
/// <param name="amount">当前交易金额</param>
/// <param name="currentType">当前交易类型</param>
/// <returns>候选交易列表</returns>
Task<List<TransactionRecord>> GetCandidatesForOffsetAsync(long currentId, decimal amount, TransactionType currentType);
/// <summary>
/// 获取待确认分类的账单列表
/// </summary>
/// <returns>待确认账单列表</returns>
Task<List<TransactionRecord>> GetUnconfirmedRecordsAsync();
/// <summary>
/// 全部确认待确认的分类
/// </summary>
/// <returns>影响行数</returns>
Task<int> ConfirmAllUnconfirmedAsync(long[] ids);
/// <summary>
/// 更新分类名称
/// </summary>
/// <param name="oldName">旧分类名称</param>
/// <param name="newName">新分类名称</param>
/// <param name="type">交易类型</param>
/// <returns>影响行数</returns>
Task<int> UpdateCategoryNameAsync(string oldName, string newName, TransactionType type);
}
public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<TransactionRecord>(freeSql), ITransactionRecordRepository
{
public async Task<TransactionRecord?> ExistsByEmailMessageIdAsync(long emailMessageId, DateTime occurredAt)
{
return await FreeSql.Select<TransactionRecord>()
.Where(t => t.EmailMessageId == emailMessageId && t.OccurredAt == occurredAt)
.FirstAsync();
}
public async Task<TransactionRecord?> ExistsByImportNoAsync(string importNo, string importFrom)
{
return await FreeSql.Select<TransactionRecord>()
.Where(t => t.ImportNo == importNo && t.ImportFrom == importFrom)
.FirstAsync();
}
public async Task<List<TransactionRecord>> GetPagedListAsync(
int pageIndex = 1,
int pageSize = 20,
string? searchKeyword = null,
string[]? classifies = null,
TransactionType? type = null,
int? year = null,
int? month = null,
DateTime? startDate = null,
DateTime? endDate = null,
string? reason = null,
bool sortByAmount = false)
{
var query = FreeSql.Select<TransactionRecord>();
// 如果提供了搜索关键词,则添加搜索条件
query = query.WhereIf(!string.IsNullOrWhiteSpace(searchKeyword),
t => t.Reason.Contains(searchKeyword!) ||
t.Classify.Contains(searchKeyword!) ||
t.Card.Contains(searchKeyword!) ||
t.ImportFrom.Contains(searchKeyword!))
.WhereIf(!string.IsNullOrWhiteSpace(reason),
t => t.Reason == reason);
// 按分类筛选
if (classifies != null && classifies.Length > 0)
{
var filterClassifies = classifies.Select(c => c == "未分类" ? string.Empty : c).ToList();
query = query.Where(t => filterClassifies.Contains(t.Classify));
}
// 按交易类型筛选
query = query.WhereIf(type.HasValue, t => t.Type == type!.Value);
// 按年月筛选
if (year.HasValue && month.HasValue)
{
var dateStart = new DateTime(year.Value, month.Value, 1);
var dateEnd = dateStart.AddMonths(1);
query = query.Where(t => t.OccurredAt >= dateStart && t.OccurredAt < dateEnd);
}
// 按日期范围筛选
query = query.WhereIf(startDate.HasValue, t => t.OccurredAt >= startDate!.Value)
.WhereIf(endDate.HasValue, t => t.OccurredAt <= endDate!.Value);
// 根据sortByAmount参数决定排序方式
if (sortByAmount)
{
// 按金额降序排列
return await query
.OrderByDescending(t => t.Amount)
.OrderByDescending(t => t.Id)
.Page(pageIndex, pageSize)
.ToListAsync();
}
else
{
// 按时间降序排列
return await query
.OrderByDescending(t => t.OccurredAt)
.OrderByDescending(t => t.Id)
.Page(pageIndex, pageSize)
.ToListAsync();
}
}
public async Task<long> GetTotalCountAsync(
string? searchKeyword = null,
string[]? classifies = null,
TransactionType? type = null,
int? year = null,
int? month = null,
DateTime? startDate = null,
DateTime? endDate = null,
string? reason = null)
{
var query = FreeSql.Select<TransactionRecord>();
// 如果提供了搜索关键词,则添加搜索条件
query = query.WhereIf(!string.IsNullOrWhiteSpace(searchKeyword),
t => t.Reason.Contains(searchKeyword!) ||
t.Classify.Contains(searchKeyword!) ||
t.Card.Contains(searchKeyword!) ||
t.ImportFrom.Contains(searchKeyword!))
.WhereIf(!string.IsNullOrWhiteSpace(reason),
t => t.Reason == reason);
// 按分类筛选
if (classifies != null && classifies.Length > 0)
{
var filterClassifies = classifies.Select(c => c == "未分类" ? string.Empty : c).ToList();
query = query.Where(t => filterClassifies.Contains(t.Classify));
}
// 按交易类型筛选
query = query.WhereIf(type.HasValue, t => t.Type == type!.Value);
// 按年月筛选
if (year.HasValue && month.HasValue)
{
var dateStart = new DateTime(year.Value, month.Value, 1);
var dateEnd = dateStart.AddMonths(1);
query = query.Where(t => t.OccurredAt >= dateStart && t.OccurredAt < dateEnd);
}
// 按日期范围筛选
query = query.WhereIf(startDate.HasValue, t => t.OccurredAt >= startDate!.Value)
.WhereIf(endDate.HasValue, t => t.OccurredAt <= endDate!.Value);
return await query.CountAsync();
}
public async Task<List<string>> GetDistinctClassifyAsync()
{
return await FreeSql.Select<TransactionRecord>()
.Where(t => !string.IsNullOrEmpty(t.Classify))
.Distinct()
.ToListAsync(t => t.Classify);
}
public async Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsAsync(int year, int month, string? savingClassify = null)
{
var startDate = new DateTime(year, month, 1);
var endDate = startDate.AddMonths(1);
return await GetDailyStatisticsByRangeAsync(startDate, endDate, savingClassify);
}
public async Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate, string? savingClassify = null)
{
var records = await FreeSql.Select<TransactionRecord>()
.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 => Math.Abs(t.Amount));
var expense = g.Where(t => t.Type == TransactionType.Expense).Sum(t => Math.Abs(t.Amount));
var saving = 0m;
if(!string.IsNullOrEmpty(savingClassify))
{
saving = g.Where(t => savingClassify.Split(',').Contains(t.Classify)).Sum(t => Math.Abs(t.Amount));
}
return (count: g.Count(), expense, income, saving);
}
);
return statistics;
}
public async Task<List<TransactionRecord>> GetByDateRangeAsync(DateTime startDate, DateTime endDate)
{
return await FreeSql.Select<TransactionRecord>()
.Where(t => t.OccurredAt >= startDate && t.OccurredAt <= endDate)
.OrderBy(t => t.OccurredAt)
.ToListAsync();
}
public async Task<int> GetCountByEmailIdAsync(long emailMessageId)
{
return (int)await FreeSql.Select<TransactionRecord>()
.Where(t => t.EmailMessageId == emailMessageId)
.CountAsync();
}
public async Task<List<TransactionRecord>> GetByEmailIdAsync(long emailMessageId)
{
return await FreeSql.Select<TransactionRecord>()
.Where(t => t.EmailMessageId == emailMessageId)
.OrderBy(t => t.OccurredAt)
.ToListAsync();
}
public async Task<int> GetUnclassifiedCountAsync()
{
return (int)await FreeSql.Select<TransactionRecord>()
.Where(t => string.IsNullOrEmpty(t.Classify))
.CountAsync();
}
public async Task<List<TransactionRecord>> GetUnclassifiedAsync(int pageSize = 10)
{
return await FreeSql.Select<TransactionRecord>()
.Where(t => string.IsNullOrEmpty(t.Classify))
.OrderByDescending(t => t.OccurredAt)
.Page(1, pageSize)
.ToListAsync();
}
public async Task<(List<ReasonGroupDto> list, int total)> GetReasonGroupsAsync(int pageIndex = 1, int pageSize = 20)
{
// 先按照Reason分组统计每个Reason的数量和总金额
var groups = await FreeSql.Select<TransactionRecord>()
.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(),
TotalAmount = g.Sum(g.Value.Amount)
});
// 按总金额绝对值降序排序
var sortedGroups = groups.OrderByDescending(g => Math.Abs(g.TotalAmount)).ToList();
var total = sortedGroups.Count;
// 分页
var pagedGroups = sortedGroups
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.ToList();
// 为每个分组获取详细信息
var result = new List<ReasonGroupDto>();
foreach (var group in pagedGroups)
{
// 获取该分组的所有记录
var records = await FreeSql.Select<TransactionRecord>()
.Where(t => t.Reason == group.Reason)
.Where(t => string.IsNullOrEmpty(t.Classify))
.ToListAsync();
if (records.Count > 0)
{
var sample = records.First();
result.Add(new ReasonGroupDto
{
Reason = group.Reason,
Count = (int)group.Count,
SampleType = sample.Type,
SampleClassify = sample.Classify ?? string.Empty,
TransactionIds = records.Select(r => r.Id).ToList(),
TotalAmount = Math.Abs(group.TotalAmount)
});
}
}
return (result, total);
}
public async Task<int> BatchUpdateByReasonAsync(string reason, TransactionType type, string classify)
{
return await FreeSql.Update<TransactionRecord>()
.Set(t => t.Type, type)
.Set(t => t.Classify, classify)
.Where(t => t.Reason == reason)
.ExecuteAffrowsAsync();
}
public async Task<List<TransactionRecord>> QueryByWhereAsync(string sql)
{
return await FreeSql.Select<TransactionRecord>()
.Where(sql)
.OrderByDescending(t => t.OccurredAt)
.ToListAsync();
}
public async Task<List<TransactionRecord>> ExecuteRawSqlAsync(string completeSql)
{
return await FreeSql.Ado.QueryAsync<TransactionRecord>(completeSql);
}
public async Task<MonthlyStatistics> GetMonthlyStatisticsAsync(int year, int month)
{
var startDate = new DateTime(year, month, 1);
var endDate = startDate.AddMonths(1);
var records = await FreeSql.Select<TransactionRecord>()
.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++;
}
else if (record.Type == TransactionType.Income)
{
statistics.TotalIncome += amount;
statistics.IncomeCount++;
}
}
statistics.Balance = statistics.TotalIncome - statistics.TotalExpense;
statistics.TotalCount = records.Count;
return statistics;
}
public async Task<List<CategoryStatistics>> GetCategoryStatisticsAsync(int year, int month, TransactionType type)
{
var startDate = new DateTime(year, month, 1);
var endDate = startDate.AddMonths(1);
var records = await FreeSql.Select<TransactionRecord>()
.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<List<TrendStatistics>> GetTrendStatisticsAsync(int startYear, int startMonth, int monthCount)
{
var trends = new List<TrendStatistics>();
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<TransactionRecord>()
.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;
}
public async Task<List<TransactionRecord>> GetClassifiedByKeywordsAsync(List<string> keywords, int limit = 10)
{
if (keywords == null || keywords.Count == 0)
{
return new List<TransactionRecord>();
}
var query = FreeSql.Select<TransactionRecord>()
.Where(t => t.Classify != ""); // 只查询已分类的账单
// 构建OR条件Reason包含任意一个关键词
if (keywords.Count > 0)
{
query = query.Where(t => keywords.Any(keyword => t.Reason.Contains(keyword)));
}
return await query
.OrderByDescending(t => t.OccurredAt)
.Limit(limit)
.ToListAsync();
}
public async Task<List<(TransactionRecord record, double relevanceScore)>> GetClassifiedByKeywordsWithScoreAsync(List<string> keywords, double minMatchRate = 0.3, int limit = 10)
{
if (keywords == null || keywords.Count == 0)
{
return new List<(TransactionRecord, double)>();
}
// 查询所有已分类且包含任意关键词的账单
var candidates = await FreeSql.Select<TransactionRecord>()
.Where(t => t.Classify != "")
.Where(t => keywords.Any(keyword => t.Reason.Contains(keyword)))
.ToListAsync();
// 计算每个候选账单的相关度分数
var scoredResults = candidates
.Select(record =>
{
var matchedCount = keywords.Count(keyword => record.Reason.Contains(keyword, StringComparison.OrdinalIgnoreCase));
var matchRate = (double)matchedCount / keywords.Count;
// 额外加分:完全匹配整个摘要(相似度更高)
var exactMatchBonus = keywords.Any(k => record.Reason.Equals(k, StringComparison.OrdinalIgnoreCase)) ? 0.2 : 0.0;
// 长度相似度加分:长度越接近,相关度越高
var avgKeywordLength = keywords.Average(k => k.Length);
var lengthSimilarity = 1.0 - Math.Min(1.0, Math.Abs(record.Reason.Length - avgKeywordLength) / Math.Max(record.Reason.Length, avgKeywordLength));
var lengthBonus = lengthSimilarity * 0.1;
var score = matchRate + exactMatchBonus + lengthBonus;
return (record, score);
})
.Where(x => x.score >= minMatchRate) // 过滤低相关度结果
.OrderByDescending(x => x.score) // 按相关度降序
.ThenByDescending(x => x.record.OccurredAt) // 相同分数时,按时间降序
.Take(limit)
.ToList();
return scoredResults;
}
public async Task<List<TransactionRecord>> GetCandidatesForOffsetAsync(long currentId, decimal amount, TransactionType currentType)
{
var absAmount = Math.Abs(amount);
var minAmount = absAmount - 5;
var maxAmount = absAmount + 5;
var currentRecord = await FreeSql.Select<TransactionRecord>()
.Where(t => t.Id == currentId)
.FirstAsync();
if (currentRecord == null)
{
return new List<TransactionRecord>();
}
var list = await FreeSql.Select<TransactionRecord>()
.Where(t => t.Id != currentId)
.Where(t => t.Type != currentType)
.Where(t => Math.Abs(t.Amount) >= minAmount && Math.Abs(t.Amount) <= maxAmount)
.Take(50)
.ToListAsync();
return list.OrderBy(t => Math.Abs(Math.Abs(t.Amount) - absAmount))
.ThenBy(x=> Math.Abs((x.OccurredAt - currentRecord.OccurredAt).TotalSeconds))
.ToList();
}
public async Task<int> UpdateCategoryNameAsync(string oldName, string newName, TransactionType type)
{
return await FreeSql.Update<TransactionRecord>()
.Set(a => a.Classify, newName)
.Where(a => a.Classify == oldName && a.Type == type)
.ExecuteAffrowsAsync();
}
public async Task<List<TransactionRecord>> GetUnconfirmedRecordsAsync()
{
return await FreeSql.Select<TransactionRecord>()
.Where(t => t.UnconfirmedClassify != null && t.UnconfirmedClassify != "")
.OrderByDescending(t => t.OccurredAt)
.ToListAsync();
}
public async Task<int> ConfirmAllUnconfirmedAsync(long[] ids)
{
return await FreeSql.Update<TransactionRecord>()
.Set(t => t.Classify == t.UnconfirmedClassify)
.Set(t => t.Type == (t.UnconfirmedType ?? t.Type))
.Set(t => t.UnconfirmedClassify, null)
.Set(t => t.UnconfirmedType, null)
.Where(t => t.UnconfirmedClassify != null && t.UnconfirmedClassify != "")
.Where(t => ids.Contains(t.Id))
.ExecuteAffrowsAsync();
}
}
/// <summary>
/// 按Reason分组统计DTO
/// </summary>
public class ReasonGroupDto
{
/// <summary>
/// 交易摘要
/// </summary>
public string Reason { get; set; } = string.Empty;
/// <summary>
/// 该摘要的记录数量
/// </summary>
public int Count { get; set; }
/// <summary>
/// 示例交易类型(该分组中第一条记录的类型)
/// </summary>
public TransactionType SampleType { get; set; }
/// <summary>
/// 示例分类(该分组中第一条记录的分类)
/// </summary>
public string SampleClassify { get; set; } = string.Empty;
/// <summary>
/// 该分组的所有账单ID列表
/// </summary>
public List<long> TransactionIds { get; set; } = new();
/// <summary>
/// 该分组的总金额(绝对值)
/// </summary>
public decimal TotalAmount { get; set; }
}
/// <summary>
/// 月度统计数据
/// </summary>
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; }
}
/// <summary>
/// 分类统计数据
/// </summary>
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; }
}
/// <summary>
/// 趋势统计数据
/// </summary>
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; }
}