新增定时账单功能
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 30s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s

This commit is contained in:
孙诚
2025-12-29 15:20:32 +08:00
parent 13bf23a48c
commit 9719c6043a
19 changed files with 2409 additions and 27 deletions

View 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 });
}
}
}

View 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; }
}