结构调整
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 14s
Docker Build & Deploy / Deploy to Production (push) Has been skipped

This commit is contained in:
2026-01-01 12:32:08 +08:00
parent c1aa4df4f3
commit 8dfe7f1688
15 changed files with 152 additions and 150 deletions

View File

@@ -4,7 +4,7 @@ using MailKit.Search;
using MailKit.Security; using MailKit.Security;
using MimeKit; using MimeKit;
namespace Service; namespace Service.EmailServices;
/// <summary> /// <summary>
/// 邮件抓取服务接口 /// 邮件抓取服务接口
@@ -15,7 +15,7 @@ public interface IEmailFetchService
/// 连接状态 /// 连接状态
/// </summary> /// </summary>
bool IsConnected { get; } bool IsConnected { get; }
/// <summary> /// <summary>
/// 连接到邮件服务器 /// 连接到邮件服务器
/// </summary> /// </summary>
@@ -30,7 +30,7 @@ public interface IEmailFetchService
/// 获取所有邮件 /// 获取所有邮件
/// </summary> /// </summary>
Task<List<(MimeMessage Message, UniqueId Uid)>> FetchAllMessagesAsync(); Task<List<(MimeMessage Message, UniqueId Uid)>> FetchAllMessagesAsync();
/// <summary> /// <summary>
/// 断开与邮件服务器的连接 /// 断开与邮件服务器的连接
/// </summary> /// </summary>
@@ -40,7 +40,7 @@ public interface IEmailFetchService
/// 标记邮件为已读 /// 标记邮件为已读
/// </summary> /// </summary>
Task MarkAsReadAsync(UniqueId uid); Task MarkAsReadAsync(UniqueId uid);
/// <summary> /// <summary>
/// 确保连接有效,如断开则自动重连 /// 确保连接有效,如断开则自动重连
/// </summary> /// </summary>
@@ -61,7 +61,7 @@ public class EmailFetchService(ILogger<EmailFetchService> logger) : IEmailFetchS
private DateTime _lastKeepAlive = DateTime.MinValue; private DateTime _lastKeepAlive = DateTime.MinValue;
private const int KeepAliveIntervalSeconds = 300; // 5分钟发送一次KeepAlive private const int KeepAliveIntervalSeconds = 300; // 5分钟发送一次KeepAlive
private readonly ILogger<EmailFetchService> _logger = logger; private readonly ILogger<EmailFetchService> _logger = logger;
public bool IsConnected => _imapClient?.IsConnected == true; public bool IsConnected => _imapClient?.IsConnected == true;
public async Task<bool> ConnectAsync(string host, int port, bool useSsl, string email, string password) public async Task<bool> ConnectAsync(string host, int port, bool useSsl, string email, string password)
@@ -74,13 +74,13 @@ public class EmailFetchService(ILogger<EmailFetchService> logger) : IEmailFetchS
_useSsl = useSsl; _useSsl = useSsl;
_email = email; _email = email;
_password = password; _password = password;
// 如果已连接,先断开 // 如果已连接,先断开
if (_imapClient?.IsConnected == true) if (_imapClient?.IsConnected == true)
{ {
await DisconnectAsync(); await DisconnectAsync();
} }
_imapClient = new ImapClient(); _imapClient = new ImapClient();
if (useSsl) if (useSsl)
@@ -206,7 +206,7 @@ public class EmailFetchService(ILogger<EmailFetchService> logger) : IEmailFetchS
// 标记邮件为已读设置Seen标记 // 标记邮件为已读设置Seen标记
await inbox.AddFlagsAsync(uid, MessageFlags.Seen, silent: false); await inbox.AddFlagsAsync(uid, MessageFlags.Seen, silent: false);
_logger.LogDebug("邮件 {Uid} 标记已读操作已提交", uid); _logger.LogDebug("邮件 {Uid} 标记已读操作已提交", uid);
} }
catch (Exception ex) catch (Exception ex)
@@ -214,7 +214,7 @@ public class EmailFetchService(ILogger<EmailFetchService> logger) : IEmailFetchS
_logger.LogError(ex, "标记邮件为已读失败: {Message}", ex.Message); _logger.LogError(ex, "标记邮件为已读失败: {Message}", ex.Message);
} }
} }
/// <summary> /// <summary>
/// 确保连接有效,如果断开则自动重连 /// 确保连接有效,如果断开则自动重连
/// </summary> /// </summary>
@@ -240,13 +240,13 @@ public class EmailFetchService(ILogger<EmailFetchService> logger) : IEmailFetchS
} }
return _imapClient?.IsConnected == true; return _imapClient?.IsConnected == true;
} }
if (string.IsNullOrEmpty(_host) || string.IsNullOrEmpty(_email)) if (string.IsNullOrEmpty(_host) || string.IsNullOrEmpty(_email))
{ {
_logger.LogWarning("未初始化连接信息,无法自动重连"); _logger.LogWarning("未初始化连接信息,无法自动重连");
return false; return false;
} }
_logger.LogInformation("检测到连接断开,尝试重新连接到 {Email}...", _email); _logger.LogInformation("检测到连接断开,尝试重新连接到 {Email}...", _email);
return await ConnectAsync(_host, _port, _useSsl, _email, _password); return await ConnectAsync(_host, _port, _useSsl, _email, _password);
} }

View File

@@ -1,6 +1,6 @@
using Service.EmailParseServices; using Service.EmailParseServices;
namespace Service; namespace Service.EmailServices;
public interface IEmailHandleService public interface IEmailHandleService
{ {

View File

@@ -72,17 +72,17 @@ public abstract class EmailParseServicesBase(
)[]?> ParseByAiAsync(string body) )[]?> ParseByAiAsync(string body)
{ {
var systemPrompt = $""" var systemPrompt = $"""
JSON数组 JSON数组
: card(), reason(), amount(), balance(), type(''或''), occurredAt(yyyy-MM-dd HH:mm:ss格式日期时间) : card(), reason(), amount(), balance(), type(''或''), occurredAt(yyyy-MM-dd HH:mm:ss格式日期时间)
[重要] {DateTime.Now:yyyy-MM-dd HH:mm:ss} [重要] {DateTime.Now:yyyy-MM-dd HH:mm:ss}
"""; """;
var userPrompt = $""" var userPrompt = $"""
JSON数组格式 JSON数组格式
: card, reason, amount, balance, type(), occurredAt() : card, reason, amount, balance, type(), occurredAt()
:\n\n{body} :\n\n{body}
"""; """;
var contentText = await openAiService.ChatAsync(systemPrompt, userPrompt); var contentText = await openAiService.ChatAsync(systemPrompt, userPrompt);
if (string.IsNullOrWhiteSpace(contentText)) if (string.IsNullOrWhiteSpace(contentText))

View File

@@ -1,9 +1,9 @@
using System.ComponentModel; using System.ComponentModel;
using MimeKit; using MimeKit;
namespace Service; namespace Service.EmailServices;
public interface IEmailBackgroundService public interface IEmailSyncService
{ {
/// <summary> /// <summary>
/// 手动触发邮件同步 /// 手动触发邮件同步
@@ -11,12 +11,12 @@ public interface IEmailBackgroundService
Task SyncEmailsAsync(); Task SyncEmailsAsync();
} }
public class EmailBackgroundService( public class EmailSyncService(
IOptions<EmailSettings> emailSettings, IOptions<EmailSettings> emailSettings,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
IEmailHandleService emailHandleService, IEmailHandleService emailHandleService,
ILogger<EmailBackgroundService> logger) ILogger<EmailSyncService> logger)
: BackgroundWorker, IEmailBackgroundService : BackgroundWorker, IEmailSyncService
{ {
private readonly Dictionary<string, IEmailFetchService> _emailFetchServices = new(); private readonly Dictionary<string, IEmailFetchService> _emailFetchServices = new();
private bool _isInitialized; private bool _isInitialized;

View File

@@ -496,4 +496,3 @@ public class ImportService(
"yyyyMMddHHmmss", "yyyyMMddHHmmss",
]; ];
} }

View File

@@ -1,5 +1,6 @@
using MimeKit; using MimeKit;
using Quartz; using Quartz;
using Service.EmailServices;
namespace Service.Jobs; namespace Service.Jobs;

View File

@@ -44,7 +44,7 @@ public class MessageRecordService(IMessageRecordRepository messageRepo) : IMessa
{ {
var message = await messageRepo.GetByIdAsync(id); var message = await messageRepo.GetByIdAsync(id);
if (message == null) return false; if (message == null) return false;
message.IsRead = true; message.IsRead = true;
message.UpdateTime = DateTime.Now; message.UpdateTime = DateTime.Now;
return await messageRepo.UpdateAsync(message); return await messageRepo.UpdateAsync(message);

View File

@@ -44,7 +44,7 @@ public class OpenAiService(
var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions"; var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions";
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");
try try
{ {
using var resp = await http.PostAsync(url, content); using var resp = await http.PostAsync(url, content);
@@ -100,7 +100,7 @@ public class OpenAiService(
var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions"; var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions";
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");
try try
{ {
using var resp = await http.PostAsync(url, content); using var resp = await http.PostAsync(url, content);
@@ -157,13 +157,13 @@ public class OpenAiService(
var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions"; var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions";
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)
{ {
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)
{ {
var err = await resp.Content.ReadAsStringAsync(); var err = await resp.Content.ReadAsStringAsync();
@@ -230,14 +230,14 @@ public class OpenAiService(
var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions"; var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions";
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");
// 使用 SendAsync 来支持 HttpCompletionOption // 使用 SendAsync 来支持 HttpCompletionOption
using var request = new HttpRequestMessage(HttpMethod.Post, url) using var request = new HttpRequestMessage(HttpMethod.Post, url)
{ {
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)
{ {
var err = await resp.Content.ReadAsStringAsync(); var err = await resp.Content.ReadAsStringAsync();

View File

@@ -19,7 +19,7 @@ public class PeriodicBillBackgroundService(
try try
{ {
var now = DateTime.Now; var now = DateTime.Now;
// 计算下次执行时间每天早上6点 // 计算下次执行时间每天早上6点
var nextRun = now.Date.AddHours(6); var nextRun = now.Date.AddHours(6);
if (now >= nextRun) if (now >= nextRun)

View File

@@ -112,33 +112,33 @@ public class SmartHandleService(
} }
var systemPrompt = $$""" var systemPrompt = $$"""
你是一个专业的账单分类助手。请根据提供的账单分组信息和分类列表,为每个分组选择最合适的分类。 你是一个专业的账单分类助手。请根据提供的账单分组信息和分类列表,为每个分组选择最合适的分类。
可用的分类列表: 可用的分类列表:
{{categoryInfo}} {{categoryInfo}}
分类规则: 分类规则:
1. 根据账单的摘要和涉及金额,选择最匹配的分类 1. 根据账单的摘要和涉及金额,选择最匹配的分类
2. 如果提供了【参考】信息,优先参考相似账单的分类,这些是历史上已分类的相似账单 2. 如果提供了【参考】信息,优先参考相似账单的分类,这些是历史上已分类的相似账单
3. 如果无法确定分类,可以选择"" 3. 如果无法确定分类,可以选择""
4. 4.
- 使 NDJSON JSON - 使 NDJSON JSON
- JSON格式严格为{"reason": "交易摘要", "type": 0, "classify": "分类名称"} - JSON格式严格为{"reason": "交易摘要", "type": 0, "classify": "分类名称"}
- -
- "classify" "其他" JSON - "classify" "其他" JSON
JSON对象NDJSON JSON对象NDJSON
"""; """;
var userPrompt = $$""" var userPrompt = $$"""
请为以下账单分组进行分类: 请为以下账单分组进行分类:
{{billsInfo}} {{billsInfo}}
请逐个输出分类结果。 请逐个输出分类结果。
"""; """;
// 流式调用AI // 流式调用AI
chunkAction(("start", $"开始分类,共 {sampleRecords.Length} 条账单")); chunkAction(("start", $"开始分类,共 {sampleRecords.Length} 条账单"));
@@ -253,56 +253,56 @@ public class SmartHandleService(
// 第一步使用AI生成聚合SQL查询 // 第一步使用AI生成聚合SQL查询
var now = DateTime.Now; var now = DateTime.Now;
var sqlPrompt = $""" var sqlPrompt = $"""
当前日期:{now:yyyy年M月d日}{now:yyyy-MM-dd} 当前日期:{now:yyyy年M月d日}{now:yyyy-MM-dd}
用户问题:{userInput} 用户问题:{userInput}
数据库类型SQLite 数据库类型SQLite
数据库表名TransactionRecord 数据库表名TransactionRecord
字段说明: 字段说明:
- Id: bigint 主键 - Id: bigint 主键
- Card: nvarchar 卡号 - Card: nvarchar 卡号
- Reason: nvarchar 交易原因/摘要 - Reason: nvarchar 交易原因/摘要
- Amount: decimal 交易金额(支出为负数,收入为正数) - Amount: decimal 交易金额(支出为负数,收入为正数)
- OccurredAt: datetime 交易发生时间TEXT类型格式'2025-12-26 10:30:00' - OccurredAt: datetime 交易发生时间TEXT类型格式'2025-12-26 10:30:00'
- Type: int 交易类型0=支出, 1=收入, 2=不计入收支) - Type: int 交易类型0=支出, 1=收入, 2=不计入收支)
- Classify: nvarchar 交易分类(如:交通、餐饮、购物等) - Classify: nvarchar 交易分类(如:交通、餐饮、购物等)
【核心原则】直接生成用户所需的聚合统计SQL而不是查询原始记录后再统计 【核心原则】直接生成用户所需的聚合统计SQL而不是查询原始记录后再统计
要求: 要求:
1. 根据用户问题判断需要什么维度的聚合数据 1. 根据用户问题判断需要什么维度的聚合数据
2. 使用 GROUP BY 按分类、时间等维度分组 2. 使用 GROUP BY 按分类、时间等维度分组
3. 使用聚合函数SUM(ABS(Amount)) 计算金额总和、COUNT(*) 计数、AVG()平均、MAX()最大、MIN()最小 3. 使用聚合函数SUM(ABS(Amount)) 计算金额总和、COUNT(*) 计数、AVG()平均、MAX()最大、MIN()最小
4. 时间范围使用 OccurredAt 字段,"X个月/"基于当前日期计算 4. 时间范围使用 OccurredAt 字段,"X个月/"基于当前日期计算
5. Type = 0 Type = 1 5. Type = 0 Type = 1
6. TotalAmount, TransactionCount, AvgAmount 6. TotalAmount, TransactionCount, AvgAmount
7. 使 ORDER BY 7. 使 ORDER BY
8. SQL语句 8. SQL语句
SQLite日期函数 SQLite日期函数
- strftime('%Y', OccurredAt) - strftime('%Y', OccurredAt)
- strftime('%m', OccurredAt) - strftime('%m', OccurredAt)
- strftime('%Y-%m-%d', OccurredAt) - strftime('%Y-%m-%d', OccurredAt)
- 使 YEAR()MONTH()DAY() SQLite不支持 - 使 YEAR()MONTH()DAY() SQLite不支持
1 1
SELECT Classify, COUNT(*) as TransactionCount, SUM(ABS(Amount)) as TotalAmount, AVG(ABS(Amount)) as AvgAmount FROM TransactionRecord WHERE Type = 0 AND OccurredAt >= '2025-10-01' AND OccurredAt < '2026-01-01' AND (Classify LIKE '%%' OR Reason LIKE '%%' OR Reason LIKE '%%' OR Reason LIKE '%%') GROUP BY Classify ORDER BY TotalAmount DESC SELECT Classify, COUNT(*) as TransactionCount, SUM(ABS(Amount)) as TotalAmount, AVG(ABS(Amount)) as AvgAmount FROM TransactionRecord WHERE Type = 0 AND OccurredAt >= '2025-10-01' AND OccurredAt < '2026-01-01' AND (Classify LIKE '%%' OR Reason LIKE '%%' OR Reason LIKE '%%' OR Reason LIKE '%%') GROUP BY Classify ORDER BY TotalAmount DESC
2 2
SELECT strftime('%Y', OccurredAt) as Year, strftime('%m', OccurredAt) as Month, COUNT(*) as TransactionCount, SUM(ABS(Amount)) as TotalAmount FROM TransactionRecord WHERE Type = 0 AND OccurredAt >= '2025-06-01' GROUP BY strftime('%Y', OccurredAt), strftime('%m', OccurredAt) ORDER BY Year, Month SELECT strftime('%Y', OccurredAt) as Year, strftime('%m', OccurredAt) as Month, COUNT(*) as TransactionCount, SUM(ABS(Amount)) as TotalAmount FROM TransactionRecord WHERE Type = 0 AND OccurredAt >= '2025-06-01' GROUP BY strftime('%Y', OccurredAt), strftime('%m', OccurredAt) ORDER BY Year, Month
3 3
SELECT COUNT(*) as TransactionCount, SUM(ABS(Amount)) as TotalAmount, AVG(ABS(Amount)) as AvgAmount, MAX(ABS(Amount)) as MaxAmount FROM TransactionRecord WHERE Type = 0 AND OccurredAt >= '2025-12-01' AND OccurredAt < '2026-01-01' SELECT COUNT(*) as TransactionCount, SUM(ABS(Amount)) as TotalAmount, AVG(ABS(Amount)) as AvgAmount, MAX(ABS(Amount)) as MaxAmount FROM TransactionRecord WHERE Type = 0 AND OccurredAt >= '2025-12-01' AND OccurredAt < '2026-01-01'
4 - 使 4 - 使
1000 1000
SELECT OccurredAt, Classify, Reason, ABS(Amount) as Amount FROM TransactionRecord WHERE Type = 0 AND ABS(Amount) > 1000 ORDER BY Amount DESC LIMIT 50 SELECT OccurredAt, Classify, Reason, ABS(Amount) as Amount FROM TransactionRecord WHERE Type = 0 AND ABS(Amount) > 1000 ORDER BY Amount DESC LIMIT 50
SQL语句 SQL语句
"""; """;
var sqlText = await openAiService.ChatAsync(sqlPrompt); var sqlText = await openAiService.ChatAsync(sqlPrompt);
@@ -339,39 +339,39 @@ public class SmartHandleService(
}); });
var dataPrompt = $""" var dataPrompt = $"""
当前日期:{DateTime.Now:yyyy年M月d日} 当前日期:{DateTime.Now:yyyy年M月d日}
用户问题:{userInput} 用户问题:{userInput}
查询结果数据JSON格式 查询结果数据JSON格式
{dataJson} {dataJson}
说明:以上数据是根据用户问题查询出的聚合统计结果,请基于这些数据生成分析报告。 说明:以上数据是根据用户问题查询出的聚合统计结果,请基于这些数据生成分析报告。
请生成一份专业的数据分析报告,严格遵守以下要求: 请生成一份专业的数据分析报告,严格遵守以下要求:
【格式要求】 【格式要求】
1. 使用HTML格式移动端H5页面风格 1. 使用HTML格式移动端H5页面风格
2. 生成清晰的报告标题(基于用户问题) 2. 生成清晰的报告标题(基于用户问题)
3. 使用表格展示统计数据table > thead/tbody > tr > th/td 3. 使用表格展示统计数据table > thead/tbody > tr > th/td
4. 使用合适的HTML标签h2标题、h3小节、p段落、table表格、ul/li列表、strong强调 4. 使用合适的HTML标签h2标题、h3小节、p段落、table表格、ul/li列表、strong强调
5. 支出金额用 <span class='expense-value'>金额</span> 包裹 5. 支出金额用 <span class='expense-value'>金额</span> 包裹
6. 收入金额用 <span class='income-value'>金额</span> 包裹 6. 收入金额用 <span class='income-value'>金额</span> 包裹
7. 重要结论用 <span class='highlight'>内容</span> 高亮 7. 重要结论用 <span class='highlight'>内容</span> 高亮
【样式限制(重要)】 【样式限制(重要)】
8. 不要包含 html、body、head 标签 8. 不要包含 html、body、head 标签
9. 不要使用任何 style 属性或 <style> 标签 9. 不要使用任何 style 属性或 <style> 标签
10. 不要设置 background、background-color、color 等样式属性 10. 不要设置 background、background-color、color 等样式属性
11. 不要使用 div 包裹大段内容 11. 不要使用 div 包裹大段内容
【内容要求】 【内容要求】
12. 准确解读数据将JSON数据转换为易读的表格和文字说明 12. 准确解读数据将JSON数据转换为易读的表格和文字说明
13. 提供洞察分析:根据数据给出有价值的发现和趋势分析 13. 提供洞察分析:根据数据给出有价值的发现和趋势分析
14. 给出实用建议:基于数据提供合理的财务建议 14. 给出实用建议:基于数据提供合理的财务建议
15. 语言专业、清晰、简洁 15. 语言专业、清晰、简洁
直接输出纯净的HTML内容不要markdown代码块标记。 直接输出纯净的HTML内容不要markdown代码块标记。
"""; """;
// 第四步流式输出AI分析结果 // 第四步流式输出AI分析结果
await foreach (var chunk in openAiService.ChatStreamAsync(dataPrompt)) await foreach (var chunk in openAiService.ChatStreamAsync(dataPrompt))

View File

@@ -39,7 +39,7 @@ public class TextSegmentService : ITextSegmentService
_logger = logger; _logger = logger;
_segmenter = new JiebaSegmenter(); _segmenter = new JiebaSegmenter();
_extractor = new TfidfExtractor(); _extractor = new TfidfExtractor();
// 仅添加JiebaNet词典中可能缺失的特定业务词汇 // 仅添加JiebaNet词典中可能缺失的特定业务词汇
AddCustomWords(); AddCustomWords();
} }

View File

@@ -31,10 +31,10 @@ public class TransactionPeriodicService(
try try
{ {
logger.LogInformation("开始执行周期性账单检查..."); logger.LogInformation("开始执行周期性账单检查...");
var pendingBills = await periodicRepository.GetPendingPeriodicBillsAsync(); var pendingBills = await periodicRepository.GetPendingPeriodicBillsAsync();
var billsList = pendingBills.ToList(); var billsList = pendingBills.ToList();
logger.LogInformation("找到 {Count} 条需要执行的周期性账单", billsList.Count); logger.LogInformation("找到 {Count} 条需要执行的周期性账单", billsList.Count);
foreach (var bill in billsList) foreach (var bill in billsList)
@@ -61,7 +61,7 @@ public class TransactionPeriodicService(
}; };
var success = await transactionRepository.AddAsync(transaction); var success = await transactionRepository.AddAsync(transaction);
if (success) if (success)
{ {
logger.LogInformation("成功创建周期性账单交易记录: {Reason}, 金额: {Amount}", logger.LogInformation("成功创建周期性账单交易记录: {Reason}, 金额: {Amount}",
@@ -80,7 +80,7 @@ public class TransactionPeriodicService(
var now = DateTime.Now; var now = DateTime.Now;
var nextTime = CalculateNextExecuteTime(bill, now); var nextTime = CalculateNextExecuteTime(bill, now);
await periodicRepository.UpdateExecuteTimeAsync(bill.Id, now, nextTime); await periodicRepository.UpdateExecuteTimeAsync(bill.Id, now, nextTime);
logger.LogInformation("周期性账单 {Id} 下次执行时间: {NextTime}", logger.LogInformation("周期性账单 {Id} 下次执行时间: {NextTime}",
bill.Id, nextTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "无"); bill.Id, nextTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "无");
} }
@@ -109,7 +109,7 @@ public class TransactionPeriodicService(
private bool ShouldExecuteToday(TransactionPeriodic bill) private bool ShouldExecuteToday(TransactionPeriodic bill)
{ {
var today = DateTime.Today; var today = DateTime.Today;
// 如果从未执行过,需要执行 // 如果从未执行过,需要执行
if (bill.LastExecuteTime == null) if (bill.LastExecuteTime == null)
{ {
@@ -224,7 +224,7 @@ public class TransactionPeriodicService(
return null; return null;
var currentDayOfWeek = (int)baseTime.DayOfWeek; var currentDayOfWeek = (int)baseTime.DayOfWeek;
// 找下一个执行日 // 找下一个执行日
var nextDay = executeDays.FirstOrDefault(d => d > currentDayOfWeek); var nextDay = executeDays.FirstOrDefault(d => d > currentDayOfWeek);
if (nextDay > 0) if (nextDay > 0)
@@ -232,7 +232,7 @@ public class TransactionPeriodicService(
var daysToAdd = nextDay - currentDayOfWeek; var daysToAdd = nextDay - currentDayOfWeek;
return baseTime.Date.AddDays(daysToAdd); return baseTime.Date.AddDays(daysToAdd);
} }
// 下周的第一个执行日 // 下周的第一个执行日
var firstDay = executeDays.First(); var firstDay = executeDays.First();
var daysUntilNextWeek = 7 - currentDayOfWeek + firstDay; var daysUntilNextWeek = 7 - currentDayOfWeek + firstDay;
@@ -281,7 +281,7 @@ public class TransactionPeriodicService(
var currentQuarterStartMonth = ((baseTime.Month - 1) / 3) * 3 + 1; var currentQuarterStartMonth = ((baseTime.Month - 1) / 3) * 3 + 1;
var nextQuarterStartMonth = currentQuarterStartMonth + 3; var nextQuarterStartMonth = currentQuarterStartMonth + 3;
var nextQuarterYear = baseTime.Year; var nextQuarterYear = baseTime.Year;
if (nextQuarterStartMonth > 12) if (nextQuarterStartMonth > 12)
{ {
nextQuarterStartMonth = 1; nextQuarterStartMonth = 1;
@@ -306,7 +306,7 @@ public class TransactionPeriodicService(
// 处理闰年情况 // 处理闰年情况
var daysInYear = DateTime.IsLeapYear(nextYear) ? 366 : 365; var daysInYear = DateTime.IsLeapYear(nextYear) ? 366 : 365;
var actualDay = Math.Min(dayOfYear, daysInYear); var actualDay = Math.Min(dayOfYear, daysInYear);
return new DateTime(nextYear, 1, 1).AddDays(actualDay - 1); return new DateTime(nextYear, 1, 1).AddDays(actualDay - 1);
} }
} }

View File

@@ -1,4 +1,6 @@
namespace WebApi.Controllers.EmailMessage; using Service.EmailServices;
namespace WebApi.Controllers.EmailMessage;
[ApiController] [ApiController]
[Route("api/[controller]/[action]")] [Route("api/[controller]/[action]")]
@@ -7,7 +9,7 @@ public class EmailMessageController(
ITransactionRecordRepository transactionRepository, ITransactionRecordRepository transactionRepository,
ILogger<EmailMessageController> logger, ILogger<EmailMessageController> logger,
IEmailHandleService emailHandleService, IEmailHandleService emailHandleService,
IEmailBackgroundService emailBackgroundService IEmailSyncService emailBackgroundService
) : ControllerBase ) : ControllerBase
{ {
/// <summary> /// <summary>