大量的代码格式化
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 38s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 38s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
This commit is contained in:
@@ -57,6 +57,7 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="year">年份</param>
|
/// <param name="year">年份</param>
|
||||||
/// <param name="month">月份</param>
|
/// <param name="month">月份</param>
|
||||||
|
/// <param name="savingClassify"></param>
|
||||||
/// <returns>每天的消费笔数和金额详情</returns>
|
/// <returns>每天的消费笔数和金额详情</returns>
|
||||||
Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsAsync(int year, int month, string? savingClassify = null);
|
Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsAsync(int year, int month, string? savingClassify = null);
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startDate">开始日期</param>
|
/// <param name="startDate">开始日期</param>
|
||||||
/// <param name="endDate">结束日期</param>
|
/// <param name="endDate">结束日期</param>
|
||||||
|
/// <param name="savingClassify"></param>
|
||||||
/// <returns>每天的消费笔数和金额详情</returns>
|
/// <returns>每天的消费笔数和金额详情</returns>
|
||||||
Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate, string? savingClassify = null);
|
Task<Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate, string? savingClassify = null);
|
||||||
|
|
||||||
@@ -149,7 +151,6 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据关键词查询交易记录(模糊匹配Reason字段)
|
/// 根据关键词查询交易记录(模糊匹配Reason字段)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="keyword">关键词</param>
|
|
||||||
/// <returns>匹配的交易记录列表</returns>
|
/// <returns>匹配的交易记录列表</returns>
|
||||||
Task<List<TransactionRecord>> QueryByWhereAsync(string sql);
|
Task<List<TransactionRecord>> QueryByWhereAsync(string sql);
|
||||||
|
|
||||||
@@ -471,7 +472,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
|||||||
Reason = group.Reason,
|
Reason = group.Reason,
|
||||||
Count = group.Count,
|
Count = group.Count,
|
||||||
SampleType = sample.Type,
|
SampleType = sample.Type,
|
||||||
SampleClassify = sample.Classify ?? string.Empty,
|
SampleClassify = sample.Classify,
|
||||||
TransactionIds = records.Select(r => r.Id).ToList(),
|
TransactionIds = records.Select(r => r.Id).ToList(),
|
||||||
TotalAmount = Math.Abs(group.TotalAmount)
|
TotalAmount = Math.Abs(group.TotalAmount)
|
||||||
});
|
});
|
||||||
@@ -549,7 +550,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var categoryGroups = records
|
var categoryGroups = records
|
||||||
.GroupBy(t => t.Classify ?? "未分类")
|
.GroupBy(t => t.Classify)
|
||||||
.Select(g => new CategoryStatistics
|
.Select(g => new CategoryStatistics
|
||||||
{
|
{
|
||||||
Classify = g.Key,
|
Classify = g.Key,
|
||||||
|
|||||||
@@ -43,12 +43,12 @@ public class ConfigService(IConfigRepository configRepository) : IConfigService
|
|||||||
var config = await configRepository.GetByKeyAsync(key);
|
var config = await configRepository.GetByKeyAsync(key);
|
||||||
var type = typeof(T) switch
|
var type = typeof(T) switch
|
||||||
{
|
{
|
||||||
Type t when t == typeof(bool) => ConfigType.Boolean,
|
{ } t when t == typeof(bool) => ConfigType.Boolean,
|
||||||
Type t when t == typeof(int)
|
{ } t when t == typeof(int)
|
||||||
|| t == typeof(double)
|
|| t == typeof(double)
|
||||||
|| t == typeof(float)
|
|| t == typeof(float)
|
||||||
|| t == typeof(decimal) => ConfigType.Number,
|
|| t == typeof(decimal) => ConfigType.Number,
|
||||||
Type t when t == typeof(string) => ConfigType.String,
|
{ } t when t == typeof(string) => ConfigType.String,
|
||||||
_ => ConfigType.Json
|
_ => ConfigType.Json
|
||||||
};
|
};
|
||||||
var valueStr = type switch
|
var valueStr = type switch
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Service.EmailParseServices;
|
using Service.EmailServices.EmailParse;
|
||||||
|
|
||||||
namespace Service.EmailServices;
|
namespace Service.EmailServices;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Service.EmailParseServices;
|
namespace Service.EmailServices.EmailParse;
|
||||||
|
|
||||||
public class EmailParseForm95555(
|
public class EmailParseForm95555(
|
||||||
ILogger<EmailParseForm95555> logger,
|
ILogger<EmailParseForm95555> logger,
|
||||||
@@ -26,7 +26,7 @@ public class EmailParseForm95555(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<(
|
public override Task<(
|
||||||
string card,
|
string card,
|
||||||
string reason,
|
string reason,
|
||||||
decimal amount,
|
decimal amount,
|
||||||
@@ -51,7 +51,7 @@ public class EmailParseForm95555(
|
|||||||
if (matches.Count <= 0)
|
if (matches.Count <= 0)
|
||||||
{
|
{
|
||||||
logger.LogWarning("未能从招商银行邮件内容中解析出交易信息");
|
logger.LogWarning("未能从招商银行邮件内容中解析出交易信息");
|
||||||
return [];
|
return Task.FromResult<(string card, string reason, decimal amount, decimal balance, TransactionType type, DateTime? occurredAt)[]>([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var results = new List<(
|
var results = new List<(
|
||||||
@@ -85,7 +85,7 @@ public class EmailParseForm95555(
|
|||||||
results.Add((card, reason, amount, balance, type, occurredAt));
|
results.Add((card, reason, amount, balance, type, occurredAt));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results.ToArray();
|
return Task.FromResult(results.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private DateTime? ParseOccurredAt(string value)
|
private DateTime? ParseOccurredAt(string value)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
|
||||||
namespace Service.EmailParseServices;
|
namespace Service.EmailServices.EmailParse;
|
||||||
|
|
||||||
public class EmailParseFormCcsvc(
|
public class EmailParseFormCcsvc(
|
||||||
ILogger<EmailParseFormCcsvc> logger,
|
ILogger<EmailParseFormCcsvc> logger,
|
||||||
@@ -44,11 +44,6 @@ public class EmailParseFormCcsvc(
|
|||||||
|
|
||||||
// 1. Get Date
|
// 1. Get Date
|
||||||
var dateNode = doc.DocumentNode.SelectSingleNode("//font[contains(text(), '您的消费明细如下')]");
|
var dateNode = doc.DocumentNode.SelectSingleNode("//font[contains(text(), '您的消费明细如下')]");
|
||||||
if (dateNode == null)
|
|
||||||
{
|
|
||||||
logger.LogWarning("Date node not found");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var dateText = dateNode.InnerText.Trim();
|
var dateText = dateNode.InnerText.Trim();
|
||||||
// "2025/12/21 您的消费明细如下:"
|
// "2025/12/21 您的消费明细如下:"
|
||||||
@@ -63,104 +58,87 @@ public class EmailParseFormCcsvc(
|
|||||||
decimal balance = 0;
|
decimal balance = 0;
|
||||||
// Find "可用额度" label
|
// Find "可用额度" label
|
||||||
var limitLabelNode = doc.DocumentNode.SelectSingleNode("//font[contains(text(), '可用额度')]");
|
var limitLabelNode = doc.DocumentNode.SelectSingleNode("//font[contains(text(), '可用额度')]");
|
||||||
if (limitLabelNode != null)
|
|
||||||
{
|
{
|
||||||
// Go up to TR
|
// Go up to TR
|
||||||
var tr = limitLabelNode.Ancestors("tr").FirstOrDefault();
|
var tr = limitLabelNode.Ancestors("tr").FirstOrDefault();
|
||||||
if (tr != null)
|
if (tr != null)
|
||||||
{
|
{
|
||||||
var prevTr = tr.PreviousSibling;
|
var prevTr = tr.PreviousSibling;
|
||||||
while (prevTr != null && prevTr.Name != "tr") prevTr = prevTr.PreviousSibling;
|
while (prevTr.Name != "tr") prevTr = prevTr.PreviousSibling;
|
||||||
|
|
||||||
if (prevTr != null)
|
var balanceNode = prevTr.SelectSingleNode(".//font[contains(text(), '¥')]");
|
||||||
{
|
var balanceStr = balanceNode.InnerText.Replace("¥", "").Replace(",", "").Trim();
|
||||||
var balanceNode = prevTr.SelectSingleNode(".//font[contains(text(), '¥')]");
|
decimal.TryParse(balanceStr, out balance);
|
||||||
if (balanceNode != null)
|
|
||||||
{
|
|
||||||
var balanceStr = balanceNode.InnerText.Replace("¥", "").Replace(",", "").Trim();
|
|
||||||
decimal.TryParse(balanceStr, out balance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Get Transactions
|
// 3. Get Transactions
|
||||||
var transactionNodes = doc.DocumentNode.SelectNodes("//span[@id='fixBand4']");
|
var transactionNodes = doc.DocumentNode.SelectNodes("//span[@id='fixBand4']");
|
||||||
if (transactionNodes != null)
|
foreach (var node in transactionNodes)
|
||||||
{
|
{
|
||||||
foreach (var node in transactionNodes)
|
try
|
||||||
{
|
{
|
||||||
try
|
// Time
|
||||||
|
var timeNode = node.SelectSingleNode(".//span[@id='fixBand5']//font");
|
||||||
|
var timeText = timeNode.InnerText.Trim(); // "10:13:43"
|
||||||
|
|
||||||
|
DateTime? occurredAt = date;
|
||||||
|
if (!string.IsNullOrEmpty(timeText) && DateTime.TryParse($"{date:yyyy-MM-dd} {timeText}", out var dt))
|
||||||
{
|
{
|
||||||
// Time
|
occurredAt = dt;
|
||||||
var timeNode = node.SelectSingleNode(".//span[@id='fixBand5']//font");
|
|
||||||
var timeText = timeNode?.InnerText.Trim(); // "10:13:43"
|
|
||||||
|
|
||||||
DateTime? occurredAt = date;
|
|
||||||
if (!string.IsNullOrEmpty(timeText) && DateTime.TryParse($"{date:yyyy-MM-dd} {timeText}", out var dt))
|
|
||||||
{
|
|
||||||
occurredAt = dt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info Block
|
|
||||||
var infoNode = node.SelectSingleNode(".//span[@id='fixBand12']");
|
|
||||||
if (infoNode == null) continue;
|
|
||||||
|
|
||||||
// Amount
|
|
||||||
var amountNode = infoNode.SelectSingleNode(".//font[contains(text(), 'CNY')]");
|
|
||||||
var amountText = amountNode?.InnerText.Replace("CNY", "").Replace(" ", "").Trim();
|
|
||||||
if (!decimal.TryParse(amountText, out var amount))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description
|
|
||||||
var descNode = infoNode.SelectSingleNode(".//tr[2]//font");
|
|
||||||
var descText = descNode?.InnerText ?? "";
|
|
||||||
// Replace and non-breaking space (\u00A0) with normal space
|
|
||||||
descText = descText.Replace(" ", " ");
|
|
||||||
descText = HtmlEntity.DeEntitize(descText).Replace((char)160, ' ').Trim();
|
|
||||||
|
|
||||||
// Parse Description: "尾号4390 消费 财付通-luckincoffee瑞幸咖啡"
|
|
||||||
var parts = descText.Split([' '], StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
string card = "";
|
|
||||||
string reason = descText;
|
|
||||||
TransactionType type = TransactionType.Expense;
|
|
||||||
|
|
||||||
if (parts.Length > 0 && parts[0].StartsWith("尾号"))
|
|
||||||
{
|
|
||||||
card = parts[0].Replace("尾号", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.Length > 1)
|
|
||||||
{
|
|
||||||
var typeStr = parts[1];
|
|
||||||
type = DetermineTransactionType(typeStr, reason, amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.Length > 2)
|
|
||||||
{
|
|
||||||
reason = string.Join(" ", parts.Skip(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 招商信用卡特殊,消费金额为正数,退款为负数
|
|
||||||
if(amount > 0)
|
|
||||||
{
|
|
||||||
type = TransactionType.Expense;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
type = TransactionType.Income;
|
|
||||||
amount = Math.Abs(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Add((card, reason, amount, balance, type, occurredAt));
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
|
// Info Block
|
||||||
|
var infoNode = node.SelectSingleNode(".//span[@id='fixBand12']");
|
||||||
|
|
||||||
|
// Amount
|
||||||
|
var amountNode = infoNode.SelectSingleNode(".//font[contains(text(), 'CNY')]");
|
||||||
|
var amountText = amountNode.InnerText.Replace("CNY", "").Replace(" ", "").Trim();
|
||||||
|
if (!decimal.TryParse(amountText, out var amount))
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Error parsing transaction node");
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
|
var descNode = infoNode.SelectSingleNode(".//tr[2]//font");
|
||||||
|
var descText = descNode.InnerText;
|
||||||
|
// Replace and non-breaking space (\u00A0) with normal space
|
||||||
|
descText = descText.Replace(" ", " ");
|
||||||
|
descText = HtmlEntity.DeEntitize(descText).Replace((char)160, ' ').Trim();
|
||||||
|
|
||||||
|
// Parse Description: "尾号4390 消费 财付通-luckincoffee瑞幸咖啡"
|
||||||
|
var parts = descText.Split([' '], StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
string card = "";
|
||||||
|
string reason = descText;
|
||||||
|
TransactionType type;
|
||||||
|
|
||||||
|
if (parts.Length > 0 && parts[0].StartsWith("尾号"))
|
||||||
|
{
|
||||||
|
card = parts[0].Replace("尾号", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.Length > 2)
|
||||||
|
{
|
||||||
|
reason = string.Join(" ", parts.Skip(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 招商信用卡特殊,消费金额为正数,退款为负数
|
||||||
|
if(amount > 0)
|
||||||
|
{
|
||||||
|
type = TransactionType.Expense;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
type = TransactionType.Income;
|
||||||
|
amount = Math.Abs(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add((card, reason, amount, balance, type, occurredAt));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error parsing transaction node");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Service.EmailParseServices;
|
namespace Service.EmailServices.EmailParse;
|
||||||
|
|
||||||
public interface IEmailParseServices
|
public interface IEmailParseServices
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ public class EmailSyncService(
|
|||||||
var unreadMessages = await emailFetchService.FetchUnreadMessagesAsync();
|
var unreadMessages = await emailFetchService.FetchUnreadMessagesAsync();
|
||||||
logger.LogInformation("邮箱 {Email} 获取到 {MessageCount} 封未读邮件", email, unreadMessages.Count);
|
logger.LogInformation("邮箱 {Email} 获取到 {MessageCount} 封未读邮件", email, unreadMessages.Count);
|
||||||
|
|
||||||
foreach (var (message, uid) in unreadMessages)
|
foreach (var (message, _) in unreadMessages)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ public class EmailSyncJob(
|
|||||||
var unreadMessages = await emailFetchService.FetchUnreadMessagesAsync();
|
var unreadMessages = await emailFetchService.FetchUnreadMessagesAsync();
|
||||||
logger.LogInformation("邮箱 {Email} 获取到 {MessageCount} 封未读邮件", email, unreadMessages.Count);
|
logger.LogInformation("邮箱 {Email} 获取到 {MessageCount} 封未读邮件", email, unreadMessages.Count);
|
||||||
|
|
||||||
foreach (var (message, uid) in unreadMessages)
|
foreach (var (message, _) in unreadMessages)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Net;
|
using WebPush;
|
||||||
using WebPush;
|
|
||||||
using PushSubscription = Entity.PushSubscription;
|
using PushSubscription = Entity.PushSubscription;
|
||||||
|
|
||||||
namespace Service;
|
namespace Service;
|
||||||
|
|||||||
@@ -158,10 +158,8 @@ public class OpenAiService(
|
|||||||
var json = JsonSerializer.Serialize(payload);
|
var json = JsonSerializer.Serialize(payload);
|
||||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Post, url)
|
using var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||||
{
|
request.Content = content;
|
||||||
Content = content
|
|
||||||
};
|
|
||||||
using var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
using var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
if (!resp.IsSuccessStatusCode)
|
if (!resp.IsSuccessStatusCode)
|
||||||
@@ -232,10 +230,8 @@ public class OpenAiService(
|
|||||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
// 使用 SendAsync 来支持 HttpCompletionOption
|
// 使用 SendAsync 来支持 HttpCompletionOption
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Post, url)
|
using var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||||
{
|
request.Content = content;
|
||||||
Content = content
|
|
||||||
};
|
|
||||||
using var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
using var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
if (!resp.IsSuccessStatusCode)
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
|||||||
@@ -50,9 +50,6 @@ public class BillImportController(
|
|||||||
return "文件大小不能超过 10MB".Fail();
|
return "文件大小不能超过 10MB".Fail();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成唯一文件名
|
|
||||||
var fileName = $"{type}_{DateTime.Now:yyyyMMddHHmmss}_{Guid.NewGuid():N}{fileExtension}";
|
|
||||||
|
|
||||||
// 保存文件
|
// 保存文件
|
||||||
var ok = false;
|
var ok = false;
|
||||||
var message = string.Empty;
|
var message = string.Empty;
|
||||||
@@ -69,6 +66,11 @@ public class BillImportController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
return message.Fail();
|
||||||
|
}
|
||||||
|
|
||||||
return message.Ok();
|
return message.Ok();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -77,4 +79,4 @@ public class BillImportController(
|
|||||||
return $"文件上传失败: {ex.Message}".Fail();
|
return $"文件上传失败: {ex.Message}".Fail();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ public class BudgetController(
|
|||||||
Type = dto.Type,
|
Type = dto.Type,
|
||||||
Limit = limit,
|
Limit = limit,
|
||||||
Category = dto.Category,
|
Category = dto.Category,
|
||||||
SelectedCategories = dto.SelectedCategories != null ? string.Join(",", dto.SelectedCategories) : string.Empty,
|
SelectedCategories = string.Join(",", dto.SelectedCategories),
|
||||||
StartDate = dto.StartDate ?? DateTime.Now,
|
StartDate = dto.StartDate ?? DateTime.Now,
|
||||||
NoLimit = dto.NoLimit,
|
NoLimit = dto.NoLimit,
|
||||||
IsMandatoryExpense = dto.IsMandatoryExpense
|
IsMandatoryExpense = dto.IsMandatoryExpense
|
||||||
@@ -182,7 +182,7 @@ public class BudgetController(
|
|||||||
budget.Type = dto.Type;
|
budget.Type = dto.Type;
|
||||||
budget.Limit = limit;
|
budget.Limit = limit;
|
||||||
budget.Category = dto.Category;
|
budget.Category = dto.Category;
|
||||||
budget.SelectedCategories = dto.SelectedCategories != null ? string.Join(",", dto.SelectedCategories) : string.Empty;
|
budget.SelectedCategories = string.Join(",", dto.SelectedCategories);
|
||||||
budget.NoLimit = dto.NoLimit;
|
budget.NoLimit = dto.NoLimit;
|
||||||
budget.IsMandatoryExpense = dto.IsMandatoryExpense;
|
budget.IsMandatoryExpense = dto.IsMandatoryExpense;
|
||||||
if (dto.StartDate.HasValue)
|
if (dto.StartDate.HasValue)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class EmailMessageDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从实体转换为DTO
|
/// 从实体转换为DTO
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static EmailMessageDto FromEntity(Entity.EmailMessage entity, int transactionCount = 0)
|
public static EmailMessageDto FromEntity(EmailMessage entity, int transactionCount = 0)
|
||||||
{
|
{
|
||||||
return new EmailMessageDto
|
return new EmailMessageDto
|
||||||
{
|
{
|
||||||
@@ -44,7 +44,7 @@ public class EmailMessageDto
|
|||||||
CreateTime = entity.CreateTime,
|
CreateTime = entity.CreateTime,
|
||||||
UpdateTime = entity.UpdateTime,
|
UpdateTime = entity.UpdateTime,
|
||||||
TransactionCount = transactionCount,
|
TransactionCount = transactionCount,
|
||||||
ToName = entity.To?.Split('<').FirstOrDefault()?.Trim() ?? "未知"
|
ToName = entity.To.Split('<').FirstOrDefault()?.Trim() ?? "未知"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
public class PagedResponse<T> : BaseResponse<T[]>
|
public class PagedResponse<T> : BaseResponse<T[]>
|
||||||
{
|
{
|
||||||
public long LastId { get; set; } = 0;
|
public long LastId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 最后一条记录的时间(用于游标分页)
|
/// 最后一条记录的时间(用于游标分页)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Service.EmailServices;
|
using Service.EmailServices;
|
||||||
|
|
||||||
namespace WebApi.Controllers.EmailMessage;
|
namespace WebApi.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]/[action]")]
|
[Route("api/[controller]/[action]")]
|
||||||
|
|||||||
@@ -268,9 +268,9 @@ public class TransactionRecordController(
|
|||||||
|
|
||||||
var statistics = await transactionRepository.GetDailyStatisticsAsync(year, month, savingClassify);
|
var statistics = await transactionRepository.GetDailyStatisticsAsync(year, month, savingClassify);
|
||||||
var result = statistics.Select(s => new DailyStatisticsDto(
|
var result = statistics.Select(s => new DailyStatisticsDto(
|
||||||
s.Key,
|
s.Key,
|
||||||
s.Value.count,
|
s.Value.count,
|
||||||
s.Value.expense,
|
s.Value.expense,
|
||||||
s.Value.income,
|
s.Value.income,
|
||||||
s.Value.saving
|
s.Value.saving
|
||||||
)).ToList();
|
)).ToList();
|
||||||
@@ -303,13 +303,13 @@ public class TransactionRecordController(
|
|||||||
var savingClassify = await configService.GetConfigByKeyAsync<string>("SavingsCategories");
|
var savingClassify = await configService.GetConfigByKeyAsync<string>("SavingsCategories");
|
||||||
|
|
||||||
var statistics = await transactionRepository.GetDailyStatisticsByRangeAsync(
|
var statistics = await transactionRepository.GetDailyStatisticsByRangeAsync(
|
||||||
effectiveStartDate,
|
effectiveStartDate,
|
||||||
effectiveEndDate,
|
effectiveEndDate,
|
||||||
savingClassify);
|
savingClassify);
|
||||||
var result = statistics.Select(s => new DailyStatisticsDto(
|
var result = statistics.Select(s => new DailyStatisticsDto(
|
||||||
s.Key,
|
s.Key,
|
||||||
s.Value.count,
|
s.Value.count,
|
||||||
s.Value.expense,
|
s.Value.expense,
|
||||||
s.Value.income,
|
s.Value.income,
|
||||||
s.Value.income - s.Value.expense
|
s.Value.income - s.Value.expense
|
||||||
)).ToList();
|
)).ToList();
|
||||||
@@ -383,7 +383,8 @@ public class TransactionRecordController(
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "获取趋势统计数据失败,开始年份: {Year}, 开始月份: {Month}, 月份数: {Count}", startYear, startMonth, monthCount);
|
logger.LogError(ex, "获取趋势统计数据失败,开始年份: {Year}, 开始月份: {Month}, 月份数: {Count}", startYear, startMonth,
|
||||||
|
monthCount);
|
||||||
return $"获取趋势统计数据失败: {ex.Message}".Fail<List<TrendStatistics>>();
|
return $"获取趋势统计数据失败: {ex.Message}".Fail<List<TrendStatistics>>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,9 +404,16 @@ public class TransactionRecordController(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await smartHandleService.AnalyzeBillAsync(request.UserInput, async chunk =>
|
await smartHandleService.AnalyzeBillAsync(request.UserInput, async void (chunk) =>
|
||||||
{
|
{
|
||||||
await WriteEventAsync(chunk);
|
try
|
||||||
|
{
|
||||||
|
await WriteEventAsync(chunk);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, "流式写入账单分析结果失败");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,11 +498,18 @@ public class TransactionRecordController(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await smartHandleService.SmartClassifyAsync(request.TransactionIds.ToArray(), async chunk =>
|
await smartHandleService.SmartClassifyAsync(request.TransactionIds.ToArray(), async void (chunk) =>
|
||||||
{
|
{
|
||||||
var (eventType, content) = chunk;
|
try
|
||||||
await TrySetUnconfirmedAsync(eventType, content);
|
{
|
||||||
await WriteEventAsync(eventType, content);
|
var (eventType, content) = chunk;
|
||||||
|
await TrySetUnconfirmedAsync(eventType, content);
|
||||||
|
await WriteEventAsync(eventType, content);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, "流式写入智能分类结果失败");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await Response.Body.FlushAsync();
|
await Response.Body.FlushAsync();
|
||||||
@@ -514,7 +529,7 @@ public class TransactionRecordController(
|
|||||||
var classify = jsonObject?["Classify"]?.GetValue<string>() ?? string.Empty;
|
var classify = jsonObject?["Classify"]?.GetValue<string>() ?? string.Empty;
|
||||||
var typeValue = jsonObject?["Type"]?.GetValue<int>() ?? -1;
|
var typeValue = jsonObject?["Type"]?.GetValue<int>() ?? -1;
|
||||||
|
|
||||||
if(id == 0 || typeValue == -1 || string.IsNullOrEmpty(classify))
|
if (id == 0 || typeValue == -1 || string.IsNullOrEmpty(classify))
|
||||||
{
|
{
|
||||||
logger.LogWarning("解析智能分类结果时,发现无效数据,内容: {Content}", content);
|
logger.LogWarning("解析智能分类结果时,发现无效数据,内容: {Content}", content);
|
||||||
return;
|
return;
|
||||||
@@ -565,14 +580,17 @@ public class TransactionRecordController(
|
|||||||
{
|
{
|
||||||
record.Type = item.Type.Value;
|
record.Type = item.Type.Value;
|
||||||
}
|
}
|
||||||
if(!string.IsNullOrEmpty(record.Classify))
|
|
||||||
|
if (!string.IsNullOrEmpty(record.Classify))
|
||||||
{
|
{
|
||||||
record.UnconfirmedClassify = null;
|
record.UnconfirmedClassify = null;
|
||||||
}
|
}
|
||||||
if(record.Type == item.Type)
|
|
||||||
|
if (record.Type == item.Type)
|
||||||
{
|
{
|
||||||
record.UnconfirmedType = TransactionType.None;
|
record.UnconfirmedType = TransactionType.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
var success = await transactionRepository.UpdateAsync(record);
|
var success = await transactionRepository.UpdateAsync(record);
|
||||||
if (success)
|
if (success)
|
||||||
successCount++;
|
successCount++;
|
||||||
@@ -735,18 +753,7 @@ 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,20 +765,7 @@ 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
|
||||||
@@ -782,28 +776,14 @@ 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
|
||||||
@@ -812,14 +792,7 @@ 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
|
||||||
@@ -828,24 +801,14 @@ 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
|
||||||
@@ -853,23 +816,12 @@ 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;
|
|
||||||
}
|
|
||||||
@@ -22,21 +22,21 @@ builder.Host.UseSerilog((context, loggerConfig) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddControllers(options =>
|
builder.Services.AddControllers(mvcOptions =>
|
||||||
{
|
{
|
||||||
var policy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
|
var policy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
|
||||||
.RequireAuthenticatedUser()
|
.RequireAuthenticatedUser()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
options.Filters.Add(new AuthorizeFilter(policy));
|
mvcOptions.Filters.Add(new AuthorizeFilter(policy));
|
||||||
});
|
});
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
// 配置 CORS
|
// 配置 CORS
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(corsOptions =>
|
||||||
{
|
{
|
||||||
options.AddDefaultPolicy(policy =>
|
corsOptions.AddDefaultPolicy(policy =>
|
||||||
{
|
{
|
||||||
policy.WithOrigins("http://localhost:5173")
|
policy.WithOrigins("http://localhost:5173")
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
@@ -55,14 +55,14 @@ var jwtSettings = builder.Configuration.GetSection("JwtSettings");
|
|||||||
var secretKey = jwtSettings["SecretKey"]!;
|
var secretKey = jwtSettings["SecretKey"]!;
|
||||||
var key = Encoding.UTF8.GetBytes(secretKey);
|
var key = Encoding.UTF8.GetBytes(secretKey);
|
||||||
|
|
||||||
builder.Services.AddAuthentication(options =>
|
builder.Services.AddAuthentication(authenticationOptions =>
|
||||||
{
|
{
|
||||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
authenticationOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
authenticationOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
})
|
})
|
||||||
.AddJwtBearer(options =>
|
.AddJwtBearer(jwtBearerOptions =>
|
||||||
{
|
{
|
||||||
options.TokenValidationParameters = new TokenValidationParameters
|
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
|
||||||
{
|
{
|
||||||
ValidateIssuer = true,
|
ValidateIssuer = true,
|
||||||
ValidateAudience = true,
|
ValidateAudience = true,
|
||||||
@@ -73,7 +73,7 @@ builder.Services.AddAuthentication(options =>
|
|||||||
IssuerSigningKey = new SymmetricSecurityKey(key),
|
IssuerSigningKey = new SymmetricSecurityKey(key),
|
||||||
ClockSkew = TimeSpan.Zero
|
ClockSkew = TimeSpan.Zero
|
||||||
};
|
};
|
||||||
options.Events = new JwtBearerEvents
|
jwtBearerOptions.Events = new JwtBearerEvents
|
||||||
{
|
{
|
||||||
OnChallenge = async context =>
|
OnChallenge = async context =>
|
||||||
{
|
{
|
||||||
|
|||||||
46
qodana.yaml
Normal file
46
qodana.yaml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#-------------------------------------------------------------------------------#
|
||||||
|
# Qodana analysis is configured by qodana.yaml file #
|
||||||
|
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||||
|
#-------------------------------------------------------------------------------#
|
||||||
|
|
||||||
|
#################################################################################
|
||||||
|
# WARNING: Do not store sensitive information in this file, #
|
||||||
|
# as its contents will be included in the Qodana report. #
|
||||||
|
#################################################################################
|
||||||
|
version: "1.0"
|
||||||
|
|
||||||
|
#Specify IDE code to run analysis without container (Applied in CI/CD pipeline)
|
||||||
|
ide: QDNET
|
||||||
|
|
||||||
|
#Specify inspection profile for code analysis
|
||||||
|
profile:
|
||||||
|
name: qodana.starter
|
||||||
|
|
||||||
|
#Enable inspections
|
||||||
|
#include:
|
||||||
|
# - name: <SomeEnabledInspectionId>
|
||||||
|
|
||||||
|
#Disable inspections
|
||||||
|
#exclude:
|
||||||
|
# - name: <SomeDisabledInspectionId>
|
||||||
|
# paths:
|
||||||
|
# - <path/where/not/run/inspection>
|
||||||
|
|
||||||
|
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||||
|
#bootstrap: sh ./prepare-qodana.sh
|
||||||
|
|
||||||
|
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||||
|
#plugins:
|
||||||
|
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||||
|
|
||||||
|
# Quality gate. Will fail the CI/CD pipeline if any condition is not met
|
||||||
|
# severityThresholds - configures maximum thresholds for different problem severities
|
||||||
|
# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code
|
||||||
|
# Code Coverage is available in Ultimate and Ultimate Plus plans
|
||||||
|
#failureConditions:
|
||||||
|
# severityThresholds:
|
||||||
|
# any: 15
|
||||||
|
# critical: 5
|
||||||
|
# testCoverageThresholds:
|
||||||
|
# fresh: 70
|
||||||
|
# total: 50
|
||||||
Reference in New Issue
Block a user