添加功能
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 29s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s

This commit is contained in:
孙诚
2025-12-29 20:30:15 +08:00
parent a13e1fe9e8
commit 0d94276a0d
22 changed files with 560706 additions and 138 deletions

View File

@@ -16,6 +16,11 @@ public interface IBaseRepository<T> where T : BaseEntity
/// </summary>
Task<T?> GetByIdAsync(long id);
/// <summary>
/// 根据ID获取单条数据
/// </summary>
Task<T[]> GetByIdsAsync(long[] ids);
/// <summary>
/// 添加数据
/// </summary>
@@ -76,6 +81,19 @@ public abstract class BaseRepository<T>(IFreeSql freeSql) : IBaseRepository<T> w
}
}
public virtual async Task<T[]> GetByIdsAsync(long[] ids)
{
try
{
var result = await FreeSql.Select<T>().Where(x => ids.Contains(x.Id)).ToListAsync();
return result.ToArray();
}
catch
{
return [];
}
}
public virtual async Task<bool> AddAsync(T entity)
{
try

View File

@@ -149,6 +149,23 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
/// <param name="completeSql">完整的SELECT SQL语句</param>
/// <returns>动态查询结果列表</returns>
Task<List<dynamic>> ExecuteDynamicSqlAsync(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);
}
public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<TransactionRecord>(freeSql), ITransactionRecordRepository
@@ -344,7 +361,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
public async Task<(List<ReasonGroupDto> list, int total)> GetReasonGroupsAsync(int pageIndex = 1, int pageSize = 20)
{
// 先按照Reason分组统计每个Reason的数量
// 先按照Reason分组统计每个Reason的数量和总金额
var groups = await FreeSql.Select<TransactionRecord>()
.Where(t => !string.IsNullOrEmpty(t.Reason))
.Where(t => string.IsNullOrEmpty(t.Classify)) // 只统计未分类的
@@ -352,11 +369,12 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
.ToListAsync(g => new
{
Reason = g.Key,
Count = g.Count()
Count = g.Count(),
TotalAmount = g.Sum(g.Value.Amount)
});
// 按数量降序排序
var sortedGroups = groups.OrderByDescending(g => g.Count).ToList();
// 按总金额绝对值降序排序
var sortedGroups = groups.OrderByDescending(g => Math.Abs(g.TotalAmount)).ToList();
var total = sortedGroups.Count;
// 分页
@@ -365,22 +383,27 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
.Take(pageSize)
.ToList();
// 为每个分组获取示例记录
// 为每个分组获取详细信息
var result = new List<ReasonGroupDto>();
foreach (var group in pagedGroups)
{
var sample = await FreeSql.Select<TransactionRecord>()
// 获取该分组的所有记录
var records = await FreeSql.Select<TransactionRecord>()
.Where(t => t.Reason == group.Reason)
.FirstAsync();
.Where(t => string.IsNullOrEmpty(t.Classify))
.ToListAsync();
if (sample != null)
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
SampleClassify = sample.Classify ?? string.Empty,
TransactionIds = records.Select(r => r.Id).ToList(),
TotalAmount = Math.Abs(group.TotalAmount)
});
}
}
@@ -542,6 +565,68 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
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;
}
}
/// <summary>
@@ -568,6 +653,16 @@ public class ReasonGroupDto
/// 示例分类(该分组中第一条记录的分类)
/// </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>