diff --git a/Common/GlobalUsings.cs b/Common/GlobalUsings.cs new file mode 100644 index 0000000..6ac2b58 --- /dev/null +++ b/Common/GlobalUsings.cs @@ -0,0 +1,3 @@ +global using System.Reflection; +global using System.Text.Json; +global using Microsoft.Extensions.DependencyInjection; \ No newline at end of file diff --git a/Common/ServiceExtension.cs b/Common/ServiceExtension.cs index 2169b30..7a8bb18 100644 --- a/Common/ServiceExtension.cs +++ b/Common/ServiceExtension.cs @@ -1,7 +1,4 @@ -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; - -namespace Common; +namespace Common; public static class TypeExtensions { @@ -10,8 +7,8 @@ public static class TypeExtensions /// public static T? DeepClone(this T source) { - var json = System.Text.Json.JsonSerializer.Serialize(source); - return System.Text.Json.JsonSerializer.Deserialize(json); + var json = JsonSerializer.Serialize(source); + return JsonSerializer.Deserialize(json); } } @@ -41,7 +38,7 @@ public static class ServiceExtension private static void RegisterServices(IServiceCollection services, Assembly assembly) { var types = assembly.GetTypes() - .Where(t => t.IsClass && !t.IsAbstract); + .Where(t => t is { IsClass: true, IsAbstract: false }); foreach (var type in types) { @@ -71,14 +68,13 @@ public static class ServiceExtension private static void RegisterRepositories(IServiceCollection services, Assembly assembly) { var types = assembly.GetTypes() - .Where(t => t.IsClass && !t.IsAbstract); + .Where(t => t is { IsClass: true, IsAbstract: false }); foreach (var type in types) { var interfaces = type.GetInterfaces() .Where(i => i.Name.StartsWith("I") - && i.Namespace == "Repository" - && !i.IsGenericType); // 排除泛型接口如 IBaseRepository + && i is { Namespace: "Repository", IsGenericType: false }); // 排除泛型接口如 IBaseRepository foreach (var @interface in interfaces) { diff --git a/Directory.Packages.props b/Directory.Packages.props index 945c95f..5485773 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,6 +3,7 @@ + diff --git a/EmailBill.sln.DotSettings b/EmailBill.sln.DotSettings index aaf5c25..0298694 100644 --- a/EmailBill.sln.DotSettings +++ b/EmailBill.sln.DotSettings @@ -1,2 +1,3 @@  - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/Entity/EmailMessage.cs b/Entity/EmailMessage.cs index 9d72176..53b0923 100644 --- a/Entity/EmailMessage.cs +++ b/Entity/EmailMessage.cs @@ -1,6 +1,4 @@ -using System.Security.Cryptography; - -namespace Entity; +namespace Entity; /// /// 邮件消息实体 @@ -39,7 +37,7 @@ public class EmailMessage : BaseEntity public string ComputeBodyHash() { using var md5 = MD5.Create(); - var inputBytes = System.Text.Encoding.UTF8.GetBytes(Body + HtmlBody); + var inputBytes = Encoding.UTF8.GetBytes(Body + HtmlBody); var hashBytes = md5.ComputeHash(inputBytes); return Convert.ToHexString(hashBytes); } diff --git a/Entity/GlobalUsings.cs b/Entity/GlobalUsings.cs index 4303997..72735c1 100644 --- a/Entity/GlobalUsings.cs +++ b/Entity/GlobalUsings.cs @@ -1 +1,3 @@ -global using FreeSql.DataAnnotations; \ No newline at end of file +global using FreeSql.DataAnnotations; +global using System.Security.Cryptography; +global using System.Text; \ No newline at end of file diff --git a/Entity/PushSubscriptionEntity.cs b/Entity/PushSubscription.cs similarity index 98% rename from Entity/PushSubscriptionEntity.cs rename to Entity/PushSubscription.cs index a204ac3..a6dd15e 100644 --- a/Entity/PushSubscriptionEntity.cs +++ b/Entity/PushSubscription.cs @@ -12,6 +12,6 @@ public class PushSubscription : BaseEntity public string? Auth { get; set; } public string? UserId { get; set; } // Optional: if you have user authentication - + public string? UserAgent { get; set; } } diff --git a/Repository/BaseRepository.cs b/Repository/BaseRepository.cs index 9e90d9d..1e03bd6 100644 --- a/Repository/BaseRepository.cs +++ b/Repository/BaseRepository.cs @@ -170,10 +170,10 @@ public abstract class BaseRepository(IFreeSql freeSql) : IBaseRepository w var dt = await FreeSql.Ado.ExecuteDataTableAsync(completeSql); var result = new List(); - foreach (System.Data.DataRow row in dt.Rows) + foreach (DataRow row in dt.Rows) { - var expando = new System.Dynamic.ExpandoObject() as IDictionary; - foreach (System.Data.DataColumn column in dt.Columns) + var expando = new ExpandoObject() as IDictionary; + foreach (DataColumn column in dt.Columns) { expando[column.ColumnName] = row[column]; } diff --git a/Repository/GlobalUsings.cs b/Repository/GlobalUsings.cs index 4ede061..defdaac 100644 --- a/Repository/GlobalUsings.cs +++ b/Repository/GlobalUsings.cs @@ -1,5 +1,6 @@  global using Entity; -global using FreeSql; global using System.Linq; +global using System.Data; +global using System.Dynamic; diff --git a/Repository/TransactionRecordRepository.cs b/Repository/TransactionRecordRepository.cs index 3578cac..ad8d0c0 100644 --- a/Repository/TransactionRecordRepository.cs +++ b/Repository/TransactionRecordRepository.cs @@ -259,7 +259,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository t.Reason == reason); // 按分类筛选 - if (classifies != null && classifies.Length > 0) + if (classifies is { Length: > 0 }) { var filterClassifies = classifies.Select(c => c == "未分类" ? string.Empty : c).ToList(); query = query.Where(t => filterClassifies.Contains(t.Classify)); @@ -290,15 +290,13 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository t.OccurredAt) - .OrderByDescending(t => t.Id) - .Page(pageIndex, pageSize) - .ToListAsync(); - } + + // 按时间降序排列 + return await query + .OrderByDescending(t => t.OccurredAt) + .OrderByDescending(t => t.Id) + .Page(pageIndex, pageSize) + .ToListAsync(); } public async Task GetTotalCountAsync( @@ -323,7 +321,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository t.Reason == reason); // 按分类筛选 - if (classifies != null && classifies.Length > 0) + if (classifies is { Length: > 0 }) { var filterClassifies = classifies.Select(c => c == "未分类" ? string.Empty : c).ToList(); query = query.Where(t => filterClassifies.Contains(t.Classify)); @@ -471,7 +469,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository r.Id).ToList(), @@ -615,9 +613,9 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository> GetClassifiedByKeywordsAsync(List keywords, int limit = 10) { - if (keywords == null || keywords.Count == 0) + if (keywords.Count == 0) { - return new List(); + return []; } var query = FreeSql.Select() @@ -637,9 +635,9 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository> GetClassifiedByKeywordsWithScoreAsync(List keywords, double minMatchRate = 0.3, int limit = 10) { - if (keywords == null || keywords.Count == 0) + if (keywords.Count == 0) { - return new List<(TransactionRecord, double)>(); + return []; } // 查询所有已分类且包含任意关键词的账单 @@ -687,7 +685,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository(); + return []; } var list = await FreeSql.Select() @@ -740,7 +738,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository() .Where(t => t.OccurredAt >= startDate && t.OccurredAt <= endDate && t.Type == type); - if (classifies != null && classifies.Any()) + if (classifies.Any()) { query = query.Where(t => classifies.Contains(t.Classify)); } @@ -753,12 +751,10 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository new DateTime(t.OccurredAt.Year, t.OccurredAt.Month, 1)) .ToDictionary(g => g.Key, g => g.Sum(x => Math.Abs(x.Amount))); } - else - { - return list - .GroupBy(t => t.OccurredAt.Date) - .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))); } } @@ -790,7 +786,7 @@ public class ReasonGroupDto /// /// 该分组的所有账单ID列表 /// - public List TransactionIds { get; set; } = new(); + public List TransactionIds { get; set; } = []; /// /// 该分组的总金额(绝对值) @@ -804,12 +800,19 @@ public class ReasonGroupDto 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; } } @@ -819,8 +822,11 @@ public class MonthlyStatistics 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; } } @@ -830,8 +836,12 @@ public class CategoryStatistics 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; } } \ No newline at end of file diff --git a/Service/AppSettingModel/AISettings.cs b/Service/AppSettingModel/AISettings.cs index f83b986..ad81805 100644 --- a/Service/AppSettingModel/AISettings.cs +++ b/Service/AppSettingModel/AISettings.cs @@ -1,6 +1,6 @@ namespace Service.AppSettingModel; -public class AISettings +public class AiSettings { public string Endpoint { get; set; } = string.Empty; public string Key { get; set; } = string.Empty; diff --git a/Service/BudgetService.cs b/Service/BudgetService.cs index f068114..f4ef175 100644 --- a/Service/BudgetService.cs +++ b/Service/BudgetService.cs @@ -1,4 +1,6 @@ -namespace Service; +using JetBrains.Annotations; + +namespace Service; public interface IBudgetService { @@ -24,6 +26,7 @@ public interface IBudgetService Task GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type); } +[UsedImplicitly] public class BudgetService( IBudgetRepository budgetRepository, IBudgetArchiveRepository budgetArchiveRepository, @@ -79,20 +82,22 @@ public class BudgetService( } // 创造虚拟的存款预算 - dtos.Add(await GetVirtualSavingsDtoAsync( + dtos.Add(await GetSavingsDtoAsync( BudgetPeriodType.Month, referenceDate, budgets)); - dtos.Add(await GetVirtualSavingsDtoAsync( + dtos.Add(await GetSavingsDtoAsync( BudgetPeriodType.Year, referenceDate, budgets)); dtos = dtos + .Where(x => x != null) + .Cast() .OrderByDescending(x => x.IsMandatoryExpense) .ThenBy(x => x.Type) .ThenByDescending(x => x.Current) - .ToList(); + .ToList()!; return [.. dtos.Where(dto => dto != null).Cast()]; } @@ -100,7 +105,7 @@ public class BudgetService( public async Task GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type) { var referenceDate = new DateTime(year, month, 1); - return await GetVirtualSavingsDtoAsync(type, referenceDate); + return await GetSavingsDtoAsync(type, referenceDate); } public async Task GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate) @@ -128,7 +133,7 @@ public class BudgetService( _ => TransactionType.None }; - if (transactionType == TransactionType.None) return new List(); + if (transactionType == TransactionType.None) return []; // 1. 获取所有预算 var budgets = (await budgetRepository.GetAllAsync()).ToList(); @@ -205,7 +210,7 @@ public class BudgetService( totalLimit += itemLimit; // 当前值累加 - var selectedCategories = budget.SelectedCategories != null ? string.Join(',', budget.SelectedCategories) : string.Empty; + var selectedCategories = string.Join(',', budget.SelectedCategories); var currentAmount = await CalculateCurrentAmountAsync(new() { Name = budget.Name, @@ -246,7 +251,7 @@ public class BudgetService( if (transactionType != TransactionType.None) { - var hasGlobalBudget = relevant.Any(b => b.SelectedCategories == null || b.SelectedCategories.Length == 0); + var hasGlobalBudget = relevant.Any(b => b.SelectedCategories.Length == 0); var allClassifies = hasGlobalBudget ? [] @@ -256,7 +261,7 @@ public class BudgetService( .ToList(); DateTime startDate, endDate; - bool groupByMonth = false; + bool groupByMonth; if (statType == BudgetPeriodType.Month) { @@ -445,7 +450,7 @@ public class BudgetService( .Where(t => { var dict = (IDictionary)t; - var classify = dict["Classify"]?.ToString() ?? ""; + var classify = dict["Classify"].ToString() ?? ""; var type = Convert.ToInt32(dict["Type"]); return type == 0 && !budgetedCategories.Contains(classify); }) @@ -551,7 +556,8 @@ public class BudgetService( // 返回实际消费和硬性消费累加中的较大值 return mandatoryAccumulation; } - else if (budget.Type == BudgetPeriodType.Year) + + if (budget.Type == BudgetPeriodType.Year) { // 计算本年的天数(考虑闰年) var daysInYear = DateTime.IsLeapYear(referenceDate.Year) ? 366 : 365; @@ -592,7 +598,7 @@ public class BudgetService( return (start, end); } - private async Task GetVirtualSavingsDtoAsync( + private async Task GetSavingsDtoAsync( BudgetPeriodType periodType, DateTime? referenceDate = null, IEnumerable? existingBudgets = null) @@ -657,7 +663,7 @@ public class BudgetService( if (b.Category == BudgetCategory.Savings) continue; processedIds.Add(b.Id); - decimal factor = 1.0m; + decimal factor; decimal historicalAmount = 0m; var historicalMonths = new List(); @@ -759,7 +765,6 @@ public class BudgetService( foreach (var group in deletedBudgets) { - var budgetId = group.Key; var months = group.Select(g => g.Key.Month).OrderBy(m => m).ToList(); var totalLimit = group.Sum(g => g.Value.HistoricalLimit); var (_, category, name) = group.First().Value; @@ -862,12 +867,12 @@ public class BudgetService( """); - foreach (var (Name, Amount) in noLimitIncomeItems) + foreach (var (name, amount) in noLimitIncomeItems) { description.Append($""" - {Name} - {Amount:N0} + {name} + {amount:N0} """); } @@ -953,12 +958,12 @@ public class BudgetService( """); - foreach (var (Name, Amount) in noLimitExpenseItems) + foreach (var (name, amount) in noLimitExpenseItems) { description.Append($""" - {Name} - {Amount:N0} + {name} + {amount:N0} """); } @@ -1080,13 +1085,13 @@ public record BudgetResult public decimal Limit { get; set; } public decimal Current { get; set; } public BudgetCategory Category { get; set; } - public string[] SelectedCategories { get; set; } = Array.Empty(); + public string[] SelectedCategories { get; set; } = []; public string StartDate { get; set; } = string.Empty; public string Period { get; set; } = string.Empty; public DateTime? PeriodStart { get; set; } public DateTime? PeriodEnd { get; set; } - public bool NoLimit { get; set; } = false; - public bool IsMandatoryExpense { get; set; } = false; + public bool NoLimit { get; set; } + public bool IsMandatoryExpense { get; set; } public string Description { get; set; } = string.Empty; public static BudgetResult FromEntity( @@ -1107,7 +1112,7 @@ public record BudgetResult Current = currentAmount, Category = entity.Category, SelectedCategories = string.IsNullOrEmpty(entity.SelectedCategories) - ? Array.Empty() + ? [] : entity.SelectedCategories.Split(','), StartDate = entity.StartDate.ToString("yyyy-MM-dd"), Period = entity.Type switch @@ -1157,7 +1162,7 @@ public class BudgetStatsDto /// /// 每日/每月累计金额趋势(对应当前周期内的实际发生额累计值) /// - public List Trend { get; set; } = new(); + public List Trend { get; set; } = []; } /// @@ -1175,6 +1180,7 @@ public class BudgetCategoryStats /// public BudgetStatsDto Year { get; set; } = new(); } + public class UncoveredCategoryDetail { public string Category { get; set; } = string.Empty; diff --git a/Service/EmailServices/EmailHandleService.cs b/Service/EmailServices/EmailHandleService.cs index d4d52de..17652b7 100644 --- a/Service/EmailServices/EmailHandleService.cs +++ b/Service/EmailServices/EmailHandleService.cs @@ -65,7 +65,7 @@ public class EmailHandleService( await messageService.AddAsync( "邮件解析失败", $"来自 {from} 发送给 {to} 的邮件(主题:{subject})未能成功解析内容,可能格式已变更或不受支持。", - url: $"/balance?tab=email" + url: "/balance?tab=email" ); logger.LogWarning("未能成功解析邮件内容,跳过账单处理"); return true; diff --git a/Service/EmailServices/EmailParse/EmailParseFormCCSVC.cs b/Service/EmailServices/EmailParse/EmailParseFormCCSVC.cs index 0f04625..e2cfd6a 100644 --- a/Service/EmailServices/EmailParse/EmailParseFormCCSVC.cs +++ b/Service/EmailServices/EmailParse/EmailParseFormCCSVC.cs @@ -2,8 +2,8 @@ namespace Service.EmailParseServices; -public class EmailParseFormCCSVC( - ILogger logger, +public class EmailParseFormCcsvc( + ILogger logger, IOpenAiService openAiService ) : EmailParseServicesBase(logger, openAiService) { @@ -47,7 +47,7 @@ public class EmailParseFormCCSVC( if (dateNode == null) { logger.LogWarning("Date node not found"); - return Array.Empty<(string, string, decimal, decimal, TransactionType, DateTime?)>(); + return []; } var dateText = dateNode.InnerText.Trim(); @@ -56,7 +56,7 @@ public class EmailParseFormCCSVC( if (!dateMatch.Success || !DateTime.TryParse(dateMatch.Value, out var date)) { logger.LogWarning("Failed to parse date from: {DateText}", dateText); - return Array.Empty<(string, string, decimal, decimal, TransactionType, DateTime?)>(); + return []; } // 2. Get Balance (Available Limit) @@ -122,7 +122,7 @@ public class EmailParseFormCCSVC( descText = HtmlEntity.DeEntitize(descText).Replace((char)160, ' ').Trim(); // Parse Description: "尾号4390 消费 财付通-luckincoffee瑞幸咖啡" - var parts = descText.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + var parts = descText.Split([' '], StringSplitOptions.RemoveEmptyEntries); string card = ""; string reason = descText; diff --git a/Service/EmailServices/EmailParse/IEmailParseServices.cs b/Service/EmailServices/EmailParse/IEmailParseServices.cs index cd79c3d..c79597a 100644 --- a/Service/EmailServices/EmailParse/IEmailParseServices.cs +++ b/Service/EmailServices/EmailParse/IEmailParseServices.cs @@ -201,7 +201,7 @@ public abstract class EmailParseServicesBase( // 收入关键词 string[] incomeKeywords = - { + [ "工资", "奖金", "退款", "返现", "收入", "转入", "存入", "利息", "分红", @@ -233,13 +233,13 @@ public abstract class EmailParseServicesBase( // 存取类 "现金存入", "柜台存入", "ATM存入", "他人转入", "他人汇入" - }; + ]; if (incomeKeywords.Any(k => lowerReason.Contains(k))) return TransactionType.Income; // 支出关键词 string[] expenseKeywords = - { + [ "消费", "支付", "购买", "转出", "取款", "支出", "扣款", "缴费", "付款", @@ -269,7 +269,7 @@ public abstract class EmailParseServicesBase( // 信用卡/花呗等场景 "信用卡还款", "花呗还款", "白条还款", "分期还款", "账单还款", "自动还款" - }; + ]; if (expenseKeywords.Any(k => lowerReason.Contains(k))) return TransactionType.Expense; diff --git a/Service/GlobalUsings.cs b/Service/GlobalUsings.cs index 2f64bb0..daeb865 100644 --- a/Service/GlobalUsings.cs +++ b/Service/GlobalUsings.cs @@ -7,10 +7,11 @@ global using System.Globalization; global using System.Text; global using System.Text.Json; global using Entity; -global using FreeSql; global using System.Linq; global using Service.AppSettingModel; global using System.Text.Json.Serialization; global using System.Text.Json.Nodes; global using Microsoft.Extensions.Configuration; -global using Common; \ No newline at end of file +global using Common; +global using System.Net; +global using System.Text.Encodings.Web; \ No newline at end of file diff --git a/Service/ImportService.cs b/Service/ImportService.cs index ceb1294..930968a 100644 --- a/Service/ImportService.cs +++ b/Service/ImportService.cs @@ -133,7 +133,7 @@ public class ImportService( return DateTime.MinValue; } - foreach (var format in DateTimeFormats) + foreach (var format in _dateTimeFormats) { if (DateTime.TryParseExact( row[key], @@ -288,7 +288,7 @@ public class ImportService( return DateTime.MinValue; } - foreach (var format in DateTimeFormats) + foreach (var format in _dateTimeFormats) { if (DateTime.TryParseExact( row[key], @@ -358,14 +358,13 @@ public class ImportService( { return await ParseCsvAsync(file); } - else if (fileExtension == ".xlsx" || fileExtension == ".xls") + + if (fileExtension == ".xlsx" || fileExtension == ".xls") { return await ParseExcelAsync(file); } - else - { - throw new NotSupportedException("不支持的文件格式"); - } + + throw new NotSupportedException("不支持的文件格式"); } private async Task[]> ParseCsvAsync(MemoryStream file) @@ -388,7 +387,7 @@ public class ImportService( if (headers == null || headers.Length == 0) { - return Array.Empty>(); + return []; } var result = new List>(); @@ -420,7 +419,7 @@ public class ImportService( if (worksheet == null || worksheet.Dimension == null) { - return Array.Empty>(); + return []; } var rowCount = worksheet.Dimension.End.Row; @@ -428,7 +427,7 @@ public class ImportService( if (rowCount < 2) { - return Array.Empty>(); + return []; } // 读取表头(第一行) @@ -458,7 +457,7 @@ public class ImportService( return await Task.FromResult(result.ToArray()); } - private static string[] DateTimeFormats = + private static string[] _dateTimeFormats = [ "yyyy-MM-dd", "yyyy-MM-dd HH", diff --git a/Service/LogCleanupService.cs b/Service/LogCleanupService.cs index 6d38ab2..8ab230f 100644 --- a/Service/LogCleanupService.cs +++ b/Service/LogCleanupService.cs @@ -68,8 +68,8 @@ public class LogCleanupService(ILogger logger) : BackgroundSe // 尝试解析日期 (格式: yyyyMMdd) if (DateTime.TryParseExact(dateStr, "yyyyMMdd", - System.Globalization.CultureInfo.InvariantCulture, - System.Globalization.DateTimeStyles.None, + CultureInfo.InvariantCulture, + DateTimeStyles.None, out var logDate)) { if (logDate < cutoffDate) diff --git a/Service/NotificationService.cs b/Service/NotificationService.cs index aa30a63..d1d8e54 100644 --- a/Service/NotificationService.cs +++ b/Service/NotificationService.cs @@ -1,11 +1,13 @@ -using WebPush; +using System.Net; +using WebPush; +using PushSubscription = Entity.PushSubscription; namespace Service; public interface INotificationService { Task GetVapidPublicKeyAsync(); - Task SubscribeAsync(Entity.PushSubscription subscription); + Task SubscribeAsync(PushSubscription subscription); Task SendNotificationAsync(string message, string? url = null); } @@ -32,7 +34,7 @@ public class NotificationService( return Task.FromResult(GetSettings().PublicKey); } - public async Task SubscribeAsync(Entity.PushSubscription subscription) + public async Task SubscribeAsync(PushSubscription subscription) { var existing = await subscriptionRepo.GetByEndpointAsync(subscription.Endpoint); if (existing != null) @@ -61,7 +63,7 @@ public class NotificationService( var webPushClient = new WebPushClient(); var subscriptions = await subscriptionRepo.GetAllAsync(); - var payload = System.Text.Json.JsonSerializer.Serialize(new + var payload = JsonSerializer.Serialize(new { title = "System Notification", body = message, @@ -78,7 +80,7 @@ public class NotificationService( } catch (WebPushException ex) { - if (ex.StatusCode == System.Net.HttpStatusCode.Gone || ex.StatusCode == System.Net.HttpStatusCode.NotFound) + if (ex.StatusCode == HttpStatusCode.Gone || ex.StatusCode == HttpStatusCode.NotFound) { await subscriptionRepo.DeleteAsync(sub.Id); } diff --git a/Service/OpenAiService.cs b/Service/OpenAiService.cs index 0571dbd..d74f3d9 100644 --- a/Service/OpenAiService.cs +++ b/Service/OpenAiService.cs @@ -11,7 +11,7 @@ public interface IOpenAiService } public class OpenAiService( - IOptions aiSettings, + IOptions aiSettings, ILogger logger ) : IOpenAiService { diff --git a/Service/Service.csproj b/Service/Service.csproj index 2ddb43e..a8a3a65 100644 --- a/Service/Service.csproj +++ b/Service/Service.csproj @@ -5,6 +5,7 @@ + diff --git a/Service/SmartHandleService.cs b/Service/SmartHandleService.cs index f325d76..0f40f76 100644 --- a/Service/SmartHandleService.cs +++ b/Service/SmartHandleService.cs @@ -143,7 +143,7 @@ public class SmartHandleService( chunkAction(("start", $"开始分类,共 {sampleRecords.Length} 条账单")); var classifyResults = new List<(string Reason, string Classify, TransactionType Type)>(); - var sendedIds = new HashSet(); + var sentIds = new HashSet(); // 将流解析逻辑提取为本地函数以减少嵌套 void HandleResult(GroupClassifyResult? result) @@ -154,16 +154,18 @@ public class SmartHandleService( if (group == null) return; foreach (var id in group.Ids) { - if (sendedIds.Add(id)) + if (!sentIds.Add(id)) { - var resultJson = JsonSerializer.Serialize(new - { - id, - result.Classify, - result.Type - }); - chunkAction(("data", resultJson)); + continue; } + + var resultJson = JsonSerializer.Serialize(new + { + id, + result.Classify, + result.Type + }); + chunkAction(("data", resultJson)); } } @@ -193,7 +195,7 @@ public class SmartHandleService( } catch (Exception exArr) { - logger.LogDebug(exArr, "按数组解析AI返回失败,回退到逐对象解析。预览: {Preview}", arrJson?.Length > 200 ? arrJson.Substring(0, 200) + "..." : arrJson); + logger.LogDebug(exArr, "按数组解析AI返回失败,回退到逐对象解析。预览: {Preview}", arrJson.Length > 200 ? arrJson.Substring(0, 200) + "..." : arrJson); } } } @@ -336,7 +338,7 @@ public class SmartHandleService( { content = $"""
-                    {System.Net.WebUtility.HtmlEncode(sqlText)}
+                    {WebUtility.HtmlEncode(sqlText)}
                     
""" }) @@ -361,7 +363,7 @@ public class SmartHandleService( var dataJson = JsonSerializer.Serialize(queryResults, new JsonSerializerOptions { WriteIndented = true, - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); var userPromptExtra = await configService.GetConfigByKeyAsync("BillAnalysisPrompt"); @@ -429,7 +431,6 @@ public class SmartHandleService( { // 获取所有分类 var categories = await categoryRepository.GetAllAsync(); - var categoryList = string.Join("、", categories.Select(c => $"{GetTypeName(c.Type)}-{c.Name}")); // 构建分类信息 var categoryInfo = new StringBuilder(); @@ -542,13 +543,13 @@ public class SmartHandleService( public record GroupClassifyResult { [JsonPropertyName("reason")] - public string Reason { get; set; } = string.Empty; + public string Reason { get; init; } = string.Empty; [JsonPropertyName("classify")] - public string? Classify { get; set; } + public string? Classify { get; init; } [JsonPropertyName("type")] - public TransactionType Type { get; set; } + public TransactionType Type { get; init; } } public record TransactionParseResult(string OccurredAt, string Classify, decimal Amount, string Reason, TransactionType Type); \ No newline at end of file diff --git a/Service/TextSegmentService.cs b/Service/TextSegmentService.cs index e7b1b27..8e6aa5e 100644 --- a/Service/TextSegmentService.cs +++ b/Service/TextSegmentService.cs @@ -1,8 +1,7 @@ -namespace Service; - +using JiebaNet.Analyser; using JiebaNet.Segmenter; -using JiebaNet.Analyser; -using Microsoft.Extensions.Logging; + +namespace Service; /// /// 文本分词服务接口 @@ -78,7 +77,7 @@ public class TextSegmentService : ITextSegmentService { if (string.IsNullOrWhiteSpace(text)) { - return new List(); + return []; } try @@ -119,7 +118,7 @@ public class TextSegmentService : ITextSegmentService { _logger.LogError(ex, "提取关键词失败,文本: {Text}", text); // 降级处理:返回原文 - return new List { text.Length > 10 ? text.Substring(0, 10) : text }; + return [text.Length > 10 ? text.Substring(0, 10) : text]; } } @@ -127,7 +126,7 @@ public class TextSegmentService : ITextSegmentService { if (string.IsNullOrWhiteSpace(text)) { - return new List(); + return []; } try @@ -146,7 +145,7 @@ public class TextSegmentService : ITextSegmentService catch (Exception ex) { _logger.LogError(ex, "分词失败,文本: {Text}", text); - return new List { text }; + return [text]; } } } diff --git a/Service/TransactionPeriodicService.cs b/Service/TransactionPeriodicService.cs index 00de993..1334644 100644 --- a/Service/TransactionPeriodicService.cs +++ b/Service/TransactionPeriodicService.cs @@ -144,7 +144,7 @@ public class TransactionPeriodicService( var dayOfWeek = (int)today.DayOfWeek; // 0=Sunday, 1=Monday, ..., 6=Saturday var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(d => int.TryParse(d.Trim(), out var day) ? day : -1) - .Where(d => d >= 0 && d <= 6) + .Where(d => d is >= 0 and <= 6) .ToList(); return executeDays.Contains(dayOfWeek); @@ -160,7 +160,7 @@ public class TransactionPeriodicService( var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(d => int.TryParse(d.Trim(), out var day) ? day : -1) - .Where(d => d >= 1 && d <= 31) + .Where(d => d is >= 1 and <= 31) .ToList(); // 如果当前为月末,且配置中有大于当月天数的日期,则也执行 @@ -223,7 +223,7 @@ public class TransactionPeriodicService( var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(d => int.TryParse(d.Trim(), out var day) ? day : -1) - .Where(d => d >= 0 && d <= 6) + .Where(d => d is >= 0 and <= 6) .OrderBy(d => d) .ToList(); @@ -253,7 +253,7 @@ public class TransactionPeriodicService( var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(d => int.TryParse(d.Trim(), out var day) ? day : -1) - .Where(d => d >= 1 && d <= 31) + .Where(d => d is >= 1 and <= 31) .OrderBy(d => d) .ToList(); diff --git a/WebApi/Controllers/Dto/BudgetDto.cs b/WebApi/Controllers/Dto/BudgetDto.cs index 281686d..3a93833 100644 --- a/WebApi/Controllers/Dto/BudgetDto.cs +++ b/WebApi/Controllers/Dto/BudgetDto.cs @@ -6,7 +6,7 @@ public class CreateBudgetDto public BudgetPeriodType Type { get; set; } = BudgetPeriodType.Month; public decimal Limit { get; set; } public BudgetCategory Category { get; set; } - public string[] SelectedCategories { get; set; } = Array.Empty(); + public string[] SelectedCategories { get; set; } = []; public DateTime? StartDate { get; set; } public bool NoLimit { get; set; } = false; public bool IsMandatoryExpense { get; set; } = false; diff --git a/WebApi/Controllers/Dto/EmailMessageDto.cs b/WebApi/Controllers/Dto/EmailMessageDto.cs index 2990b00..592bf81 100644 --- a/WebApi/Controllers/Dto/EmailMessageDto.cs +++ b/WebApi/Controllers/Dto/EmailMessageDto.cs @@ -6,21 +6,28 @@ public class EmailMessageDto { public long Id { get; set; } + public string Subject { get; set; } = string.Empty; + public string From { get; set; } = string.Empty; + public string Body { get; set; } = string.Empty; + public string HtmlBody { get; set; } = string.Empty; + public DateTime ReceivedDate { get; set; } + public DateTime CreateTime { get; set; } + public DateTime? UpdateTime { get; set; } - + /// /// 已解析的账单数量 /// public int TransactionCount { get; set; } public string ToName { get; set; } = string.Empty; - + /// /// 从实体转换为DTO /// diff --git a/WebApi/Controllers/EmailMessageController.cs b/WebApi/Controllers/EmailMessageController.cs index 9ccafb5..f2b09f7 100644 --- a/WebApi/Controllers/EmailMessageController.cs +++ b/WebApi/Controllers/EmailMessageController.cs @@ -86,10 +86,8 @@ public class EmailMessageController( { return BaseResponse.Done(); } - else - { - return "删除邮件失败,邮件不存在".Fail(); - } + + return "删除邮件失败,邮件不存在".Fail(); } catch (Exception ex) { @@ -117,10 +115,8 @@ public class EmailMessageController( { return BaseResponse.Done(); } - else - { - return "重新分析失败".Fail(); - } + + return "重新分析失败".Fail(); } catch (Exception ex) { diff --git a/WebApi/Controllers/JobController.cs b/WebApi/Controllers/JobController.cs index 6b1b022..fa4cef8 100644 --- a/WebApi/Controllers/JobController.cs +++ b/WebApi/Controllers/JobController.cs @@ -1,4 +1,5 @@ using Quartz; +using Quartz.Impl.Matchers; namespace WebApi.Controllers; @@ -12,7 +13,7 @@ public class JobController(ISchedulerFactory schedulerFactory, ILogger.AnyGroup()); + var jobKeys = await scheduler.GetJobKeys(GroupMatcher.AnyGroup()); var jobStatuses = new List(); foreach (var jobKey in jobKeys) @@ -101,9 +102,13 @@ public class JobController(ISchedulerFactory schedulerFactory, ILogger logger) : ControllerBase var currentLog = new StringBuilder(); // 日志行开始的正则表达式 - var logStartPattern = new System.Text.RegularExpressions.Regex( + var logStartPattern = new Regex( @"^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2}\]" ); @@ -151,10 +153,10 @@ public class LogController(ILogger logger) : ControllerBase // 日志格式示例: [2025-12-29 16:38:45.730 +08:00] [INF] Message here // 使用正则表达式解析 // 使用 Singleline 模式使 '.' 可以匹配换行,这样 multi-line 消息可以被完整捕获。 - var match = System.Text.RegularExpressions.Regex.Match( + var match = Regex.Match( line, @"^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2})\] \[([A-Z]{2,5})\] ([\s\S]*)$", - System.Text.RegularExpressions.RegexOptions.Singleline + RegexOptions.Singleline ); if (match.Success) diff --git a/WebApi/Controllers/NotificationController.cs b/WebApi/Controllers/NotificationController.cs index 126e901..e2ba7e0 100644 --- a/WebApi/Controllers/NotificationController.cs +++ b/WebApi/Controllers/NotificationController.cs @@ -4,7 +4,7 @@ [Route("api/[controller]/[action]")] public class NotificationController(INotificationService notificationService) : ControllerBase { - [HttpGet()] + [HttpGet] public async Task> GetVapidPublicKey() { try diff --git a/WebApi/Controllers/TransactionCategoryController.cs b/WebApi/Controllers/TransactionCategoryController.cs index fe94ee8..a8c3818 100644 --- a/WebApi/Controllers/TransactionCategoryController.cs +++ b/WebApi/Controllers/TransactionCategoryController.cs @@ -85,10 +85,8 @@ public class TransactionCategoryController( { return category.Id.Ok(); } - else - { - return "创建分类失败".Fail(); - } + + return "创建分类失败".Fail(); } catch (Exception ex) { @@ -133,10 +131,8 @@ public class TransactionCategoryController( { return "更新分类成功".Ok(); } - else - { - return "更新分类失败".Fail(); - } + + return "更新分类失败".Fail(); } catch (Exception ex) { @@ -165,10 +161,8 @@ public class TransactionCategoryController( { return BaseResponse.Done(); } - else - { - return "删除分类失败,分类不存在".Fail(); - } + + return "删除分类失败,分类不存在".Fail(); } catch (Exception ex) { @@ -196,10 +190,8 @@ public class TransactionCategoryController( { return categories.Count.Ok(); } - else - { - return "批量创建分类失败".Fail(); - } + + return "批量创建分类失败".Fail(); } catch (Exception ex) { diff --git a/WebApi/Controllers/TransactionPeriodicController.cs b/WebApi/Controllers/TransactionPeriodicController.cs index 5fdad73..a447504 100644 --- a/WebApi/Controllers/TransactionPeriodicController.cs +++ b/WebApi/Controllers/TransactionPeriodicController.cs @@ -1,7 +1,5 @@ namespace WebApi.Controllers; -using Repository; - /// /// 周期性账单控制器 /// diff --git a/WebApi/Controllers/TransactionRecordController.cs b/WebApi/Controllers/TransactionRecordController.cs index 672ae90..d6f7297 100644 --- a/WebApi/Controllers/TransactionRecordController.cs +++ b/WebApi/Controllers/TransactionRecordController.cs @@ -1,8 +1,4 @@ -namespace WebApi.Controllers; - -using System.Text.Json; -using System.Text.Json.Nodes; -using Repository; +namespace WebApi.Controllers; [ApiController] [Route("api/[controller]/[action]")] @@ -183,10 +179,8 @@ public class TransactionRecordController( { return BaseResponse.Done(); } - else - { - return "创建交易记录失败".Fail(); - } + + return "创建交易记录失败".Fail(); } catch (Exception ex) { @@ -225,10 +219,8 @@ public class TransactionRecordController( { return BaseResponse.Done(); } - else - { - return "更新交易记录失败".Fail(); - } + + return "更新交易记录失败".Fail(); } catch (Exception ex) { @@ -250,10 +242,8 @@ public class TransactionRecordController( { return BaseResponse.Done(); } - else - { - return "删除交易记录失败,记录不存在".Fail(); - } + + return "删除交易记录失败,记录不存在".Fail(); } catch (Exception ex) { @@ -413,7 +403,7 @@ public class TransactionRecordController( return; } - await smartHandleService.AnalyzeBillAsync(request.UserInput, async (chunk) => + await smartHandleService.AnalyzeBillAsync(request.UserInput, async chunk => { await WriteEventAsync(chunk); }); @@ -500,7 +490,7 @@ public class TransactionRecordController( return; } - await smartHandleService.SmartClassifyAsync(request.TransactionIds.ToArray(), async (chunk) => + await smartHandleService.SmartClassifyAsync(request.TransactionIds.ToArray(), async chunk => { var (eventType, content) = chunk; await TrySetUnconfirmedAsync(eventType, content); @@ -550,7 +540,6 @@ public class TransactionRecordController( catch (Exception ex) { logger.LogError(ex, "解析智能分类结果失败,内容: {Content}", content); - return; } } @@ -746,7 +735,18 @@ public record CreateTransactionDto( decimal Amount, TransactionType Type, string? Classify -); +) +{ + public string OccurredAt { get; init; } = OccurredAt; + + public string? Reason { get; init; } = Reason; + + public decimal Amount { get; init; } = Amount; + + public TransactionType Type { get; init; } = Type; + + public string? Classify { get; init; } = Classify; +} /// /// 更新交易记录DTO @@ -758,7 +758,20 @@ public record UpdateTransactionDto( decimal Balance, TransactionType Type, string? Classify -); +) +{ + public long Id { get; init; } = Id; + + public string? Reason { get; init; } = Reason; + + public decimal Amount { get; init; } = Amount; + + public decimal Balance { get; init; } = Balance; + + public TransactionType Type { get; init; } = Type; + + public string? Classify { get; init; } = Classify; +} /// /// 日历统计响应DTO @@ -769,14 +782,28 @@ public record DailyStatisticsDto( decimal Expense, decimal Income, decimal Balance -); +) +{ + public string Date { get; init; } = Date; + + public int Count { get; init; } = Count; + + public decimal Expense { get; init; } = Expense; + + public decimal Income { get; init; } = Income; + + public decimal Balance { get; init; } = Balance; +} /// /// 智能分类请求DTO /// public record SmartClassifyRequest( List? TransactionIds = null -); +) +{ + public List? TransactionIds { get; init; } = TransactionIds; +} /// /// 批量更新分类项DTO @@ -785,7 +812,14 @@ public record BatchUpdateClassifyItem( long Id, string? Classify, TransactionType? Type = null -); +) +{ + public long Id { get; init; } = Id; + + public string? Classify { get; init; } = Classify; + + public TransactionType? Type { get; init; } = Type; +} /// /// 按摘要批量更新DTO @@ -794,14 +828,24 @@ public record BatchUpdateByReasonDto( string Reason, TransactionType Type, string Classify -); +) +{ + public string Reason { get; init; } = Reason; + + public TransactionType Type { get; init; } = Type; + + public string Classify { get; init; } = Classify; +} /// /// 账单分析请求DTO /// public record BillAnalysisRequest( string UserInput -); +) +{ + public string UserInput { get; init; } = UserInput; +} /// /// 抵账请求DTO @@ -809,13 +853,23 @@ public record BillAnalysisRequest( public record OffsetTransactionDto( long Id1, long Id2 -); +) +{ + public long Id1 { get; init; } = Id1; + public long Id2 { get; init; } = Id2; +} public record ParseOneLineRequestDto( string Text -); +) +{ + public string Text { get; init; } = Text; +} public record ConfirmAllUnconfirmedRequestDto( long[] Ids -); \ No newline at end of file +) +{ + public long[] Ids { get; init; } = Ids; +} \ No newline at end of file diff --git a/WebApi/Expand.cs b/WebApi/Expand.cs index b2fb640..e74d1ea 100644 --- a/WebApi/Expand.cs +++ b/WebApi/Expand.cs @@ -1,4 +1,5 @@ using Quartz; +using Service.Jobs; namespace WebApi; @@ -13,7 +14,7 @@ public static class Expand // 配置邮件同步任务 - 每10分钟执行一次 var emailJobKey = new JobKey("EmailSyncJob"); - q.AddJob(opts => opts + q.AddJob(opts => opts .WithIdentity(emailJobKey) .WithDescription("邮件同步任务")); q.AddTrigger(opts => opts @@ -24,7 +25,7 @@ public static class Expand // 配置周期性账单任务 - 每天早上6点执行 var periodicBillJobKey = new JobKey("PeriodicBillJob"); - q.AddJob(opts => opts + q.AddJob(opts => opts .WithIdentity(periodicBillJobKey) .WithDescription("周期性账单任务")); q.AddTrigger(opts => opts @@ -35,7 +36,7 @@ public static class Expand // 配置预算归档任务 - 每个月1号晚11点执行 var budgetArchiveJobKey = new JobKey("BudgetArchiveJob"); - q.AddJob(opts => opts + q.AddJob(opts => opts .WithIdentity(budgetArchiveJobKey) .WithDescription("预算归档任务")); q.AddTrigger(opts => opts diff --git a/WebApi/GlobalUsings.cs b/WebApi/GlobalUsings.cs index 9587307..00cfe31 100644 --- a/WebApi/GlobalUsings.cs +++ b/WebApi/GlobalUsings.cs @@ -5,3 +5,5 @@ global using WebApi.Controllers.Dto; global using Repository; global using Entity; global using System.Text; +global using System.Text.Json; +global using System.Text.Json.Nodes; diff --git a/WebApi/Program.cs b/WebApi/Program.cs index 183c196..51e9619 100644 --- a/WebApi/Program.cs +++ b/WebApi/Program.cs @@ -46,7 +46,7 @@ builder.Services.AddCors(options => // 绑定配置 builder.Services.Configure(builder.Configuration.GetSection("EmailSettings")); -builder.Services.Configure(builder.Configuration.GetSection("OpenAI")); +builder.Services.Configure(builder.Configuration.GetSection("OpenAI")); builder.Services.Configure(builder.Configuration.GetSection("JwtSettings")); builder.Services.Configure(builder.Configuration.GetSection("AuthSettings"));