first commot
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 8s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s

This commit is contained in:
孙诚
2025-12-25 11:20:56 +08:00
commit 4526cc6396
104 changed files with 11070 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
namespace WebApi.Controllers;
/// <summary>
/// 账单导入控制器
/// </summary>
[ApiController]
[Route("api/[controller]/[action]")]
public class BillImportController(
ILogger<BillImportController> logger,
IImportService importService
) : ControllerBase
{
/// <summary>
/// 上传账单文件
/// </summary>
/// <param name="file">账单文件</param>
/// <param name="type">账单类型Alipay | WeChat</param>
/// <returns></returns>
[HttpPost]
public async Task<BaseResponse<object>> UploadFile(
[FromForm] IFormFile file,
[FromForm] string type
)
{
try
{
// 验证参数
if (file.Length == 0)
{
return BaseResponse<object>.Fail("请选择要上传的文件");
}
if (string.IsNullOrWhiteSpace(type) || (type != "Alipay" && type != "WeChat"))
{
return BaseResponse<object>.Fail("账单类型参数错误,必须是 Alipay 或 WeChat");
}
// 验证文件类型
var allowedExtensions = new[] { ".csv", ".xlsx", ".xls" };
var fileExtension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(fileExtension))
{
return BaseResponse<object>.Fail("只支持 CSV 或 Excel 文件格式");
}
// 验证文件大小10MB限制
const long maxFileSize = 10 * 1024 * 1024;
if (file.Length > maxFileSize)
{
return BaseResponse<object>.Fail("文件大小不能超过 10MB");
}
// 生成唯一文件名
var fileName = $"{type}_{DateTime.Now:yyyyMMddHHmmss}_{Guid.NewGuid():N}{fileExtension}";
// 保存文件
var ok = false;
var message = string.Empty;
await using (var stream = new MemoryStream())
{
await file.CopyToAsync(stream);
if (type == "Alipay")
{
(ok, message) = await importService.ImportAlipayAsync(stream, fileExtension);
}
else if (type == "WeChat")
{
(ok, message) = await importService.ImportWeChatAsync(stream, fileExtension);
}
}
return new BaseResponse<object>
{
Success = ok,
Message = message
};
}
catch (Exception ex)
{
logger.LogError(ex, "文件上传失败,类型: {Type}", type);
return BaseResponse<object>.Fail($"文件上传失败: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,41 @@
namespace WebApi.Controllers.Dto;
public class BaseResponse
{
/// <summary>
/// 是否成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 错误消息
/// </summary>
public string? Message { get; set; }
public static BaseResponse Fail(string message)
{
return new BaseResponse
{
Success = false,
Message = message
};
}
}
public class BaseResponse<T> : BaseResponse
{
/// <summary>
/// 返回数据
/// </summary>
public T? Data { get; set; }
public new static BaseResponse<T> Fail(string message)
{
return new BaseResponse<T>
{
Success = false,
Message = message
};
}
}

View File

@@ -0,0 +1,40 @@
namespace WebApi.Controllers.Dto;
/// <summary>
/// 邮件消息DTO包含额外的统计信息
/// </summary>
public class EmailMessageDto
{
public long Id { get; set; }
public string Subject { get; set; } = string.Empty;
public string From { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
public string HtmlBody { get; set; } = string.Empty;
public DateTime ReceivedDate { get; set; }
public DateTime CreateTime { get; set; }
public DateTime? UpdateTime { get; set; }
/// <summary>
/// 已解析的账单数量
/// </summary>
public int TransactionCount { get; set; }
/// <summary>
/// 从实体转换为DTO
/// </summary>
public static EmailMessageDto FromEntity(Entity.EmailMessage entity, int transactionCount = 0)
{
return new EmailMessageDto
{
Id = entity.Id,
Subject = entity.Subject,
From = entity.From,
Body = entity.Body,
HtmlBody = entity.HtmlBody,
ReceivedDate = entity.ReceivedDate,
CreateTime = entity.CreateTime,
UpdateTime = entity.UpdateTime,
TransactionCount = transactionCount
};
}
}

View File

@@ -0,0 +1,25 @@
namespace WebApi.Controllers.Dto;
public class PagedResponse<T> : BaseResponse<T[]>
{
public long LastId { get; set; } = 0;
/// <summary>
/// 最后一条记录的时间(用于游标分页)
/// </summary>
public DateTime? LastTime { get; set; }
/// <summary>
/// 总记录数
/// </summary>
public int Total { get; set; }
public new static PagedResponse<T> Fail(string message)
{
return new PagedResponse<T>
{
Success = false,
Message = message
};
}
}

View File

@@ -0,0 +1,161 @@
namespace WebApi.Controllers.EmailMessage;
[ApiController]
[Route("api/[controller]/[action]")]
public class EmailMessageController(
IEmailMessageRepository emailRepository,
ITransactionRecordRepository transactionRepository,
ILogger<EmailMessageController> logger,
IEmailHandleService emailHandleService,
IEmailBackgroundService emailBackgroundService
) : ControllerBase
{
/// <summary>
/// 获取邮件列表(分页)
/// </summary>
[HttpGet]
public async Task<PagedResponse<EmailMessageDto>> GetListAsync(
[FromQuery] DateTime? lastReceivedDate = null,
[FromQuery] long? lastId = null
)
{
try
{
var (list, lastTime, lastIdResult) = await emailRepository.GetPagedListAsync(lastReceivedDate, lastId);
var total = await emailRepository.GetTotalCountAsync();
// 为每个邮件获取账单数量
var emailDtos = new List<EmailMessageDto>();
foreach (var email in list)
{
var transactionCount = await transactionRepository.GetCountByEmailIdAsync(email.Id);
emailDtos.Add(EmailMessageDto.FromEntity(email, transactionCount));
}
return new PagedResponse<EmailMessageDto>
{
Success = true,
Data = emailDtos.ToArray(),
Total = (int)total,
LastId = lastIdResult,
LastTime = lastTime
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取邮件列表失败,时间: {LastTime}, ID: {LastId}", lastReceivedDate, lastId);
return PagedResponse<EmailMessageDto>.Fail($"获取邮件列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据ID获取邮件详情
/// </summary>
[HttpGet("{id}")]
public async Task<BaseResponse<EmailMessageDto>> GetByIdAsync(long id)
{
try
{
var email = await emailRepository.GetByIdAsync(id);
if (email == null)
{
return BaseResponse<EmailMessageDto>.Fail("邮件不存在");
}
// 获取账单数量
var transactionCount = await transactionRepository.GetCountByEmailIdAsync(id);
var emailDto = EmailMessageDto.FromEntity(email, transactionCount);
return new BaseResponse<EmailMessageDto>
{
Success = true,
Data = emailDto
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取邮件详情失败邮件ID: {EmailId}", id);
return BaseResponse<EmailMessageDto>.Fail($"获取邮件详情失败: {ex.Message}");
}
}
public async Task<BaseResponse> DeleteByIdAsync(long id)
{
try
{
var success = await emailRepository.DeleteAsync(id);
if (success)
{
return new BaseResponse
{
Success = true
};
}
else
{
return BaseResponse.Fail("删除邮件失败,邮件不存在");
}
}
catch (Exception ex)
{
logger.LogError(ex, "删除邮件失败邮件ID: {EmailId}", id);
return BaseResponse.Fail($"删除邮件失败: {ex.Message}");
}
}
/// <summary>
/// 重新分析邮件并刷新交易记录
/// </summary>
[HttpPost]
public async Task<BaseResponse> RefreshTransactionRecordsAsync([FromQuery] long id)
{
try
{
var email = await emailRepository.GetByIdAsync(id);
if (email == null)
{
return BaseResponse.Fail("邮件不存在");
}
var success = await emailHandleService.RefreshTransactionRecordsAsync(id);
if (success)
{
return new BaseResponse
{
Success = true
};
}
else
{
return BaseResponse.Fail("重新分析失败");
}
}
catch (Exception ex)
{
logger.LogError(ex, "重新分析邮件失败邮件ID: {EmailId}", id);
return BaseResponse.Fail($"重新分析失败: {ex.Message}");
}
}
/// <summary>
/// 立即同步邮件
/// </summary>
[HttpPost]
public async Task<BaseResponse> SyncEmailsAsync()
{
try
{
await emailBackgroundService.SyncEmailsAsync();
return new BaseResponse
{
Success = true,
Message = "同步成功"
};
}
catch (Exception ex)
{
logger.LogError(ex, "同步邮件失败");
return BaseResponse.Fail($"同步邮件失败: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,301 @@
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class TransactionCategoryController(
ITransactionCategoryRepository categoryRepository,
ILogger<TransactionCategoryController> logger
) : ControllerBase
{
/// <summary>
/// 获取分类树(支持按类型筛选)
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TransactionCategoryTreeDto>>> GetTreeAsync([FromQuery] TransactionType? type = null)
{
try
{
var tree = await categoryRepository.GetCategoryTreeAsync(type);
return new BaseResponse<List<TransactionCategoryTreeDto>>
{
Success = true,
Data = tree
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取分类树失败");
return BaseResponse<List<TransactionCategoryTreeDto>>.Fail($"获取分类树失败: {ex.Message}");
}
}
/// <summary>
/// 获取顶级分类列表(按类型)
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TransactionCategory>>> GetTopLevelAsync([FromQuery] TransactionType type)
{
try
{
var categories = await categoryRepository.GetTopLevelCategoriesByTypeAsync(type);
return new BaseResponse<List<TransactionCategory>>
{
Success = true,
Data = categories
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取顶级分类失败, Type: {Type}", type);
return BaseResponse<List<TransactionCategory>>.Fail($"获取顶级分类失败: {ex.Message}");
}
}
/// <summary>
/// 获取子分类列表
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TransactionCategory>>> GetChildrenAsync([FromQuery] long parentId)
{
try
{
var categories = await categoryRepository.GetChildCategoriesAsync(parentId);
return new BaseResponse<List<TransactionCategory>>
{
Success = true,
Data = categories
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取子分类失败, ParentId: {ParentId}", parentId);
return BaseResponse<List<TransactionCategory>>.Fail($"获取子分类失败: {ex.Message}");
}
}
/// <summary>
/// 根据ID获取分类详情
/// </summary>
[HttpGet("{id}")]
public async Task<BaseResponse<TransactionCategory>> GetByIdAsync(long id)
{
try
{
var category = await categoryRepository.GetByIdAsync(id);
if (category == null)
{
return BaseResponse<TransactionCategory>.Fail("分类不存在");
}
return new BaseResponse<TransactionCategory>
{
Success = true,
Data = category
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取分类详情失败, Id: {Id}", id);
return BaseResponse<TransactionCategory>.Fail($"获取分类详情失败: {ex.Message}");
}
}
/// <summary>
/// 创建分类
/// </summary>
[HttpPost]
public async Task<BaseResponse<long>> CreateAsync([FromBody] CreateCategoryDto dto)
{
try
{
// 检查同名分类
var existing = await categoryRepository.GetByNameAndParentAsync(dto.Name, dto.ParentId, dto.Type);
if (existing != null)
{
return BaseResponse<long>.Fail("同级已存在相同名称的分类");
}
var category = new TransactionCategory
{
Name = dto.Name,
ParentId = dto.ParentId,
Type = dto.Type,
Level = dto.Level,
SortOrder = dto.SortOrder,
Icon = dto.Icon,
Remark = dto.Remark
};
var result = await categoryRepository.AddAsync(category);
if (result)
{
return new BaseResponse<long>
{
Success = true,
Data = category.Id
};
}
else
{
return BaseResponse<long>.Fail("创建分类失败");
}
}
catch (Exception ex)
{
logger.LogError(ex, "创建分类失败, Dto: {@Dto}", dto);
return BaseResponse<long>.Fail($"创建分类失败: {ex.Message}");
}
}
/// <summary>
/// 更新分类
/// </summary>
[HttpPost]
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateCategoryDto dto)
{
try
{
var category = await categoryRepository.GetByIdAsync(dto.Id);
if (category == null)
{
return BaseResponse.Fail("分类不存在");
}
// 如果修改了名称,检查同名
if (category.Name != dto.Name)
{
var existing = await categoryRepository.GetByNameAndParentAsync(dto.Name, category.ParentId, category.Type);
if (existing != null && existing.Id != dto.Id)
{
return BaseResponse.Fail("同级已存在相同名称的分类");
}
}
category.Name = dto.Name;
category.SortOrder = dto.SortOrder;
category.Icon = dto.Icon;
category.IsEnabled = dto.IsEnabled;
category.Remark = dto.Remark;
category.UpdateTime = DateTime.Now;
var success = await categoryRepository.UpdateAsync(category);
if (success)
{
return new BaseResponse { Success = true };
}
else
{
return BaseResponse.Fail("更新分类失败");
}
}
catch (Exception ex)
{
logger.LogError(ex, "更新分类失败, Dto: {@Dto}", dto);
return BaseResponse.Fail($"更新分类失败: {ex.Message}");
}
}
/// <summary>
/// 删除分类
/// </summary>
[HttpPost]
public async Task<BaseResponse> DeleteAsync([FromQuery] long id)
{
try
{
// 检查是否有子分类
var children = await categoryRepository.GetChildCategoriesAsync(id);
if (children.Any())
{
return BaseResponse.Fail("该分类下存在子分类,无法删除");
}
// 检查是否被使用
var inUse = await categoryRepository.IsCategoryInUseAsync(id);
if (inUse)
{
return BaseResponse.Fail("该分类已被使用,无法删除");
}
var success = await categoryRepository.DeleteAsync(id);
if (success)
{
return new BaseResponse { Success = true };
}
else
{
return BaseResponse.Fail("删除分类失败,分类不存在");
}
}
catch (Exception ex)
{
logger.LogError(ex, "删除分类失败, Id: {Id}", id);
return BaseResponse.Fail($"删除分类失败: {ex.Message}");
}
}
/// <summary>
/// 批量创建分类(用于初始化)
/// </summary>
[HttpPost]
public async Task<BaseResponse<int>> BatchCreateAsync([FromBody] List<CreateCategoryDto> dtoList)
{
try
{
var categories = dtoList.Select(dto => new TransactionCategory
{
Name = dto.Name,
ParentId = dto.ParentId,
Type = dto.Type,
Level = dto.Level,
SortOrder = dto.SortOrder,
Icon = dto.Icon,
Remark = dto.Remark
}).ToList();
var result = await categoryRepository.AddRangeAsync(categories);
if (result)
{
return new BaseResponse<int>
{
Success = true,
Data = categories.Count
};
}
else
{
return BaseResponse<int>.Fail("批量创建分类失败");
}
}
catch (Exception ex)
{
logger.LogError(ex, "批量创建分类失败, Count: {Count}", dtoList.Count);
return BaseResponse<int>.Fail($"批量创建分类失败: {ex.Message}");
}
}
}
/// <summary>
/// 创建分类DTO
/// </summary>
public record CreateCategoryDto(
string Name,
long ParentId,
TransactionType Type,
int Level,
int SortOrder = 0,
string? Icon = null,
string? Remark = null
);
/// <summary>
/// 更新分类DTO
/// </summary>
public record UpdateCategoryDto(
long Id,
string Name,
int SortOrder,
string? Icon,
bool IsEnabled,
string? Remark
);

View File

@@ -0,0 +1,302 @@
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class TransactionRecordController(
ITransactionRecordRepository transactionRepository,
ILogger<TransactionRecordController> logger
) : ControllerBase
{
/// <summary>
/// 获取交易记录列表(分页)
/// </summary>
[HttpGet]
public async Task<PagedResponse<Entity.TransactionRecord>> GetListAsync(
[FromQuery] DateTime? lastOccurredAt = null,
[FromQuery] long? lastId = null,
[FromQuery] string? searchKeyword = null
)
{
try
{
var (list, lastTime, lastIdResult) = await transactionRepository.GetPagedListAsync(lastOccurredAt, lastId, 20, searchKeyword);
var total = await transactionRepository.GetTotalCountAsync();
return new PagedResponse<Entity.TransactionRecord>
{
Success = true,
Data = list.ToArray(),
Total = (int)total,
LastId = lastIdResult,
LastTime = lastTime
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取交易记录列表失败,时间: {LastTime}, ID: {LastId}", lastOccurredAt, lastId);
return PagedResponse<Entity.TransactionRecord>.Fail($"获取交易记录列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据ID获取交易记录详情
/// </summary>
[HttpGet("{id}")]
public async Task<BaseResponse<Entity.TransactionRecord>> GetByIdAsync(long id)
{
try
{
var transaction = await transactionRepository.GetByIdAsync(id);
if (transaction == null)
{
return BaseResponse<Entity.TransactionRecord>.Fail("交易记录不存在");
}
return new BaseResponse<Entity.TransactionRecord>
{
Success = true,
Data = transaction
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取交易记录详情失败交易ID: {TransactionId}", id);
return BaseResponse<Entity.TransactionRecord>.Fail($"获取交易记录详情失败: {ex.Message}");
}
}
/// <summary>
/// 根据邮件ID获取交易记录列表
/// </summary>
[HttpGet("{emailId}")]
public async Task<BaseResponse<List<Entity.TransactionRecord>>> GetByEmailIdAsync(long emailId)
{
try
{
var transactions = await transactionRepository.GetByEmailIdAsync(emailId);
return new BaseResponse<List<Entity.TransactionRecord>>
{
Success = true,
Data = transactions
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取邮件交易记录失败邮件ID: {EmailId}", emailId);
return BaseResponse<List<Entity.TransactionRecord>>.Fail($"获取邮件交易记录失败: {ex.Message}");
}
}
/// <summary>
/// 创建交易记录
/// </summary>
[HttpPost]
public async Task<BaseResponse> CreateAsync([FromBody] CreateTransactionDto dto)
{
try
{
// 解析日期字符串
if (!DateTime.TryParse(dto.OccurredAt, out var occurredAt))
{
return BaseResponse.Fail("交易时间格式不正确");
}
var transaction = new Entity.TransactionRecord
{
OccurredAt = occurredAt,
Reason = dto.Reason ?? string.Empty,
Amount = dto.Amount,
Type = dto.Type,
Classify = dto.Classify ?? string.Empty,
SubClassify = dto.SubClassify ?? string.Empty,
ImportFrom = "手动录入",
EmailMessageId = 0 // 手动录入的记录EmailMessageId 设为 0
};
var result = await transactionRepository.AddAsync(transaction);
if (result)
{
return new BaseResponse
{
Success = true
};
}
else
{
return BaseResponse.Fail("创建交易记录失败");
}
}
catch (Exception ex)
{
logger.LogError(ex, "创建交易记录失败,交易信息: {@TransactionDto}", dto);
return BaseResponse.Fail($"创建交易记录失败: {ex.Message}");
}
}
/// <summary>
/// 更新交易记录
/// </summary>
[HttpPost]
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateTransactionDto dto)
{
try
{
var transaction = await transactionRepository.GetByIdAsync(dto.Id);
if (transaction == null)
{
return BaseResponse.Fail("交易记录不存在");
}
// 更新可编辑字段
transaction.Reason = dto.Reason ?? string.Empty;
transaction.Amount = dto.Amount;
transaction.Balance = dto.Balance;
transaction.Type = dto.Type;
transaction.Classify = dto.Classify ?? string.Empty;
transaction.SubClassify = dto.SubClassify ?? string.Empty;
var success = await transactionRepository.UpdateAsync(transaction);
if (success)
{
return new BaseResponse
{
Success = true
};
}
else
{
return BaseResponse.Fail("更新交易记录失败");
}
}
catch (Exception ex)
{
logger.LogError(ex, "更新交易记录失败交易ID: {TransactionId}, 交易信息: {@TransactionDto}", dto.Id, dto);
return BaseResponse.Fail($"更新交易记录失败: {ex.Message}");
}
}
/// <summary>
/// 删除交易记录
/// </summary>
[HttpPost]
public async Task<BaseResponse> DeleteByIdAsync([FromQuery] long id)
{
try
{
var success = await transactionRepository.DeleteAsync(id);
if (success)
{
return new BaseResponse
{
Success = true
};
}
else
{
return BaseResponse.Fail("删除交易记录失败,记录不存在");
}
}
catch (Exception ex)
{
logger.LogError(ex, "删除交易记录失败交易ID: {TransactionId}", id);
return BaseResponse.Fail($"删除交易记录失败: {ex.Message}");
}
}
/// <summary>
/// 获取指定月份每天的消费统计
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<DailyStatisticsDto>>> GetDailyStatisticsAsync(
[FromQuery] int year,
[FromQuery] int month
)
{
try
{
var statistics = await transactionRepository.GetDailyStatisticsAsync(year, month);
var result = statistics.Select(s => new DailyStatisticsDto(s.Key, s.Value.count, s.Value.amount)).ToList();
return new BaseResponse<List<DailyStatisticsDto>>
{
Success = true,
Data = result
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取日历统计数据失败,年份: {Year}, 月份: {Month}", year, month);
return BaseResponse<List<DailyStatisticsDto>>.Fail($"获取日历统计数据失败: {ex.Message}");
}
}
/// <summary>
/// 获取指定日期的交易记录
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<Entity.TransactionRecord>>> GetByDateAsync(
[FromQuery] string date
)
{
try
{
if (!DateTime.TryParse(date, out var targetDate))
{
return BaseResponse<List<Entity.TransactionRecord>>.Fail("日期格式不正确");
}
// 获取当天的开始和结束时间
var startDate = targetDate.Date;
var endDate = startDate.AddDays(1);
var records = await transactionRepository.GetByDateRangeAsync(startDate, endDate);
return new BaseResponse<List<Entity.TransactionRecord>>
{
Success = true,
Data = records
};
}
catch (Exception ex)
{
logger.LogError(ex, "获取指定日期的交易记录失败,日期: {Date}", date);
return BaseResponse<List<Entity.TransactionRecord>>.Fail($"获取指定日期的交易记录失败: {ex.Message}");
}
}
}
/// <summary>
/// 创建交易记录DTO
/// </summary>
public record CreateTransactionDto(
string OccurredAt,
string? Reason,
decimal Amount,
TransactionType Type,
string? Classify,
string? SubClassify
);
/// <summary>
/// 更新交易记录DTO
/// </summary>
public record UpdateTransactionDto(
long Id,
string? Reason,
decimal Amount,
decimal Balance,
TransactionType Type,
string? Classify,
string? SubClassify
);
/// <summary>
/// 日历统计响应DTO
/// </summary>
public record DailyStatisticsDto(
string Date,
int Count,
decimal Amount
);

6
WebApi/GlobalUsings.cs Normal file
View File

@@ -0,0 +1,6 @@
global using Service;
global using Common;
global using Microsoft.AspNetCore.Mvc;
global using WebApi.Controllers.Dto;
global using Repository;
global using Entity;

112
WebApi/Program.cs Normal file
View File

@@ -0,0 +1,112 @@
using FreeSql;
using Scalar.AspNetCore;
using Serilog;
using Service.AppSettingModel;
using Yitter.IdGenerator;
// 初始化雪花算法ID生成器
var options = new IdGeneratorOptions(1); // WorkerId 为 1可根据实际部署情况调整
YitIdHelper.SetIdGenerator(options);
var builder = WebApplication.CreateBuilder(args);
// 配置 Serilog
builder.Host.UseSerilog((context, loggerConfig) =>
{
loggerConfig.ReadFrom.Configuration(context.Configuration);
});
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.Services.AddHttpClient();
// 配置 CORS
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// 绑定配置
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));
builder.Services.Configure<AISettings>(builder.Configuration.GetSection("OpenAI"));
// 配置 FreeSql + SQLite
var dbPath = Path.Combine(AppContext.BaseDirectory, "database");
if (!Directory.Exists(dbPath))
{
Directory.CreateDirectory(dbPath);
}
// 使用绝对路径作为数据库文件路径
var dbFilePath = Path.Combine(dbPath, "EmailBill.db");
var connectionString = $"Data Source={dbFilePath}";
Log.Information("数据库路径: {DbPath}", dbFilePath);
var fsql = new FreeSqlBuilder()
.UseConnectionString(DataType.Sqlite, connectionString)
.UseAutoSyncStructure(true)
.UseLazyLoading(true)
.UseMonitorCommand(
cmd =>
{
Log.Information("执行SQL: {Sql}", cmd.CommandText);
}
)
.Build();
builder.Services.AddSingleton(fsql);
// 自动扫描注册服务和仓储
builder.Services.AddServices();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
// 启用静态文件服务
app.UseDefaultFiles();
app.UseStaticFiles();
// 启用 CORS
app.UseCors();
app.MapControllers();
// 添加 SPA 回退路由(用于前端路由)
app.MapFallbackToFile("index.html");
// 启动后台邮件抓取服务(必须只注册一次)
app.Lifetime.ApplicationStarted.Register(() =>
{
try
{
if (app.Services.GetRequiredService<IEmailBackgroundService>() is not EmailBackgroundService emailService)
{
return;
}
// 检查是否已在运行,避免重复启动
if (!emailService.IsBusy)
{
emailService.RunWorkerAsync();
}
}
catch (Exception ex)
{
var logger = app.Services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "启动后台服务失败");
}
});
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5071",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7275;http://localhost:5071",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

26
WebApi/WebApi.csproj Normal file
View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="Scalar.AspNetCore" />
<PackageReference Include="FreeSql.Provider.Sqlite" />
<PackageReference Include="Serilog.AspNetCore" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Service\Service.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\Repository\Repository.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="wwwroot\**\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Watch Remove="logs/**" />
</ItemGroup>
</Project>

6
WebApi/WebApi.http Normal file
View File

@@ -0,0 +1,6 @@
@WebApi_HostAddress = http://localhost:5071
GET {{WebApi_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,28 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Information",
"Microsoft.EntityFrameworkCore": "Information"
}
},
"WriteTo": [
{
"Name": "Console"
}
],
"Enrich": ["FromLogContext"]
},
"OpenAI": {
"Endpoint": "https://api.deepseek.com/v1",
"Key": "sk-2240d91e2ab1475881147e3810b343d3",
"Model": "deepseek-chat"
}
}

52
WebApi/appsettings.json Normal file
View File

@@ -0,0 +1,52 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Data Source=database/EmailBill.db"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"WriteTo": [
{
"Name": "Console"
},
{
"Name": "File",
"Args": {
"path": "logs/log-.txt",
"rollingInterval": "Day",
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
],
"Enrich": ["FromLogContext"]
},
"EmailSettings": {
"CheckIntervalMinutes": 1,
"SmtpList": [
{
"Name": "SC",
"Email": "779343834@qq.com",
"Password": "zxjkkkilfdgvbfjf",
"ImapHost": "imap.qq.com",
"ImapPort": 993,
"UseSsl": true
}
],
"FilterFromAddresses": [
"95555@message.cmbchina.com",
"ccsvc@message.cmbchina.com"
]
}
}