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:
@@ -29,7 +29,7 @@ public static class ServiceExtension
|
||||
|
||||
// 注册所有服务实现
|
||||
RegisterServices(services, serviceAssembly);
|
||||
|
||||
|
||||
// 注册所有仓储实现
|
||||
RegisterRepositories(services, repositoryAssembly);
|
||||
|
||||
@@ -74,7 +74,7 @@ public static class ServiceExtension
|
||||
foreach (var type in types)
|
||||
{
|
||||
var interfaces = type.GetInterfaces()
|
||||
.Where(i => i.Name.StartsWith("I")
|
||||
.Where(i => i.Name.StartsWith("I")
|
||||
&& i is { Namespace: "Repository", IsGenericType: false }); // 排除泛型接口如 IBaseRepository<T>
|
||||
|
||||
foreach (var @interface in interfaces)
|
||||
|
||||
@@ -18,7 +18,7 @@ public class TransactionRecord : BaseEntity
|
||||
/// <summary>
|
||||
/// 交易金额
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 交易后余额
|
||||
@@ -60,7 +60,7 @@ public decimal Amount { get; set; }
|
||||
/// </summary>
|
||||
public string ImportNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// 导入来源
|
||||
/// </summary>
|
||||
public string ImportFrom { get; set; } = string.Empty;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
public interface IBudgetRepository : IBaseRepository<BudgetRecord>
|
||||
{
|
||||
Task<decimal> GetCurrentAmountAsync(BudgetRecord budget, DateTime startDate, DateTime endDate);
|
||||
|
||||
|
||||
Task UpdateBudgetCategoryNameAsync(string oldName, string newName, TransactionType type);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public class BudgetRepository(IFreeSql freeSql) : BaseRepository<BudgetRecord>(f
|
||||
public async Task UpdateBudgetCategoryNameAsync(string oldName, string newName, TransactionType type)
|
||||
{
|
||||
var records = await FreeSql.Select<BudgetRecord>()
|
||||
.Where(b => b.SelectedCategories.Contains(oldName) &&
|
||||
.Where(b => b.SelectedCategories.Contains(oldName) &&
|
||||
((type == TransactionType.Expense && b.Category == BudgetCategory.Expense) ||
|
||||
(type == TransactionType.Income && b.Category == BudgetCategory.Income)))
|
||||
.ToListAsync();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
public interface IEmailMessageRepository : IBaseRepository<EmailMessage>
|
||||
{
|
||||
Task<EmailMessage?> ExistsAsync(string md5);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 分页获取邮件列表(游标分页)
|
||||
/// </summary>
|
||||
@@ -12,7 +12,7 @@ public interface IEmailMessageRepository : IBaseRepository<EmailMessage>
|
||||
/// <param name="pageSize">每页数量</param>
|
||||
/// <returns>邮件列表、最后接收时间和最后ID</returns>
|
||||
Task<(List<EmailMessage> list, DateTime? lastReceivedDate, long lastId)> GetPagedListAsync(DateTime? lastReceivedDate, long? lastId, int pageSize = 20);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取总数
|
||||
/// </summary>
|
||||
@@ -31,20 +31,20 @@ public class EmailMessageRepository(IFreeSql freeSql) : BaseRepository<EmailMess
|
||||
public async Task<(List<EmailMessage> list, DateTime? lastReceivedDate, long lastId)> GetPagedListAsync(DateTime? lastReceivedDate, long? lastId, int pageSize = 20)
|
||||
{
|
||||
var query = FreeSql.Select<EmailMessage>();
|
||||
|
||||
|
||||
// 如果提供了游标,则获取小于游标位置的记录
|
||||
if (lastReceivedDate.HasValue && lastId.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.ReceivedDate < lastReceivedDate.Value ||
|
||||
query = query.Where(e => e.ReceivedDate < lastReceivedDate.Value ||
|
||||
(e.ReceivedDate == lastReceivedDate.Value && e.Id < lastId.Value));
|
||||
}
|
||||
|
||||
|
||||
var list = await query
|
||||
.OrderByDescending(e => e.ReceivedDate)
|
||||
.OrderByDescending(e => e.Id)
|
||||
.Page(1, pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
var lastRecord = list.Count > 0 ? list.Last() : null;
|
||||
return (list, lastRecord?.ReceivedDate, lastRecord?.Id ?? 0);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public class MessageRecordRepository(IFreeSql freeSql) : BaseRepository<MessageR
|
||||
.Count(out var total)
|
||||
.Page(pageIndex, pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
return (list, total);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@ public interface ITransactionPeriodicRepository : IBaseRepository<TransactionPer
|
||||
/// <summary>
|
||||
/// 周期性账单仓储实现
|
||||
/// </summary>
|
||||
public class TransactionPeriodicRepository(IFreeSql freeSql)
|
||||
public class TransactionPeriodicRepository(IFreeSql freeSql)
|
||||
: BaseRepository<TransactionPeriodic>(freeSql), ITransactionPeriodicRepository
|
||||
{
|
||||
public async Task<IEnumerable<TransactionPeriodic>> GetPagedListAsync(
|
||||
int pageIndex,
|
||||
int pageSize,
|
||||
int pageIndex,
|
||||
int pageSize,
|
||||
string? searchKeyword = null)
|
||||
{
|
||||
var query = FreeSql.Select<TransactionPeriodic>();
|
||||
@@ -42,8 +42,8 @@ public class TransactionPeriodicRepository(IFreeSql freeSql)
|
||||
// 搜索关键词
|
||||
if (!string.IsNullOrWhiteSpace(searchKeyword))
|
||||
{
|
||||
query = query.Where(x =>
|
||||
x.Reason.Contains(searchKeyword) ||
|
||||
query = query.Where(x =>
|
||||
x.Reason.Contains(searchKeyword) ||
|
||||
x.Classify.Contains(searchKeyword));
|
||||
}
|
||||
|
||||
@@ -60,8 +60,8 @@ public class TransactionPeriodicRepository(IFreeSql freeSql)
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchKeyword))
|
||||
{
|
||||
query = query.Where(x =>
|
||||
x.Reason.Contains(searchKeyword) ||
|
||||
query = query.Where(x =>
|
||||
x.Reason.Contains(searchKeyword) ||
|
||||
x.Classify.Contains(searchKeyword));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ export const createTransaction = (data) => {
|
||||
* @param {number} data.balance - 交易后余额
|
||||
* @param {number} data.type - 交易类型 (0:支出, 1:收入, 2:不计入收支)
|
||||
* @param {string} data.classify - 交易分类
|
||||
* @param {string} [data.occurredAt] - 交易时间
|
||||
* @returns {Promise<{success: boolean}>}
|
||||
*/
|
||||
export const updateTransaction = (data) => {
|
||||
|
||||
184
Web/src/assets/theme.css
Normal file
184
Web/src/assets/theme.css
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* EmailBill 主题系统 - 根据 v2.pen 设计稿
|
||||
* 用于保持整个应用色彩和布局一致性
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ============ 颜色变量 - 浅色主题 ============ */
|
||||
|
||||
/* 背景色 */
|
||||
--bg-primary: #FFFFFF;
|
||||
--bg-secondary: #F6F7F8;
|
||||
--bg-tertiary: #F3F4F6;
|
||||
--bg-button: #F5F5F5;
|
||||
|
||||
/* 文字颜色 */
|
||||
--text-primary: #1A1A1A;
|
||||
--text-secondary: #6B7280;
|
||||
--text-tertiary: #9CA3AF;
|
||||
|
||||
/* 强调色 */
|
||||
--accent-primary: #FF6B6B;
|
||||
--accent-danger: #EF4444;
|
||||
--accent-warning: #D97706;
|
||||
--accent-warning-bg: #FFFBEB;
|
||||
--accent-success: #22C55E;
|
||||
--accent-success-bg: #F0FDF4;
|
||||
--accent-info: #6366F1;
|
||||
--accent-info-bg: #E0E7FF;
|
||||
|
||||
/* 图标色 */
|
||||
--icon-star: #FF6B6B;
|
||||
--icon-coffee: #FCD34D;
|
||||
|
||||
/* ============ 布局变量 ============ */
|
||||
|
||||
/* 间距 */
|
||||
--spacing-xs: 2px;
|
||||
--spacing-sm: 4px;
|
||||
--spacing-md: 8px
|
||||
--spacing-lg: 12px;
|
||||
--spacing-xl: 16px;
|
||||
--spacing-2xl: 20px;
|
||||
--spacing-3xl: 24px;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 12px;
|
||||
--radius-md: 16px;
|
||||
--radius-lg: 20px;
|
||||
--radius-full: 22px;
|
||||
|
||||
/* 字体大小 */
|
||||
--font-xs: 9px;
|
||||
--font-sm: 11px;
|
||||
--font-base: 12px;
|
||||
--font-md: 13px;
|
||||
--font-lg: 15px;
|
||||
--font-xl: 18px;
|
||||
--font-2xl: 24px;
|
||||
--font-3xl: 32px;
|
||||
|
||||
/* 字体粗细 */
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
--font-extrabold: 800;
|
||||
|
||||
/* 字体 */
|
||||
--font-primary: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-display: 'Bricolage Grotesque', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
|
||||
/* 阴影 (可选) */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* ============ 深色主题 ============ */
|
||||
[data-theme="dark"] {
|
||||
/* 背景色 */
|
||||
--bg-primary: #09090B;
|
||||
--bg-secondary: #18181b;
|
||||
--bg-tertiary: #27272a;
|
||||
--bg-button: #27272a;
|
||||
|
||||
/* 文字颜色 */
|
||||
--text-primary: #f4f4f5;
|
||||
--text-secondary: #a1a1aa;
|
||||
--text-tertiary: #71717a;
|
||||
|
||||
/* 强调色 (深色主题调整) */
|
||||
--accent-primary: #FF6B6B;
|
||||
--accent-danger: #f87171;
|
||||
--accent-warning: #fbbf24;
|
||||
--accent-warning-bg: #451a03;
|
||||
--accent-success: #4ade80;
|
||||
--accent-success-bg: #064e3b;
|
||||
--accent-info: #818cf8;
|
||||
--accent-info-bg: #312e81;
|
||||
|
||||
/* 图标色 (深色主题) */
|
||||
--icon-star: #FF6B6B;
|
||||
--icon-coffee: #FCD34D;
|
||||
}
|
||||
|
||||
/* ============ 通用工具类 ============ */
|
||||
|
||||
/* 文字 */
|
||||
.text-primary {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.text-tertiary {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--accent-danger);
|
||||
}
|
||||
|
||||
/* 背景 */
|
||||
.bg-primary {
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.bg-secondary {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.bg-tertiary {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
/* 布局容器 */
|
||||
.container-fluid {
|
||||
width: 100%;
|
||||
max-width: 402px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Flex 布局 */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 间距 */
|
||||
.gap-xs { gap: var(--spacing-xs); }
|
||||
.gap-sm { gap: var(--spacing-sm); }
|
||||
.gap-md { gap: var(--spacing-md); }
|
||||
.gap-lg { gap: var(--spacing-lg); }
|
||||
.gap-xl { gap: var(--spacing-xl); }
|
||||
.gap-2xl { gap: var(--spacing-2xl); }
|
||||
.gap-3xl { gap: var(--spacing-3xl); }
|
||||
|
||||
/* 内边距 */
|
||||
.p-sm { padding: var(--spacing-md); }
|
||||
.p-md { padding: var(--spacing-xl); }
|
||||
.p-lg { padding: var(--spacing-2xl); }
|
||||
.p-xl { padding: var(--spacing-3xl); }
|
||||
|
||||
/* 圆角 */
|
||||
.rounded-sm { border-radius: var(--radius-sm); }
|
||||
.rounded-md { border-radius: var(--radius-md); }
|
||||
.rounded-lg { border-radius: var(--radius-lg); }
|
||||
.rounded-full { border-radius: var(--radius-full); }
|
||||
@@ -275,7 +275,7 @@ const handleTypeChange = () => {
|
||||
const onConfirmDate = ({ selectedValues }) => {
|
||||
const dateStr = selectedValues.join('-')
|
||||
const timeStr = currentTime.value.join(':')
|
||||
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).toISOString()
|
||||
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).format('YYYY-MM-DDTHH:mm:ss')
|
||||
showDatePicker.value = false
|
||||
// 接着选时间
|
||||
showTimePicker.value = true
|
||||
@@ -285,7 +285,7 @@ const onConfirmTime = ({ selectedValues }) => {
|
||||
currentTime.value = selectedValues
|
||||
const dateStr = currentDate.value.join('-')
|
||||
const timeStr = selectedValues.join(':')
|
||||
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).toISOString()
|
||||
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).format('YYYY-MM-DDTHH:mm:ss')
|
||||
showTimePicker.value = false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import './assets/main.css'
|
||||
import './assets/theme.css'
|
||||
import './styles/common.css'
|
||||
import './styles/rich-content.css'
|
||||
|
||||
|
||||
630
Web/src/views/CalendarV2.vue
Normal file
630
Web/src/views/CalendarV2.vue
Normal file
@@ -0,0 +1,630 @@
|
||||
<template>
|
||||
<div
|
||||
class="calendar-v2"
|
||||
:data-theme="theme"
|
||||
>
|
||||
<!-- 头部 -->
|
||||
<header class="calendar-header">
|
||||
<div class="header-content">
|
||||
<h1 class="header-title">
|
||||
{{ currentMonth }}
|
||||
</h1>
|
||||
</div>
|
||||
<button
|
||||
class="notif-btn"
|
||||
aria-label="通知"
|
||||
>
|
||||
<van-icon name="bell" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- 日历容器 -->
|
||||
<div class="calendar-container">
|
||||
<!-- 星期标题 -->
|
||||
<div class="week-days">
|
||||
<span
|
||||
v-for="day in weekDays"
|
||||
:key="day"
|
||||
class="week-day"
|
||||
>{{ day }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 日历网格 -->
|
||||
<div class="calendar-grid">
|
||||
<div
|
||||
v-for="(week, weekIndex) in calendarWeeks"
|
||||
:key="weekIndex"
|
||||
class="calendar-week"
|
||||
>
|
||||
<div
|
||||
v-for="day in week"
|
||||
:key="day.date"
|
||||
class="day-cell"
|
||||
@click="onDayClick(day)"
|
||||
>
|
||||
<div
|
||||
class="day-number"
|
||||
:class="{
|
||||
'day-today': day.isToday,
|
||||
'day-selected': day.isSelected,
|
||||
'day-has-data': day.hasData,
|
||||
'day-over-limit': day.isOverLimit,
|
||||
'day-other-month': !day.isCurrentMonth
|
||||
}"
|
||||
>
|
||||
{{ day.dayNumber }}
|
||||
</div>
|
||||
<div
|
||||
v-if="day.amount"
|
||||
class="day-amount"
|
||||
:class="{ 'amount-over': day.isOverLimit }"
|
||||
>
|
||||
{{ day.amount }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 每日统计 -->
|
||||
<div class="daily-stats">
|
||||
<div class="stats-header">
|
||||
<h2 class="stats-title">
|
||||
Daily Stats
|
||||
</h2>
|
||||
<span class="stats-date">{{ selectedDateFormatted }}</span>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<div class="stats-row">
|
||||
<span class="stats-label">Total Spent</span>
|
||||
<div class="stats-badge">
|
||||
Daily Limit: {{ dailyLimit }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-value">
|
||||
¥ {{ totalSpent }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交易列表 -->
|
||||
<div class="transactions">
|
||||
<div class="txn-header">
|
||||
<h2 class="txn-title">
|
||||
Transactions
|
||||
</h2>
|
||||
<div class="txn-actions">
|
||||
<div class="txn-badge badge-success">
|
||||
{{ transactionCount }} Items
|
||||
</div>
|
||||
<button class="smart-btn">
|
||||
<van-icon name="star-o" />
|
||||
<span>Smart</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交易卡片 -->
|
||||
<div class="txn-list">
|
||||
<div
|
||||
v-for="txn in transactions"
|
||||
:key="txn.id"
|
||||
class="txn-card"
|
||||
@click="onTransactionClick(txn)"
|
||||
>
|
||||
<div
|
||||
class="txn-icon"
|
||||
:style="{ backgroundColor: txn.iconBg }"
|
||||
>
|
||||
<van-icon
|
||||
:name="txn.icon"
|
||||
:color="txn.iconColor"
|
||||
/>
|
||||
</div>
|
||||
<div class="txn-content">
|
||||
<div class="txn-name">
|
||||
{{ txn.name }}
|
||||
</div>
|
||||
<div class="txn-time">
|
||||
{{ txn.time }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="txn-amount">
|
||||
{{ txn.amount }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div class="bottom-spacer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
// 当前主题
|
||||
const theme = ref('light') // 'light' | 'dark'
|
||||
|
||||
// 星期标题
|
||||
const weekDays = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
|
||||
|
||||
// 当前日期
|
||||
const currentDate = ref(new Date())
|
||||
const selectedDate = ref(new Date())
|
||||
|
||||
// 当前月份格式化
|
||||
const currentMonth = computed(() => {
|
||||
return currentDate.value.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long'
|
||||
})
|
||||
})
|
||||
|
||||
// 选中日期格式化
|
||||
const selectedDateFormatted = computed(() => {
|
||||
return selectedDate.value.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
})
|
||||
|
||||
// 生成日历数据
|
||||
const calendarWeeks = computed(() => {
|
||||
const year = currentDate.value.getFullYear()
|
||||
const month = currentDate.value.getMonth()
|
||||
|
||||
// 获取当月第一天
|
||||
const firstDay = new Date(year, month, 1)
|
||||
// 获取当月最后一天
|
||||
const lastDay = new Date(year, month + 1, 0)
|
||||
|
||||
// 获取第一天是星期几 (0=Sunday, 调整为 0=Monday)
|
||||
let startDayOfWeek = firstDay.getDay() - 1
|
||||
if (startDayOfWeek === -1) {startDayOfWeek = 6}
|
||||
|
||||
const weeks = []
|
||||
let currentWeek = []
|
||||
|
||||
// 填充上月日期
|
||||
for (let i = 0; i < startDayOfWeek; i++) {
|
||||
const date = new Date(year, month, -(startDayOfWeek - i - 1))
|
||||
currentWeek.push(createDayObject(date, false))
|
||||
}
|
||||
|
||||
// 填充当月日期
|
||||
for (let day = 1; day <= lastDay.getDate(); day++) {
|
||||
const date = new Date(year, month, day)
|
||||
currentWeek.push(createDayObject(date, true))
|
||||
|
||||
if (currentWeek.length === 7) {
|
||||
weeks.push(currentWeek)
|
||||
currentWeek = []
|
||||
}
|
||||
}
|
||||
|
||||
// 填充下月日期
|
||||
if (currentWeek.length > 0) {
|
||||
const remainingDays = 7 - currentWeek.length
|
||||
for (let i = 1; i <= remainingDays; i++) {
|
||||
const date = new Date(year, month + 1, i)
|
||||
currentWeek.push(createDayObject(date, false))
|
||||
}
|
||||
weeks.push(currentWeek)
|
||||
}
|
||||
|
||||
return weeks
|
||||
})
|
||||
|
||||
// 创建日期对象
|
||||
const createDayObject = (date, isCurrentMonth) => {
|
||||
const today = new Date()
|
||||
const isToday =
|
||||
date.getDate() === today.getDate() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getFullYear() === today.getFullYear()
|
||||
|
||||
const isSelected =
|
||||
date.getDate() === selectedDate.value.getDate() &&
|
||||
date.getMonth() === selectedDate.value.getMonth() &&
|
||||
date.getFullYear() === selectedDate.value.getFullYear()
|
||||
|
||||
// 模拟数据 - 实际应该从 API 获取
|
||||
const mockData = getMockDataForDate(date)
|
||||
|
||||
return {
|
||||
date: date.getTime(),
|
||||
dayNumber: date.getDate(),
|
||||
isCurrentMonth,
|
||||
isToday,
|
||||
isSelected,
|
||||
hasData: mockData.hasData,
|
||||
amount: mockData.amount,
|
||||
isOverLimit: mockData.isOverLimit
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟数据获取
|
||||
const getMockDataForDate = (date) => {
|
||||
const day = date.getDate()
|
||||
|
||||
// 模拟一些有数据的日期
|
||||
if (day >= 4 && day <= 28 && date.getMonth() === currentDate.value.getMonth()) {
|
||||
const amounts = [128, 45, 230, 12, 88, 223, 15, 34, 120, 56, 442]
|
||||
const amount = amounts[day % amounts.length]
|
||||
return {
|
||||
hasData: true,
|
||||
amount: amount || '',
|
||||
isOverLimit: amount > 200 // 超过限额标红
|
||||
}
|
||||
}
|
||||
|
||||
return { hasData: false, amount: '', isOverLimit: false }
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
const dailyLimit = ref('2500')
|
||||
const totalSpent = ref('1,248.50')
|
||||
const transactionCount = computed(() => transactions.value.length)
|
||||
|
||||
// 交易列表数据
|
||||
const transactions = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Lunch',
|
||||
time: '12:30 PM',
|
||||
amount: '-58.00',
|
||||
icon: 'star',
|
||||
iconColor: '#FF6B6B',
|
||||
iconBg: '#FFFFFF'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Coffee',
|
||||
time: '08:15 AM',
|
||||
amount: '-24.50',
|
||||
icon: 'coffee-o',
|
||||
iconColor: '#FCD34D',
|
||||
iconBg: '#FFFFFF'
|
||||
}
|
||||
])
|
||||
|
||||
// 点击日期
|
||||
const onDayClick = (day) => {
|
||||
if (!day.isCurrentMonth) {return}
|
||||
selectedDate.value = new Date(day.date)
|
||||
// TODO: 加载选中日期的数据
|
||||
console.log('Selected date:', day)
|
||||
}
|
||||
|
||||
// 点击交易
|
||||
const onTransactionClick = (txn) => {
|
||||
console.log('Transaction clicked:', txn)
|
||||
// TODO: 打开交易详情
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
const toggleTheme = () => {
|
||||
theme.value = theme.value === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
// 暴露切换主题方法供外部调用
|
||||
defineExpose({
|
||||
toggleTheme
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '@/assets/theme.css';
|
||||
|
||||
.calendar-v2 {
|
||||
min-height: 100vh;
|
||||
background-color: var(--bg-primary);
|
||||
font-family: var(--font-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ========== 头部 ========== */
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 24px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-family: var(--font-primary);
|
||||
font-size: var(--font-xl);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notif-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-full);
|
||||
background-color: var(--bg-button);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.notif-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ========== 日历容器 ========== */
|
||||
.calendar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.week-days {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.week-day {
|
||||
width: 44px;
|
||||
text-align: center;
|
||||
font-size: var(--font-base);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.calendar-week {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
width: 44px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
font-size: var(--font-md);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.day-number.day-has-data {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.day-number.day-selected {
|
||||
background-color: var(--accent-primary);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.day-number.day-other-month {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.day-amount {
|
||||
font-size: var(--font-xs);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.day-amount.amount-over {
|
||||
color: var(--accent-danger);
|
||||
}
|
||||
|
||||
/* ========== 统计卡片 ========== */
|
||||
.daily-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--font-xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-date {
|
||||
font-size: var(--font-md);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: var(--font-md);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stats-badge {
|
||||
padding: 6px 10px;
|
||||
background-color: var(--accent-warning-bg);
|
||||
color: var(--accent-warning);
|
||||
font-size: var(--font-sm);
|
||||
font-weight: var(--font-semibold);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--font-3xl);
|
||||
font-weight: var(--font-extrabold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ========== 交易列表 ========== */
|
||||
.transactions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.txn-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.txn-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--font-xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.txn-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.txn-badge {
|
||||
padding: 6px 12px;
|
||||
font-size: var(--font-base);
|
||||
font-weight: var(--font-semibold);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: var(--accent-success-bg);
|
||||
color: var(--accent-success);
|
||||
}
|
||||
|
||||
.smart-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background-color: var(--accent-info-bg);
|
||||
color: var(--accent-info);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-base);
|
||||
font-weight: var(--font-semibold);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.smart-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.txn-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.txn-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.txn-card:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.txn-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-full);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.txn-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.txn-name {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.txn-time {
|
||||
font-size: var(--font-md);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.txn-amount {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 底部安全距离 */
|
||||
.bottom-spacer {
|
||||
height: calc(60px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
</style>
|
||||
@@ -21,7 +21,7 @@ public class BudgetStatsTest : BaseTest
|
||||
public BudgetStatsTest()
|
||||
{
|
||||
_dateTimeProvider.Now.Returns(new DateTime(2024, 1, 15));
|
||||
|
||||
|
||||
IBudgetStatsService budgetStatsService = new BudgetStatsService(
|
||||
_budgetRepository,
|
||||
_budgetArchiveRepository,
|
||||
@@ -29,7 +29,7 @@ public class BudgetStatsTest : BaseTest
|
||||
_dateTimeProvider,
|
||||
Substitute.For<ILogger<BudgetStatsService>>()
|
||||
);
|
||||
|
||||
|
||||
_service = new BudgetService(
|
||||
_budgetRepository,
|
||||
_budgetArchiveRepository,
|
||||
@@ -89,7 +89,7 @@ public class BudgetStatsTest : BaseTest
|
||||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(0m); // 实际支出的金额为0
|
||||
|
||||
|
||||
_dateTimeProvider.Now.Returns(referenceDate);
|
||||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
|
||||
.Returns(new Dictionary<DateTime, decimal>());
|
||||
@@ -117,7 +117,7 @@ public class BudgetStatsTest : BaseTest
|
||||
};
|
||||
|
||||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||||
|
||||
|
||||
// 月度统计使用趋势统计数据(只包含月度预算的分类)
|
||||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 1),
|
||||
@@ -128,7 +128,7 @@ public class BudgetStatsTest : BaseTest
|
||||
{
|
||||
{ new DateTime(2024, 1, 15), 800m } // 1月15日月度吃饭累计800
|
||||
});
|
||||
|
||||
|
||||
// 年度统计使用GetCurrentAmountAsync
|
||||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(args =>
|
||||
@@ -136,22 +136,22 @@ public class BudgetStatsTest : BaseTest
|
||||
var b = (BudgetRecord)args[0];
|
||||
var startDate = (DateTime)args[1];
|
||||
var endDate = (DateTime)args[2];
|
||||
|
||||
|
||||
// 月度范围查询 - 月度吃饭(1月)
|
||||
if (startDate.Month == 1 && startDate.Day == 1 && endDate.Month == 1)
|
||||
{
|
||||
return b.Name == "月度吃饭" ? 800m : 0m;
|
||||
}
|
||||
|
||||
|
||||
// 年度范围查询 - 年度旅游
|
||||
if (startDate.Month == 1 && startDate.Day == 1 && endDate.Month == 12)
|
||||
{
|
||||
return b.Name == "年度旅游" ? 2000m : 0m;
|
||||
}
|
||||
|
||||
|
||||
return 0m;
|
||||
});
|
||||
|
||||
|
||||
// 年度趋势统计(包含所有分类)
|
||||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 1),
|
||||
@@ -172,7 +172,7 @@ public class BudgetStatsTest : BaseTest
|
||||
result.Month.Limit.Should().Be(3000); // 月度吃饭3000
|
||||
result.Month.Current.Should().Be(800); // 月度吃饭已用800(从GetCurrentAmountAsync获取)
|
||||
result.Month.Count.Should().Be(1); // 只包含1个月度预算
|
||||
|
||||
|
||||
// 年度统计中:包含所有预算(月度预算按剩余月份折算)
|
||||
// 1月时,月度预算分为:当前月(1月) + 剩余月份(2-12月共11个月)
|
||||
result.Year.Limit.Should().Be(12000 + (3000 * 12)); // 年度旅游12000 + 月度吃饭折算年度(3000*12=36000) = 48000
|
||||
@@ -239,7 +239,7 @@ public class BudgetStatsTest : BaseTest
|
||||
{
|
||||
// Arrange
|
||||
var referenceDate = new DateTime(2024, 1, 15);
|
||||
|
||||
|
||||
// 设置预算:包含月度预算和年度预算
|
||||
var budgets = new List<BudgetRecord>
|
||||
{
|
||||
@@ -257,7 +257,7 @@ public class BudgetStatsTest : BaseTest
|
||||
};
|
||||
|
||||
_budgetRepository.GetAllAsync().Returns(budgets);
|
||||
|
||||
|
||||
// 设置月度预算的当前金额
|
||||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(args =>
|
||||
@@ -272,7 +272,7 @@ public class BudgetStatsTest : BaseTest
|
||||
_ => 0m
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// 设置月度趋势统计数据:只包含月度预算相关的分类(餐饮、零食、交通)
|
||||
// 注意:不应包含年度预算的分类(旅游、度假、奖金、年终奖)
|
||||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||||
@@ -284,7 +284,7 @@ public class BudgetStatsTest : BaseTest
|
||||
{
|
||||
{ new DateTime(2024, 1, 15), 1500m } // 1月15日累计1500(吃喝1200+交通300,不包含年度旅游2000)
|
||||
});
|
||||
|
||||
|
||||
// 设置年度趋势统计数据:包含所有预算相关的分类
|
||||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 1),
|
||||
@@ -296,7 +296,7 @@ public class BudgetStatsTest : BaseTest
|
||||
{
|
||||
{ new DateTime(2024, 1, 1), 3500m } // 1月累计3500(吃喝1200+交通300+年度旅游2000)
|
||||
});
|
||||
|
||||
|
||||
// 设置收入相关的趋势统计数据
|
||||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1),
|
||||
@@ -304,7 +304,7 @@ public class BudgetStatsTest : BaseTest
|
||||
TransactionType.Income,
|
||||
Arg.Any<List<string>>())
|
||||
.Returns(new Dictionary<DateTime, decimal>()); // 月度收入为空
|
||||
|
||||
|
||||
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 1 && d.Day == 1),
|
||||
Arg.Is<DateTime>(d => d.Year == 2024 && d.Month == 12 && d.Day == 31),
|
||||
@@ -318,7 +318,7 @@ public class BudgetStatsTest : BaseTest
|
||||
|
||||
// Act - 测试支出统计
|
||||
var expenseResult = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||||
|
||||
|
||||
// Act - 测试收入统计
|
||||
var incomeResult = await _service.GetCategoryStatsAsync(BudgetCategory.Income, referenceDate);
|
||||
|
||||
@@ -345,17 +345,17 @@ public class BudgetStatsTest : BaseTest
|
||||
incomeResult.Year.Count.Should().Be(1); // 包含1个年度收入预算
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStats_年度_3月_2月预算变更_Test()
|
||||
{
|
||||
// Arrange
|
||||
// 测试场景:2024年3月查看年度预算统计,其中2月份发生了预算变更(吃喝预算从2000增加到2500)
|
||||
var referenceDate = new DateTime(2024, 3, 15);
|
||||
|
||||
|
||||
// 设置当前时间,确保3月被认为是当前月份
|
||||
_dateTimeProvider.Now.Returns(new DateTime(2024, 3, 15));
|
||||
|
||||
|
||||
// 当前3月份有效的预算
|
||||
var currentBudgets = new List<BudgetRecord>
|
||||
{
|
||||
@@ -426,11 +426,11 @@ public class BudgetStatsTest : BaseTest
|
||||
|
||||
// 设置仓储响应
|
||||
_budgetRepository.GetAllAsync().Returns(currentBudgets);
|
||||
|
||||
|
||||
// 设置归档仓储响应
|
||||
_budgetArchiveRepository.GetArchiveAsync(2024, 2).Returns(febArchive);
|
||||
_budgetArchiveRepository.GetArchiveAsync(2024, 1).Returns(janArchive);
|
||||
|
||||
|
||||
// 设置月度预算的当前金额查询(仅用于3月份)
|
||||
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Is<DateTime>(d => d.Month == 3), Arg.Is<DateTime>(d => d.Month == 3))
|
||||
.Returns(args =>
|
||||
@@ -443,11 +443,11 @@ public class BudgetStatsTest : BaseTest
|
||||
_ => 0m
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// 年度旅游的年度金额查询
|
||||
_budgetRepository.GetCurrentAmountAsync(
|
||||
Arg.Is<BudgetRecord>(b => b.Id == 3),
|
||||
Arg.Is<DateTime>(d => d.Month == 1),
|
||||
Arg.Is<BudgetRecord>(b => b.Id == 3),
|
||||
Arg.Is<DateTime>(d => d.Month == 1),
|
||||
Arg.Is<DateTime>(d => d.Month == 12))
|
||||
.Returns(2500m); // 年度旅游1-3月已花费2500
|
||||
|
||||
@@ -477,11 +477,11 @@ public class BudgetStatsTest : BaseTest
|
||||
// 3月累计:月度预算1000 + 年度旅游2500 = 3500
|
||||
{ new DateTime(2024, 3, 1), 3500m }
|
||||
});
|
||||
|
||||
|
||||
// 补充:年度旅游的GetCurrentAmountAsync调用(用于计算Current)
|
||||
_budgetRepository.GetCurrentAmountAsync(
|
||||
Arg.Is<BudgetRecord>(b => b.Id == 3),
|
||||
Arg.Is<DateTime>(d => d.Month == 1),
|
||||
Arg.Is<BudgetRecord>(b => b.Id == 3),
|
||||
Arg.Is<DateTime>(d => d.Month == 1),
|
||||
Arg.Is<DateTime>(d => d.Month == 12))
|
||||
.Returns(2500m); // 年度旅游1-3月已花费2500
|
||||
|
||||
@@ -494,7 +494,7 @@ public class BudgetStatsTest : BaseTest
|
||||
_dateTimeProvider,
|
||||
Substitute.For<ILogger<BudgetStatsService>>()
|
||||
);
|
||||
|
||||
|
||||
var result = await budgetStatsService.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
|
||||
|
||||
// Assert - 月度统计(3月份)
|
||||
@@ -502,7 +502,7 @@ public class BudgetStatsTest : BaseTest
|
||||
result.Month.Limit.Should().Be(3000); // 吃喝2500 + 交通500(使用变更后的预算)
|
||||
result.Month.Current.Should().Be(1000); // 吃喝800 + 交通200
|
||||
result.Month.Count.Should().Be(2); // 包含2个月度预算
|
||||
|
||||
|
||||
// Assert - 年度统计(需要考虑预算变更和剩余月份)
|
||||
// 新逻辑:
|
||||
// 1. 对于归档数据,直接使用归档的限额,不折算
|
||||
@@ -515,7 +515,7 @@ public class BudgetStatsTest : BaseTest
|
||||
// 年度旅游:12000
|
||||
// 总计:2500 + 2500 + 30000 + 12000 = 47000
|
||||
result.Year.Limit.Should().Be(47000);
|
||||
|
||||
|
||||
// 预期年度实际金额:
|
||||
// 根据趋势统计数据,3月累计: 月度预算1000 + 年度旅游2500 = 3500
|
||||
// 但业务代码会累加所有预算项的Current值:
|
||||
@@ -528,7 +528,7 @@ public class BudgetStatsTest : BaseTest
|
||||
// - 年度旅游:2500
|
||||
// 总计:1500+250+1800+300+800+200+2500 = 7350
|
||||
result.Year.Current.Should().Be(7350);
|
||||
|
||||
|
||||
// 应该包含:
|
||||
// - 1月归档的月度预算:吃喝、1个
|
||||
// - 1月归档的月度预算:交通、1个
|
||||
@@ -541,7 +541,7 @@ public class BudgetStatsTest : BaseTest
|
||||
// - 年度旅游:1个
|
||||
// 总计:9个
|
||||
result.Year.Count.Should().Be(9);
|
||||
|
||||
|
||||
// 验证使用率计算正确
|
||||
result.Month.Rate.Should().BeApproximately(1000m / 3000m * 100, 0.01m);
|
||||
// 年度使用率:7350 / 47000 * 100 = 15.64%
|
||||
|
||||
@@ -60,7 +60,7 @@ public class BudgetRepositoryTest : TransactionTestBase
|
||||
var b1_updated = all.First(b => b.Name == "B1");
|
||||
b1_updated.SelectedCategories.Should().Contain("美食");
|
||||
b1_updated.SelectedCategories.Should().NotContain("餐饮");
|
||||
|
||||
|
||||
var b2_updated = all.First(b => b.Name == "B2");
|
||||
b2_updated.SelectedCategories.Should().Be("美食");
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ public class ConfigRepositoryTest : RepositoryTestBase
|
||||
public async Task GetByKeyAsync_获取配置_Test()
|
||||
{
|
||||
await _repository.AddAsync(new ConfigEntity { Key = "k1", Value = "v1" });
|
||||
|
||||
|
||||
var config = await _repository.GetByKeyAsync("k1");
|
||||
config.Should().NotBeNull();
|
||||
config.Value.Should().Be("v1");
|
||||
|
||||
@@ -13,10 +13,10 @@ public class EmailMessageRepositoryTest : RepositoryTestBase
|
||||
public async Task ExistsAsync_检查存在_Test()
|
||||
{
|
||||
await _repository.AddAsync(new EmailMessage { Md5 = "md5_value", Subject = "Test" });
|
||||
|
||||
|
||||
var msg = await _repository.ExistsAsync("md5_value");
|
||||
msg.Should().NotBeNull();
|
||||
|
||||
|
||||
var notfound = await _repository.ExistsAsync("other");
|
||||
notfound.Should().BeNull();
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public class EmailMessageRepositoryTest : RepositoryTestBase
|
||||
// Assuming ID order follows insertion (mostly true for snowflakes if generated sequentially)
|
||||
// But ReceivedDate is the primary sort in logic usually.
|
||||
// Let's verify standard cursor pagination usually sorts by Date DESC, ID DESC.
|
||||
|
||||
|
||||
await _repository.AddAsync(m1);
|
||||
await _repository.AddAsync(m2);
|
||||
await _repository.AddAsync(m3);
|
||||
|
||||
@@ -13,7 +13,7 @@ public class PushSubscriptionRepositoryTest : RepositoryTestBase
|
||||
public async Task GetByEndpointAsync_通过Endpoint获取_Test()
|
||||
{
|
||||
await _repository.AddAsync(new PushSubscription { Endpoint = "ep1" });
|
||||
|
||||
|
||||
var sub = await _repository.GetByEndpointAsync("ep1");
|
||||
sub.Should().NotBeNull();
|
||||
sub.Endpoint.Should().Be("ep1");
|
||||
|
||||
@@ -14,10 +14,10 @@ public class TransactionPeriodicRepositoryTest : TransactionTestBase
|
||||
{
|
||||
// 应该执行的:NextExecuteTime <= Now
|
||||
await _repository.AddAsync(new TransactionPeriodic { Reason = "Bill1", NextExecuteTime = DateTime.Now.AddDays(-1), IsEnabled = true });
|
||||
|
||||
|
||||
// 不该执行的:NextExecuteTime > Now
|
||||
await _repository.AddAsync(new TransactionPeriodic { Reason = "Bill2", NextExecuteTime = DateTime.Now.AddDays(1), IsEnabled = true });
|
||||
|
||||
|
||||
// 不该执行的:未激活
|
||||
await _repository.AddAsync(new TransactionPeriodic { Reason = "Bill3", NextExecuteTime = DateTime.Now.AddDays(-1), IsEnabled = false });
|
||||
|
||||
|
||||
@@ -47,11 +47,11 @@ public class TransactionRecordRepositoryTest : TransactionTestBase
|
||||
var results = await _repository.QueryAsync(
|
||||
startDate: new DateTime(2023, 1, 1),
|
||||
endDate: new DateTime(2023, 2, 28)); // Include Feb
|
||||
|
||||
|
||||
results.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact]
|
||||
public async Task QueryAsync_按年月筛选_Test()
|
||||
{
|
||||
await _repository.AddAsync(CreateExpense(100, new DateTime(2023, 1, 15)));
|
||||
@@ -99,7 +99,7 @@ public class TransactionRecordRepositoryTest : TransactionTestBase
|
||||
|
||||
var records = await _repository.QueryAsync(reason: "麦当劳");
|
||||
records.All(r => r.Classify == "快餐").Should().BeTrue();
|
||||
|
||||
|
||||
var kfc = await _repository.QueryAsync(reason: "肯德基");
|
||||
kfc.First().Classify.Should().Be("餐饮");
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class TransactionPeriodicServiceTest : BaseTest
|
||||
// Assert
|
||||
// Service inserts Amount directly from periodicBill.Amount (100 is positive)
|
||||
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t =>
|
||||
t.Amount == 100m &&
|
||||
t.Amount == 100m &&
|
||||
t.Type == TransactionType.Expense &&
|
||||
t.Classify == "餐饮" &&
|
||||
t.Reason == "每日餐费" &&
|
||||
@@ -69,7 +69,7 @@ public class TransactionPeriodicServiceTest : BaseTest
|
||||
|
||||
await _periodicRepository.Received(1).UpdateExecuteTimeAsync(
|
||||
Arg.Is(1L),
|
||||
Arg.Any<DateTime>(),
|
||||
Arg.Any<DateTime>(),
|
||||
Arg.Any<DateTime?>()
|
||||
);
|
||||
}
|
||||
@@ -149,7 +149,7 @@ public class TransactionPeriodicServiceTest : BaseTest
|
||||
m.Content.Contains("每月工资")
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task ExecutePeriodicBillsAsync_未达到执行时间()
|
||||
{
|
||||
@@ -158,7 +158,7 @@ public class TransactionPeriodicServiceTest : BaseTest
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Weekly,
|
||||
PeriodicConfig = "1,3,5",
|
||||
PeriodicConfig = "1,3,5",
|
||||
Amount = 200m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "交通",
|
||||
@@ -191,7 +191,7 @@ public class TransactionPeriodicServiceTest : BaseTest
|
||||
Classify = "餐饮",
|
||||
Reason = "每日餐费",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = DateTime.Today,
|
||||
LastExecuteTime = DateTime.Today,
|
||||
NextExecuteTime = DateTime.Today.AddDays(1)
|
||||
};
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ public class TransactionStatisticsServiceTest : BaseTest
|
||||
// Mock Logic: filter by year (Arg[0]) and month (Arg[1]) and type (Arg[4]) if provided
|
||||
_transactionRepository.QueryAsync(
|
||||
Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<DateTime?>(), Arg.Any<DateTime?>(), Arg.Any<TransactionType?>(), Arg.Any<string[]>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<bool>()
|
||||
).Returns(callInfo =>
|
||||
).Returns(callInfo =>
|
||||
{
|
||||
var y = callInfo.ArgAt<int?>(0);
|
||||
var m = callInfo.ArgAt<int?>(1);
|
||||
@@ -106,7 +106,7 @@ public class TransactionStatisticsServiceTest : BaseTest
|
||||
// In GetTrendStatisticsAsync: transactionRepository.QueryAsync(year: targetYear, month: targetMonth...)
|
||||
// It does NOT pass type. So type is null.
|
||||
// But Service THEN filters by Type in memory.
|
||||
|
||||
|
||||
return query.ToList();
|
||||
});
|
||||
|
||||
@@ -178,7 +178,7 @@ public class TransactionStatisticsServiceTest : BaseTest
|
||||
// Assert
|
||||
result[("餐饮", TransactionType.Expense)].Should().Be(-150m); // Expect Negative (Sum of amounts)
|
||||
}
|
||||
|
||||
|
||||
// Additional tests from original file to maintain coverage, with minimal adjustments if needed
|
||||
[Fact]
|
||||
public async Task GetCategoryStatisticsAsync_支出分类()
|
||||
@@ -190,18 +190,18 @@ public class TransactionStatisticsServiceTest : BaseTest
|
||||
new() { Amount = -50m, Type = TransactionType.Expense, Classify = "餐饮" },
|
||||
new() { Amount = -200m, Type = TransactionType.Expense, Classify = "交通" }
|
||||
};
|
||||
|
||||
|
||||
// Mock filtering by Type
|
||||
_transactionRepository.QueryAsync(
|
||||
Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<DateTime?>(), Arg.Any<DateTime?>(), Arg.Any<TransactionType?>(), Arg.Any<string[]>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<bool>()
|
||||
).Returns(callInfo =>
|
||||
{
|
||||
var type = callInfo.ArgAt<TransactionType?>(4);
|
||||
return testData.Where(t => !type.HasValue || t.Type == type).ToList();
|
||||
});
|
||||
_transactionRepository.QueryAsync(
|
||||
Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<DateTime?>(), Arg.Any<DateTime?>(), Arg.Any<TransactionType?>(), Arg.Any<string[]>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<bool>()
|
||||
).Returns(callInfo =>
|
||||
{
|
||||
var type = callInfo.ArgAt<TransactionType?>(4);
|
||||
return testData.Where(t => !type.HasValue || t.Type == type).ToList();
|
||||
});
|
||||
|
||||
var result = await _service.GetCategoryStatisticsAsync(year, month, TransactionType.Expense);
|
||||
|
||||
|
||||
result.First(c => c.Classify == "餐饮").Amount.Should().Be(150m);
|
||||
result.First(c => c.Classify == "交通").Amount.Should().Be(200m);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ public class BaseResponse<T> : BaseResponse
|
||||
/// 返回数据
|
||||
/// </summary>
|
||||
public T? Data { get; set; }
|
||||
|
||||
|
||||
public new static BaseResponse<T> Fail(string message)
|
||||
{
|
||||
return new BaseResponse<T>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
public class PagedResponse<T> : BaseResponse<T[]>
|
||||
{
|
||||
public long LastId { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 最后一条记录的时间(用于游标分页)
|
||||
/// </summary>
|
||||
@@ -13,7 +13,7 @@ public class PagedResponse<T> : BaseResponse<T[]>
|
||||
/// 总记录数
|
||||
/// </summary>
|
||||
public int Total { get; set; }
|
||||
|
||||
|
||||
public new static PagedResponse<T> Fail(string message)
|
||||
{
|
||||
return new PagedResponse<T>
|
||||
|
||||
@@ -21,7 +21,7 @@ public class JobController(ISchedulerFactory schedulerFactory, ILogger<JobContro
|
||||
var jobDetail = await scheduler.GetJobDetail(jobKey);
|
||||
var triggers = await scheduler.GetTriggersOfJob(jobKey);
|
||||
var trigger = triggers.FirstOrDefault();
|
||||
|
||||
|
||||
var status = "Unknown";
|
||||
DateTime? nextFireTime = null;
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@ public class LogController(ILogger<LogController> logger) : ControllerBase
|
||||
/// <summary>
|
||||
/// 获取日志列表(分页)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<PagedResponse<LogEntry>> GetListAsync(
|
||||
[FromQuery] int pageIndex = 1,
|
||||
[FromQuery] int pageSize = 50,
|
||||
[FromQuery] string? searchKeyword = null,
|
||||
[FromQuery] string? logLevel = null,
|
||||
[FromQuery] string? date = null,
|
||||
[FromQuery] string? className = null
|
||||
)
|
||||
[HttpGet]
|
||||
public async Task<PagedResponse<LogEntry>> GetListAsync(
|
||||
[FromQuery] int pageIndex = 1,
|
||||
[FromQuery] int pageSize = 50,
|
||||
[FromQuery] string? searchKeyword = null,
|
||||
[FromQuery] string? logLevel = null,
|
||||
[FromQuery] string? date = null,
|
||||
[FromQuery] string? className = null
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -221,7 +221,7 @@ public async Task<PagedResponse<LogEntry>> GetListAsync(
|
||||
logger.LogError(ex, "获取类名列表失败");
|
||||
return $"获取类名列表失败: {ex.Message}".Fail<string[]>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合并多行日志(已废弃,现在在流式读取中处理)
|
||||
@@ -385,27 +385,27 @@ public async Task<PagedResponse<LogEntry>> GetListAsync(
|
||||
/// <summary>
|
||||
/// 读取日志
|
||||
/// </summary>
|
||||
private async Task<(List<LogEntry> entries, int total)> ReadLogsAsync(
|
||||
string path,
|
||||
int pageIndex,
|
||||
int pageSize,
|
||||
string? searchKeyword,
|
||||
string? logLevel,
|
||||
string? className)
|
||||
{
|
||||
var allLines = await ReadAllLinesAsync(path);
|
||||
|
||||
var merged = MergeMultiLineLog(allLines);
|
||||
|
||||
var parsed = new List<LogEntry>();
|
||||
foreach (var line in merged)
|
||||
private async Task<(List<LogEntry> entries, int total)> ReadLogsAsync(
|
||||
string path,
|
||||
int pageIndex,
|
||||
int pageSize,
|
||||
string? searchKeyword,
|
||||
string? logLevel,
|
||||
string? className)
|
||||
{
|
||||
var entry = ParseLogLine(line);
|
||||
if (entry != null && PassFilter(entry, searchKeyword, logLevel, className))
|
||||
var allLines = await ReadAllLinesAsync(path);
|
||||
|
||||
var merged = MergeMultiLineLog(allLines);
|
||||
|
||||
var parsed = new List<LogEntry>();
|
||||
foreach (var line in merged)
|
||||
{
|
||||
parsed.Add(entry);
|
||||
var entry = ParseLogLine(line);
|
||||
if (entry != null && PassFilter(entry, searchKeyword, logLevel, className))
|
||||
{
|
||||
parsed.Add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsed.Reverse();
|
||||
|
||||
@@ -419,28 +419,28 @@ private async Task<(List<LogEntry> entries, int total)> ReadLogsAsync(
|
||||
/// <summary>
|
||||
/// 检查日志条目是否通过过滤条件
|
||||
/// </summary>
|
||||
private bool PassFilter(LogEntry logEntry, string? searchKeyword, string? logLevel, string? className)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(searchKeyword) &&
|
||||
!logEntry.Message.Contains(searchKeyword, StringComparison.OrdinalIgnoreCase))
|
||||
private bool PassFilter(LogEntry logEntry, string? searchKeyword, string? logLevel, string? className)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(searchKeyword) &&
|
||||
!logEntry.Message.Contains(searchKeyword, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(logLevel) &&
|
||||
!logEntry.Level.Equals(logLevel, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(logLevel) &&
|
||||
!logEntry.Level.Equals(logLevel, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(className) &&
|
||||
!logEntry.ClassName.Equals(className, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(className) &&
|
||||
!logEntry.ClassName.Equals(className, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取文件所有行(支持共享读取)
|
||||
|
||||
@@ -32,7 +32,7 @@ public class NotificationController(INotificationService notificationService) :
|
||||
return ex.Message.Fail();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<BaseResponse> TestNotification([FromQuery] string message)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -26,7 +26,7 @@ public class TransactionCategoryController(
|
||||
{
|
||||
categories = (await categoryRepository.GetAllAsync()).ToList();
|
||||
}
|
||||
|
||||
|
||||
return categories.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -38,18 +38,18 @@ public class TransactionRecordController(
|
||||
: classify.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null;
|
||||
var list = await transactionRepository.QueryAsync(
|
||||
year: year,
|
||||
month: month,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
type: transactionType,
|
||||
classifies: classifies,
|
||||
searchKeyword: searchKeyword,
|
||||
reason: reason,
|
||||
pageIndex: pageIndex,
|
||||
pageSize: pageSize,
|
||||
sortByAmount: sortByAmount);
|
||||
var list = await transactionRepository.QueryAsync(
|
||||
year: year,
|
||||
month: month,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
type: transactionType,
|
||||
classifies: classifies,
|
||||
searchKeyword: searchKeyword,
|
||||
reason: reason,
|
||||
pageIndex: pageIndex,
|
||||
pageSize: pageSize,
|
||||
sortByAmount: sortByAmount);
|
||||
var total = await transactionRepository.CountAsync(
|
||||
year: year,
|
||||
month: month,
|
||||
@@ -214,6 +214,12 @@ var list = await transactionRepository.QueryAsync(
|
||||
transaction.Type = dto.Type;
|
||||
transaction.Classify = dto.Classify ?? string.Empty;
|
||||
|
||||
// 更新交易时间
|
||||
if (!string.IsNullOrEmpty(dto.OccurredAt) && DateTime.TryParse(dto.OccurredAt, out var occurredAt))
|
||||
{
|
||||
transaction.OccurredAt = occurredAt;
|
||||
}
|
||||
|
||||
// 清除待确认状态
|
||||
transaction.UnconfirmedClassify = null;
|
||||
transaction.UnconfirmedType = null;
|
||||
@@ -272,7 +278,7 @@ var list = await transactionRepository.QueryAsync(
|
||||
|
||||
// 获取每日统计数据
|
||||
var statistics = await transactionStatisticsService.GetDailyStatisticsAsync(year, month, savingClassify);
|
||||
|
||||
|
||||
// 按日期排序并计算累积余额
|
||||
var sortedStats = statistics.OrderBy(s => s.Key).ToList();
|
||||
var result = new List<BalanceStatisticsDto>();
|
||||
@@ -693,7 +699,7 @@ var list = await transactionRepository.QueryAsync(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteEventAsync(string eventType, string data)
|
||||
private async Task WriteEventAsync(string eventType, string data)
|
||||
{
|
||||
var message = $"event: {eventType}\ndata: {data}\n\n";
|
||||
await Response.WriteAsync(message);
|
||||
@@ -728,7 +734,8 @@ public record UpdateTransactionDto(
|
||||
decimal Amount,
|
||||
decimal Balance,
|
||||
TransactionType Type,
|
||||
string? Classify
|
||||
string? Classify,
|
||||
string? OccurredAt = null
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -14,9 +14,9 @@ public class RequestIdMiddleware
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var requestId = context.Request.Headers["X-Request-ID"].FirstOrDefault() ?? Guid.NewGuid().ToString("N");
|
||||
|
||||
|
||||
context.Items["RequestId"] = requestId;
|
||||
|
||||
|
||||
using (LogContext.PushProperty("RequestId", requestId))
|
||||
{
|
||||
await _next(context);
|
||||
@@ -30,7 +30,7 @@ public static class RequestIdExtensions
|
||||
{
|
||||
return context.Items["RequestId"] as string;
|
||||
}
|
||||
|
||||
|
||||
public static IApplicationBuilder UseRequestId(this IApplicationBuilder builder)
|
||||
{
|
||||
return builder.UseMiddleware<RequestIdMiddleware>();
|
||||
|
||||
@@ -110,7 +110,7 @@ var fsql = new FreeSqlBuilder()
|
||||
.UseAutoSyncStructure(true)
|
||||
.UseLazyLoading(true)
|
||||
.UseMonitorCommand(
|
||||
cmd =>
|
||||
cmd =>
|
||||
{
|
||||
Log.Verbose("执行SQL: {Sql}", cmd.CommandText);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user