新增定时账单功能
This commit is contained in:
175
WebApi/Controllers/JobController.cs
Normal file
175
WebApi/Controllers/JobController.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Quartz;
|
||||
|
||||
namespace WebApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 定时任务管理控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class JobController(
|
||||
ISchedulerFactory schedulerFactory,
|
||||
ILogger<JobController> logger) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 手动触发邮件同步任务
|
||||
/// </summary>
|
||||
[HttpPost("sync-email")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> TriggerEmailSync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var scheduler = await schedulerFactory.GetScheduler();
|
||||
var jobKey = new JobKey("EmailSyncJob");
|
||||
|
||||
// 立即触发任务
|
||||
await scheduler.TriggerJob(jobKey);
|
||||
|
||||
logger.LogInformation("手动触发邮件同步任务成功");
|
||||
return Ok(new { message = "邮件同步任务已触发" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "触发邮件同步任务失败");
|
||||
return StatusCode(500, new { message = "触发任务失败", error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动触发周期性账单任务
|
||||
/// </summary>
|
||||
[HttpPost("periodic-bill")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> TriggerPeriodicBill()
|
||||
{
|
||||
try
|
||||
{
|
||||
var scheduler = await schedulerFactory.GetScheduler();
|
||||
var jobKey = new JobKey("PeriodicBillJob");
|
||||
|
||||
// 立即触发任务
|
||||
await scheduler.TriggerJob(jobKey);
|
||||
|
||||
logger.LogInformation("手动触发周期性账单任务成功");
|
||||
return Ok(new { message = "周期性账单任务已触发" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "触发周期性账单任务失败");
|
||||
return StatusCode(500, new { message = "触发任务失败", error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有任务的状态
|
||||
/// </summary>
|
||||
[HttpGet("status")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GetJobStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
var scheduler = await schedulerFactory.GetScheduler();
|
||||
var jobGroups = await scheduler.GetJobGroupNames();
|
||||
var jobStatuses = new List<object>();
|
||||
|
||||
foreach (var group in jobGroups)
|
||||
{
|
||||
var jobKeys = await scheduler.GetJobKeys(Quartz.Impl.Matchers.GroupMatcher<JobKey>.GroupEquals(group));
|
||||
|
||||
foreach (var jobKey in jobKeys)
|
||||
{
|
||||
var triggers = await scheduler.GetTriggersOfJob(jobKey);
|
||||
var jobDetail = await scheduler.GetJobDetail(jobKey);
|
||||
|
||||
foreach (var trigger in triggers)
|
||||
{
|
||||
var triggerState = await scheduler.GetTriggerState(trigger.Key);
|
||||
var nextFireTime = trigger.GetNextFireTimeUtc();
|
||||
var previousFireTime = trigger.GetPreviousFireTimeUtc();
|
||||
|
||||
jobStatuses.Add(new
|
||||
{
|
||||
jobName = jobKey.Name,
|
||||
jobGroup = jobKey.Group,
|
||||
triggerName = trigger.Key.Name,
|
||||
triggerState = triggerState.ToString(),
|
||||
nextFireTime = nextFireTime?.LocalDateTime,
|
||||
previousFireTime = previousFireTime?.LocalDateTime,
|
||||
description = trigger.Description,
|
||||
jobType = jobDetail?.JobType.Name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(jobStatuses);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "获取任务状态失败");
|
||||
return StatusCode(500, new { message = "获取任务状态失败", error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停指定任务
|
||||
/// </summary>
|
||||
[HttpPost("pause/{jobName}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> PauseJob(string jobName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var scheduler = await schedulerFactory.GetScheduler();
|
||||
var jobKey = new JobKey(jobName);
|
||||
|
||||
if (!await scheduler.CheckExists(jobKey))
|
||||
{
|
||||
return NotFound(new { message = $"任务 {jobName} 不存在" });
|
||||
}
|
||||
|
||||
await scheduler.PauseJob(jobKey);
|
||||
logger.LogInformation("任务 {JobName} 已暂停", jobName);
|
||||
|
||||
return Ok(new { message = $"任务 {jobName} 已暂停" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "暂停任务 {JobName} 失败", jobName);
|
||||
return StatusCode(500, new { message = "暂停任务失败", error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复指定任务
|
||||
/// </summary>
|
||||
[HttpPost("resume/{jobName}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> ResumeJob(string jobName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var scheduler = await schedulerFactory.GetScheduler();
|
||||
var jobKey = new JobKey(jobName);
|
||||
|
||||
if (!await scheduler.CheckExists(jobKey))
|
||||
{
|
||||
return NotFound(new { message = $"任务 {jobName} 不存在" });
|
||||
}
|
||||
|
||||
await scheduler.ResumeJob(jobKey);
|
||||
logger.LogInformation("任务 {JobName} 已恢复", jobName);
|
||||
|
||||
return Ok(new { message = $"任务 {jobName} 已恢复" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "恢复任务 {JobName} 失败", jobName);
|
||||
return StatusCode(500, new { message = "恢复任务失败", error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
249
WebApi/Controllers/TransactionPeriodicController.cs
Normal file
249
WebApi/Controllers/TransactionPeriodicController.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
namespace WebApi.Controllers;
|
||||
|
||||
using Repository;
|
||||
|
||||
/// <summary>
|
||||
/// 周期性账单控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]/[action]")]
|
||||
public class TransactionPeriodicController(
|
||||
ITransactionPeriodicRepository periodicRepository,
|
||||
ITransactionPeriodicService periodicService,
|
||||
ILogger<TransactionPeriodicController> logger
|
||||
) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取周期性账单列表(分页)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<PagedResponse<TransactionPeriodic>> GetListAsync(
|
||||
[FromQuery] int pageIndex = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? searchKeyword = null
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await periodicRepository.GetPagedListAsync(pageIndex, pageSize, searchKeyword);
|
||||
var total = await periodicRepository.GetTotalCountAsync(searchKeyword);
|
||||
|
||||
return new PagedResponse<TransactionPeriodic>
|
||||
{
|
||||
Success = true,
|
||||
Data = list.ToArray(),
|
||||
Total = (int)total
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "获取周期性账单列表失败");
|
||||
return PagedResponse<TransactionPeriodic>.Fail($"获取列表失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取周期性账单详情
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<BaseResponse<TransactionPeriodic>> GetByIdAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var periodic = await periodicRepository.GetByIdAsync(id);
|
||||
if (periodic == null)
|
||||
{
|
||||
return BaseResponse<TransactionPeriodic>.Fail("周期性账单不存在");
|
||||
}
|
||||
|
||||
return new BaseResponse<TransactionPeriodic>
|
||||
{
|
||||
Success = true,
|
||||
Data = periodic
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "获取周期性账单详情失败,ID: {Id}", id);
|
||||
return BaseResponse<TransactionPeriodic>.Fail($"获取详情失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建周期性账单
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse<TransactionPeriodic>> CreateAsync([FromBody] CreatePeriodicRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var periodic = new TransactionPeriodic
|
||||
{
|
||||
PeriodicType = request.PeriodicType,
|
||||
PeriodicConfig = request.PeriodicConfig ?? string.Empty,
|
||||
Amount = request.Amount,
|
||||
Type = request.Type,
|
||||
Classify = request.Classify ?? string.Empty,
|
||||
Reason = request.Reason ?? string.Empty,
|
||||
IsEnabled = true
|
||||
};
|
||||
|
||||
// 计算下次执行时间
|
||||
periodic.NextExecuteTime = periodicService.CalculateNextExecuteTime(periodic, DateTime.Now);
|
||||
|
||||
var success = await periodicRepository.AddAsync(periodic);
|
||||
if (!success)
|
||||
{
|
||||
return BaseResponse<TransactionPeriodic>.Fail("创建周期性账单失败");
|
||||
}
|
||||
|
||||
return new BaseResponse<TransactionPeriodic>
|
||||
{
|
||||
Success = true,
|
||||
Data = periodic,
|
||||
Message = "创建成功"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "创建周期性账单失败");
|
||||
return BaseResponse<TransactionPeriodic>.Fail($"创建失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新周期性账单
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse<object>> UpdateAsync([FromBody] UpdatePeriodicRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var periodic = await periodicRepository.GetByIdAsync(request.Id);
|
||||
if (periodic == null)
|
||||
{
|
||||
return BaseResponse<object>.Fail("周期性账单不存在");
|
||||
}
|
||||
|
||||
periodic.PeriodicType = request.PeriodicType;
|
||||
periodic.PeriodicConfig = request.PeriodicConfig ?? string.Empty;
|
||||
periodic.Amount = request.Amount;
|
||||
periodic.Type = request.Type;
|
||||
periodic.Classify = request.Classify ?? string.Empty;
|
||||
periodic.Reason = request.Reason ?? string.Empty;
|
||||
periodic.IsEnabled = request.IsEnabled;
|
||||
periodic.UpdateTime = DateTime.Now;
|
||||
|
||||
// 重新计算下次执行时间
|
||||
periodic.NextExecuteTime = periodicService.CalculateNextExecuteTime(periodic, DateTime.Now);
|
||||
|
||||
var success = await periodicRepository.UpdateAsync(periodic);
|
||||
if (!success)
|
||||
{
|
||||
return BaseResponse<object>.Fail("更新周期性账单失败");
|
||||
}
|
||||
|
||||
return new BaseResponse<object>
|
||||
{
|
||||
Success = true,
|
||||
Message = "更新成功"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "更新周期性账单失败,ID: {Id}", request.Id);
|
||||
return BaseResponse<object>.Fail($"更新失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除周期性账单
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse<object>> DeleteByIdAsync([FromQuery] long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = await periodicRepository.DeleteAsync(id);
|
||||
if (!success)
|
||||
{
|
||||
return BaseResponse<object>.Fail("删除周期性账单失败");
|
||||
}
|
||||
|
||||
return new BaseResponse<object>
|
||||
{
|
||||
Success = true,
|
||||
Message = "删除成功"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "删除周期性账单失败,ID: {Id}", id);
|
||||
return BaseResponse<object>.Fail($"删除失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用/禁用周期性账单
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse<object>> ToggleEnabledAsync([FromQuery] long id, [FromQuery] bool enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
var periodic = await periodicRepository.GetByIdAsync(id);
|
||||
if (periodic == null)
|
||||
{
|
||||
return BaseResponse<object>.Fail("周期性账单不存在");
|
||||
}
|
||||
|
||||
periodic.IsEnabled = enabled;
|
||||
periodic.UpdateTime = DateTime.Now;
|
||||
|
||||
var success = await periodicRepository.UpdateAsync(periodic);
|
||||
if (!success)
|
||||
{
|
||||
return BaseResponse<object>.Fail("操作失败");
|
||||
}
|
||||
|
||||
return new BaseResponse<object>
|
||||
{
|
||||
Success = true,
|
||||
Message = enabled ? "已启用" : "已禁用"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "启用/禁用周期性账单失败,ID: {Id}", id);
|
||||
return BaseResponse<object>.Fail($"操作失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建周期性账单请求
|
||||
/// </summary>
|
||||
public class CreatePeriodicRequest
|
||||
{
|
||||
public PeriodicType PeriodicType { get; set; }
|
||||
public string? PeriodicConfig { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public TransactionType Type { get; set; }
|
||||
public string? Classify { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新周期性账单请求
|
||||
/// </summary>
|
||||
public class UpdatePeriodicRequest
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public PeriodicType PeriodicType { get; set; }
|
||||
public string? PeriodicConfig { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
public TransactionType Type { get; set; }
|
||||
public string? Classify { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
public bool IsEnabled { get; set; }
|
||||
}
|
||||
40
WebApi/Expand.cs
Normal file
40
WebApi/Expand.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Quartz;
|
||||
|
||||
namespace WebApi;
|
||||
|
||||
public static class Expand
|
||||
{
|
||||
public static void AddScheduler(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddQuartz(q =>
|
||||
{
|
||||
// 配置调度器
|
||||
q.SchedulerId = "EmailBillScheduler";
|
||||
|
||||
// 配置邮件同步任务 - 每10分钟执行一次
|
||||
var emailJobKey = new JobKey("EmailSyncJob");
|
||||
q.AddJob<Service.Jobs.EmailSyncJob>(opts => opts.WithIdentity(emailJobKey));
|
||||
q.AddTrigger(opts => opts
|
||||
.ForJob(emailJobKey)
|
||||
.WithIdentity("EmailSyncTrigger")
|
||||
.WithCronSchedule("0 0/20 * * * ?") // 每20分钟执行
|
||||
.WithDescription("每20分钟同步一次邮件"));
|
||||
|
||||
// 配置周期性账单任务 - 每天早上6点执行
|
||||
var periodicBillJobKey = new JobKey("PeriodicBillJob");
|
||||
q.AddJob<Service.Jobs.PeriodicBillJob>(opts => opts.WithIdentity(periodicBillJobKey));
|
||||
q.AddTrigger(opts => opts
|
||||
.ForJob(periodicBillJobKey)
|
||||
.WithIdentity("PeriodicBillTrigger")
|
||||
.WithCronSchedule("0 0 6 * * ?") // 每天早上6点执行
|
||||
.WithDescription("每天早上6点执行周期性账单检查"));
|
||||
});
|
||||
|
||||
// 添加 Quartz Hosted Service
|
||||
builder.Services.AddQuartzHostedService(options =>
|
||||
{
|
||||
// 等待任务完成后再关闭
|
||||
options.WaitForJobsToComplete = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Text;
|
||||
using FreeSql;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Scalar.AspNetCore;
|
||||
using Serilog;
|
||||
using Service.AppSettingModel;
|
||||
using WebApi;
|
||||
using Yitter.IdGenerator;
|
||||
|
||||
// 初始化雪花算法ID生成器
|
||||
@@ -97,6 +97,9 @@ builder.Services.AddSingleton(fsql);
|
||||
// 自动扫描注册服务和仓储
|
||||
builder.Services.AddServices();
|
||||
|
||||
// 配置 Quartz.NET 定时任务
|
||||
builder.AddScheduler();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
@@ -122,27 +125,4 @@ 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();
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
"AuthSettings": {
|
||||
"Password": "SCsunch940622"
|
||||
},
|
||||
"Quartz": {
|
||||
"quartz.scheduler.instanceName": "EmailBillScheduler",
|
||||
"quartz.jobStore.type": "Quartz.Simpl.RAMJobStore, Quartz",
|
||||
"quartz.threadPool.threadCount": 10
|
||||
},
|
||||
"OpenAI": {
|
||||
"Endpoint": "https://api.deepseek.com/v1",
|
||||
"Key": "sk-2240d91e2ab1475881147e3810b343d3",
|
||||
|
||||
Reference in New Issue
Block a user