2026-01-28 11:19:23 +08:00
|
|
|
|
namespace Service.Transaction;
|
2026-01-28 10:58:15 +08:00
|
|
|
|
|
|
|
|
|
|
public interface ITransactionStatisticsService
|
|
|
|
|
|
{
|
|
|
|
|
|
Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsAsync(int year, int month, string? savingClassify = null);
|
|
|
|
|
|
|
|
|
|
|
|
Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate, string? savingClassify = null);
|
|
|
|
|
|
|
|
|
|
|
|
Task<MonthlyStatistics> GetMonthlyStatisticsAsync(int year, int month);
|
|
|
|
|
|
|
2026-02-10 17:49:19 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 按日期范围获取汇总统计数据(新统一接口)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
Task<MonthlyStatistics> GetSummaryByRangeAsync(DateTime startDate, DateTime endDate);
|
|
|
|
|
|
|
2026-01-28 10:58:15 +08:00
|
|
|
|
Task<List<CategoryStatistics>> GetCategoryStatisticsAsync(int year, int month, TransactionType type);
|
|
|
|
|
|
|
2026-02-09 19:25:51 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 按日期范围获取分类统计数据
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
Task<List<CategoryStatistics>> GetCategoryStatisticsByDateRangeAsync(DateTime startDate, DateTime endDate, TransactionType type);
|
|
|
|
|
|
|
2026-01-28 10:58:15 +08:00
|
|
|
|
Task<List<TrendStatistics>> GetTrendStatisticsAsync(int startYear, int startMonth, int monthCount);
|
|
|
|
|
|
|
|
|
|
|
|
Task<(List<ReasonGroupDto> list, int total)> GetReasonGroupsAsync(int pageIndex = 1, int pageSize = 20);
|
|
|
|
|
|
|
|
|
|
|
|
Task<List<(TransactionRecord record, double relevanceScore)>> GetClassifiedByKeywordsWithScoreAsync(List<string> keywords, double minMatchRate = 0.3, int limit = 10);
|
|
|
|
|
|
|
|
|
|
|
|
Task<Dictionary<DateTime, decimal>> GetFilteredTrendStatisticsAsync(
|
|
|
|
|
|
DateTime startDate,
|
|
|
|
|
|
DateTime endDate,
|
|
|
|
|
|
TransactionType type,
|
|
|
|
|
|
IEnumerable<string> classifies,
|
|
|
|
|
|
bool groupByMonth = false);
|
|
|
|
|
|
|
|
|
|
|
|
Task<Dictionary<(string, TransactionType), decimal>> GetAmountGroupByClassifyAsync(DateTime startTime, DateTime endTime);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public class TransactionStatisticsService(
|
|
|
|
|
|
ITransactionRecordRepository transactionRepository
|
|
|
|
|
|
) : ITransactionStatisticsService
|
|
|
|
|
|
{
|
|
|
|
|
|
public async Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsAsync(int year, int month, string? savingClassify = null)
|
|
|
|
|
|
{
|
2026-02-01 10:27:04 +08:00
|
|
|
|
// 当 month=0 时,表示查询整年数据
|
|
|
|
|
|
DateTime startDate;
|
|
|
|
|
|
DateTime endDate;
|
|
|
|
|
|
|
|
|
|
|
|
if (month == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 查询整年:1月1日至12月31日
|
|
|
|
|
|
startDate = new DateTime(year, 1, 1);
|
|
|
|
|
|
endDate = new DateTime(year, 12, 31).AddDays(1); // 包含12月31日
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 查询指定月份
|
|
|
|
|
|
startDate = new DateTime(year, month, 1);
|
|
|
|
|
|
endDate = startDate.AddMonths(1);
|
|
|
|
|
|
}
|
2026-01-28 10:58:15 +08:00
|
|
|
|
|
|
|
|
|
|
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 transactionRepository.QueryAsync(
|
|
|
|
|
|
startDate: startDate,
|
|
|
|
|
|
endDate: endDate,
|
|
|
|
|
|
pageSize: int.MaxValue);
|
|
|
|
|
|
|
|
|
|
|
|
return 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<MonthlyStatistics> GetMonthlyStatisticsAsync(int year, int month)
|
|
|
|
|
|
{
|
|
|
|
|
|
var records = await transactionRepository.QueryAsync(
|
|
|
|
|
|
year: year,
|
|
|
|
|
|
month: month,
|
|
|
|
|
|
pageSize: int.MaxValue);
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 17:49:19 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 按日期范围获取汇总统计数据(新统一接口)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<MonthlyStatistics> GetSummaryByRangeAsync(DateTime startDate, DateTime endDate)
|
|
|
|
|
|
{
|
|
|
|
|
|
var records = await transactionRepository.QueryAsync(
|
|
|
|
|
|
startDate: startDate,
|
|
|
|
|
|
endDate: endDate,
|
|
|
|
|
|
pageSize: int.MaxValue);
|
|
|
|
|
|
|
|
|
|
|
|
var statistics = new MonthlyStatistics
|
|
|
|
|
|
{
|
|
|
|
|
|
Year = startDate.Year,
|
|
|
|
|
|
Month = startDate.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-28 10:58:15 +08:00
|
|
|
|
public async Task<List<CategoryStatistics>> GetCategoryStatisticsAsync(int year, int month, TransactionType type)
|
|
|
|
|
|
{
|
|
|
|
|
|
var records = await transactionRepository.QueryAsync(
|
|
|
|
|
|
year: year,
|
|
|
|
|
|
month: month,
|
|
|
|
|
|
type: type,
|
|
|
|
|
|
pageSize: int.MaxValue);
|
2026-02-09 19:25:51 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 按日期范围获取分类统计数据
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task<List<CategoryStatistics>> GetCategoryStatisticsByDateRangeAsync(DateTime startDate, DateTime endDate, TransactionType type)
|
|
|
|
|
|
{
|
|
|
|
|
|
var records = await transactionRepository.QueryAsync(
|
|
|
|
|
|
startDate: startDate,
|
|
|
|
|
|
endDate: endDate,
|
|
|
|
|
|
type: type,
|
|
|
|
|
|
pageSize: int.MaxValue);
|
2026-01-28 10:58:15 +08:00
|
|
|
|
|
|
|
|
|
|
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>();
|
|
|
|
|
|
|
2026-01-28 17:00:58 +08:00
|
|
|
|
for (var i = 0; i < monthCount; i++)
|
2026-01-28 10:58:15 +08:00
|
|
|
|
{
|
|
|
|
|
|
var targetYear = startYear;
|
|
|
|
|
|
var targetMonth = startMonth + i;
|
|
|
|
|
|
|
|
|
|
|
|
while (targetMonth > 12)
|
|
|
|
|
|
{
|
|
|
|
|
|
targetMonth -= 12;
|
|
|
|
|
|
targetYear++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var records = await transactionRepository.QueryAsync(
|
|
|
|
|
|
year: targetYear,
|
|
|
|
|
|
month: targetMonth,
|
|
|
|
|
|
pageSize: int.MaxValue);
|
|
|
|
|
|
|
|
|
|
|
|
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<ReasonGroupDto> list, int total)> GetReasonGroupsAsync(int pageIndex = 1, int pageSize = 20)
|
|
|
|
|
|
{
|
|
|
|
|
|
var records = await transactionRepository.QueryAsync(
|
|
|
|
|
|
pageSize: int.MaxValue);
|
|
|
|
|
|
|
|
|
|
|
|
var unclassifiedRecords = records
|
|
|
|
|
|
.Where(t => !string.IsNullOrEmpty(t.Reason) && string.IsNullOrEmpty(t.Classify))
|
|
|
|
|
|
.GroupBy(t => t.Reason)
|
|
|
|
|
|
.Select(g => new
|
|
|
|
|
|
{
|
|
|
|
|
|
Reason = g.Key,
|
|
|
|
|
|
Count = g.Count(),
|
|
|
|
|
|
TotalAmount = g.Sum(r => r.Amount),
|
|
|
|
|
|
SampleType = g.First().Type,
|
|
|
|
|
|
SampleClassify = g.First().Classify,
|
|
|
|
|
|
TransactionIds = g.Select(r => r.Id).ToList()
|
|
|
|
|
|
})
|
|
|
|
|
|
.OrderByDescending(g => Math.Abs(g.TotalAmount))
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
var total = unclassifiedRecords.Count;
|
|
|
|
|
|
var pagedGroups = unclassifiedRecords
|
|
|
|
|
|
.Skip((pageIndex - 1) * pageSize)
|
|
|
|
|
|
.Take(pageSize)
|
|
|
|
|
|
.Select(g => new ReasonGroupDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Reason = g.Reason,
|
|
|
|
|
|
Count = g.Count,
|
|
|
|
|
|
SampleType = g.SampleType,
|
|
|
|
|
|
SampleClassify = g.SampleClassify,
|
|
|
|
|
|
TransactionIds = g.TransactionIds,
|
|
|
|
|
|
TotalAmount = Math.Abs(g.TotalAmount)
|
|
|
|
|
|
})
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
return (pagedGroups, total);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<List<(TransactionRecord record, double relevanceScore)>> GetClassifiedByKeywordsWithScoreAsync(List<string> keywords, double minMatchRate = 0.3, int limit = 10)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (keywords.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var candidates = await transactionRepository.GetClassifiedByKeywordsAsync(keywords, limit: int.MaxValue);
|
|
|
|
|
|
|
|
|
|
|
|
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<Dictionary<DateTime, decimal>> GetFilteredTrendStatisticsAsync(
|
|
|
|
|
|
DateTime startDate,
|
|
|
|
|
|
DateTime endDate,
|
|
|
|
|
|
TransactionType type,
|
|
|
|
|
|
IEnumerable<string> classifies,
|
|
|
|
|
|
bool groupByMonth = false)
|
|
|
|
|
|
{
|
|
|
|
|
|
var records = await transactionRepository.QueryAsync(
|
|
|
|
|
|
startDate: startDate,
|
|
|
|
|
|
endDate: endDate,
|
|
|
|
|
|
type: type,
|
2026-01-28 17:00:58 +08:00
|
|
|
|
classifies: classifies.ToArray(),
|
2026-01-28 10:58:15 +08:00
|
|
|
|
pageSize: int.MaxValue);
|
|
|
|
|
|
|
|
|
|
|
|
if (groupByMonth)
|
|
|
|
|
|
{
|
|
|
|
|
|
return records
|
|
|
|
|
|
.GroupBy(t => new DateTime(t.OccurredAt.Year, t.OccurredAt.Month, 1))
|
|
|
|
|
|
.ToDictionary(g => g.Key, g => g.Sum(x => Math.Abs(x.Amount)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return records
|
|
|
|
|
|
.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 records = await transactionRepository.QueryAsync(
|
|
|
|
|
|
startDate: startTime,
|
|
|
|
|
|
endDate: endTime,
|
|
|
|
|
|
pageSize: int.MaxValue);
|
|
|
|
|
|
|
|
|
|
|
|
return records
|
|
|
|
|
|
.GroupBy(t => new { t.Classify, t.Type })
|
|
|
|
|
|
.ToDictionary(g => (g.Key.Classify, g.Key.Type), g => g.Sum(t => t.Amount));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public record 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 List<long> TransactionIds { get; set; } = [];
|
|
|
|
|
|
|
|
|
|
|
|
public decimal TotalAmount { get; set; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public record 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 record CategoryStatistics
|
|
|
|
|
|
{
|
|
|
|
|
|
public string Classify { get; set; } = string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
public decimal Amount { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
public int Count { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
public decimal Percent { get; set; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public record 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; }
|
|
|
|
|
|
}
|