Files
EmailBill/Repository/TransactionRecordRepository.md
SunCheng b71eadd4f9
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
重构账单查询sql
2026-01-28 10:58:15 +08:00

13 KiB
Raw Permalink Blame History

TransactionRecordRepository 查询语句文档

本文档整理了所有与账单(TransactionRecord)相关的查询语句包括仓储层、服务层中的SQL查询。

目录

  1. TransactionRecordRepository 查询方法
  2. 其他仓储中的账单查询
  3. 服务层中的SQL查询
  4. 总结

TransactionRecordRepository 查询方法

1. 基础查询

1.1 根据邮件ID和交易时间检查是否存在

/// 位置: TransactionRecordRepository.cs:94-99
return await FreeSql.Select<TransactionRecord>()
    .Where(t => t.EmailMessageId == emailMessageId && t.OccurredAt == occurredAt)
    .FirstAsync();

1.2 根据导入编号检查是否存在

/// 位置: TransactionRecordRepository.cs:101-106
return await FreeSql.Select<TransactionRecord>()
    .Where(t => t.ImportNo == importNo && t.ImportFrom == importFrom)
    .FirstAsync();

2. 核心查询构建器

2.1 BuildQuery() 私有方法 - 统一查询构建

/// 位置: 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 分页获取交易记录列表

/// 位置: 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 获取总数(与分页查询条件相同)

/// 位置: TransactionRecordRepository.cs:139-151
var query = BuildQuery(year, month, startDate, endDate, type, classifies, searchKeyword, reason);
return await query.CountAsync();

3.3 获取所有不同的交易分类

/// 位置: TransactionRecordRepository.cs:153-159
return await FreeSql.Select<TransactionRecord>()
    .Where(t => !string.IsNullOrEmpty(t.Classify))
    .Distinct()
    .ToListAsync(t => t.Classify);

4. 按邮件相关查询

4.1 获取指定邮件的交易记录列表

/// 位置: TransactionRecordRepository.cs:161-167
return await FreeSql.Select<TransactionRecord>()
    .Where(t => t.EmailMessageId == emailMessageId)
    .OrderBy(t => t.OccurredAt)
    .ToListAsync();

4.2 获取指定邮件的交易记录数量

/// 位置: TransactionRecordRepository.cs:169-174
return (int)await FreeSql.Select<TransactionRecord>()
    .Where(t => t.EmailMessageId == emailMessageId)
    .CountAsync();

5. 未分类账单查询

5.1 获取未分类的账单列表

/// 位置: 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 根据关键词查询已分类的账单

/// 位置: 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 获取待确认分类的账单列表

/// 位置: TransactionRecordRepository.cs:206-212
return await FreeSql.Select<TransactionRecord>()
    .Where(t => t.UnconfirmedClassify != null && t.UnconfirmedClassify != "")
    .OrderByDescending(t => t.OccurredAt)
    .ToListAsync();

8. 批量更新操作

8.1 按摘要批量更新交易记录的分类

/// 位置: 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 更新分类名称

/// 位置: 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 确认待确认的分类

/// 位置: 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. 获取预算当前金额

/// 位置: 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. 检查分类是否被使用

/// 位置: 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

/// 位置: SmartHandleService.cs:351
queryResults = await transactionRepository.ExecuteDynamicSqlAsync(sqlText);

说明: 此方法接收AI生成的SQL语句并执行SQL内容由AI根据用户问题动态生成例如

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. 获取归档摘要 - 年度交易统计

/// 位置: 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. 获取归档摘要 - 月度交易统计

/// 位置: 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. 获取按分类分组的交易金额(用于存款预算计算)

/// 位置: 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查询都遵循以下模式

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使用