fix
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 3s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 3s
This commit is contained in:
@@ -18,8 +18,8 @@ public class OpenAiService(
|
||||
public async Task<string?> ChatAsync(string systemPrompt, string userPrompt)
|
||||
{
|
||||
var cfg = aiSettings.Value;
|
||||
if (string.IsNullOrWhiteSpace(cfg.Endpoint) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Key) ||
|
||||
if (string.IsNullOrWhiteSpace(cfg.Endpoint) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Key) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Model))
|
||||
{
|
||||
logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI");
|
||||
@@ -44,7 +44,7 @@ public class OpenAiService(
|
||||
var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions";
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
using var resp = await http.PostAsync(url, content);
|
||||
@@ -75,8 +75,8 @@ public class OpenAiService(
|
||||
public async Task<string?> ChatAsync(string prompt)
|
||||
{
|
||||
var cfg = aiSettings.Value;
|
||||
if (string.IsNullOrWhiteSpace(cfg.Endpoint) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Key) ||
|
||||
if (string.IsNullOrWhiteSpace(cfg.Endpoint) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Key) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Model))
|
||||
{
|
||||
logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI");
|
||||
@@ -100,7 +100,7 @@ public class OpenAiService(
|
||||
var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions";
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
using var resp = await http.PostAsync(url, content);
|
||||
@@ -131,8 +131,8 @@ public class OpenAiService(
|
||||
public async IAsyncEnumerable<string> ChatStreamAsync(string prompt)
|
||||
{
|
||||
var cfg = aiSettings.Value;
|
||||
if (string.IsNullOrWhiteSpace(cfg.Endpoint) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Key) ||
|
||||
if (string.IsNullOrWhiteSpace(cfg.Endpoint) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Key) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Model))
|
||||
{
|
||||
logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI");
|
||||
@@ -157,11 +157,11 @@ public class OpenAiService(
|
||||
var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions";
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
request.Content = content;
|
||||
using var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
var err = await resp.Content.ReadAsStringAsync();
|
||||
@@ -201,8 +201,8 @@ public class OpenAiService(
|
||||
public async IAsyncEnumerable<string> ChatStreamAsync(string systemPrompt, string userPrompt)
|
||||
{
|
||||
var cfg = aiSettings.Value;
|
||||
if (string.IsNullOrWhiteSpace(cfg.Endpoint) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Key) ||
|
||||
if (string.IsNullOrWhiteSpace(cfg.Endpoint) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Key) ||
|
||||
string.IsNullOrWhiteSpace(cfg.Model))
|
||||
{
|
||||
logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI");
|
||||
@@ -228,12 +228,12 @@ public class OpenAiService(
|
||||
var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions";
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
|
||||
// 使用 SendAsync 来支持 HttpCompletionOption
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
request.Content = content;
|
||||
using var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
var err = await resp.Content.ReadAsStringAsync();
|
||||
|
||||
@@ -38,7 +38,7 @@ public class TextSegmentService : ITextSegmentService
|
||||
_logger = logger;
|
||||
_segmenter = new JiebaSegmenter();
|
||||
_extractor = new TfidfExtractor();
|
||||
|
||||
|
||||
// 仅添加JiebaNet词典中可能缺失的特定业务词汇
|
||||
AddCustomWords();
|
||||
}
|
||||
@@ -109,7 +109,7 @@ public class TextSegmentService : ITextSegmentService
|
||||
filteredKeywords.Add(text.Length > 10 ? text.Substring(0, 10) : text);
|
||||
}
|
||||
|
||||
_logger.LogDebug("从文本 '{Text}' 中提取关键词: {Keywords}",
|
||||
_logger.LogDebug("从文本 '{Text}' 中提取关键词: {Keywords}",
|
||||
text, string.Join(", ", filteredKeywords));
|
||||
|
||||
return filteredKeywords;
|
||||
|
||||
@@ -375,14 +375,14 @@ public class BudgetSavingsService(
|
||||
var currentActual = 0m;
|
||||
if (!string.IsNullOrEmpty(savingsCategories))
|
||||
{
|
||||
var cats = new HashSet<string>(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
||||
foreach(var kvp in transactionClassify)
|
||||
{
|
||||
if (cats.Contains(kvp.Key.Item1))
|
||||
{
|
||||
currentActual += kvp.Value;
|
||||
}
|
||||
}
|
||||
var cats = new HashSet<string>(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
||||
foreach (var kvp in transactionClassify)
|
||||
{
|
||||
if (cats.Contains(kvp.Key.Item1))
|
||||
{
|
||||
currentActual += kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var record = new BudgetRecord
|
||||
@@ -595,16 +595,14 @@ public class BudgetSavingsService(
|
||||
""");
|
||||
}
|
||||
description.AppendLine("</tbody></table>");
|
||||
|
||||
|
||||
description.AppendLine($"""
|
||||
<p>
|
||||
预算收入合计:
|
||||
<span class='expense-value'>
|
||||
<strong>
|
||||
{
|
||||
currentMonthlyIncomeItems.Sum(i => i.limit * i.factor)
|
||||
+ currentYearlyIncomeItems.Sum(i => i.limit)
|
||||
:N0}
|
||||
{currentMonthlyIncomeItems.Sum(i => i.limit * i.factor)
|
||||
+ currentYearlyIncomeItems.Sum(i => i.limit):N0}
|
||||
</strong>
|
||||
</span>
|
||||
</p>
|
||||
@@ -644,7 +642,7 @@ public class BudgetSavingsService(
|
||||
""");
|
||||
}
|
||||
description.AppendLine("</tbody></table>");
|
||||
|
||||
|
||||
archiveExpenseDiff = archiveExpenseItems.Sum(i => i.limit * i.months.Length) - archiveExpenseItems.Sum(i => i.current);
|
||||
description.AppendLine($"""
|
||||
<p>
|
||||
@@ -714,10 +712,8 @@ public class BudgetSavingsService(
|
||||
支出预算合计:
|
||||
<span class='expense-value'>
|
||||
<strong>
|
||||
{
|
||||
currentMonthlyExpenseItems.Sum(i => i.limit * i.factor)
|
||||
+ currentYearlyExpenseItems.Sum(i => i.limit)
|
||||
:N0}
|
||||
{currentMonthlyExpenseItems.Sum(i => i.limit * i.factor)
|
||||
+ currentYearlyExpenseItems.Sum(i => i.limit):N0}
|
||||
</strong>
|
||||
</span>
|
||||
</p>
|
||||
@@ -769,18 +765,18 @@ public class BudgetSavingsService(
|
||||
#endregion
|
||||
|
||||
var savingsCategories = await configService.GetConfigByKeyAsync<string>("SavingsCategories") ?? string.Empty;
|
||||
|
||||
|
||||
var currentActual = 0m;
|
||||
if (!string.IsNullOrEmpty(savingsCategories))
|
||||
{
|
||||
var cats = new HashSet<string>(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
||||
foreach(var kvp in transactionClassify)
|
||||
{
|
||||
if (cats.Contains(kvp.Key.Item1))
|
||||
{
|
||||
currentActual += kvp.Value;
|
||||
}
|
||||
}
|
||||
var cats = new HashSet<string>(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
||||
foreach (var kvp in transactionClassify)
|
||||
{
|
||||
if (cats.Contains(kvp.Key.Item1))
|
||||
{
|
||||
currentActual += kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var record = new BudgetRecord
|
||||
|
||||
@@ -21,7 +21,7 @@ public class BudgetStatsService(
|
||||
{
|
||||
public async Task<BudgetCategoryStats> GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
|
||||
{
|
||||
logger.LogInformation("开始计算分类统计信息: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
|
||||
logger.LogInformation("开始计算分类统计信息: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
|
||||
category, referenceDate);
|
||||
|
||||
var result = new BudgetCategoryStats();
|
||||
@@ -31,20 +31,20 @@ public class BudgetStatsService(
|
||||
// 获取月度统计
|
||||
logger.LogDebug("开始计算月度统计");
|
||||
result.Month = await CalculateMonthlyCategoryStatsAsync(category, referenceDate);
|
||||
logger.LogInformation("月度统计计算完成: Count={Count}, Limit={Limit}, Current={Current}, Rate={Rate:F2}%",
|
||||
logger.LogInformation("月度统计计算完成: Count={Count}, Limit={Limit}, Current={Current}, Rate={Rate:F2}%",
|
||||
result.Month.Count, result.Month.Limit, result.Month.Current, result.Month.Rate);
|
||||
|
||||
// 获取年度统计
|
||||
logger.LogDebug("开始计算年度统计");
|
||||
result.Year = await CalculateYearlyCategoryStatsAsync(category, referenceDate);
|
||||
logger.LogInformation("年度统计计算完成: Count={Count}, Limit={Limit}, Current={Current}, Rate={Rate:F2}%",
|
||||
logger.LogInformation("年度统计计算完成: Count={Count}, Limit={Limit}, Current={Current}, Rate={Rate:F2}%",
|
||||
result.Year.Count, result.Year.Limit, result.Year.Current, result.Year.Rate);
|
||||
|
||||
logger.LogInformation("分类统计信息计算完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "计算分类统计信息时发生错误: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
|
||||
logger.LogError(ex, "计算分类统计信息时发生错误: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
|
||||
category, referenceDate);
|
||||
throw;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ public class BudgetStatsService(
|
||||
|
||||
private async Task<BudgetStatsDto> CalculateMonthlyCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
|
||||
{
|
||||
logger.LogDebug("开始计算月度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM}",
|
||||
logger.LogDebug("开始计算月度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM}",
|
||||
category, referenceDate);
|
||||
|
||||
var result = new BudgetStatsDto
|
||||
@@ -94,7 +94,7 @@ public class BudgetStatsService(
|
||||
// 使用 GetAllBudgetsWithArchiveAsync 中已经计算好的 Current 值
|
||||
var totalCurrent = budgets.Sum(b => b.Current);
|
||||
logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)", totalCurrent, budgets.Count);
|
||||
|
||||
|
||||
var transactionType = category switch
|
||||
{
|
||||
BudgetCategory.Expense => TransactionType.Expense,
|
||||
@@ -129,7 +129,7 @@ public class BudgetStatsService(
|
||||
decimal accumulated = 0;
|
||||
var daysInMonth = DateTime.DaysInMonth(startDate.Year, startDate.Month);
|
||||
logger.LogDebug("本月天数: {DaysInMonth}", daysInMonth);
|
||||
|
||||
|
||||
for (var i = 1; i <= daysInMonth; i++)
|
||||
{
|
||||
var currentDate = new DateTime(startDate.Year, startDate.Month, i);
|
||||
@@ -143,21 +143,21 @@ public class BudgetStatsService(
|
||||
if (dailyStats.TryGetValue(currentDate.Date, out var amount))
|
||||
{
|
||||
accumulated += amount;
|
||||
logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 金额={Amount}, 累计={Accumulated}",
|
||||
logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 金额={Amount}, 累计={Accumulated}",
|
||||
currentDate, amount, accumulated);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 无交易数据,累计={Accumulated}",
|
||||
logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 无交易数据,累计={Accumulated}",
|
||||
currentDate, accumulated);
|
||||
}
|
||||
|
||||
|
||||
// 对每一天的累计值应用硬性预算调整
|
||||
var adjustedAccumulated = accumulated;
|
||||
if (transactionType == TransactionType.Expense)
|
||||
{
|
||||
var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList();
|
||||
|
||||
|
||||
// 检查是否为当前月份
|
||||
var isCurrentMonth = referenceDate.Year == now.Year && referenceDate.Month == now.Month;
|
||||
if (isCurrentMonth && currentDate.Date <= now.Date)
|
||||
@@ -166,31 +166,31 @@ public class BudgetStatsService(
|
||||
// totalMandatoryVirtual是所有硬性预算的虚拟消耗
|
||||
// 但如果硬性预算有实际交易,accumulated中已经包含了,会重复
|
||||
// 所以需要:accumulated + (totalMandatoryVirtual - 硬性预算的实际交易部分)
|
||||
|
||||
|
||||
// 更简单的理解:
|
||||
// - 如果某个硬性预算本月完全没有交易记录,它的虚拟值应该加到accumulated上
|
||||
// - 如果某个硬性预算有部分交易记录,应该补齐到虚拟值
|
||||
// - 实现:取 max(accumulated, totalMandatoryVirtual) 是不对的
|
||||
// - 正确:accumulated + 硬性预算中没有实际交易的那部分的虚拟值
|
||||
|
||||
|
||||
// 由于无法精确区分,采用近似方案:
|
||||
// 计算所有硬性预算的Current总和,这个值已经包含了虚拟消耗(在CalculateCurrentAmountAsync中处理)
|
||||
|
||||
// 计算非硬性预算的交易累计(这部分在accumulated中)
|
||||
// 但accumulated是所有交易的累计,包括硬性预算的实际交易
|
||||
|
||||
|
||||
// 最终简化方案:
|
||||
// dailyStats包含所有实际交易(包括硬性预算的实际交易)
|
||||
// 对于没有实际交易的硬性预算,它们的虚拟消耗没有在dailyStats中
|
||||
// 所以:adjustedAccumulated = accumulated + 没有实际交易的硬性预算的虚拟消耗
|
||||
|
||||
|
||||
// 实用方法:每个硬性预算,取 max(它在dailyStats中的累计, 虚拟值)
|
||||
// 但我们无法从dailyStats中提取单个预算的数据
|
||||
|
||||
|
||||
// 终极简化:如果硬性预算的Current值等于虚拟值,说明没有实际交易
|
||||
// 这种情况下,accumulated中不包含这部分,需要加上虚拟值
|
||||
// 如果Current值大于虚拟值,说明有实际交易,accumulated中已包含,不需要调整
|
||||
|
||||
|
||||
decimal mandatoryAdjustment = 0;
|
||||
foreach (var budget in mandatoryBudgets)
|
||||
{
|
||||
@@ -205,16 +205,16 @@ public class BudgetStatsService(
|
||||
var dayOfYear = currentDate.DayOfYear;
|
||||
dailyVirtual = budget.Limit * dayOfYear / daysInYear;
|
||||
}
|
||||
|
||||
|
||||
// 如果budget.Current约等于整月的虚拟值,说明没有实际交易
|
||||
// 但Current是整个月的,dailyVirtual是到当前天的
|
||||
// 需要判断该预算是否有实际交易记录
|
||||
// 简化:假设如果硬性预算的Current等于虚拟值(误差<1元),就没有实际交易
|
||||
|
||||
var monthlyVirtual = budget.Type == BudgetPeriodType.Month
|
||||
|
||||
var monthlyVirtual = budget.Type == BudgetPeriodType.Month
|
||||
? budget.Limit * now.Day / daysInMonth
|
||||
: budget.Limit * now.DayOfYear / (DateTime.IsLeapYear(now.Year) ? 366 : 365);
|
||||
|
||||
|
||||
if (Math.Abs(budget.Current - monthlyVirtual) < 1)
|
||||
{
|
||||
// 没有实际交易,需要添加虚拟消耗
|
||||
@@ -223,14 +223,14 @@ public class BudgetStatsService(
|
||||
currentDate, budget.Name, dailyVirtual);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
adjustedAccumulated += mandatoryAdjustment;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
result.Trend.Add(adjustedAccumulated);
|
||||
}
|
||||
|
||||
|
||||
logger.LogDebug("趋势图数据计算完成(已应用硬性预算调整)");
|
||||
}
|
||||
else
|
||||
@@ -265,7 +265,7 @@ public class BudgetStatsService(
|
||||
logger.LogInformation("【使用率】{Current}元 ÷ {Limit}元 × 100% = {Rate:F2}%", result.Current, totalLimit, result.Rate);
|
||||
|
||||
// 5. 生成预算明细汇总日志
|
||||
var budgetDetails = budgets.Select(b =>
|
||||
var budgetDetails = budgets.Select(b =>
|
||||
{
|
||||
var limit = CalculateBudgetLimit(b, BudgetPeriodType.Month, referenceDate);
|
||||
var prefix = b.IsArchive ? $"({b.ArchiveMonth}月归档)" : "";
|
||||
@@ -285,7 +285,7 @@ public class BudgetStatsService(
|
||||
|
||||
private async Task<BudgetStatsDto> CalculateYearlyCategoryStatsAsync(BudgetCategory category, DateTime referenceDate)
|
||||
{
|
||||
logger.LogDebug("开始计算年度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy}",
|
||||
logger.LogDebug("开始计算年度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy}",
|
||||
category, referenceDate);
|
||||
|
||||
var result = new BudgetStatsDto
|
||||
@@ -338,7 +338,7 @@ public class BudgetStatsService(
|
||||
|
||||
// 计算当前实际值,考虑硬性预算的特殊逻辑
|
||||
var totalCurrent = budgets.Sum(b => b.Current);
|
||||
logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)",
|
||||
logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)",
|
||||
totalCurrent, budgets.Count);
|
||||
|
||||
var now = dateTimeProvider.Now;
|
||||
@@ -380,21 +380,21 @@ public class BudgetStatsService(
|
||||
if (dailyStats.TryGetValue(currentMonthDate, out var amount))
|
||||
{
|
||||
accumulated += amount;
|
||||
logger.LogTrace("月份 {Month:yyyy-MM}: 金额={Amount}, 累计={Accumulated}",
|
||||
logger.LogTrace("月份 {Month:yyyy-MM}: 金额={Amount}, 累计={Accumulated}",
|
||||
currentMonthDate, amount, accumulated);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogTrace("月份 {Month:yyyy-MM}: 无交易数据,累计={Accumulated}",
|
||||
logger.LogTrace("月份 {Month:yyyy-MM}: 无交易数据,累计={Accumulated}",
|
||||
currentMonthDate, accumulated);
|
||||
}
|
||||
|
||||
|
||||
// 对每个月的累计值应用硬性预算调整
|
||||
var adjustedAccumulated = accumulated;
|
||||
if (transactionType == TransactionType.Expense)
|
||||
{
|
||||
var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList();
|
||||
|
||||
|
||||
// 检查是否为当前年份
|
||||
var isCurrentYear = referenceDate.Year == now.Year;
|
||||
if (isCurrentYear && currentMonthDate <= now)
|
||||
@@ -403,7 +403,7 @@ public class BudgetStatsService(
|
||||
foreach (var budget in mandatoryBudgets)
|
||||
{
|
||||
decimal monthlyVirtual = 0;
|
||||
|
||||
|
||||
if (budget.Type == BudgetPeriodType.Month)
|
||||
{
|
||||
// 月度硬性预算:如果该月已完成,累加整月;如果是当前月,按天数比例
|
||||
@@ -424,12 +424,12 @@ public class BudgetStatsService(
|
||||
var dayOfYear = i < now.Month ? lastDayOfMonth.DayOfYear : now.DayOfYear;
|
||||
monthlyVirtual = budget.Limit * dayOfYear / daysInYear;
|
||||
}
|
||||
|
||||
|
||||
// 判断该硬性预算是否有实际交易
|
||||
var yearlyVirtual = budget.Type == BudgetPeriodType.Month
|
||||
var yearlyVirtual = budget.Type == BudgetPeriodType.Month
|
||||
? budget.Limit * now.Month + (budget.Limit * now.Day / DateTime.DaysInMonth(now.Year, now.Month)) - budget.Limit
|
||||
: budget.Limit * now.DayOfYear / (DateTime.IsLeapYear(now.Year) ? 366 : 365);
|
||||
|
||||
|
||||
if (Math.Abs(budget.Current - yearlyVirtual) < 1)
|
||||
{
|
||||
// 没有实际交易,需要添加虚拟消耗
|
||||
@@ -438,11 +438,11 @@ public class BudgetStatsService(
|
||||
currentMonthDate, budget.Name, monthlyVirtual);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
adjustedAccumulated += mandatoryAdjustment;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
result.Trend.Add(adjustedAccumulated);
|
||||
}
|
||||
|
||||
@@ -480,7 +480,7 @@ public class BudgetStatsService(
|
||||
logger.LogInformation("【使用率】{Current}元 ÷ {Limit}元 × 100% = {Rate:F2}%", result.Current, totalLimit, result.Rate);
|
||||
|
||||
// 5. 生成预算明细汇总日志
|
||||
var budgetDetails = budgets.Select(b =>
|
||||
var budgetDetails = budgets.Select(b =>
|
||||
{
|
||||
var limit = CalculateBudgetLimit(b, BudgetPeriodType.Year, referenceDate);
|
||||
var prefix = b.IsArchive ? "(归档)" : b.RemainingMonths > 0 ? $"(剩余{b.RemainingMonths}月)" : "";
|
||||
@@ -503,7 +503,7 @@ public class BudgetStatsService(
|
||||
BudgetPeriodType statType,
|
||||
DateTime referenceDate)
|
||||
{
|
||||
logger.LogDebug("开始获取预算数据: Category={Category}, StatType={StatType}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
|
||||
logger.LogDebug("开始获取预算数据: Category={Category}, StatType={StatType}, ReferenceDate={ReferenceDate:yyyy-MM-dd}",
|
||||
category, statType, referenceDate);
|
||||
|
||||
var result = new List<BudgetStatsItem>();
|
||||
@@ -516,7 +516,7 @@ public class BudgetStatsService(
|
||||
if (statType == BudgetPeriodType.Year)
|
||||
{
|
||||
logger.LogDebug("年度统计:开始获取整年的预算数据");
|
||||
|
||||
|
||||
// 获取当前有效的预算(用于当前月及未来月)
|
||||
var currentBudgets = await budgetRepository.GetAllAsync();
|
||||
var currentBudgetsDict = currentBudgets
|
||||
@@ -526,7 +526,7 @@ public class BudgetStatsService(
|
||||
|
||||
// 用于跟踪已处理的预算ID,避免重复
|
||||
var processedBudgetIds = new HashSet<long>();
|
||||
|
||||
|
||||
// 1. 处理历史归档月份(1月到当前月-1)
|
||||
if (referenceDate.Year == now.Year && now.Month > 1)
|
||||
{
|
||||
@@ -585,7 +585,7 @@ public class BudgetStatsService(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 2. 处理当前月及未来月(使用当前预算)
|
||||
logger.LogDebug("开始处理当前及未来月份预算");
|
||||
foreach (var budget in currentBudgetsDict.Values)
|
||||
@@ -675,7 +675,7 @@ public class BudgetStatsService(
|
||||
// 获取归档数据
|
||||
logger.LogDebug("开始获取归档数据: Year={Year}, Month={Month}", year, month);
|
||||
var archive = await budgetArchiveRepository.GetArchiveAsync(year, month);
|
||||
|
||||
|
||||
if (archive != null)
|
||||
{
|
||||
var itemCount = archive.Content.Count();
|
||||
@@ -714,7 +714,7 @@ public class BudgetStatsService(
|
||||
var budgets = await budgetRepository.GetAllAsync();
|
||||
var budgetCount = budgets.Count();
|
||||
logger.LogDebug("获取到 {BudgetCount} 个预算", budgetCount);
|
||||
|
||||
|
||||
foreach (var budget in budgets)
|
||||
{
|
||||
if (budget.Category == category && ShouldIncludeBudget(budget, statType))
|
||||
@@ -845,7 +845,7 @@ public class BudgetStatsService(
|
||||
|
||||
logger.LogInformation("预算 {BudgetName} 限额计算: 原始预算={OriginalLimit}, 计算后限额={CalculatedLimit}, 算法: {Algorithm}",
|
||||
budget.Name, budget.Limit, itemLimit, algorithmDescription);
|
||||
|
||||
|
||||
return itemLimit;
|
||||
}
|
||||
|
||||
@@ -888,13 +888,13 @@ public class BudgetStatsService(
|
||||
|
||||
private decimal ApplyMandatoryBudgetAdjustment(List<BudgetStatsItem> budgets, decimal currentTotal, DateTime referenceDate, BudgetPeriodType statType)
|
||||
{
|
||||
logger.LogDebug("开始应用硬性预算调整: 当前总计={CurrentTotal}, 统计类型={StatType}, 参考日期={ReferenceDate:yyyy-MM-dd}",
|
||||
logger.LogDebug("开始应用硬性预算调整: 当前总计={CurrentTotal}, 统计类型={StatType}, 参考日期={ReferenceDate:yyyy-MM-dd}",
|
||||
currentTotal, statType, referenceDate);
|
||||
|
||||
var now = dateTimeProvider.Now;
|
||||
var adjustedTotal = currentTotal;
|
||||
var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList();
|
||||
|
||||
|
||||
logger.LogDebug("找到 {MandatoryCount} 个硬性预算", mandatoryBudgets.Count);
|
||||
|
||||
var mandatoryIndex = 0;
|
||||
@@ -906,14 +906,14 @@ public class BudgetStatsService(
|
||||
if (statType == BudgetPeriodType.Month)
|
||||
{
|
||||
isCurrentPeriod = referenceDate.Year == now.Year && referenceDate.Month == now.Month;
|
||||
logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} - 检查月度周期: 参考月份={RefMonth}, 当前月份={CurrentMonth}, 是否为当前周期={IsCurrent}",
|
||||
mandatoryIndex, mandatoryBudgets.Count, budget.Name,
|
||||
logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} - 检查月度周期: 参考月份={RefMonth}, 当前月份={CurrentMonth}, 是否为当前周期={IsCurrent}",
|
||||
mandatoryIndex, mandatoryBudgets.Count, budget.Name,
|
||||
referenceDate.ToString("yyyy-MM"), now.ToString("yyyy-MM"), isCurrentPeriod);
|
||||
}
|
||||
else // Year
|
||||
{
|
||||
isCurrentPeriod = referenceDate.Year == now.Year;
|
||||
logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} - 检查年度周期: 参考年份={RefYear}, 当前年份={CurrentYear}, 是否为当前周期={IsCurrent}",
|
||||
logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} - 检查年度周期: 参考年份={RefYear}, 当前年份={CurrentYear}, 是否为当前周期={IsCurrent}",
|
||||
mandatoryIndex, mandatoryBudgets.Count, budget.Name,
|
||||
referenceDate.Year, now.Year, isCurrentPeriod);
|
||||
}
|
||||
@@ -923,7 +923,7 @@ public class BudgetStatsService(
|
||||
// 计算硬性预算的应累加值
|
||||
decimal mandatoryAccumulation = 0;
|
||||
var accumulationAlgorithm = "";
|
||||
|
||||
|
||||
if (budget.Type == BudgetPeriodType.Month)
|
||||
{
|
||||
// 月度硬性预算按天数比例累加
|
||||
@@ -931,7 +931,7 @@ public class BudgetStatsService(
|
||||
var daysElapsed = now.Day;
|
||||
mandatoryAccumulation = budget.Limit * daysElapsed / daysInMonth;
|
||||
accumulationAlgorithm = $"月度硬性预算按天数比例: {budget.Limit} × {daysElapsed} ÷ {daysInMonth} = {mandatoryAccumulation:F2}";
|
||||
logger.LogDebug("月度硬性预算 {BudgetName}: 限额={Limit}, 本月天数={DaysInMonth}, 已过天数={DaysElapsed}, 应累加值={Accumulation}",
|
||||
logger.LogDebug("月度硬性预算 {BudgetName}: 限额={Limit}, 本月天数={DaysInMonth}, 已过天数={DaysElapsed}, 应累加值={Accumulation}",
|
||||
budget.Name, budget.Limit, daysInMonth, daysElapsed, mandatoryAccumulation);
|
||||
}
|
||||
else if (budget.Type == BudgetPeriodType.Year)
|
||||
@@ -941,7 +941,7 @@ public class BudgetStatsService(
|
||||
var daysElapsed = now.DayOfYear;
|
||||
mandatoryAccumulation = budget.Limit * daysElapsed / daysInYear;
|
||||
accumulationAlgorithm = $"年度硬性预算按天数比例: {budget.Limit} × {daysElapsed} ÷ {daysInYear} = {mandatoryAccumulation:F2}";
|
||||
logger.LogDebug("年度硬性预算 {BudgetName}: 限额={Limit}, 本年天数={DaysInYear}, 已过天数={DaysElapsed}, 应累加值={Accumulation}",
|
||||
logger.LogDebug("年度硬性预算 {BudgetName}: 限额={Limit}, 本年天数={DaysInYear}, 已过天数={DaysElapsed}, 应累加值={Accumulation}",
|
||||
budget.Name, budget.Limit, daysInYear, daysElapsed, mandatoryAccumulation);
|
||||
}
|
||||
|
||||
@@ -958,7 +958,7 @@ public class BudgetStatsService(
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("硬性预算 {BudgetName} 未触发调整: 当前总计={CurrentTotal} >= 应累加值={MandatoryAccumulation}",
|
||||
logger.LogDebug("硬性预算 {BudgetName} 未触发调整: 当前总计={CurrentTotal} >= 应累加值={MandatoryAccumulation}",
|
||||
budget.Name, adjustedTotal, mandatoryAccumulation);
|
||||
}
|
||||
}
|
||||
@@ -1029,7 +1029,7 @@ public class BudgetStatsService(
|
||||
var budgetLimit = CalculateBudgetLimit(budget, BudgetPeriodType.Month, referenceDate);
|
||||
var typeLabel = budget.IsMandatoryExpense ? "硬性" : "普通";
|
||||
var archiveLabel = budget.IsArchive ? $" ({budget.ArchiveMonth}月归档)" : "";
|
||||
|
||||
|
||||
description.AppendLine($"""
|
||||
<tr>
|
||||
<td>{budget.Name}{archiveLabel}</td>
|
||||
@@ -1138,8 +1138,8 @@ public class BudgetStatsService(
|
||||
{
|
||||
var budgetLimit = CalculateBudgetLimit(budget, BudgetPeriodType.Year, referenceDate);
|
||||
var typeStr = budget.IsCurrentMonth ? "当前月" : "未来月";
|
||||
var calcStr = budget.IsCurrentMonth
|
||||
? $"1月×{budget.Limit:N0}"
|
||||
var calcStr = budget.IsCurrentMonth
|
||||
? $"1月×{budget.Limit:N0}"
|
||||
: $"{budget.RemainingMonths}月×{budget.Limit:N0}";
|
||||
|
||||
description.AppendLine($"""
|
||||
@@ -1195,7 +1195,7 @@ public class BudgetStatsService(
|
||||
description.AppendLine($"<h3>计算公式</h3>");
|
||||
description.AppendLine($"<p><strong>年度预算额度合计:</strong>");
|
||||
var limitParts = new List<string>();
|
||||
|
||||
|
||||
// 归档月度预算部分
|
||||
foreach (var group in archivedMonthlyBudgets.GroupBy(b => b.Id))
|
||||
{
|
||||
@@ -1204,7 +1204,7 @@ public class BudgetStatsService(
|
||||
var groupTotalLimit = first.Limit * count;
|
||||
limitParts.Add($"{first.Name}(归档{count}月×{first.Limit:N0}={groupTotalLimit:N0})");
|
||||
}
|
||||
|
||||
|
||||
// 当前月度预算部分
|
||||
foreach (var budget in currentMonthlyBudgets)
|
||||
{
|
||||
@@ -1218,18 +1218,18 @@ public class BudgetStatsService(
|
||||
limitParts.Add($"{budget.Name}(剩余{budget.RemainingMonths}月×{budget.Limit:N0}={budgetLimit:N0})");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 年度预算部分
|
||||
foreach (var budget in archivedYearlyBudgets.Concat(currentYearlyBudgets))
|
||||
{
|
||||
limitParts.Add($"{budget.Name}({budget.Limit:N0})");
|
||||
}
|
||||
|
||||
|
||||
description.AppendLine($"{string.Join(" + ", limitParts)} = <span class='{(category == BudgetCategory.Expense ? "expense-value" : "income-value")}'><strong>{totalLimit:N0}</strong></span></p>");
|
||||
|
||||
description.AppendLine($"<p><strong>实际{categoryName}合计:</strong>");
|
||||
var currentParts = new List<string>();
|
||||
|
||||
|
||||
// 归档月度预算的实际值
|
||||
foreach (var group in archivedMonthlyBudgets.GroupBy(b => b.Id))
|
||||
{
|
||||
@@ -1237,13 +1237,13 @@ public class BudgetStatsService(
|
||||
var groupTotalCurrent = group.Sum(b => b.Current);
|
||||
currentParts.Add($"{first.Name}(归档{groupTotalCurrent:N1})");
|
||||
}
|
||||
|
||||
|
||||
// 年度预算的实际值
|
||||
foreach (var budget in archivedYearlyBudgets.Concat(currentYearlyBudgets))
|
||||
{
|
||||
currentParts.Add($"{budget.Name}({budget.Current:N1})");
|
||||
}
|
||||
|
||||
|
||||
description.AppendLine($"{string.Join(" + ", currentParts)} = <span class='{(category == BudgetCategory.Expense ? "expense-value" : "income-value")}'><strong>{totalCurrent:N1}</strong></span></p>");
|
||||
|
||||
var rate = totalLimit > 0 ? totalCurrent / totalLimit * 100 : 0;
|
||||
|
||||
@@ -74,13 +74,13 @@ public class EmailFetchService(ILogger<EmailFetchService> logger) : IEmailFetchS
|
||||
_useSsl = useSsl;
|
||||
_email = email;
|
||||
_password = password;
|
||||
|
||||
|
||||
// 如果已连接,先断开
|
||||
if (_imapClient?.IsConnected == true)
|
||||
{
|
||||
await DisconnectAsync();
|
||||
}
|
||||
|
||||
|
||||
_imapClient = new ImapClient();
|
||||
|
||||
if (useSsl)
|
||||
@@ -206,7 +206,7 @@ public class EmailFetchService(ILogger<EmailFetchService> logger) : IEmailFetchS
|
||||
|
||||
// 标记邮件为已读(设置Seen标记)
|
||||
await inbox.AddFlagsAsync(uid, MessageFlags.Seen, silent: false);
|
||||
|
||||
|
||||
_logger.LogDebug("邮件 {Uid} 标记已读操作已提交", uid);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -240,13 +240,13 @@ public class EmailFetchService(ILogger<EmailFetchService> logger) : IEmailFetchS
|
||||
}
|
||||
return _imapClient?.IsConnected == true;
|
||||
}
|
||||
|
||||
|
||||
if (string.IsNullOrEmpty(_host) || string.IsNullOrEmpty(_email))
|
||||
{
|
||||
_logger.LogWarning("未初始化连接信息,无法自动重连");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
_logger.LogInformation("检测到连接断开,尝试重新连接到 {Email}...", _email);
|
||||
return await ConnectAsync(_host, _port, _useSsl, _email, _password);
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ public class EmailHandleService(
|
||||
{
|
||||
var clone = records.ToArray().DeepClone();
|
||||
|
||||
if(clone?.Any() != true)
|
||||
if (clone?.Any() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public class EmailParseForm95555(
|
||||
var balanceStr = match.Groups["balance"].Value;
|
||||
var typeStr = match.Groups["type"].Value;
|
||||
var reason = match.Groups["reason"].Value;
|
||||
if(string.IsNullOrEmpty(reason))
|
||||
if (string.IsNullOrEmpty(reason))
|
||||
{
|
||||
reason = typeStr;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ public partial class EmailParseFormCcsvc(
|
||||
{
|
||||
[GeneratedRegex("<.*?>")]
|
||||
private static partial Regex HtmlRegex();
|
||||
|
||||
|
||||
public override bool CanParse(string from, string subject, string body)
|
||||
{
|
||||
if (!from.Contains("ccsvc@message.cmbchina.com"))
|
||||
@@ -141,7 +141,7 @@ public partial class EmailParseFormCcsvc(
|
||||
}
|
||||
|
||||
// 招商信用卡特殊,消费金额为正数,退款为负数
|
||||
if(amount > 0)
|
||||
if (amount > 0)
|
||||
{
|
||||
type = TransactionType.Expense;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public abstract class EmailParseServicesBase(
|
||||
// AI兜底
|
||||
result = await ParseByAiAsync(emailContent) ?? [];
|
||||
|
||||
if(result.Length == 0)
|
||||
if (result.Length == 0)
|
||||
{
|
||||
logger.LogWarning("AI解析邮件内容也未能提取到任何交易记录");
|
||||
}
|
||||
@@ -65,10 +65,10 @@ public abstract class EmailParseServicesBase(
|
||||
)[]> ParseEmailContentAsync(string emailContent);
|
||||
|
||||
private async Task<(
|
||||
string card,
|
||||
string reason,
|
||||
decimal amount,
|
||||
decimal balance,
|
||||
string card,
|
||||
string reason,
|
||||
decimal amount,
|
||||
decimal balance,
|
||||
TransactionType type,
|
||||
DateTime? occurredAt
|
||||
)[]?> ParseByAiAsync(string body)
|
||||
@@ -175,7 +175,7 @@ public abstract class EmailParseServicesBase(
|
||||
}
|
||||
|
||||
var occurredAt = (DateTime?)null;
|
||||
if(DateTime.TryParse(occurredAtStr, out var occurredAtValue))
|
||||
if (DateTime.TryParse(occurredAtStr, out var occurredAtValue))
|
||||
{
|
||||
occurredAt = occurredAtValue;
|
||||
}
|
||||
|
||||
@@ -199,12 +199,12 @@ public class EmailSyncService(
|
||||
message.TextBody ?? message.HtmlBody ?? string.Empty
|
||||
) || (DateTime.Now - message.Date.DateTime > TimeSpan.FromDays(3)))
|
||||
{
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
logger.LogDebug("DEBUG 模式下,跳过标记已读步骤");
|
||||
#else
|
||||
#else
|
||||
// 标记邮件为已读
|
||||
await emailFetchService.MarkAsReadAsync(uid);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -283,7 +283,7 @@ public class ImportService(
|
||||
|
||||
DateTime GetDateTimeValue(IDictionary<string, string> row, string key)
|
||||
{
|
||||
if(!row.ContainsKey(key))
|
||||
if (!row.ContainsKey(key))
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class BudgetArchiveJob(
|
||||
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var budgetService = scope.ServiceProvider.GetRequiredService<IBudgetService>();
|
||||
|
||||
|
||||
// 归档月度数据
|
||||
var result = await budgetService.ArchiveBudgetsAsync(year, month);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public class DbBackupJob(
|
||||
try
|
||||
{
|
||||
logger.LogInformation("开始执行数据库备份任务");
|
||||
|
||||
|
||||
// 数据库文件路径 (基于 appsettings.json 中的配置: database/EmailBill.db)
|
||||
var dbPath = Path.Combine(env.ContentRootPath, "database", "EmailBill.db");
|
||||
var backupDir = Path.Combine(env.ContentRootPath, "database", "backups");
|
||||
@@ -48,7 +48,7 @@ public class DbBackupJob(
|
||||
var filesToDelete = files.Skip(20);
|
||||
foreach (var file in filesToDelete)
|
||||
{
|
||||
try
|
||||
try
|
||||
{
|
||||
file.Delete();
|
||||
logger.LogInformation("删除过期备份: {Name}", file.Name);
|
||||
|
||||
@@ -144,12 +144,12 @@ public class EmailSyncJob(
|
||||
message.TextBody ?? message.HtmlBody ?? string.Empty
|
||||
) || (DateTime.Now - message.Date.DateTime > TimeSpan.FromDays(3)))
|
||||
{
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
logger.LogDebug("DEBUG 模式下,跳过标记已读步骤");
|
||||
#else
|
||||
#else
|
||||
// 标记邮件为已读
|
||||
await emailFetchService.MarkAsReadAsync(uid);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -30,9 +30,9 @@ public class MessageService(IMessageRecordRepository messageRepo, INotificationS
|
||||
}
|
||||
|
||||
public async Task<bool> AddAsync(
|
||||
string title,
|
||||
string content,
|
||||
MessageType type = MessageType.Text,
|
||||
string title,
|
||||
string content,
|
||||
MessageType type = MessageType.Text,
|
||||
string? url = null
|
||||
)
|
||||
{
|
||||
@@ -56,7 +56,7 @@ public class MessageService(IMessageRecordRepository messageRepo, INotificationS
|
||||
{
|
||||
var message = await messageRepo.GetByIdAsync(id);
|
||||
if (message == null) return false;
|
||||
|
||||
|
||||
message.IsRead = true;
|
||||
message.UpdateTime = DateTime.Now;
|
||||
return await messageRepo.UpdateAsync(message);
|
||||
|
||||
@@ -31,10 +31,10 @@ public class TransactionPeriodicService(
|
||||
try
|
||||
{
|
||||
logger.LogInformation("开始执行周期性账单检查...");
|
||||
|
||||
|
||||
var pendingBills = await periodicRepository.GetPendingPeriodicBillsAsync();
|
||||
var billsList = pendingBills.ToList();
|
||||
|
||||
|
||||
logger.LogInformation("找到 {Count} 条需要执行的周期性账单", billsList.Count);
|
||||
|
||||
foreach (var bill in billsList)
|
||||
@@ -61,10 +61,10 @@ public class TransactionPeriodicService(
|
||||
};
|
||||
|
||||
var success = await transactionRepository.AddAsync(transaction);
|
||||
|
||||
|
||||
if (success)
|
||||
{
|
||||
logger.LogInformation("成功创建周期性账单交易记录: {Reason}, 金额: {Amount}",
|
||||
logger.LogInformation("成功创建周期性账单交易记录: {Reason}, 金额: {Amount}",
|
||||
bill.Reason, bill.Amount);
|
||||
|
||||
// 创建未读消息
|
||||
@@ -80,8 +80,8 @@ public class TransactionPeriodicService(
|
||||
var now = DateTime.Now;
|
||||
var nextTime = CalculateNextExecuteTime(bill, now);
|
||||
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") ?? "无");
|
||||
}
|
||||
else
|
||||
@@ -114,7 +114,7 @@ public class TransactionPeriodicService(
|
||||
}
|
||||
|
||||
var today = DateTime.Today;
|
||||
|
||||
|
||||
// 如果从未执行过,需要执行
|
||||
if (bill.LastExecuteTime == null)
|
||||
{
|
||||
@@ -236,7 +236,7 @@ public class TransactionPeriodicService(
|
||||
return null;
|
||||
|
||||
var currentDayOfWeek = (int)baseTime.DayOfWeek;
|
||||
|
||||
|
||||
// 找下一个执行日
|
||||
var nextDay = executeDays.FirstOrDefault(d => d > currentDayOfWeek);
|
||||
if (nextDay > 0)
|
||||
@@ -244,7 +244,7 @@ public class TransactionPeriodicService(
|
||||
var daysToAdd = nextDay - currentDayOfWeek;
|
||||
return baseTime.Date.AddDays(daysToAdd);
|
||||
}
|
||||
|
||||
|
||||
// 下周的第一个执行日
|
||||
var firstDay = executeDays.First();
|
||||
var daysUntilNextWeek = 7 - currentDayOfWeek + firstDay;
|
||||
@@ -293,7 +293,7 @@ public class TransactionPeriodicService(
|
||||
var currentQuarterStartMonth = ((baseTime.Month - 1) / 3) * 3 + 1;
|
||||
var nextQuarterStartMonth = currentQuarterStartMonth + 3;
|
||||
var nextQuarterYear = baseTime.Year;
|
||||
|
||||
|
||||
if (nextQuarterStartMonth > 12)
|
||||
{
|
||||
nextQuarterStartMonth = 1;
|
||||
@@ -318,7 +318,7 @@ public class TransactionPeriodicService(
|
||||
// 处理闰年情况
|
||||
var daysInYear = DateTime.IsLeapYear(nextYear) ? 366 : 365;
|
||||
var actualDay = Math.Min(dayOfYear, daysInYear);
|
||||
|
||||
|
||||
return new DateTime(nextYear, 1, 1).AddDays(actualDay - 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user