重构账单查询sql
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 26s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 26s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
This commit is contained in:
46
Repository/AGENTS.md
Normal file
46
Repository/AGENTS.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# REPOSITORY LAYER KNOWLEDGE BASE
|
||||
|
||||
**Generated:** 2026-01-28
|
||||
**Parent:** EmailBill/AGENTS.md
|
||||
|
||||
## OVERVIEW
|
||||
Data access layer using FreeSql with BaseRepository pattern and global usings.
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
Repository/
|
||||
├── BaseRepository.cs # Generic repository base
|
||||
├── GlobalUsings.cs # Common imports
|
||||
├── BudgetRepository.cs # Budget data access
|
||||
├── TransactionRecordRepository.cs # Transaction data access
|
||||
├── EmailMessageRepository.cs # Email data access
|
||||
└── TransactionStatisticsDto.cs # Statistics DTOs
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| Base patterns | BaseRepository.cs | Generic CRUD operations |
|
||||
| Budget data | BudgetRepository.cs | Budget queries and updates |
|
||||
| Transaction data | TransactionRecordRepository.cs | Financial data access |
|
||||
| Email data | EmailMessageRepository.cs | Email processing storage |
|
||||
| Statistics | TransactionStatisticsDto.cs | Data transfer objects |
|
||||
|
||||
## CONVENTIONS
|
||||
- Inherit from BaseRepository<T> for all repositories
|
||||
- Use GlobalUsings.cs for shared imports
|
||||
- Async/await pattern for all database operations
|
||||
- Method names: GetAllAsync, GetByIdAsync, InsertAsync, UpdateAsync
|
||||
- Return domain entities, not DTOs (except in query results)
|
||||
|
||||
## ANTI-PATTERNS (THIS LAYER)
|
||||
- Never return anonymous types from methods
|
||||
- Don't expose FreeSql ISelect directly
|
||||
- Avoid business logic in repositories
|
||||
- No synchronous database calls
|
||||
- Don't mix data access with service logic
|
||||
|
||||
## UNIQUE STYLES
|
||||
- Generic constraints: where T : BaseEntity
|
||||
- Fluent query building with FreeSql extension methods
|
||||
- Paged query patterns for large datasets
|
||||
@@ -3,4 +3,5 @@ global using Entity;
|
||||
global using System.Linq;
|
||||
global using System.Data;
|
||||
global using System.Dynamic;
|
||||
global using FreeSql;
|
||||
|
||||
|
||||
@@ -6,214 +6,91 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
|
||||
|
||||
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,
|
||||
Task<List<TransactionRecord>> QueryAsync(
|
||||
int? year = null,
|
||||
int? month = null,
|
||||
DateTime? startDate = null,
|
||||
DateTime? endDate = null,
|
||||
TransactionType? type = null,
|
||||
string[]? classifies = null,
|
||||
string? searchKeyword = null,
|
||||
string? reason = null,
|
||||
int pageIndex = 1,
|
||||
int pageSize = int.MaxValue,
|
||||
bool sortByAmount = false);
|
||||
|
||||
/// <summary>
|
||||
/// 获取总数
|
||||
/// </summary>
|
||||
Task<long> GetTotalCountAsync(
|
||||
string? searchKeyword = null,
|
||||
string[]? classifies = null,
|
||||
TransactionType? type = null,
|
||||
Task<long> CountAsync(
|
||||
int? year = null,
|
||||
int? month = null,
|
||||
DateTime? startDate = null,
|
||||
DateTime? endDate = null,
|
||||
TransactionType? type = null,
|
||||
string[]? classifies = null,
|
||||
string? searchKeyword = null,
|
||||
string? reason = null);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有不同的交易分类
|
||||
/// </summary>
|
||||
Task<List<string>> GetDistinctClassifyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定月份每天的消费统计
|
||||
/// </summary>
|
||||
/// <param name="year">年份</param>
|
||||
/// <param name="month">月份</param>
|
||||
/// <param name="savingClassify"></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>
|
||||
/// <param name="savingClassify"></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();
|
||||
Task<int> GetCountByEmailIdAsync(long emailMessageId);
|
||||
|
||||
/// <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>
|
||||
/// <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>
|
||||
/// <returns>待确认账单列表</returns>
|
||||
Task<List<TransactionRecord>> GetUnconfirmedRecordsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 全部确认待确认的分类
|
||||
/// </summary>
|
||||
/// <returns>影响行数</returns>
|
||||
Task<int> ConfirmAllUnconfirmedAsync(long[] ids);
|
||||
Task<int> BatchUpdateByReasonAsync(string reason, TransactionType type, string classify);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定分类在指定时间范围内的每日/每月统计趋势
|
||||
/// </summary>
|
||||
Task<Dictionary<DateTime, decimal>> GetFilteredTrendStatisticsAsync(
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
TransactionType type,
|
||||
IEnumerable<string> classifies,
|
||||
bool groupByMonth = false);
|
||||
|
||||
/// <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);
|
||||
|
||||
Task<Dictionary<(string, TransactionType), decimal>> GetAmountGroupByClassifyAsync(DateTime startTime, DateTime endTime);
|
||||
Task<int> ConfirmAllUnconfirmedAsync(long[] ids);
|
||||
}
|
||||
|
||||
public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<TransactionRecord>(freeSql), ITransactionRecordRepository
|
||||
{
|
||||
private ISelect<TransactionRecord> BuildQuery(
|
||||
int? year = null,
|
||||
int? month = null,
|
||||
DateTime? startDate = null,
|
||||
DateTime? endDate = null,
|
||||
TransactionType? type = null,
|
||||
string[]? classifies = null,
|
||||
string? searchKeyword = 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 is { 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 query;
|
||||
}
|
||||
|
||||
public async Task<TransactionRecord?> ExistsByEmailMessageIdAsync(long emailMessageId, DateTime occurredAt)
|
||||
{
|
||||
return await FreeSql.Select<TransactionRecord>()
|
||||
@@ -228,56 +105,23 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
.FirstAsync();
|
||||
}
|
||||
|
||||
public async Task<List<TransactionRecord>> GetPagedListAsync(
|
||||
int pageIndex = 1,
|
||||
int pageSize = 20,
|
||||
string? searchKeyword = null,
|
||||
string[]? classifies = null,
|
||||
TransactionType? type = null,
|
||||
public async Task<List<TransactionRecord>> QueryAsync(
|
||||
int? year = null,
|
||||
int? month = null,
|
||||
DateTime? startDate = null,
|
||||
DateTime? endDate = null,
|
||||
TransactionType? type = null,
|
||||
string[]? classifies = null,
|
||||
string? searchKeyword = null,
|
||||
string? reason = null,
|
||||
int pageIndex = 1,
|
||||
int pageSize = int.MaxValue,
|
||||
bool sortByAmount = false)
|
||||
{
|
||||
var query = FreeSql.Select<TransactionRecord>();
|
||||
var query = BuildQuery(year, month, startDate, endDate, type, classifies, searchKeyword, reason);
|
||||
|
||||
// 如果提供了搜索关键词,则添加搜索条件
|
||||
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 is { 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)
|
||||
@@ -285,7 +129,6 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
// 按时间降序排列
|
||||
return await query
|
||||
.OrderByDescending(t => t.OccurredAt)
|
||||
.OrderByDescending(t => t.Id)
|
||||
@@ -293,49 +136,17 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<long> GetTotalCountAsync(
|
||||
string? searchKeyword = null,
|
||||
string[]? classifies = null,
|
||||
TransactionType? type = null,
|
||||
public async Task<long> CountAsync(
|
||||
int? year = null,
|
||||
int? month = null,
|
||||
DateTime? startDate = null,
|
||||
DateTime? endDate = null,
|
||||
TransactionType? type = null,
|
||||
string[]? classifies = null,
|
||||
string? searchKeyword = 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 is { 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);
|
||||
|
||||
var query = BuildQuery(year, month, startDate, endDate, type, classifies, searchKeyword, reason);
|
||||
return await query.CountAsync();
|
||||
}
|
||||
|
||||
@@ -347,58 +158,6 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
.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>()
|
||||
@@ -407,10 +166,10 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int> GetUnclassifiedCountAsync()
|
||||
public async Task<int> GetCountByEmailIdAsync(long emailMessageId)
|
||||
{
|
||||
return (int)await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => string.IsNullOrEmpty(t.Classify))
|
||||
.Where(t => t.EmailMessageId == emailMessageId)
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
@@ -423,188 +182,6 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
.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 = group.Count,
|
||||
SampleType = sample.Type,
|
||||
SampleClassify = sample.Classify,
|
||||
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.Count == 0)
|
||||
@@ -613,9 +190,8 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
}
|
||||
|
||||
var query = FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.Classify != ""); // 只查询已分类的账单
|
||||
.Where(t => t.Classify != "");
|
||||
|
||||
// 构建OR条件:Reason包含任意一个关键词
|
||||
if (keywords.Count > 0)
|
||||
{
|
||||
query = query.Where(t => keywords.Any(keyword => t.Reason.Contains(keyword)));
|
||||
@@ -627,44 +203,21 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<(TransactionRecord record, double relevanceScore)>> GetClassifiedByKeywordsWithScoreAsync(List<string> keywords, double minMatchRate = 0.3, int limit = 10)
|
||||
public async Task<List<TransactionRecord>> GetUnconfirmedRecordsAsync()
|
||||
{
|
||||
if (keywords.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// 查询所有已分类且包含任意关键词的账单
|
||||
var candidates = await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.Classify != "")
|
||||
.Where(t => keywords.Any(keyword => t.Reason.Contains(keyword)))
|
||||
return await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.UnconfirmedClassify != null && t.UnconfirmedClassify != "")
|
||||
.OrderByDescending(t => t.OccurredAt)
|
||||
.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<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<int> UpdateCategoryNameAsync(string oldName, string newName, TransactionType type)
|
||||
@@ -675,14 +228,6 @@ return scoredResults;
|
||||
.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>()
|
||||
@@ -694,136 +239,4 @@ return scoredResults;
|
||||
.Where(t => ids.Contains(t.Id))
|
||||
.ExecuteAffrowsAsync();
|
||||
}
|
||||
|
||||
public async Task<Dictionary<DateTime, decimal>> GetFilteredTrendStatisticsAsync(
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
TransactionType type,
|
||||
IEnumerable<string> classifies,
|
||||
bool groupByMonth = false)
|
||||
{
|
||||
var query = FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.OccurredAt >= startDate && t.OccurredAt <= endDate && t.Type == type);
|
||||
|
||||
if (classifies.Any())
|
||||
{
|
||||
query = query.Where(t => classifies.Contains(t.Classify));
|
||||
}
|
||||
|
||||
var list = await query.ToListAsync(t => new { t.OccurredAt, t.Amount });
|
||||
|
||||
if (groupByMonth)
|
||||
{
|
||||
return list
|
||||
.GroupBy(t => new DateTime(t.OccurredAt.Year, t.OccurredAt.Month, 1))
|
||||
.ToDictionary(g => g.Key, g => g.Sum(x => Math.Abs(x.Amount)));
|
||||
}
|
||||
|
||||
return list
|
||||
.GroupBy(t => t.OccurredAt.Date)
|
||||
.ToDictionary(g => g.Key, g => g.Sum(x => Math.Abs(x.Amount)));
|
||||
}
|
||||
|
||||
public async Task<Dictionary<(string, TransactionType), decimal>> GetAmountGroupByClassifyAsync(DateTime startTime, DateTime endTime)
|
||||
{
|
||||
var result = await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.OccurredAt >= startTime && t.OccurredAt < endTime)
|
||||
.GroupBy(t => new { t.Classify, t.Type })
|
||||
.ToListAsync(g => new
|
||||
{
|
||||
g.Key.Classify,
|
||||
g.Key.Type,
|
||||
TotalAmount = g.Sum(g.Value.Amount)
|
||||
});
|
||||
|
||||
return result.ToDictionary(x => (x.Classify, x.Type), x => x.TotalAmount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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; } = [];
|
||||
|
||||
/// <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; }
|
||||
}
|
||||
456
Repository/TransactionRecordRepository.md
Normal file
456
Repository/TransactionRecordRepository.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# TransactionRecordRepository 查询语句文档
|
||||
|
||||
本文档整理了所有与账单(TransactionRecord)相关的查询语句,包括仓储层、服务层中的SQL查询。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [TransactionRecordRepository 查询方法](#transactionrecordrepository-查询方法)
|
||||
2. [其他仓储中的账单查询](#其他仓储中的账单查询)
|
||||
3. [服务层中的SQL查询](#服务层中的sql查询)
|
||||
4. [总结](#总结)
|
||||
|
||||
---
|
||||
|
||||
## TransactionRecordRepository 查询方法
|
||||
|
||||
### 1. 基础查询
|
||||
|
||||
#### 1.1 根据邮件ID和交易时间检查是否存在
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:94-99
|
||||
return await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.EmailMessageId == emailMessageId && t.OccurredAt == occurredAt)
|
||||
.FirstAsync();
|
||||
```
|
||||
|
||||
#### 1.2 根据导入编号检查是否存在
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:101-106
|
||||
return await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.ImportNo == importNo && t.ImportFrom == importFrom)
|
||||
.FirstAsync();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 核心查询构建器
|
||||
|
||||
#### 2.1 BuildQuery() 私有方法 - 统一查询构建
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:53-92
|
||||
private ISelect<TransactionRecord> BuildQuery(
|
||||
int? year = null,
|
||||
int? month = null,
|
||||
DateTime? startDate = null,
|
||||
DateTime? endDate = null,
|
||||
TransactionType? type = null,
|
||||
string[]? classifies = null,
|
||||
string? searchKeyword = null,
|
||||
string? reason = null)
|
||||
{
|
||||
var query = FreeSql.Select<TransactionRecord>();
|
||||
|
||||
// 搜索关键词条件(Reason/Classify/Card/ImportFrom)
|
||||
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 is { 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 query;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 分页查询与统计
|
||||
|
||||
#### 3.1 分页获取交易记录列表
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:108-137
|
||||
var query = BuildQuery(year, month, startDate, endDate, type, classifies, searchKeyword, reason);
|
||||
|
||||
// 排序:按金额或按时间
|
||||
if (sortByAmount)
|
||||
{
|
||||
return await query
|
||||
.OrderByDescending(t => t.Amount)
|
||||
.OrderByDescending(t => t.Id)
|
||||
.Page(pageIndex, pageSize)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderByDescending(t => t.OccurredAt)
|
||||
.OrderByDescending(t => t.Id)
|
||||
.Page(pageIndex, pageSize)
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
#### 3.2 获取总数(与分页查询条件相同)
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:139-151
|
||||
var query = BuildQuery(year, month, startDate, endDate, type, classifies, searchKeyword, reason);
|
||||
return await query.CountAsync();
|
||||
```
|
||||
|
||||
#### 3.3 获取所有不同的交易分类
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:153-159
|
||||
return await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => !string.IsNullOrEmpty(t.Classify))
|
||||
.Distinct()
|
||||
.ToListAsync(t => t.Classify);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 按邮件相关查询
|
||||
|
||||
#### 4.1 获取指定邮件的交易记录列表
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:161-167
|
||||
return await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.EmailMessageId == emailMessageId)
|
||||
.OrderBy(t => t.OccurredAt)
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
#### 4.2 获取指定邮件的交易记录数量
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:169-174
|
||||
return (int)await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.EmailMessageId == emailMessageId)
|
||||
.CountAsync();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 未分类账单查询
|
||||
|
||||
#### 5.1 获取未分类的账单列表
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:176-183
|
||||
return await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => string.IsNullOrEmpty(t.Classify))
|
||||
.OrderByDescending(t => t.OccurredAt)
|
||||
.Page(1, pageSize)
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 智能分类相关查询
|
||||
|
||||
#### 6.1 根据关键词查询已分类的账单
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:185-204
|
||||
if (keywords.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
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();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 待确认分类查询
|
||||
|
||||
#### 7.1 获取待确认分类的账单列表
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:206-212
|
||||
return await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.UnconfirmedClassify != null && t.UnconfirmedClassify != "")
|
||||
.OrderByDescending(t => t.OccurredAt)
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. 批量更新操作
|
||||
|
||||
#### 8.1 按摘要批量更新交易记录的分类
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:214-221
|
||||
return await FreeSql.Update<TransactionRecord>()
|
||||
.Set(t => t.Type, type)
|
||||
.Set(t => t.Classify, classify)
|
||||
.Where(t => t.Reason == reason)
|
||||
.ExecuteAffrowsAsync();
|
||||
```
|
||||
|
||||
#### 8.2 更新分类名称
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:223-229
|
||||
return await FreeSql.Update<TransactionRecord>()
|
||||
.Set(a => a.Classify, newName)
|
||||
.Where(a => a.Classify == oldName && a.Type == type)
|
||||
.ExecuteAffrowsAsync();
|
||||
```
|
||||
|
||||
#### 8.3 确认待确认的分类
|
||||
```csharp
|
||||
/// 位置: TransactionRecordRepository.cs:231-241
|
||||
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();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 其他仓储中的账单查询
|
||||
|
||||
### BudgetRepository
|
||||
|
||||
#### 1. 获取预算当前金额
|
||||
```csharp
|
||||
/// 位置: BudgetRepository.cs:12-33
|
||||
var query = FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.OccurredAt >= startDate && t.OccurredAt <= endDate);
|
||||
|
||||
if (!string.IsNullOrEmpty(budget.SelectedCategories))
|
||||
{
|
||||
var categoryList = budget.SelectedCategories.Split(',');
|
||||
query = query.Where(t => categoryList.Contains(t.Classify));
|
||||
}
|
||||
|
||||
if (budget.Category == BudgetCategory.Expense)
|
||||
{
|
||||
query = query.Where(t => t.Type == TransactionType.Expense);
|
||||
}
|
||||
else if (budget.Category == BudgetCategory.Income)
|
||||
{
|
||||
query = query.Where(t => t.Type == TransactionType.Income);
|
||||
}
|
||||
|
||||
return await query.SumAsync(t => t.Amount);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### TransactionCategoryRepository
|
||||
|
||||
#### 1. 检查分类是否被使用
|
||||
```csharp
|
||||
/// 位置: TransactionCategoryRepository.cs:53-63
|
||||
var count = await FreeSql.Select<TransactionRecord>()
|
||||
.Where(r => r.Classify == category.Name && r.Type == category.Type)
|
||||
.CountAsync();
|
||||
|
||||
return count > 0;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 服务层中的SQL查询
|
||||
|
||||
### SmartHandleService
|
||||
|
||||
#### 1. 智能分析账单 - 执行AI生成的SQL
|
||||
```csharp
|
||||
/// 位置: SmartHandleService.cs:351
|
||||
queryResults = await transactionRepository.ExecuteDynamicSqlAsync(sqlText);
|
||||
```
|
||||
|
||||
**说明**: 此方法接收AI生成的SQL语句并执行,SQL内容由AI根据用户问题动态生成,例如:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
COUNT(*) AS TransactionCount,
|
||||
SUM(ABS(Amount)) AS TotalAmount,
|
||||
Type,
|
||||
Classify
|
||||
FROM TransactionRecord
|
||||
WHERE OccurredAt >= '2025-01-01'
|
||||
AND OccurredAt < '2026-01-01'
|
||||
GROUP BY Type, Classify
|
||||
ORDER BY TotalAmount DESC
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### BudgetService
|
||||
|
||||
#### 1. 获取归档摘要 - 年度交易统计
|
||||
```csharp
|
||||
/// 位置: BudgetService.cs:239-252
|
||||
var yearTransactions = await transactionRecordRepository.ExecuteDynamicSqlAsync(
|
||||
$"""
|
||||
SELECT
|
||||
COUNT(*) AS TransactionCount,
|
||||
SUM(ABS(Amount)) AS TotalAmount,
|
||||
Type,
|
||||
Classify
|
||||
FROM TransactionRecord
|
||||
WHERE OccurredAt >= '{year}-01-01'
|
||||
AND OccurredAt < '{year + 1}-01-01'
|
||||
GROUP BY Type, Classify
|
||||
ORDER BY TotalAmount DESC
|
||||
"""
|
||||
);
|
||||
```
|
||||
|
||||
#### 2. 获取归档摘要 - 月度交易统计
|
||||
```csharp
|
||||
/// 位置: BudgetService.cs:254-267
|
||||
var monthYear = new DateTime(year, month, 1).AddMonths(1);
|
||||
var monthTransactions = await transactionRecordRepository.ExecuteDynamicSqlAsync(
|
||||
$"""
|
||||
SELECT
|
||||
COUNT(*) AS TransactionCount,
|
||||
SUM(ABS(Amount)) AS TotalAmount,
|
||||
Type,
|
||||
Classify
|
||||
FROM TransactionRecord
|
||||
WHERE OccurredAt >= '{year}-{month:00}-01'
|
||||
AND OccurredAt < '{monthYear:yyyy-MM-dd}'
|
||||
GROUP BY Type, Classify
|
||||
ORDER BY TotalAmount DESC
|
||||
"""
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### BudgetSavingsService
|
||||
|
||||
#### 1. 获取按分类分组的交易金额(用于存款预算计算)
|
||||
```csharp
|
||||
/// 位置: BudgetSavingsService.cs:62-65
|
||||
var transactionClassify = await transactionsRepository.GetAmountGroupByClassifyAsync(
|
||||
new DateTime(year, month, 1),
|
||||
new DateTime(year, month, 1).AddMonths(1)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
### 查询方法分类
|
||||
|
||||
| 分类 | 方法数 | 说明 |
|
||||
|------|--------|------|
|
||||
| 基础查询 | 2 | 检查记录是否存在(去重) |
|
||||
| 核心构建器 | 1 | BuildQuery() 私有方法,统一查询逻辑 |
|
||||
| 分页查询 | 2 | 分页列表 + 总数统计 |
|
||||
| 分类查询 | 1 | 获取所有不同分类 |
|
||||
| 邮件相关 | 2 | 按邮件ID查询列表和数量 |
|
||||
| 未分类查询 | 1 | 获取未分类账单列表 |
|
||||
| 智能分类 | 1 | 关键词匹配查询 |
|
||||
| 待确认分类 | 1 | 获取待确认账单列表 |
|
||||
| 批量更新 | 3 | 批量更新分类和确认操作 |
|
||||
| 其他仓储查询 | 2 | 预算/分类仓储中的账单查询 |
|
||||
| 服务层SQL | 3 | AI生成SQL + 归档统计 |
|
||||
|
||||
### 关键发现
|
||||
|
||||
1. **简化的架构**:新实现移除了复杂的统计方法,专注于核心的CRUD操作和查询功能。
|
||||
|
||||
2. **统一的查询构建**:`BuildQuery()` 私有方法(第53-92行)被 `QueryAsync()` 和 `CountAsync()` 共享使用,确保查询逻辑一致性。
|
||||
|
||||
3. **去重检查**:`ExistsByEmailMessageIdAsync()` 和 `ExistsByImportNoAsync()` 用于防止重复导入。
|
||||
|
||||
4. **灵活的查询条件**:支持按年月、日期范围、交易类型、分类、关键词等多维度筛选。
|
||||
|
||||
5. **批量操作优化**:提供批量更新分类、确认待确认记录等高效操作。
|
||||
|
||||
6. **服务层SQL保持不变**:AI生成SQL和归档统计等高级查询功能仍然通过 `ExecuteDynamicSqlAsync()` 实现。
|
||||
|
||||
### SQL查询模式
|
||||
|
||||
所有SQL查询都遵循以下模式:
|
||||
```sql
|
||||
SELECT [字段] FROM TransactionRecord
|
||||
WHERE [条件]
|
||||
ORDER BY [排序字段]
|
||||
LIMIT [限制数量]
|
||||
```
|
||||
|
||||
常用查询条件:
|
||||
- `EmailMessageId == ? AND OccurredAt == ?` - 精确匹配去重
|
||||
- `ImportNo == ? AND ImportFrom == ?` - 导入记录去重
|
||||
- `Classify != ""` - 已分类记录
|
||||
- `Classify == "" OR Classify IS NULL` - 未分类记录
|
||||
- `UnconfirmedClassify != ""` - 待确认记录
|
||||
- `Reason.Contains(?) OR Classify.Contains(?)` - 关键词搜索
|
||||
|
||||
### 字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| Id | bigint | 主键 |
|
||||
| Card | nvarchar | 卡号 |
|
||||
| Reason | nvarchar | 交易原因/摘要 |
|
||||
| Amount | decimal | 交易金额(支出为负数,收入为正数) |
|
||||
| OccurredAt | datetime | 交易发生时间 |
|
||||
| Type | int | 交易类型(0=支出, 1=收入, 2=不计入收支) |
|
||||
| Classify | nvarchar | 交易分类(空字符串表示未分类) |
|
||||
| EmailMessageId | bigint | 关联邮件ID |
|
||||
| ImportNo | nvarchar | 导入编号 |
|
||||
| ImportFrom | nvarchar | 导入来源 |
|
||||
| UnconfirmedClassify | nvarchar | 待确认分类 |
|
||||
| UnconfirmedType | int? | 待确认类型 |
|
||||
|
||||
### 接口方法总览
|
||||
|
||||
**ITransactionRecordRepository 接口定义(17个方法):**
|
||||
|
||||
1. `ExistsByEmailMessageIdAsync()` - 邮件去重检查
|
||||
2. `ExistsByImportNoAsync()` - 导入去重检查
|
||||
3. `QueryAsync()` - 分页查询(支持多维度筛选)
|
||||
4. `CountAsync()` - 总数统计(与QueryAsync条件相同)
|
||||
5. `GetDistinctClassifyAsync()` - 获取所有分类
|
||||
6. `GetByEmailIdAsync()` - 按邮件ID查询记录
|
||||
7. `GetCountByEmailIdAsync()` - 按邮件ID统计数量
|
||||
8. `GetUnclassifiedAsync()` - 获取未分类记录
|
||||
9. `GetClassifiedByKeywordsAsync()` - 关键词匹配查询
|
||||
10. `GetUnconfirmedRecordsAsync()` - 获取待确认记录
|
||||
11. `BatchUpdateByReasonAsync()` - 按摘要批量更新
|
||||
12. `UpdateCategoryNameAsync()` - 更新分类名称
|
||||
13. `ConfirmAllUnconfirmedAsync()` - 确认待确认记录
|
||||
|
||||
**私有辅助方法:**
|
||||
- `BuildQuery()` - 统一查询构建器(被QueryAsync和CountAsync使用)
|
||||
Reference in New Issue
Block a user