大量后端代码格式化

This commit is contained in:
SunCheng
2026-01-18 22:04:56 +08:00
parent 298ce03aa6
commit 4e2bf0da6c
36 changed files with 278 additions and 200 deletions

3
Common/GlobalUsings.cs Normal file
View File

@@ -0,0 +1,3 @@
global using System.Reflection;
global using System.Text.Json;
global using Microsoft.Extensions.DependencyInjection;

View File

@@ -1,7 +1,4 @@
using System.Reflection; namespace Common;
using Microsoft.Extensions.DependencyInjection;
namespace Common;
public static class TypeExtensions public static class TypeExtensions
{ {
@@ -10,8 +7,8 @@ public static class TypeExtensions
/// </summary> /// </summary>
public static T? DeepClone<T>(this T source) public static T? DeepClone<T>(this T source)
{ {
var json = System.Text.Json.JsonSerializer.Serialize(source); var json = JsonSerializer.Serialize(source);
return System.Text.Json.JsonSerializer.Deserialize<T>(json); return JsonSerializer.Deserialize<T>(json);
} }
} }
@@ -41,7 +38,7 @@ public static class ServiceExtension
private static void RegisterServices(IServiceCollection services, Assembly assembly) private static void RegisterServices(IServiceCollection services, Assembly assembly)
{ {
var types = assembly.GetTypes() var types = assembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract); .Where(t => t is { IsClass: true, IsAbstract: false });
foreach (var type in types) foreach (var type in types)
{ {
@@ -71,14 +68,13 @@ public static class ServiceExtension
private static void RegisterRepositories(IServiceCollection services, Assembly assembly) private static void RegisterRepositories(IServiceCollection services, Assembly assembly)
{ {
var types = assembly.GetTypes() var types = assembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract); .Where(t => t is { IsClass: true, IsAbstract: false });
foreach (var type in types) foreach (var type in types)
{ {
var interfaces = type.GetInterfaces() var interfaces = type.GetInterfaces()
.Where(i => i.Name.StartsWith("I") .Where(i => i.Name.StartsWith("I")
&& i.Namespace == "Repository" && i is { Namespace: "Repository", IsGenericType: false }); // 排除泛型接口如 IBaseRepository<T>
&& !i.IsGenericType); // 排除泛型接口如 IBaseRepository<T>
foreach (var @interface in interfaces) foreach (var @interface in interfaces)
{ {

View File

@@ -3,6 +3,7 @@
<!-- Email & MIME Libraries --> <!-- Email & MIME Libraries -->
<PackageVersion Include="FreeSql" Version="3.5.305" /> <PackageVersion Include="FreeSql" Version="3.5.305" />
<PackageVersion Include="FreeSql.Extensions.JsonMap" Version="3.5.305" /> <PackageVersion Include="FreeSql.Extensions.JsonMap" Version="3.5.305" />
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.4" />
<PackageVersion Include="MailKit" Version="4.14.1" /> <PackageVersion Include="MailKit" Version="4.14.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" /> <PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
<PackageVersion Include="MimeKit" Version="4.14.0" /> <PackageVersion Include="MimeKit" Version="4.14.0" />

View File

@@ -1,2 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=fsql/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=fsql/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=strftime/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -1,6 +1,4 @@
using System.Security.Cryptography; namespace Entity;
namespace Entity;
/// <summary> /// <summary>
/// 邮件消息实体 /// 邮件消息实体
@@ -39,7 +37,7 @@ public class EmailMessage : BaseEntity
public string ComputeBodyHash() public string ComputeBodyHash()
{ {
using var md5 = MD5.Create(); 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); var hashBytes = md5.ComputeHash(inputBytes);
return Convert.ToHexString(hashBytes); return Convert.ToHexString(hashBytes);
} }

View File

@@ -1 +1,3 @@
global using FreeSql.DataAnnotations; global using FreeSql.DataAnnotations;
global using System.Security.Cryptography;
global using System.Text;

View File

@@ -12,6 +12,6 @@ public class PushSubscription : BaseEntity
public string? Auth { get; set; } public string? Auth { get; set; }
public string? UserId { get; set; } // Optional: if you have user authentication public string? UserId { get; set; } // Optional: if you have user authentication
public string? UserAgent { get; set; } public string? UserAgent { get; set; }
} }

View File

@@ -170,10 +170,10 @@ public abstract class BaseRepository<T>(IFreeSql freeSql) : IBaseRepository<T> w
var dt = await FreeSql.Ado.ExecuteDataTableAsync(completeSql); var dt = await FreeSql.Ado.ExecuteDataTableAsync(completeSql);
var result = new List<dynamic>(); var result = new List<dynamic>();
foreach (System.Data.DataRow row in dt.Rows) foreach (DataRow row in dt.Rows)
{ {
var expando = new System.Dynamic.ExpandoObject() as IDictionary<string, object>; var expando = new ExpandoObject() as IDictionary<string, object>;
foreach (System.Data.DataColumn column in dt.Columns) foreach (DataColumn column in dt.Columns)
{ {
expando[column.ColumnName] = row[column]; expando[column.ColumnName] = row[column];
} }

View File

@@ -1,5 +1,6 @@
global using Entity; global using Entity;
global using FreeSql;
global using System.Linq; global using System.Linq;
global using System.Data;
global using System.Dynamic;

View File

@@ -259,7 +259,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
t => t.Reason == reason); t => t.Reason == reason);
// 按分类筛选 // 按分类筛选
if (classifies != null && classifies.Length > 0) if (classifies is { Length: > 0 })
{ {
var filterClassifies = classifies.Select(c => c == "未分类" ? string.Empty : c).ToList(); var filterClassifies = classifies.Select(c => c == "未分类" ? string.Empty : c).ToList();
query = query.Where(t => filterClassifies.Contains(t.Classify)); query = query.Where(t => filterClassifies.Contains(t.Classify));
@@ -290,15 +290,13 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
.Page(pageIndex, pageSize) .Page(pageIndex, pageSize)
.ToListAsync(); .ToListAsync();
} }
else
{ // 按时间降序排列
// 按时间降序排列 return await query
return await query .OrderByDescending(t => t.OccurredAt)
.OrderByDescending(t => t.OccurredAt) .OrderByDescending(t => t.Id)
.OrderByDescending(t => t.Id) .Page(pageIndex, pageSize)
.Page(pageIndex, pageSize) .ToListAsync();
.ToListAsync();
}
} }
public async Task<long> GetTotalCountAsync( public async Task<long> GetTotalCountAsync(
@@ -323,7 +321,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
t => t.Reason == reason); t => t.Reason == reason);
// 按分类筛选 // 按分类筛选
if (classifies != null && classifies.Length > 0) if (classifies is { Length: > 0 })
{ {
var filterClassifies = classifies.Select(c => c == "未分类" ? string.Empty : c).ToList(); var filterClassifies = classifies.Select(c => c == "未分类" ? string.Empty : c).ToList();
query = query.Where(t => filterClassifies.Contains(t.Classify)); query = query.Where(t => filterClassifies.Contains(t.Classify));
@@ -471,7 +469,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
result.Add(new ReasonGroupDto result.Add(new ReasonGroupDto
{ {
Reason = group.Reason, Reason = group.Reason,
Count = (int)group.Count, Count = group.Count,
SampleType = sample.Type, SampleType = sample.Type,
SampleClassify = sample.Classify ?? string.Empty, SampleClassify = sample.Classify ?? string.Empty,
TransactionIds = records.Select(r => r.Id).ToList(), TransactionIds = records.Select(r => r.Id).ToList(),
@@ -615,9 +613,9 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
public async Task<List<TransactionRecord>> GetClassifiedByKeywordsAsync(List<string> keywords, int limit = 10) public async Task<List<TransactionRecord>> GetClassifiedByKeywordsAsync(List<string> keywords, int limit = 10)
{ {
if (keywords == null || keywords.Count == 0) if (keywords.Count == 0)
{ {
return new List<TransactionRecord>(); return [];
} }
var query = FreeSql.Select<TransactionRecord>() var query = FreeSql.Select<TransactionRecord>()
@@ -637,9 +635,9 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
public async Task<List<(TransactionRecord record, double relevanceScore)>> GetClassifiedByKeywordsWithScoreAsync(List<string> keywords, double minMatchRate = 0.3, int limit = 10) 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) if (keywords.Count == 0)
{ {
return new List<(TransactionRecord, double)>(); return [];
} }
// 查询所有已分类且包含任意关键词的账单 // 查询所有已分类且包含任意关键词的账单
@@ -687,7 +685,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
if (currentRecord == null) if (currentRecord == null)
{ {
return new List<TransactionRecord>(); return [];
} }
var list = await FreeSql.Select<TransactionRecord>() var list = await FreeSql.Select<TransactionRecord>()
@@ -740,7 +738,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
var query = FreeSql.Select<TransactionRecord>() var query = FreeSql.Select<TransactionRecord>()
.Where(t => t.OccurredAt >= startDate && t.OccurredAt <= endDate && t.Type == type); .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)); query = query.Where(t => classifies.Contains(t.Classify));
} }
@@ -753,12 +751,10 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
.GroupBy(t => new DateTime(t.OccurredAt.Year, t.OccurredAt.Month, 1)) .GroupBy(t => new DateTime(t.OccurredAt.Year, t.OccurredAt.Month, 1))
.ToDictionary(g => g.Key, g => g.Sum(x => Math.Abs(x.Amount))); .ToDictionary(g => g.Key, g => g.Sum(x => Math.Abs(x.Amount)));
} }
else
{ return list
return list .GroupBy(t => t.OccurredAt.Date)
.GroupBy(t => t.OccurredAt.Date) .ToDictionary(g => g.Key, g => g.Sum(x => Math.Abs(x.Amount)));
.ToDictionary(g => g.Key, g => g.Sum(x => Math.Abs(x.Amount)));
}
} }
} }
@@ -790,7 +786,7 @@ public class ReasonGroupDto
/// <summary> /// <summary>
/// 该分组的所有账单ID列表 /// 该分组的所有账单ID列表
/// </summary> /// </summary>
public List<long> TransactionIds { get; set; } = new(); public List<long> TransactionIds { get; set; } = [];
/// <summary> /// <summary>
/// 该分组的总金额(绝对值) /// 该分组的总金额(绝对值)
@@ -804,12 +800,19 @@ public class ReasonGroupDto
public class MonthlyStatistics public class MonthlyStatistics
{ {
public int Year { get; set; } public int Year { get; set; }
public int Month { get; set; } public int Month { get; set; }
public decimal TotalExpense { get; set; } public decimal TotalExpense { get; set; }
public decimal TotalIncome { get; set; } public decimal TotalIncome { get; set; }
public decimal Balance { get; set; } public decimal Balance { get; set; }
public int ExpenseCount { get; set; } public int ExpenseCount { get; set; }
public int IncomeCount { get; set; } public int IncomeCount { get; set; }
public int TotalCount { get; set; } public int TotalCount { get; set; }
} }
@@ -819,8 +822,11 @@ public class MonthlyStatistics
public class CategoryStatistics public class CategoryStatistics
{ {
public string Classify { get; set; } = string.Empty; public string Classify { get; set; } = string.Empty;
public decimal Amount { get; set; } public decimal Amount { get; set; }
public int Count { get; set; } public int Count { get; set; }
public decimal Percent { get; set; } public decimal Percent { get; set; }
} }
@@ -830,8 +836,12 @@ public class CategoryStatistics
public class TrendStatistics public class TrendStatistics
{ {
public int Year { get; set; } public int Year { get; set; }
public int Month { get; set; } public int Month { get; set; }
public decimal Expense { get; set; } public decimal Expense { get; set; }
public decimal Income { get; set; } public decimal Income { get; set; }
public decimal Balance { get; set; } public decimal Balance { get; set; }
} }

View File

@@ -1,6 +1,6 @@
namespace Service.AppSettingModel; namespace Service.AppSettingModel;
public class AISettings public class AiSettings
{ {
public string Endpoint { get; set; } = string.Empty; public string Endpoint { get; set; } = string.Empty;
public string Key { get; set; } = string.Empty; public string Key { get; set; } = string.Empty;

View File

@@ -1,4 +1,6 @@
namespace Service; using JetBrains.Annotations;
namespace Service;
public interface IBudgetService public interface IBudgetService
{ {
@@ -24,6 +26,7 @@ public interface IBudgetService
Task<BudgetResult?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type); Task<BudgetResult?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type);
} }
[UsedImplicitly]
public class BudgetService( public class BudgetService(
IBudgetRepository budgetRepository, IBudgetRepository budgetRepository,
IBudgetArchiveRepository budgetArchiveRepository, IBudgetArchiveRepository budgetArchiveRepository,
@@ -79,20 +82,22 @@ public class BudgetService(
} }
// 创造虚拟的存款预算 // 创造虚拟的存款预算
dtos.Add(await GetVirtualSavingsDtoAsync( dtos.Add(await GetSavingsDtoAsync(
BudgetPeriodType.Month, BudgetPeriodType.Month,
referenceDate, referenceDate,
budgets)); budgets));
dtos.Add(await GetVirtualSavingsDtoAsync( dtos.Add(await GetSavingsDtoAsync(
BudgetPeriodType.Year, BudgetPeriodType.Year,
referenceDate, referenceDate,
budgets)); budgets));
dtos = dtos dtos = dtos
.Where(x => x != null)
.Cast<BudgetResult>()
.OrderByDescending(x => x.IsMandatoryExpense) .OrderByDescending(x => x.IsMandatoryExpense)
.ThenBy(x => x.Type) .ThenBy(x => x.Type)
.ThenByDescending(x => x.Current) .ThenByDescending(x => x.Current)
.ToList(); .ToList()!;
return [.. dtos.Where(dto => dto != null).Cast<BudgetResult>()]; return [.. dtos.Where(dto => dto != null).Cast<BudgetResult>()];
} }
@@ -100,7 +105,7 @@ public class BudgetService(
public async Task<BudgetResult?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type) public async Task<BudgetResult?> GetSavingsBudgetAsync(int year, int month, BudgetPeriodType type)
{ {
var referenceDate = new DateTime(year, month, 1); var referenceDate = new DateTime(year, month, 1);
return await GetVirtualSavingsDtoAsync(type, referenceDate); return await GetSavingsDtoAsync(type, referenceDate);
} }
public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate) public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
@@ -128,7 +133,7 @@ public class BudgetService(
_ => TransactionType.None _ => TransactionType.None
}; };
if (transactionType == TransactionType.None) return new List<UncoveredCategoryDetail>(); if (transactionType == TransactionType.None) return [];
// 1. 获取所有预算 // 1. 获取所有预算
var budgets = (await budgetRepository.GetAllAsync()).ToList(); var budgets = (await budgetRepository.GetAllAsync()).ToList();
@@ -205,7 +210,7 @@ public class BudgetService(
totalLimit += itemLimit; totalLimit += itemLimit;
// 当前值累加 // 当前值累加
var selectedCategories = budget.SelectedCategories != null ? string.Join(',', budget.SelectedCategories) : string.Empty; var selectedCategories = string.Join(',', budget.SelectedCategories);
var currentAmount = await CalculateCurrentAmountAsync(new() var currentAmount = await CalculateCurrentAmountAsync(new()
{ {
Name = budget.Name, Name = budget.Name,
@@ -246,7 +251,7 @@ public class BudgetService(
if (transactionType != TransactionType.None) 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 var allClassifies = hasGlobalBudget
? [] ? []
@@ -256,7 +261,7 @@ public class BudgetService(
.ToList(); .ToList();
DateTime startDate, endDate; DateTime startDate, endDate;
bool groupByMonth = false; bool groupByMonth;
if (statType == BudgetPeriodType.Month) if (statType == BudgetPeriodType.Month)
{ {
@@ -445,7 +450,7 @@ public class BudgetService(
.Where(t => .Where(t =>
{ {
var dict = (IDictionary<string, object>)t; var dict = (IDictionary<string, object>)t;
var classify = dict["Classify"]?.ToString() ?? ""; var classify = dict["Classify"].ToString() ?? "";
var type = Convert.ToInt32(dict["Type"]); var type = Convert.ToInt32(dict["Type"]);
return type == 0 && !budgetedCategories.Contains(classify); return type == 0 && !budgetedCategories.Contains(classify);
}) })
@@ -551,7 +556,8 @@ public class BudgetService(
// 返回实际消费和硬性消费累加中的较大值 // 返回实际消费和硬性消费累加中的较大值
return mandatoryAccumulation; return mandatoryAccumulation;
} }
else if (budget.Type == BudgetPeriodType.Year)
if (budget.Type == BudgetPeriodType.Year)
{ {
// 计算本年的天数(考虑闰年) // 计算本年的天数(考虑闰年)
var daysInYear = DateTime.IsLeapYear(referenceDate.Year) ? 366 : 365; var daysInYear = DateTime.IsLeapYear(referenceDate.Year) ? 366 : 365;
@@ -592,7 +598,7 @@ public class BudgetService(
return (start, end); return (start, end);
} }
private async Task<BudgetResult?> GetVirtualSavingsDtoAsync( private async Task<BudgetResult?> GetSavingsDtoAsync(
BudgetPeriodType periodType, BudgetPeriodType periodType,
DateTime? referenceDate = null, DateTime? referenceDate = null,
IEnumerable<BudgetRecord>? existingBudgets = null) IEnumerable<BudgetRecord>? existingBudgets = null)
@@ -657,7 +663,7 @@ public class BudgetService(
if (b.Category == BudgetCategory.Savings) continue; if (b.Category == BudgetCategory.Savings) continue;
processedIds.Add(b.Id); processedIds.Add(b.Id);
decimal factor = 1.0m; decimal factor;
decimal historicalAmount = 0m; decimal historicalAmount = 0m;
var historicalMonths = new List<int>(); var historicalMonths = new List<int>();
@@ -759,7 +765,6 @@ public class BudgetService(
foreach (var group in deletedBudgets) foreach (var group in deletedBudgets)
{ {
var budgetId = group.Key;
var months = group.Select(g => g.Key.Month).OrderBy(m => m).ToList(); var months = group.Select(g => g.Key.Month).OrderBy(m => m).ToList();
var totalLimit = group.Sum(g => g.Value.HistoricalLimit); var totalLimit = group.Sum(g => g.Value.HistoricalLimit);
var (_, category, name) = group.First().Value; var (_, category, name) = group.First().Value;
@@ -862,12 +867,12 @@ public class BudgetService(
</thead> </thead>
<tbody> <tbody>
"""); """);
foreach (var (Name, Amount) in noLimitIncomeItems) foreach (var (name, amount) in noLimitIncomeItems)
{ {
description.Append($""" description.Append($"""
<tr> <tr>
<td>{Name}</td> <td>{name}</td>
<td><span class='income-value'>{Amount:N0}</span></td> <td><span class='income-value'>{amount:N0}</span></td>
</tr> </tr>
"""); """);
} }
@@ -953,12 +958,12 @@ public class BudgetService(
</thead> </thead>
<tbody> <tbody>
"""); """);
foreach (var (Name, Amount) in noLimitExpenseItems) foreach (var (name, amount) in noLimitExpenseItems)
{ {
description.Append($""" description.Append($"""
<tr> <tr>
<td>{Name}</td> <td>{name}</td>
<td><span class='expense-value'>{Amount:N0}</span></td> <td><span class='expense-value'>{amount:N0}</span></td>
</tr> </tr>
"""); """);
} }
@@ -1080,13 +1085,13 @@ public record BudgetResult
public decimal Limit { get; set; } public decimal Limit { get; set; }
public decimal Current { get; set; } public decimal Current { get; set; }
public BudgetCategory Category { get; set; } public BudgetCategory Category { get; set; }
public string[] SelectedCategories { get; set; } = Array.Empty<string>(); public string[] SelectedCategories { get; set; } = [];
public string StartDate { get; set; } = string.Empty; public string StartDate { get; set; } = string.Empty;
public string Period { get; set; } = string.Empty; public string Period { get; set; } = string.Empty;
public DateTime? PeriodStart { get; set; } public DateTime? PeriodStart { get; set; }
public DateTime? PeriodEnd { get; set; } public DateTime? PeriodEnd { get; set; }
public bool NoLimit { get; set; } = false; public bool NoLimit { get; set; }
public bool IsMandatoryExpense { get; set; } = false; public bool IsMandatoryExpense { get; set; }
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public static BudgetResult FromEntity( public static BudgetResult FromEntity(
@@ -1107,7 +1112,7 @@ public record BudgetResult
Current = currentAmount, Current = currentAmount,
Category = entity.Category, Category = entity.Category,
SelectedCategories = string.IsNullOrEmpty(entity.SelectedCategories) SelectedCategories = string.IsNullOrEmpty(entity.SelectedCategories)
? Array.Empty<string>() ? []
: entity.SelectedCategories.Split(','), : entity.SelectedCategories.Split(','),
StartDate = entity.StartDate.ToString("yyyy-MM-dd"), StartDate = entity.StartDate.ToString("yyyy-MM-dd"),
Period = entity.Type switch Period = entity.Type switch
@@ -1157,7 +1162,7 @@ public class BudgetStatsDto
/// <summary> /// <summary>
/// 每日/每月累计金额趋势(对应当前周期内的实际发生额累计值) /// 每日/每月累计金额趋势(对应当前周期内的实际发生额累计值)
/// </summary> /// </summary>
public List<decimal?> Trend { get; set; } = new(); public List<decimal?> Trend { get; set; } = [];
} }
/// <summary> /// <summary>
@@ -1175,6 +1180,7 @@ public class BudgetCategoryStats
/// </summary> /// </summary>
public BudgetStatsDto Year { get; set; } = new(); public BudgetStatsDto Year { get; set; } = new();
} }
public class UncoveredCategoryDetail public class UncoveredCategoryDetail
{ {
public string Category { get; set; } = string.Empty; public string Category { get; set; } = string.Empty;

View File

@@ -65,7 +65,7 @@ public class EmailHandleService(
await messageService.AddAsync( await messageService.AddAsync(
"邮件解析失败", "邮件解析失败",
$"来自 {from} 发送给 {to} 的邮件(主题:{subject})未能成功解析内容,可能格式已变更或不受支持。", $"来自 {from} 发送给 {to} 的邮件(主题:{subject})未能成功解析内容,可能格式已变更或不受支持。",
url: $"/balance?tab=email" url: "/balance?tab=email"
); );
logger.LogWarning("未能成功解析邮件内容,跳过账单处理"); logger.LogWarning("未能成功解析邮件内容,跳过账单处理");
return true; return true;

View File

@@ -2,8 +2,8 @@
namespace Service.EmailParseServices; namespace Service.EmailParseServices;
public class EmailParseFormCCSVC( public class EmailParseFormCcsvc(
ILogger<EmailParseFormCCSVC> logger, ILogger<EmailParseFormCcsvc> logger,
IOpenAiService openAiService IOpenAiService openAiService
) : EmailParseServicesBase(logger, openAiService) ) : EmailParseServicesBase(logger, openAiService)
{ {
@@ -47,7 +47,7 @@ public class EmailParseFormCCSVC(
if (dateNode == null) if (dateNode == null)
{ {
logger.LogWarning("Date node not found"); logger.LogWarning("Date node not found");
return Array.Empty<(string, string, decimal, decimal, TransactionType, DateTime?)>(); return [];
} }
var dateText = dateNode.InnerText.Trim(); var dateText = dateNode.InnerText.Trim();
@@ -56,7 +56,7 @@ public class EmailParseFormCCSVC(
if (!dateMatch.Success || !DateTime.TryParse(dateMatch.Value, out var date)) if (!dateMatch.Success || !DateTime.TryParse(dateMatch.Value, out var date))
{ {
logger.LogWarning("Failed to parse date from: {DateText}", dateText); logger.LogWarning("Failed to parse date from: {DateText}", dateText);
return Array.Empty<(string, string, decimal, decimal, TransactionType, DateTime?)>(); return [];
} }
// 2. Get Balance (Available Limit) // 2. Get Balance (Available Limit)
@@ -122,7 +122,7 @@ public class EmailParseFormCCSVC(
descText = HtmlEntity.DeEntitize(descText).Replace((char)160, ' ').Trim(); descText = HtmlEntity.DeEntitize(descText).Replace((char)160, ' ').Trim();
// Parse Description: "尾号4390 消费 财付通-luckincoffee瑞幸咖啡" // Parse Description: "尾号4390 消费 财付通-luckincoffee瑞幸咖啡"
var parts = descText.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); var parts = descText.Split([' '], StringSplitOptions.RemoveEmptyEntries);
string card = ""; string card = "";
string reason = descText; string reason = descText;

View File

@@ -201,7 +201,7 @@ public abstract class EmailParseServicesBase(
// 收入关键词 // 收入关键词
string[] incomeKeywords = string[] incomeKeywords =
{ [
"工资", "奖金", "退款", "工资", "奖金", "退款",
"返现", "收入", "转入", "返现", "收入", "转入",
"存入", "利息", "分红", "存入", "利息", "分红",
@@ -233,13 +233,13 @@ public abstract class EmailParseServicesBase(
// 存取类 // 存取类
"现金存入", "柜台存入", "ATM存入", "现金存入", "柜台存入", "ATM存入",
"他人转入", "他人汇入" "他人转入", "他人汇入"
}; ];
if (incomeKeywords.Any(k => lowerReason.Contains(k))) if (incomeKeywords.Any(k => lowerReason.Contains(k)))
return TransactionType.Income; return TransactionType.Income;
// 支出关键词 // 支出关键词
string[] expenseKeywords = string[] expenseKeywords =
{ [
"消费", "支付", "购买", "消费", "支付", "购买",
"转出", "取款", "支出", "转出", "取款", "支出",
"扣款", "缴费", "付款", "扣款", "缴费", "付款",
@@ -269,7 +269,7 @@ public abstract class EmailParseServicesBase(
// 信用卡/花呗等场景 // 信用卡/花呗等场景
"信用卡还款", "花呗还款", "白条还款", "信用卡还款", "花呗还款", "白条还款",
"分期还款", "账单还款", "自动还款" "分期还款", "账单还款", "自动还款"
}; ];
if (expenseKeywords.Any(k => lowerReason.Contains(k))) if (expenseKeywords.Any(k => lowerReason.Contains(k)))
return TransactionType.Expense; return TransactionType.Expense;

View File

@@ -7,10 +7,11 @@ global using System.Globalization;
global using System.Text; global using System.Text;
global using System.Text.Json; global using System.Text.Json;
global using Entity; global using Entity;
global using FreeSql;
global using System.Linq; global using System.Linq;
global using Service.AppSettingModel; global using Service.AppSettingModel;
global using System.Text.Json.Serialization; global using System.Text.Json.Serialization;
global using System.Text.Json.Nodes; global using System.Text.Json.Nodes;
global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.Configuration;
global using Common; global using Common;
global using System.Net;
global using System.Text.Encodings.Web;

View File

@@ -133,7 +133,7 @@ public class ImportService(
return DateTime.MinValue; return DateTime.MinValue;
} }
foreach (var format in DateTimeFormats) foreach (var format in _dateTimeFormats)
{ {
if (DateTime.TryParseExact( if (DateTime.TryParseExact(
row[key], row[key],
@@ -288,7 +288,7 @@ public class ImportService(
return DateTime.MinValue; return DateTime.MinValue;
} }
foreach (var format in DateTimeFormats) foreach (var format in _dateTimeFormats)
{ {
if (DateTime.TryParseExact( if (DateTime.TryParseExact(
row[key], row[key],
@@ -358,14 +358,13 @@ public class ImportService(
{ {
return await ParseCsvAsync(file); return await ParseCsvAsync(file);
} }
else if (fileExtension == ".xlsx" || fileExtension == ".xls")
if (fileExtension == ".xlsx" || fileExtension == ".xls")
{ {
return await ParseExcelAsync(file); return await ParseExcelAsync(file);
} }
else
{ throw new NotSupportedException("不支持的文件格式");
throw new NotSupportedException("不支持的文件格式");
}
} }
private async Task<IDictionary<string, string>[]> ParseCsvAsync(MemoryStream file) private async Task<IDictionary<string, string>[]> ParseCsvAsync(MemoryStream file)
@@ -388,7 +387,7 @@ public class ImportService(
if (headers == null || headers.Length == 0) if (headers == null || headers.Length == 0)
{ {
return Array.Empty<IDictionary<string, string>>(); return [];
} }
var result = new List<IDictionary<string, string>>(); var result = new List<IDictionary<string, string>>();
@@ -420,7 +419,7 @@ public class ImportService(
if (worksheet == null || worksheet.Dimension == null) if (worksheet == null || worksheet.Dimension == null)
{ {
return Array.Empty<IDictionary<string, string>>(); return [];
} }
var rowCount = worksheet.Dimension.End.Row; var rowCount = worksheet.Dimension.End.Row;
@@ -428,7 +427,7 @@ public class ImportService(
if (rowCount < 2) if (rowCount < 2)
{ {
return Array.Empty<IDictionary<string, string>>(); return [];
} }
// 读取表头(第一行) // 读取表头(第一行)
@@ -458,7 +457,7 @@ public class ImportService(
return await Task.FromResult(result.ToArray()); return await Task.FromResult(result.ToArray());
} }
private static string[] DateTimeFormats = private static string[] _dateTimeFormats =
[ [
"yyyy-MM-dd", "yyyy-MM-dd",
"yyyy-MM-dd HH", "yyyy-MM-dd HH",

View File

@@ -68,8 +68,8 @@ public class LogCleanupService(ILogger<LogCleanupService> logger) : BackgroundSe
// 尝试解析日期 (格式: yyyyMMdd) // 尝试解析日期 (格式: yyyyMMdd)
if (DateTime.TryParseExact(dateStr, "yyyyMMdd", if (DateTime.TryParseExact(dateStr, "yyyyMMdd",
System.Globalization.CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, DateTimeStyles.None,
out var logDate)) out var logDate))
{ {
if (logDate < cutoffDate) if (logDate < cutoffDate)

View File

@@ -1,11 +1,13 @@
using WebPush; using System.Net;
using WebPush;
using PushSubscription = Entity.PushSubscription;
namespace Service; namespace Service;
public interface INotificationService public interface INotificationService
{ {
Task<string> GetVapidPublicKeyAsync(); Task<string> GetVapidPublicKeyAsync();
Task SubscribeAsync(Entity.PushSubscription subscription); Task SubscribeAsync(PushSubscription subscription);
Task SendNotificationAsync(string message, string? url = null); Task SendNotificationAsync(string message, string? url = null);
} }
@@ -32,7 +34,7 @@ public class NotificationService(
return Task.FromResult(GetSettings().PublicKey); 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); var existing = await subscriptionRepo.GetByEndpointAsync(subscription.Endpoint);
if (existing != null) if (existing != null)
@@ -61,7 +63,7 @@ public class NotificationService(
var webPushClient = new WebPushClient(); var webPushClient = new WebPushClient();
var subscriptions = await subscriptionRepo.GetAllAsync(); var subscriptions = await subscriptionRepo.GetAllAsync();
var payload = System.Text.Json.JsonSerializer.Serialize(new var payload = JsonSerializer.Serialize(new
{ {
title = "System Notification", title = "System Notification",
body = message, body = message,
@@ -78,7 +80,7 @@ public class NotificationService(
} }
catch (WebPushException ex) 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); await subscriptionRepo.DeleteAsync(sub.Id);
} }

View File

@@ -11,7 +11,7 @@ public interface IOpenAiService
} }
public class OpenAiService( public class OpenAiService(
IOptions<AISettings> aiSettings, IOptions<AiSettings> aiSettings,
ILogger<OpenAiService> logger ILogger<OpenAiService> logger
) : IOpenAiService ) : IOpenAiService
{ {

View File

@@ -5,6 +5,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="MailKit" /> <PackageReference Include="MailKit" />
<PackageReference Include="MimeKit" /> <PackageReference Include="MimeKit" />
<PackageReference Include="Microsoft.Extensions.Configuration" /> <PackageReference Include="Microsoft.Extensions.Configuration" />

View File

@@ -143,7 +143,7 @@ public class SmartHandleService(
chunkAction(("start", $"开始分类,共 {sampleRecords.Length} 条账单")); chunkAction(("start", $"开始分类,共 {sampleRecords.Length} 条账单"));
var classifyResults = new List<(string Reason, string Classify, TransactionType Type)>(); var classifyResults = new List<(string Reason, string Classify, TransactionType Type)>();
var sendedIds = new HashSet<long>(); var sentIds = new HashSet<long>();
// 将流解析逻辑提取为本地函数以减少嵌套 // 将流解析逻辑提取为本地函数以减少嵌套
void HandleResult(GroupClassifyResult? result) void HandleResult(GroupClassifyResult? result)
@@ -154,16 +154,18 @@ public class SmartHandleService(
if (group == null) return; if (group == null) return;
foreach (var id in group.Ids) foreach (var id in group.Ids)
{ {
if (sendedIds.Add(id)) if (!sentIds.Add(id))
{ {
var resultJson = JsonSerializer.Serialize(new continue;
{
id,
result.Classify,
result.Type
});
chunkAction(("data", resultJson));
} }
var resultJson = JsonSerializer.Serialize(new
{
id,
result.Classify,
result.Type
});
chunkAction(("data", resultJson));
} }
} }
@@ -193,7 +195,7 @@ public class SmartHandleService(
} }
catch (Exception exArr) 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 = $""" content = $"""
<pre style="max-height: 80px; font-size: 8px; overflow-y: auto; padding: 8px; border: 1px solid #3c3c3c"> <pre style="max-height: 80px; font-size: 8px; overflow-y: auto; padding: 8px; border: 1px solid #3c3c3c">
{System.Net.WebUtility.HtmlEncode(sqlText)} {WebUtility.HtmlEncode(sqlText)}
</pre> </pre>
""" """
}) })
@@ -361,7 +363,7 @@ public class SmartHandleService(
var dataJson = JsonSerializer.Serialize(queryResults, new JsonSerializerOptions var dataJson = JsonSerializer.Serialize(queryResults, new JsonSerializerOptions
{ {
WriteIndented = true, WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
}); });
var userPromptExtra = await configService.GetConfigByKeyAsync<string>("BillAnalysisPrompt"); var userPromptExtra = await configService.GetConfigByKeyAsync<string>("BillAnalysisPrompt");
@@ -429,7 +431,6 @@ public class SmartHandleService(
{ {
// 获取所有分类 // 获取所有分类
var categories = await categoryRepository.GetAllAsync(); var categories = await categoryRepository.GetAllAsync();
var categoryList = string.Join("、", categories.Select(c => $"{GetTypeName(c.Type)}-{c.Name}"));
// 构建分类信息 // 构建分类信息
var categoryInfo = new StringBuilder(); var categoryInfo = new StringBuilder();
@@ -542,13 +543,13 @@ public class SmartHandleService(
public record GroupClassifyResult public record GroupClassifyResult
{ {
[JsonPropertyName("reason")] [JsonPropertyName("reason")]
public string Reason { get; set; } = string.Empty; public string Reason { get; init; } = string.Empty;
[JsonPropertyName("classify")] [JsonPropertyName("classify")]
public string? Classify { get; set; } public string? Classify { get; init; }
[JsonPropertyName("type")] [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); public record TransactionParseResult(string OccurredAt, string Classify, decimal Amount, string Reason, TransactionType Type);

View File

@@ -1,8 +1,7 @@
namespace Service; using JiebaNet.Analyser;
using JiebaNet.Segmenter; using JiebaNet.Segmenter;
using JiebaNet.Analyser;
using Microsoft.Extensions.Logging; namespace Service;
/// <summary> /// <summary>
/// 文本分词服务接口 /// 文本分词服务接口
@@ -78,7 +77,7 @@ public class TextSegmentService : ITextSegmentService
{ {
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))
{ {
return new List<string>(); return [];
} }
try try
@@ -119,7 +118,7 @@ public class TextSegmentService : ITextSegmentService
{ {
_logger.LogError(ex, "提取关键词失败,文本: {Text}", text); _logger.LogError(ex, "提取关键词失败,文本: {Text}", text);
// 降级处理:返回原文 // 降级处理:返回原文
return new List<string> { 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)) if (string.IsNullOrWhiteSpace(text))
{ {
return new List<string>(); return [];
} }
try try
@@ -146,7 +145,7 @@ public class TextSegmentService : ITextSegmentService
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "分词失败,文本: {Text}", text); _logger.LogError(ex, "分词失败,文本: {Text}", text);
return new List<string> { text }; return [text];
} }
} }
} }

View File

@@ -144,7 +144,7 @@ public class TransactionPeriodicService(
var dayOfWeek = (int)today.DayOfWeek; // 0=Sunday, 1=Monday, ..., 6=Saturday var dayOfWeek = (int)today.DayOfWeek; // 0=Sunday, 1=Monday, ..., 6=Saturday
var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries) var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(d => int.TryParse(d.Trim(), out var day) ? day : -1) .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(); .ToList();
return executeDays.Contains(dayOfWeek); return executeDays.Contains(dayOfWeek);
@@ -160,7 +160,7 @@ public class TransactionPeriodicService(
var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries) var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(d => int.TryParse(d.Trim(), out var day) ? day : -1) .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(); .ToList();
// 如果当前为月末,且配置中有大于当月天数的日期,则也执行 // 如果当前为月末,且配置中有大于当月天数的日期,则也执行
@@ -223,7 +223,7 @@ public class TransactionPeriodicService(
var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries) var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(d => int.TryParse(d.Trim(), out var day) ? day : -1) .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) .OrderBy(d => d)
.ToList(); .ToList();
@@ -253,7 +253,7 @@ public class TransactionPeriodicService(
var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries) var executeDays = config.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(d => int.TryParse(d.Trim(), out var day) ? day : -1) .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) .OrderBy(d => d)
.ToList(); .ToList();

View File

@@ -6,7 +6,7 @@ public class CreateBudgetDto
public BudgetPeriodType Type { get; set; } = BudgetPeriodType.Month; public BudgetPeriodType Type { get; set; } = BudgetPeriodType.Month;
public decimal Limit { get; set; } public decimal Limit { get; set; }
public BudgetCategory Category { get; set; } public BudgetCategory Category { get; set; }
public string[] SelectedCategories { get; set; } = Array.Empty<string>(); public string[] SelectedCategories { get; set; } = [];
public DateTime? StartDate { get; set; } public DateTime? StartDate { get; set; }
public bool NoLimit { get; set; } = false; public bool NoLimit { get; set; } = false;
public bool IsMandatoryExpense { get; set; } = false; public bool IsMandatoryExpense { get; set; } = false;

View File

@@ -6,21 +6,28 @@
public class EmailMessageDto public class EmailMessageDto
{ {
public long Id { get; set; } public long Id { get; set; }
public string Subject { get; set; } = string.Empty; public string Subject { get; set; } = string.Empty;
public string From { get; set; } = string.Empty; public string From { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty; public string Body { get; set; } = string.Empty;
public string HtmlBody { get; set; } = string.Empty; public string HtmlBody { get; set; } = string.Empty;
public DateTime ReceivedDate { get; set; } public DateTime ReceivedDate { get; set; }
public DateTime CreateTime { get; set; } public DateTime CreateTime { get; set; }
public DateTime? UpdateTime { get; set; } public DateTime? UpdateTime { get; set; }
/// <summary> /// <summary>
/// 已解析的账单数量 /// 已解析的账单数量
/// </summary> /// </summary>
public int TransactionCount { get; set; } public int TransactionCount { get; set; }
public string ToName { get; set; } = string.Empty; public string ToName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 从实体转换为DTO /// 从实体转换为DTO
/// </summary> /// </summary>

View File

@@ -86,10 +86,8 @@ public class EmailMessageController(
{ {
return BaseResponse.Done(); return BaseResponse.Done();
} }
else
{ return "删除邮件失败,邮件不存在".Fail();
return "删除邮件失败,邮件不存在".Fail();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -117,10 +115,8 @@ public class EmailMessageController(
{ {
return BaseResponse.Done(); return BaseResponse.Done();
} }
else
{ return "重新分析失败".Fail();
return "重新分析失败".Fail();
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -1,4 +1,5 @@
using Quartz; using Quartz;
using Quartz.Impl.Matchers;
namespace WebApi.Controllers; namespace WebApi.Controllers;
@@ -12,7 +13,7 @@ public class JobController(ISchedulerFactory schedulerFactory, ILogger<JobContro
try try
{ {
var scheduler = await schedulerFactory.GetScheduler(); var scheduler = await schedulerFactory.GetScheduler();
var jobKeys = await scheduler.GetJobKeys(Quartz.Impl.Matchers.GroupMatcher<JobKey>.AnyGroup()); var jobKeys = await scheduler.GetJobKeys(GroupMatcher<JobKey>.AnyGroup());
var jobStatuses = new List<JobStatus>(); var jobStatuses = new List<JobStatus>();
foreach (var jobKey in jobKeys) foreach (var jobKey in jobKeys)
@@ -101,9 +102,13 @@ public class JobController(ISchedulerFactory schedulerFactory, ILogger<JobContro
public class JobStatus public class JobStatus
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string JobDescription { get; set; } = string.Empty; public string JobDescription { get; set; } = string.Empty;
public string TriggerDescription { get; set; } = string.Empty; public string TriggerDescription { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty; public string Status { get; set; } = string.Empty;
public string NextRunTime { get; set; } = string.Empty; public string NextRunTime { get; set; } = string.Empty;
} }

View File

@@ -1,4 +1,6 @@
namespace WebApi.Controllers; using System.Text.RegularExpressions;
namespace WebApi.Controllers;
[ApiController] [ApiController]
[Route("api/[controller]/[action]")] [Route("api/[controller]/[action]")]
@@ -102,7 +104,7 @@ public class LogController(ILogger<LogController> logger) : ControllerBase
var currentLog = new StringBuilder(); 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}\]" @"^\[\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<LogController> logger) : ControllerBase
// 日志格式示例: [2025-12-29 16:38:45.730 +08:00] [INF] Message here // 日志格式示例: [2025-12-29 16:38:45.730 +08:00] [INF] Message here
// 使用正则表达式解析 // 使用正则表达式解析
// 使用 Singleline 模式使 '.' 可以匹配换行,这样 multi-line 消息可以被完整捕获。 // 使用 Singleline 模式使 '.' 可以匹配换行,这样 multi-line 消息可以被完整捕获。
var match = System.Text.RegularExpressions.Regex.Match( var match = Regex.Match(
line, 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]*)$", @"^\[(\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) if (match.Success)

View File

@@ -4,7 +4,7 @@
[Route("api/[controller]/[action]")] [Route("api/[controller]/[action]")]
public class NotificationController(INotificationService notificationService) : ControllerBase public class NotificationController(INotificationService notificationService) : ControllerBase
{ {
[HttpGet()] [HttpGet]
public async Task<BaseResponse<string>> GetVapidPublicKey() public async Task<BaseResponse<string>> GetVapidPublicKey()
{ {
try try

View File

@@ -85,10 +85,8 @@ public class TransactionCategoryController(
{ {
return category.Id.Ok(); return category.Id.Ok();
} }
else
{ return "创建分类失败".Fail<long>();
return "创建分类失败".Fail<long>();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -133,10 +131,8 @@ public class TransactionCategoryController(
{ {
return "更新分类成功".Ok(); return "更新分类成功".Ok();
} }
else
{ return "更新分类失败".Fail();
return "更新分类失败".Fail();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -165,10 +161,8 @@ public class TransactionCategoryController(
{ {
return BaseResponse.Done(); return BaseResponse.Done();
} }
else
{ return "删除分类失败,分类不存在".Fail();
return "删除分类失败,分类不存在".Fail();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -196,10 +190,8 @@ public class TransactionCategoryController(
{ {
return categories.Count.Ok(); return categories.Count.Ok();
} }
else
{ return "批量创建分类失败".Fail<int>();
return "批量创建分类失败".Fail<int>();
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -1,7 +1,5 @@
namespace WebApi.Controllers; namespace WebApi.Controllers;
using Repository;
/// <summary> /// <summary>
/// 周期性账单控制器 /// 周期性账单控制器
/// </summary> /// </summary>

View File

@@ -1,8 +1,4 @@
namespace WebApi.Controllers; namespace WebApi.Controllers;
using System.Text.Json;
using System.Text.Json.Nodes;
using Repository;
[ApiController] [ApiController]
[Route("api/[controller]/[action]")] [Route("api/[controller]/[action]")]
@@ -183,10 +179,8 @@ public class TransactionRecordController(
{ {
return BaseResponse.Done(); return BaseResponse.Done();
} }
else
{ return "创建交易记录失败".Fail();
return "创建交易记录失败".Fail();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -225,10 +219,8 @@ public class TransactionRecordController(
{ {
return BaseResponse.Done(); return BaseResponse.Done();
} }
else
{ return "更新交易记录失败".Fail();
return "更新交易记录失败".Fail();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -250,10 +242,8 @@ public class TransactionRecordController(
{ {
return BaseResponse.Done(); return BaseResponse.Done();
} }
else
{ return "删除交易记录失败,记录不存在".Fail();
return "删除交易记录失败,记录不存在".Fail();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -413,7 +403,7 @@ public class TransactionRecordController(
return; return;
} }
await smartHandleService.AnalyzeBillAsync(request.UserInput, async (chunk) => await smartHandleService.AnalyzeBillAsync(request.UserInput, async chunk =>
{ {
await WriteEventAsync(chunk); await WriteEventAsync(chunk);
}); });
@@ -500,7 +490,7 @@ public class TransactionRecordController(
return; return;
} }
await smartHandleService.SmartClassifyAsync(request.TransactionIds.ToArray(), async (chunk) => await smartHandleService.SmartClassifyAsync(request.TransactionIds.ToArray(), async chunk =>
{ {
var (eventType, content) = chunk; var (eventType, content) = chunk;
await TrySetUnconfirmedAsync(eventType, content); await TrySetUnconfirmedAsync(eventType, content);
@@ -550,7 +540,6 @@ public class TransactionRecordController(
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "解析智能分类结果失败,内容: {Content}", content); logger.LogError(ex, "解析智能分类结果失败,内容: {Content}", content);
return;
} }
} }
@@ -746,7 +735,18 @@ public record CreateTransactionDto(
decimal Amount, decimal Amount,
TransactionType Type, TransactionType Type,
string? Classify 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;
}
/// <summary> /// <summary>
/// 更新交易记录DTO /// 更新交易记录DTO
@@ -758,7 +758,20 @@ public record UpdateTransactionDto(
decimal Balance, decimal Balance,
TransactionType Type, TransactionType Type,
string? Classify 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;
}
/// <summary> /// <summary>
/// 日历统计响应DTO /// 日历统计响应DTO
@@ -769,14 +782,28 @@ public record DailyStatisticsDto(
decimal Expense, decimal Expense,
decimal Income, decimal Income,
decimal Balance 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;
}
/// <summary> /// <summary>
/// 智能分类请求DTO /// 智能分类请求DTO
/// </summary> /// </summary>
public record SmartClassifyRequest( public record SmartClassifyRequest(
List<long>? TransactionIds = null List<long>? TransactionIds = null
); )
{
public List<long>? TransactionIds { get; init; } = TransactionIds;
}
/// <summary> /// <summary>
/// 批量更新分类项DTO /// 批量更新分类项DTO
@@ -785,7 +812,14 @@ public record BatchUpdateClassifyItem(
long Id, long Id,
string? Classify, string? Classify,
TransactionType? Type = null TransactionType? Type = null
); )
{
public long Id { get; init; } = Id;
public string? Classify { get; init; } = Classify;
public TransactionType? Type { get; init; } = Type;
}
/// <summary> /// <summary>
/// 按摘要批量更新DTO /// 按摘要批量更新DTO
@@ -794,14 +828,24 @@ public record BatchUpdateByReasonDto(
string Reason, string Reason,
TransactionType Type, TransactionType Type,
string Classify string Classify
); )
{
public string Reason { get; init; } = Reason;
public TransactionType Type { get; init; } = Type;
public string Classify { get; init; } = Classify;
}
/// <summary> /// <summary>
/// 账单分析请求DTO /// 账单分析请求DTO
/// </summary> /// </summary>
public record BillAnalysisRequest( public record BillAnalysisRequest(
string UserInput string UserInput
); )
{
public string UserInput { get; init; } = UserInput;
}
/// <summary> /// <summary>
/// 抵账请求DTO /// 抵账请求DTO
@@ -809,13 +853,23 @@ public record BillAnalysisRequest(
public record OffsetTransactionDto( public record OffsetTransactionDto(
long Id1, long Id1,
long Id2 long Id2
); )
{
public long Id1 { get; init; } = Id1;
public long Id2 { get; init; } = Id2;
}
public record ParseOneLineRequestDto( public record ParseOneLineRequestDto(
string Text string Text
); )
{
public string Text { get; init; } = Text;
}
public record ConfirmAllUnconfirmedRequestDto( public record ConfirmAllUnconfirmedRequestDto(
long[] Ids long[] Ids
); )
{
public long[] Ids { get; init; } = Ids;
}

View File

@@ -1,4 +1,5 @@
using Quartz; using Quartz;
using Service.Jobs;
namespace WebApi; namespace WebApi;
@@ -13,7 +14,7 @@ public static class Expand
// 配置邮件同步任务 - 每10分钟执行一次 // 配置邮件同步任务 - 每10分钟执行一次
var emailJobKey = new JobKey("EmailSyncJob"); var emailJobKey = new JobKey("EmailSyncJob");
q.AddJob<Service.Jobs.EmailSyncJob>(opts => opts q.AddJob<EmailSyncJob>(opts => opts
.WithIdentity(emailJobKey) .WithIdentity(emailJobKey)
.WithDescription("邮件同步任务")); .WithDescription("邮件同步任务"));
q.AddTrigger(opts => opts q.AddTrigger(opts => opts
@@ -24,7 +25,7 @@ public static class Expand
// 配置周期性账单任务 - 每天早上6点执行 // 配置周期性账单任务 - 每天早上6点执行
var periodicBillJobKey = new JobKey("PeriodicBillJob"); var periodicBillJobKey = new JobKey("PeriodicBillJob");
q.AddJob<Service.Jobs.PeriodicBillJob>(opts => opts q.AddJob<PeriodicBillJob>(opts => opts
.WithIdentity(periodicBillJobKey) .WithIdentity(periodicBillJobKey)
.WithDescription("周期性账单任务")); .WithDescription("周期性账单任务"));
q.AddTrigger(opts => opts q.AddTrigger(opts => opts
@@ -35,7 +36,7 @@ public static class Expand
// 配置预算归档任务 - 每个月1号晚11点执行 // 配置预算归档任务 - 每个月1号晚11点执行
var budgetArchiveJobKey = new JobKey("BudgetArchiveJob"); var budgetArchiveJobKey = new JobKey("BudgetArchiveJob");
q.AddJob<Service.Jobs.BudgetArchiveJob>(opts => opts q.AddJob<BudgetArchiveJob>(opts => opts
.WithIdentity(budgetArchiveJobKey) .WithIdentity(budgetArchiveJobKey)
.WithDescription("预算归档任务")); .WithDescription("预算归档任务"));
q.AddTrigger(opts => opts q.AddTrigger(opts => opts

View File

@@ -5,3 +5,5 @@ global using WebApi.Controllers.Dto;
global using Repository; global using Repository;
global using Entity; global using Entity;
global using System.Text; global using System.Text;
global using System.Text.Json;
global using System.Text.Json.Nodes;

View File

@@ -46,7 +46,7 @@ builder.Services.AddCors(options =>
// 绑定配置 // 绑定配置
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings")); builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));
builder.Services.Configure<AISettings>(builder.Configuration.GetSection("OpenAI")); builder.Services.Configure<AiSettings>(builder.Configuration.GetSection("OpenAI"));
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("JwtSettings")); builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("JwtSettings"));
builder.Services.Configure<AuthSettings>(builder.Configuration.GetSection("AuthSettings")); builder.Services.Configure<AuthSettings>(builder.Configuration.GetSection("AuthSettings"));