diff --git a/QYZH.InteractiveMagazine.IService/IOperationLogService.cs b/QYZH.InteractiveMagazine.IService/IOperationLogService.cs index 06ba847..9f98eb2 100644 --- a/QYZH.InteractiveMagazine.IService/IOperationLogService.cs +++ b/QYZH.InteractiveMagazine.IService/IOperationLogService.cs @@ -21,6 +21,12 @@ public interface IOperationLogService : IBaseService /// IP地址(可选) Task LogAsync(long operatorId, string operatorName, string actionType, string targetType, long targetId, string? targetName = null, string? detail = null, string? ipAddress = null); + /// + /// 记录操作日志 + /// + /// 操作日志记录输入 + Task LogAsync(OperationLogRecordInput input); + /// /// 分页查询操作日志 /// diff --git a/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs b/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs index c0e8c3b..9ad59c6 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs @@ -33,6 +33,7 @@ public static class DependencyInjectionExtensions services.AddTransient(); services.AddTransient(); + services.AddScoped(); } private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null) diff --git a/QYZH.InteractiveMagazine.Infrastructure/Extensions/InteractiveMagazineApiDefaultsExtensions.cs b/QYZH.InteractiveMagazine.Infrastructure/Extensions/InteractiveMagazineApiDefaultsExtensions.cs index 5b9dfe5..9d0521b 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/Extensions/InteractiveMagazineApiDefaultsExtensions.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/Extensions/InteractiveMagazineApiDefaultsExtensions.cs @@ -37,6 +37,7 @@ public static class InteractiveMagazineApiDefaultsExtensions { options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true; options.Filters.Add(); + options.Filters.AddService(); }) .AddJsonOptions(options => { diff --git a/QYZH.InteractiveMagazine.Infrastructure/Middleware/OperationLogActionFilter.cs b/QYZH.InteractiveMagazine.Infrastructure/Middleware/OperationLogActionFilter.cs new file mode 100644 index 0000000..73b6e40 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/Middleware/OperationLogActionFilter.cs @@ -0,0 +1,397 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Dto; +using System.Collections; +using System.Diagnostics; +using System.Reflection; +using System.Security.Claims; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace QYZH.InteractiveMagazine.Infrastructure.Middleware; + +/// +/// 操作日志过滤器 +/// +public class OperationLogActionFilter( + IOperationLogService operationLogService, + ILogger logger) : IAsyncActionFilter +{ + private static readonly HashSet SensitiveNames = new(StringComparer.OrdinalIgnoreCase) + { + "password", + "oldPassword", + "newPassword", + "token", + "secret", + "authorization" + }; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReferenceHandler = ReferenceHandler.IgnoreCycles + }; + + /// + /// 执行操作日志过滤器 + /// + /// Action 执行上下文 + /// 后续执行委托 + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var attribute = context.ActionDescriptor.EndpointMetadata + .OfType() + .FirstOrDefault(); + + if (attribute == null) + { + await next(); + return; + } + + var stopwatch = Stopwatch.StartNew(); + var executedContext = await next(); + stopwatch.Stop(); + + if (executedContext.Exception != null && !executedContext.ExceptionHandled) + { + return; + } + + if (!IsSuccessResult(executedContext.Result, context.HttpContext.Response.StatusCode)) + { + return; + } + + try + { + var operatorId = GetOperatorId(context); + if (!operatorId.HasValue) + { + return; + } + + var operatorName = GetOperatorName(context); + var responseValue = GetResponseValue(executedContext.Result); + var responseResult = GetResponseResult(responseValue); + var targetId = ResolveTargetId(attribute, context, responseResult, operatorId.Value); + var targetName = ResolveTargetName(attribute, context, responseResult); + + var detail = BuildDetail(attribute, context, responseValue, stopwatch.ElapsedMilliseconds); + + await operationLogService.LogAsync(new OperationLogRecordInput + { + OperatorId = operatorId.Value, + OperatorName = operatorName, + ActionType = attribute.ActionType, + TargetType = attribute.TargetType, + TargetId = targetId, + TargetName = targetName, + Detail = detail, + IpAddress = GetClientIp(context) + }); + } + catch (Exception ex) + { + logger.LogError(ex, "自动记录操作日志失败,Path: {Path}", context.HttpContext.Request.Path); + } + } + + private static bool IsSuccessResult(IActionResult? result, int responseStatusCode) + { + var responseValue = GetResponseValue(result); + if (responseValue is BaseResponse response) + { + return response.isSuccess; + } + + if (result is ObjectResult objectResult && objectResult.StatusCode.HasValue) + { + return IsSuccessStatusCode(objectResult.StatusCode.Value); + } + + if (result is StatusCodeResult statusCodeResult) + { + return IsSuccessStatusCode(statusCodeResult.StatusCode); + } + + return IsSuccessStatusCode(responseStatusCode == 0 ? StatusCodes.Status200OK : responseStatusCode); + } + + private static bool IsSuccessStatusCode(int statusCode) + { + return statusCode >= StatusCodes.Status200OK && statusCode < StatusCodes.Status300MultipleChoices; + } + + private static long? GetOperatorId(ActionExecutingContext context) + { + var value = context.HttpContext.User.Claims + .FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier) + ?.Value; + + return long.TryParse(value, out var operatorId) ? operatorId : null; + } + + private static string GetOperatorName(ActionExecutingContext context) + { + return context.HttpContext.User.Claims + .FirstOrDefault(c => c.Type == ClaimTypes.Name) + ?.Value ?? string.Empty; + } + + private static string? GetClientIp(ActionExecutingContext context) + { + var request = context.HttpContext.Request; + var forwardedFor = request.Headers["X-Forwarded-For"].FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(forwardedFor)) + { + return forwardedFor.Split(',')[0].Trim(); + } + + var realIp = request.Headers["X-Real-IP"].FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(realIp)) + { + return realIp; + } + + return context.HttpContext.Connection.RemoteIpAddress?.ToString(); + } + + private static long ResolveTargetId(OperationLogAttribute attribute, ActionExecutingContext context, object? responseResult, long operatorId) + { + if (attribute.UseOperatorAsTargetId) + { + return operatorId; + } + + if (TryGetLongRouteValue(context, attribute.TargetIdRouteKey, out var routeId)) + { + return routeId; + } + + if (TryGetLongArgumentValue(context, attribute.TargetIdArgumentName, out var argumentId)) + { + return argumentId; + } + + foreach (var key in new[] { "id", "userId", "adminUserId", "roleId", "taskId", "recordId" }) + { + if (TryGetLongRouteValue(context, key, out routeId) || TryGetLongArgumentValue(context, key, out argumentId)) + { + return routeId != 0 ? routeId : argumentId; + } + } + + if (TryConvertToLong(responseResult, out var responseId)) + { + return responseId; + } + + return TryGetLongPropertyValue(responseResult, "Id", out responseId) ? responseId : 0; + } + + private static string? ResolveTargetName(OperationLogAttribute attribute, ActionExecutingContext context, object? responseResult) + { + if (!string.IsNullOrWhiteSpace(attribute.TargetNameArgumentName) + && context.ActionArguments.TryGetValue(attribute.TargetNameArgumentName, out var nameArgument)) + { + if (nameArgument is string name) + { + return name; + } + + if (!string.IsNullOrWhiteSpace(attribute.TargetNameProperty) + && TryGetStringPropertyValue(nameArgument, attribute.TargetNameProperty, out name)) + { + return name; + } + } + + foreach (var value in context.ActionArguments.Values) + { + if (!string.IsNullOrWhiteSpace(attribute.TargetNameProperty) + && TryGetStringPropertyValue(value, attribute.TargetNameProperty, out var configuredName)) + { + return configuredName; + } + + if (TryGetStringPropertyValue(value, "Name", out var name) + || TryGetStringPropertyValue(value, "Title", out name)) + { + return name; + } + } + + if (!string.IsNullOrWhiteSpace(attribute.TargetNameProperty) + && TryGetStringPropertyValue(responseResult, attribute.TargetNameProperty, out var responseName)) + { + return responseName; + } + + return TryGetStringPropertyValue(responseResult, "Name", out var defaultName) + || TryGetStringPropertyValue(responseResult, "Title", out defaultName) + ? defaultName + : null; + } + + private static string BuildDetail(OperationLogAttribute attribute, ActionExecutingContext context, object? responseValue, long elapsedMilliseconds) + { + var detail = new Dictionary + { + ["method"] = context.HttpContext.Request.Method, + ["path"] = context.HttpContext.Request.Path.Value, + ["routeValues"] = context.RouteData.Values.ToDictionary(k => k.Key, v => v.Value?.ToString()), + ["arguments"] = attribute.LogArguments ? SanitizeValue(context.ActionArguments, 0) : null, + ["responseMessage"] = GetResponseMessage(responseValue), + ["elapsedMilliseconds"] = elapsedMilliseconds + }; + + return JsonSerializer.Serialize(detail, JsonOptions); + } + + private static string? GetResponseMessage(object? responseValue) + { + return responseValue is BaseResponse response ? response.message : null; + } + + private static object? GetResponseValue(IActionResult? result) + { + return result switch + { + ObjectResult objectResult => objectResult.Value, + JsonResult jsonResult => jsonResult.Value, + _ => null + }; + } + + private static object? GetResponseResult(object? responseValue) + { + if (responseValue == null) + { + return null; + } + + return responseValue.GetType() + .GetProperty("result", BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase) + ?.GetValue(responseValue); + } + + private static object? SanitizeValue(object? value, int depth) + { + if (value == null) + { + return null; + } + + if (depth > 4) + { + return value.ToString(); + } + + var type = value.GetType(); + if (type.IsPrimitive || value is string or decimal or DateTime or DateTimeOffset or Guid || type.IsEnum) + { + return value; + } + + if (value is IDictionary dictionary) + { + var result = new Dictionary(); + foreach (DictionaryEntry item in dictionary) + { + var key = item.Key?.ToString() ?? string.Empty; + result[key] = SensitiveNames.Contains(key) ? "***" : SanitizeValue(item.Value, depth + 1); + } + + return result; + } + + if (value is IEnumerable enumerable && value is not string) + { + return enumerable.Cast() + .Take(20) + .Select(item => SanitizeValue(item, depth + 1)) + .ToList(); + } + + return type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.GetIndexParameters().Length == 0) + .ToDictionary( + p => p.Name, + p => SensitiveNames.Contains(p.Name) ? "***" : SanitizeValue(p.GetValue(value), depth + 1)); + } + + private static bool TryGetLongRouteValue(ActionExecutingContext context, string? key, out long value) + { + value = 0; + if (string.IsNullOrWhiteSpace(key) || !context.RouteData.Values.TryGetValue(key, out var routeValue)) + { + return false; + } + + return long.TryParse(routeValue?.ToString(), out value); + } + + private static bool TryGetLongArgumentValue(ActionExecutingContext context, string? key, out long value) + { + value = 0; + if (string.IsNullOrWhiteSpace(key) || !context.ActionArguments.TryGetValue(key, out var argumentValue)) + { + return false; + } + + if (TryConvertToLong(argumentValue, out value)) + { + return true; + } + + return TryGetLongPropertyValue(argumentValue, "Id", out value); + } + + private static bool TryGetLongPropertyValue(object? source, string propertyName, out long value) + { + value = 0; + if (source == null) + { + return false; + } + + var property = source.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); + return property != null && TryConvertToLong(property.GetValue(source), out value); + } + + private static bool TryConvertToLong(object? source, out long value) + { + value = 0; + return source switch + { + long longValue => SetValue(longValue, out value), + int intValue => SetValue(intValue, out value), + string stringValue => long.TryParse(stringValue, out value), + _ => long.TryParse(source?.ToString(), out value) + }; + } + + private static bool TryGetStringPropertyValue(object? source, string propertyName, out string? value) + { + value = null; + if (source == null) + { + return false; + } + + var property = source.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); + value = property?.GetValue(source)?.ToString(); + return !string.IsNullOrWhiteSpace(value); + } + + private static bool SetValue(long source, out long value) + { + value = source; + return true; + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/Middleware/OperationLogAttribute.cs b/QYZH.InteractiveMagazine.Infrastructure/Middleware/OperationLogAttribute.cs new file mode 100644 index 0000000..8477d6d --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/Middleware/OperationLogAttribute.cs @@ -0,0 +1,59 @@ +namespace QYZH.InteractiveMagazine.Infrastructure.Middleware; + +/// +/// 操作日志标记 +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class OperationLogAttribute : Attribute +{ + /// + /// 构造函数 + /// + /// 操作类型 + /// 目标类型 + public OperationLogAttribute(string actionType, string targetType) + { + ActionType = actionType; + TargetType = targetType; + } + + /// + /// 操作类型 + /// + public string ActionType { get; } + + /// + /// 目标类型 + /// + public string TargetType { get; } + + /// + /// 目标Id路由键 + /// + public string? TargetIdRouteKey { get; set; } = "id"; + + /// + /// 目标Id参数名 + /// + public string? TargetIdArgumentName { get; set; } + + /// + /// 目标名称参数名 + /// + public string? TargetNameArgumentName { get; set; } + + /// + /// 目标名称属性名 + /// + public string? TargetNameProperty { get; set; } + + /// + /// 使用当前操作人Id作为目标Id + /// + public bool UseOperatorAsTargetId { get; set; } + + /// + /// 是否记录参数摘要 + /// + public bool LogArguments { get; set; } = true; +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/OperationLogDto.cs b/QYZH.InteractiveMagazine.Models/Dto/OperationLogDto.cs index c4c2bf6..b41efaf 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/OperationLogDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/OperationLogDto.cs @@ -5,6 +5,36 @@ namespace QYZH.InteractiveMagazine.Models.Dto; /// public static class OperationLogActionType { + /// + /// 新增 + /// + public const string Create = "Create"; + + /// + /// 修改 + /// + public const string Update = "Update"; + + /// + /// 删除 + /// + public const string Delete = "Delete"; + + /// + /// 状态变更 + /// + public const string StatusChange = "StatusChange"; + + /// + /// 分配 + /// + public const string Assign = "Assign"; + + /// + /// 修改密码 + /// + public const string ChangePassword = "ChangePassword"; + /// /// 手动增加积分 /// @@ -31,17 +61,163 @@ public static class OperationLogActionType /// public static class OperationLogTargetType { + /// + /// 管理员 + /// + public const string AdminUser = "AdminUser"; + + /// + /// 权限菜单 + /// + public const string AdminMenu = "AdminMenu"; + + /// + /// 管理员角色 + /// + public const string AdminRole = "AdminRole"; + /// /// 用户 /// public const string User = "User"; + /// + /// 社区留言 + /// + public const string CommunityMessage = "CommunityMessage"; + + /// + /// 勋章 + /// + public const string Medal = "Medal"; + + /// + /// AI 基础提示词 + /// + public const string AiBasePrompt = "AiBasePrompt"; + + /// + /// 签到配置 + /// + public const string CheckInConfig = "CheckInConfig"; + + /// + /// Banner + /// + public const string Banner = "Banner"; + + /// + /// 素材 + /// + public const string Material = "Material"; + + /// + /// 商品 + /// + public const string Product = "Product"; + + /// + /// 宠物 + /// + public const string Pet = "Pet"; + + /// + /// 宠物进化 + /// + public const string PetEvolution = "PetEvolution"; + + /// + /// 宠物皮肤 + /// + public const string PetSkin = "PetSkin"; + + /// + /// 宠物皮肤图片 + /// + public const string PetSkinImage = "PetSkinImage"; + + /// + /// 用户期刊二维码 + /// + public const string UserJournalQrCode = "UserJournalQrCode"; + + /// + /// 期刊 + /// + public const string Journal = "Journal"; + + /// + /// 期刊目录 + /// + public const string JournalCatalog = "JournalCatalog"; + + /// + /// 期刊书页 + /// + public const string JournalPage = "JournalPage"; + + /// + /// 期刊书页任务 + /// + public const string JournalPageTask = "JournalPageTask"; + + /// + /// 期刊任务答案 + /// + public const string JournalTaskAnswer = "JournalTaskAnswer"; + /// /// 补偿任务 /// public const string CompensationTask = "CompensationTask"; } +/// +/// 操作日志记录输入 +/// +public class OperationLogRecordInput +{ + /// + /// 操作人Id + /// + public long OperatorId { get; set; } + + /// + /// 操作人用户名 + /// + public string OperatorName { get; set; } = string.Empty; + + /// + /// 操作类型 + /// + public string ActionType { get; set; } = string.Empty; + + /// + /// 目标类型 + /// + public string TargetType { get; set; } = string.Empty; + + /// + /// 目标记录Id + /// + public long TargetId { get; set; } + + /// + /// 目标名称 + /// + public string? TargetName { get; set; } + + /// + /// 操作详情 JSON + /// + public string? Detail { get; set; } + + /// + /// IP地址 + /// + public string? IpAddress { get; set; } +} + /// /// 操作日志分页查询输入 /// diff --git a/QYZH.InteractiveMagazine.Service/CompensationManageService.cs b/QYZH.InteractiveMagazine.Service/CompensationManageService.cs index 71fbfff..e912ebb 100644 --- a/QYZH.InteractiveMagazine.Service/CompensationManageService.cs +++ b/QYZH.InteractiveMagazine.Service/CompensationManageService.cs @@ -98,11 +98,16 @@ public class CompensationManageService( UserId = task.UserId }); - await operationLogService.LogAsync( - operatorId, operatorName, - OperationLogActionType.CompensationRetry, - OperationLogTargetType.CompensationTask, - taskId, null, detail, ipAddress); + await operationLogService.LogAsync(new OperationLogRecordInput + { + OperatorId = operatorId, + OperatorName = operatorName, + ActionType = OperationLogActionType.CompensationRetry, + TargetType = OperationLogTargetType.CompensationTask, + TargetId = taskId, + Detail = detail, + IpAddress = ipAddress + }); logger.LogInformation("补偿任务手动重试成功,TaskId: {TaskId}", taskId); } @@ -140,11 +145,16 @@ public class CompensationManageService( UserId = task.UserId }); - await operationLogService.LogAsync( - operatorId, operatorName, - OperationLogActionType.CompensationResolve, - OperationLogTargetType.CompensationTask, - taskId, null, detail, ipAddress); + await operationLogService.LogAsync(new OperationLogRecordInput + { + OperatorId = operatorId, + OperatorName = operatorName, + ActionType = OperationLogActionType.CompensationResolve, + TargetType = OperationLogTargetType.CompensationTask, + TargetId = taskId, + Detail = detail, + IpAddress = ipAddress + }); logger.LogInformation("补偿任务标记已解决成功,TaskId: {TaskId}", taskId); } diff --git a/QYZH.InteractiveMagazine.Service/OperationLogService.cs b/QYZH.InteractiveMagazine.Service/OperationLogService.cs index e855ffe..1ee8350 100644 --- a/QYZH.InteractiveMagazine.Service/OperationLogService.cs +++ b/QYZH.InteractiveMagazine.Service/OperationLogService.cs @@ -24,23 +24,41 @@ public class OperationLogService( /// 记录操作日志 /// public async Task LogAsync(long operatorId, string operatorName, string actionType, string targetType, long targetId, string? targetName = null, string? detail = null, string? ipAddress = null) + { + await LogAsync(new OperationLogRecordInput + { + OperatorId = operatorId, + OperatorName = operatorName, + ActionType = actionType, + TargetType = targetType, + TargetId = targetId, + TargetName = targetName, + Detail = detail, + IpAddress = ipAddress + }); + } + + /// + /// 记录操作日志 + /// + public async Task LogAsync(OperationLogRecordInput input) { try { var log = new OperationLog { - OperatorId = operatorId, - OperatorName = operatorName, - ActionType = actionType, - TargetType = targetType, - TargetId = targetId, - TargetName = targetName, - Detail = detail, - IpAddress = ipAddress, + OperatorId = input.OperatorId, + OperatorName = input.OperatorName, + ActionType = input.ActionType, + TargetType = input.TargetType, + TargetId = input.TargetId, + TargetName = input.TargetName, + Detail = input.Detail, + IpAddress = input.IpAddress, IsDeleted = false, - CreatedBy = operatorName, + CreatedBy = input.OperatorName, CreatedAt = DateTime.Now, - UpdatedBy = operatorName, + UpdatedBy = input.OperatorName, UpdatedAt = DateTime.Now }; @@ -48,12 +66,12 @@ public class OperationLogService( logger.LogInformation( "记录操作日志,Operator: {Operator}, Action: {Action}, Target: {TargetType}/{TargetId}", - operatorName, actionType, targetType, targetId); + input.OperatorName, input.ActionType, input.TargetType, input.TargetId); } catch (Exception ex) { // 日志记录不应影响主业务流程 - logger.LogError(ex, "记录操作日志失败,Operator: {Operator}, Action: {Action}", operatorName, actionType); + logger.LogError(ex, "记录操作日志失败,Operator: {Operator}, Action: {Action}", input.OperatorName, input.ActionType); } } diff --git a/QYZH.InteractiveMagazine.Service/UsersService.cs b/QYZH.InteractiveMagazine.Service/UsersService.cs index 240027f..24d7b2f 100644 --- a/QYZH.InteractiveMagazine.Service/UsersService.cs +++ b/QYZH.InteractiveMagazine.Service/UsersService.cs @@ -252,11 +252,17 @@ public class UsersService( RecordId = result.RecordId }); - await operationLogService.LogAsync( - operatorId, operatorName, - OperationLogActionType.ManualAddPoints, - OperationLogTargetType.User, - userId, user.Name, detail, ipAddress); + await operationLogService.LogAsync(new OperationLogRecordInput + { + OperatorId = operatorId, + OperatorName = operatorName, + ActionType = OperationLogActionType.ManualAddPoints, + TargetType = OperationLogTargetType.User, + TargetId = userId, + TargetName = user.Name, + Detail = detail, + IpAddress = ipAddress + }); return new ManualPointsOutput { @@ -302,11 +308,17 @@ public class UsersService( RecordId = result.RecordId }); - await operationLogService.LogAsync( - operatorId, operatorName, - OperationLogActionType.ManualDeductPoints, - OperationLogTargetType.User, - userId, user.Name, detail, ipAddress); + await operationLogService.LogAsync(new OperationLogRecordInput + { + OperatorId = operatorId, + OperatorName = operatorName, + ActionType = OperationLogActionType.ManualDeductPoints, + TargetType = OperationLogTargetType.User, + TargetId = userId, + TargetName = user.Name, + Detail = detail, + IpAddress = ipAddress + }); return new ManualPointsOutput { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/AdminController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/AdminController.cs index b6ce435..cba11d9 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/AdminController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/AdminController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; @@ -76,6 +77,7 @@ public class AdminController : BaseController /// /// 修改密码输入 /// 修改密码结果 + [OperationLog(OperationLogActionType.ChangePassword, OperationLogTargetType.AdminUser, UseOperatorAsTargetId = true, LogArguments = false)] [HttpPost("changePassword")] public async Task> ChangePasswordAsync([FromBody] ChangePasswordInput input) { @@ -94,6 +96,7 @@ public class AdminController : BaseController /// /// 管理员输入 /// 创建的管理员信息 + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.AdminUser, TargetNameProperty = "UserName")] [HttpPost("users")] public async Task> CreateUserAsync([FromBody] AdminUserInput input) { @@ -120,6 +123,7 @@ public class AdminController : BaseController /// 管理员ID /// 管理员输入 /// 更新后的管理员信息 + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.AdminUser, TargetNameProperty = "UserName")] [HttpPut("users/{id}")] public async Task> UpdateUserAsync(long id, [FromBody] AdminUserInput input) { @@ -145,6 +149,7 @@ public class AdminController : BaseController /// /// 管理员ID /// 操作结果 + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.AdminUser)] [HttpDelete("users/{id}")] public async Task> DeleteUserAsync(long id) { @@ -220,6 +225,7 @@ public class AdminController : BaseController /// /// 管理员ID /// 操作结果 + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.AdminUser)] [HttpPut("users/{id}/status")] public async Task> ToggleUserStatusAsync(long id) { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/AiBasePromptController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/AiBasePromptController.cs index e34f3f3..8d85d4f 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/AiBasePromptController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/AiBasePromptController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; @@ -28,6 +29,7 @@ public class AiBasePromptController : BaseController /// /// Prompt配置信息 /// 创建的Prompt配置信息 + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.AiBasePrompt)] [HttpPost] public async Task> CreateAsync([FromBody] AiBasePromptInput input) { @@ -54,6 +56,7 @@ public class AiBasePromptController : BaseController /// Prompt配置ID /// Prompt配置信息 /// 更新后的Prompt配置信息 + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.AiBasePrompt)] [HttpPut("{id}")] public async Task> UpdateAsync(long id, [FromBody] AiBasePromptInput input) { @@ -79,6 +82,7 @@ public class AiBasePromptController : BaseController /// /// Prompt配置ID /// 操作结果 + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.AiBasePrompt)] [HttpDelete("{id}")] public async Task> DeleteAsync(long id) { @@ -178,6 +182,7 @@ public class AiBasePromptController : BaseController /// /// Prompt配置ID /// 操作结果 + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.AiBasePrompt)] [HttpPut("{id}/toggle-status")] public async Task> ToggleStatusAsync(long id) { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/BannerController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/BannerController.cs index 4d767fa..107ac77 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/BannerController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/BannerController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto.Banner; @@ -19,6 +20,7 @@ public class BannerController(IBannerService bannerService) : BaseController /// /// 轮播图信息 /// 创建后的轮播图信息 + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.Banner)] [HttpPost] public async Task> CreateAsync([FromBody] BannerInput input) { @@ -32,6 +34,7 @@ public class BannerController(IBannerService bannerService) : BaseController /// 轮播图ID /// 轮播图信息 /// 更新后的轮播图信息 + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.Banner)] [HttpPut("{id}")] public async Task> UpdateAsync(long id, [FromBody] BannerInput input) { @@ -44,6 +47,7 @@ public class BannerController(IBannerService bannerService) : BaseController /// /// 轮播图ID /// 操作结果 + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.Banner)] [HttpDelete("{id}")] public async Task> DeleteAsync(long id) { @@ -80,6 +84,7 @@ public class BannerController(IBannerService bannerService) : BaseController /// /// 轮播图ID /// 操作结果 + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Banner)] [HttpPut("{id}/status")] public async Task> UpdateStatusAsync(long id) { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/CheckInConfigController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/CheckInConfigController.cs index 6e9608b..1952fe0 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/CheckInConfigController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/CheckInConfigController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; @@ -29,6 +30,7 @@ public class CheckInConfigController : BaseController /// /// 签到配置信息 /// 创建的签到配置信息 + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.CheckInConfig)] [HttpPost] public async Task> CreateAsync([FromBody] CheckInConfigInput input) { @@ -55,6 +57,7 @@ public class CheckInConfigController : BaseController /// 签到配置ID /// 签到配置信息 /// 更新后的签到配置信息 + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.CheckInConfig)] [HttpPut("{id}")] public async Task> UpdateAsync(long id, [FromBody] CheckInConfigInput input) { @@ -80,6 +83,7 @@ public class CheckInConfigController : BaseController /// /// 签到配置ID /// 操作结果 + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.CheckInConfig)] [HttpDelete("{id}")] public async Task> DeleteAsync(long id) { @@ -155,6 +159,7 @@ public class CheckInConfigController : BaseController /// /// 签到配置ID /// 操作结果 + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.CheckInConfig)] [HttpPut("{id}/status")] public async Task> UpdateStatusAsync(long id) { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/CommunityMessageController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/CommunityMessageController.cs index 0e6025f..58e752b 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/CommunityMessageController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/CommunityMessageController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; @@ -72,6 +73,7 @@ public class CommunityMessageController : BaseController /// /// 删除消息 /// + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.CommunityMessage)] [HttpDelete("{id}")] public async Task> DeleteAsync(long id) { @@ -95,6 +97,7 @@ public class CommunityMessageController : BaseController /// /// 冻结/解冻消息 /// + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.CommunityMessage)] [HttpPut("{id}/freeze")] public async Task> FreezeAsync(long id, [FromBody] AdminFreezeInput input) { @@ -118,6 +121,7 @@ public class CommunityMessageController : BaseController /// /// 设置/取消精选 /// + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.CommunityMessage)] [HttpPut("{id}/featured")] public async Task> SetFeaturedAsync(long id, [FromBody] AdminSetFeaturedInput input) { @@ -141,6 +145,7 @@ public class CommunityMessageController : BaseController /// /// 设置排序权重 /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.CommunityMessage)] [HttpPut("{id}/sort")] public async Task> SetSortOrderAsync(long id, [FromBody] AdminSetSortOrderInput input) { @@ -164,6 +169,7 @@ public class CommunityMessageController : BaseController /// /// 批量发布消息 /// + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.CommunityMessage, TargetIdRouteKey = null)] [HttpPost("batch-publish")] public async Task> BatchPublishAsync([FromBody] AdminBatchPublishInput input) { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/JournalController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/JournalController.cs index 89255f1..f673543 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/JournalController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/JournalController.cs @@ -6,6 +6,7 @@ using QYZH.InteractiveMagazine.Models.Enum; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MiniExcelLibs; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.Infrastructure.OSS; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; @@ -76,6 +77,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// /// 创建杂志 /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.Journal)] [HttpPost, Route("journal/add")] public async Task> AddAsync([FromBody] JournalAddDto dto) { @@ -88,6 +90,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// /// 书籍对象 /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.Journal, TargetIdArgumentName = "dto")] [HttpPost, Route("journal/update")] public async Task Edit([FromBody] JournalEditDto dto) { @@ -100,6 +103,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// /// /// + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.Journal, TargetIdRouteKey = null)] [HttpPost, Route("journal/delete")] public async Task> DeleteAsync([Required][FromBody] List ids) { @@ -111,6 +115,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 书籍起始页 /// /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.Journal)] [HttpPost, Route("journal/startpage")] public async Task> StartPageAsync([FromQuery]long id, [FromQuery] int index) { @@ -122,6 +127,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 书籍归档 /// /// + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Journal)] [HttpPost, Route("journal/rchive/{id:long}")] public async Task> Archive(long id) { @@ -133,6 +139,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 书籍废弃 /// /// + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Journal)] [HttpPost] [HttpPost, Route("journal/abandon/{id:long}")] public async Task> Abandon(long id) @@ -145,6 +152,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 书籍发布 /// /// + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Journal)] [HttpPost, Route("journal/publish/{id:long}")] public async Task> Publish(long id) { @@ -156,6 +164,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 书籍铺码 /// /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.Journal)] [HttpPost, Route("journal/printcode/{id:long}")] public async Task> PrintCodeAsync(long id) { @@ -168,6 +177,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// /// /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.Journal, TargetIdArgumentName = "input")] [HttpPost, Route("journal/resultreport")] public async Task> GenerateAsync([FromBody] DotMatrixNoteJournalReportInput input, [FromForm] IFormFile? file) { @@ -185,6 +195,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// /// /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.Journal)] [HttpPost, Route("journal/import")] public async Task> JournalImport(JournalImportDto input) { @@ -266,6 +277,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// file /// file /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.JournalCatalog, TargetIdRouteKey = "JournalId")] [HttpPost, Route("catalog/import/{JournalId}")] public async Task> ImportAsync(long JournalId, [FromForm(Name = "file")] IFormFile file) { @@ -283,6 +295,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// /// /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.JournalCatalog)] [HttpPost, Route("catalog/add")] public async Task> CatalogAdd(JournalCatalogInput input) { @@ -294,6 +307,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// /// /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalCatalog, TargetIdArgumentName = "input")] [HttpPost, Route("catalog/update")] public async Task> CatalogUpdate(JournalCatalogUpdateInput input) { @@ -305,6 +319,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// /// /// + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.JournalCatalog)] [HttpPost, Route("catalog/delete/{id:long}")] public async Task> CatalogDelete(long id) { @@ -318,6 +333,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// /// /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalCatalog, TargetIdArgumentName = "input")] [HttpPost, Route("catalog/move")] public async Task> CatalogMove(MoveInput input) { @@ -333,6 +349,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 书页新增 /// /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.JournalPage)] [HttpPost, Route("page/add")] public async Task> PageAdd(JournalAddV2Input input) { @@ -344,6 +361,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 书页修改 /// /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalPage, TargetIdArgumentName = "input")] [HttpPost, Route("page/update")] public async Task> PageUpdate(PageLayoutInput input) { @@ -355,6 +373,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 自动铺码 /// /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalPage, TargetIdRouteKey = "JournalId")] [HttpPost, Route("page/updatepageno/{JournalId:long}")] public async Task> PageUpdatePageNo([Required(ErrorMessage = "书籍编号不允许为空")] long JournalId) { @@ -418,6 +437,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 书页问题新增 /// /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.JournalPageTask)] [HttpPost, Route("page/task/add")] [ProducesResponseType(typeof(BaseResponse), 200)] public async Task> PageTaskAdd(JournalPageTaskAddInput input) @@ -430,6 +450,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 书页问题更新 /// /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalPageTask, TargetIdArgumentName = "input")] [HttpPost, Route("page/task/update")] [ProducesResponseType(typeof(BaseResponse), 200)] public async Task> PageTaskUpdate(JournalPageTaskUpdateInput input) @@ -454,6 +475,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 书页问题删除 /// /// + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.JournalPageTask)] [HttpPost, Route("page/task/delete/{id:long}")] public async Task> PageTaskDelete(long id) { @@ -465,6 +487,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 书页问题补充 /// /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalPageTask, TargetIdArgumentName = "input")] [HttpPost, Route("page/task/complement")] public async Task> ComplementAnsync(JournalPageTaskComplementInput input) { @@ -476,6 +499,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 答案新增 /// /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.JournalTaskAnswer)] [HttpPost, Route("page/task/answer/add")] public async Task> AnswerAdd(JournalTaskAnswerAddInput input) { @@ -487,6 +511,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 答案更新 /// /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalTaskAnswer, TargetIdArgumentName = "input")] [HttpPost, Route("page/task/answer/update")] public async Task> AnswerUpdate(JournalTaskAnswerUpdateInput input) { @@ -498,6 +523,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers /// 答案删除 /// /// + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.JournalTaskAnswer)] [HttpPost, Route("page/task/answer/delete/{id:long}")] public async Task> AnswerDelete(long id) { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/MaterialController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/MaterialController.cs index ef2a38e..e96d584 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/MaterialController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/MaterialController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto.Material; @@ -19,6 +20,7 @@ public class MaterialController(IMaterialService materialService) : BaseControll /// /// 资料信息 /// 创建后的资料信息 + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.Material)] [HttpPost] public async Task> CreateAsync([FromBody] MaterialInput input) { @@ -32,6 +34,7 @@ public class MaterialController(IMaterialService materialService) : BaseControll /// 资料ID /// 资料信息 /// 更新后的资料信息 + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.Material)] [HttpPut("{id}")] public async Task> UpdateAsync(long id, [FromBody] MaterialInput input) { @@ -44,6 +47,7 @@ public class MaterialController(IMaterialService materialService) : BaseControll /// /// 资料ID /// 操作结果 + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.Material)] [HttpDelete("{id}")] public async Task> DeleteAsync(long id) { @@ -80,6 +84,7 @@ public class MaterialController(IMaterialService materialService) : BaseControll /// /// 资料ID /// 操作结果 + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Material)] [HttpPut("{id}/status")] public async Task> UpdateStatusAsync(long id) { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/MedalController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/MedalController.cs index b4a2c21..6767d11 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/MedalController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/MedalController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; @@ -28,6 +29,7 @@ public class MedalController : BaseController /// /// 勋章信息 /// 创建的勋章信息 + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.Medal)] [HttpPost] public async Task> CreateAsync([FromBody] MedalInput input) { @@ -54,6 +56,7 @@ public class MedalController : BaseController /// 勋章ID /// 勋章信息 /// 更新后的勋章信息 + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.Medal)] [HttpPut("{id}")] public async Task> UpdateAsync(long id, [FromBody] MedalInput input) { @@ -79,6 +82,7 @@ public class MedalController : BaseController /// /// 勋章ID /// 操作结果 + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.Medal)] [HttpDelete("{id}")] public async Task> DeleteAsync(long id) { @@ -155,6 +159,7 @@ public class MedalController : BaseController /// 勋章ID /// 状态: 0=禁用, 1=启用 /// 操作结果 + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Medal)] [HttpPut("{id}/status")] public async Task> UpdateStatusAsync(long id) { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/PermissionController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/PermissionController.cs index 05bd1a4..9afd4dd 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/PermissionController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/PermissionController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; @@ -17,6 +18,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService /// /// 创建菜单 /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.AdminMenu)] [HttpPost("menus")] public async Task> CreateMenuAsync([FromBody] AdminMenuInput input) { @@ -27,6 +29,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService /// /// 更新菜单 /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.AdminMenu)] [HttpPut("menus/{id}")] public async Task> UpdateMenuAsync(long id, [FromBody] AdminMenuInput input) { @@ -37,6 +40,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService /// /// 删除菜单 /// + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.AdminMenu)] [HttpDelete("menus/{id}")] public async Task> DeleteMenuAsync(long id) { @@ -70,6 +74,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService /// /// 创建角色 /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.AdminRole)] [HttpPost("roles")] public async Task> CreateRoleAsync([FromBody] AdminRoleInput input) { @@ -80,6 +85,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService /// /// 更新角色 /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.AdminRole)] [HttpPut("roles/{id}")] public async Task> UpdateRoleAsync(long id, [FromBody] AdminRoleInput input) { @@ -90,6 +96,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService /// /// 删除角色 /// + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.AdminRole)] [HttpDelete("roles/{id}")] public async Task> DeleteRoleAsync(long id) { @@ -120,6 +127,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService /// /// 分配角色菜单 /// + [OperationLog(OperationLogActionType.Assign, OperationLogTargetType.AdminRole, TargetIdRouteKey = "roleId")] [HttpPut("roles/{roleId}/menus")] public async Task> AssignRoleMenusAsync(long roleId, [FromBody] AssignRoleMenusInput input) { @@ -130,6 +138,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService /// /// 分配管理员角色 /// + [OperationLog(OperationLogActionType.Assign, OperationLogTargetType.AdminUser, TargetIdRouteKey = "adminUserId")] [HttpPut("users/{adminUserId}/roles")] public async Task> AssignAdminUserRolesAsync(long adminUserId, [FromBody] AssignAdminUserRolesInput input) { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/PetManageController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/PetManageController.cs index 85ba2a8..2f7d702 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/PetManageController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/PetManageController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; @@ -29,6 +30,7 @@ public class PetManageController : BaseController /// /// 创建宠物模板 /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.Pet)] [HttpPost] public async Task> CreateTemplateAsync([FromBody] PetTemplateInput input) { @@ -52,6 +54,7 @@ public class PetManageController : BaseController /// /// 更新宠物模板 /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.Pet)] [HttpPut("{id}")] public async Task> UpdateTemplateAsync(long id, [FromBody] PetTemplateInput input) { @@ -75,6 +78,7 @@ public class PetManageController : BaseController /// /// 删除宠物模板 /// + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.Pet)] [HttpDelete("{id}")] public async Task> DeleteTemplateAsync(long id) { @@ -144,6 +148,7 @@ public class PetManageController : BaseController /// /// 更新宠物模板状态(启用/禁用) /// + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Pet)] [HttpPut("{id}/status")] public async Task> UpdateTemplateStatusAsync(long id) { @@ -169,6 +174,7 @@ public class PetManageController : BaseController /// /// 创建进化阶段 /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.PetEvolution)] [HttpPost("evolution")] public async Task> CreateEvolutionAsync([FromBody] PetEvolutionInput input) { @@ -192,6 +198,7 @@ public class PetManageController : BaseController /// /// 更新进化阶段 /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.PetEvolution)] [HttpPut("evolution/{id}")] public async Task> UpdateEvolutionAsync(long id, [FromBody] PetEvolutionInput input) { @@ -215,6 +222,7 @@ public class PetManageController : BaseController /// /// 删除进化阶段 /// + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.PetEvolution)] [HttpDelete("evolution/{id}")] public async Task> DeleteEvolutionAsync(long id) { @@ -286,6 +294,7 @@ public class PetManageController : BaseController /// /// 创建皮肤 /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.PetSkin)] [HttpPost("skin")] public async Task> CreateSkinAsync([FromBody] PetSkinInput input) { @@ -309,6 +318,7 @@ public class PetManageController : BaseController /// /// 更新皮肤 /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.PetSkin)] [HttpPut("skin/{id}")] public async Task> UpdateSkinAsync(long id, [FromBody] PetSkinInput input) { @@ -332,6 +342,7 @@ public class PetManageController : BaseController /// /// 删除皮肤 /// + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.PetSkin)] [HttpDelete("skin/{id}")] public async Task> DeleteSkinAsync(long id) { @@ -403,6 +414,7 @@ public class PetManageController : BaseController /// /// 创建皮肤图片 /// + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.PetSkinImage)] [HttpPost("skin-image")] public async Task> CreateSkinImageAsync([FromBody] PetSkinImageInput input) { @@ -426,6 +438,7 @@ public class PetManageController : BaseController /// /// 更新皮肤图片 /// + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.PetSkinImage)] [HttpPut("skin-image/{id}")] public async Task> UpdateSkinImageAsync(long id, [FromBody] PetSkinImageInput input) { @@ -449,6 +462,7 @@ public class PetManageController : BaseController /// /// 删除皮肤图片 /// + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.PetSkinImage)] [HttpDelete("skin-image/{id}")] public async Task> DeleteSkinImageAsync(long id) { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/ProductController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/ProductController.cs index ae56d51..371cae0 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/ProductController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/ProductController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; @@ -28,6 +29,7 @@ public class ProductController : BaseController /// /// 商品信息 /// 创建的商品信息 + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.Product)] [HttpPost] public async Task> CreateAsync([FromBody] ProductInput input) { @@ -54,6 +56,7 @@ public class ProductController : BaseController /// 商品ID /// 商品信息 /// 更新后的商品信息 + [OperationLog(OperationLogActionType.Update, OperationLogTargetType.Product)] [HttpPut("{id}")] public async Task> UpdateAsync(long id, [FromBody] ProductInput input) { @@ -79,6 +82,7 @@ public class ProductController : BaseController /// /// 商品ID /// 操作结果 + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.Product)] [HttpDelete("{id}")] public async Task> DeleteAsync(long id) { @@ -154,6 +158,7 @@ public class ProductController : BaseController /// /// 商品ID /// 操作结果 + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Product)] [HttpPut("{id}/status")] public async Task> UpdateSaleStatusAsync(long id) { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/UserJournalQrCodeController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/UserJournalQrCodeController.cs index 9115b76..fddc629 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/UserJournalQrCodeController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/UserJournalQrCodeController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Enum; @@ -18,6 +19,7 @@ public class UserJournalQrCodeController(IUserJournalService userJournalService) /// /// 生成输入 /// 二维码记录 + [OperationLog(OperationLogActionType.Create, OperationLogTargetType.UserJournalQrCode)] [HttpPost("add")] public async Task> AddAsync([FromBody] CreateUserJournalQrCodeInput input) { @@ -66,6 +68,7 @@ public class UserJournalQrCodeController(IUserJournalService userJournalService) /// /// 删除输入 /// 是否成功 + [OperationLog(OperationLogActionType.Delete, OperationLogTargetType.UserJournalQrCode, TargetIdRouteKey = null)] [HttpPost("delete")] public async Task> DeleteAsync([FromBody] DeleteUserJournalQrCodeInput input) { diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/UsersController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/UsersController.cs index 4873972..36b0ce4 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/UsersController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/UsersController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; @@ -82,6 +83,7 @@ public class UsersController : BaseController /// /// 更新用户状态 /// + [OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.User)] [HttpPut("{id}/status")] public async Task UpdateStatus(long id) { diff --git a/QYZH.InteractiveMagazine.WorkService/Jobs/JournalTaskAiScoreJob.cs b/QYZH.InteractiveMagazine.WorkService/Jobs/JournalTaskAiScoreJob.cs index de81194..d057d98 100644 --- a/QYZH.InteractiveMagazine.WorkService/Jobs/JournalTaskAiScoreJob.cs +++ b/QYZH.InteractiveMagazine.WorkService/Jobs/JournalTaskAiScoreJob.cs @@ -1,4 +1,3 @@ -using Hangfire; using QYZH.InteractiveMagazine.Infrastructure.OSS; using QYZH.InteractiveMagazine.Models.Entity; using QYZH.InteractiveMagazine.Models.Enum; @@ -27,67 +26,73 @@ public class JournalTaskAiScoreJob( private const int DefaultAiScoreRetryDelayMilliseconds = 1000; private const int DefaultAiMaxConcurrency = 1; private const long DefaultMaxImageBytes = 10 * 1024 * 1024; - private const string InvalidAnswerResult = "作答内容不符合要求"; private const string AiProcessingMessage = "AI批阅中,请稍后"; private static readonly object AiSemaphoreLock = new(); private static readonly object RunWindowLock = new(); + private static readonly object ExecutionLock = new(); private static SemaphoreSlim? aiSemaphore; private static int aiSemaphoreLimit; private static DateTime? lastRunAt; - - /// - /// 执行AI批改。 - /// - [DisableConcurrentExecution(1800)] - public void Execute() - { - ExecuteAsync().GetAwaiter().GetResult(); - } + private static bool isExecuting; + private static DateTime executionStartedAt; /// /// 异步执行AI批改。 /// public async Task ExecuteAsync() { - using var scope = scopeFactory.CreateScope(); - var client = scope.ServiceProvider.GetRequiredService(); - var windowEnd = DateTime.Now; - var windowStart = GetWindowStart(windowEnd); - var batchSize = GetPendingAnswerBatchSize(); - - var pendingAnswers = await client.Queryable() - .Where(a => !a.IsDeleted - && a.Status == (int)UserAnswerStatusEnum.Processing - && ((a.UpdatedAt != null && a.UpdatedAt > windowStart && a.UpdatedAt <= windowEnd) - || (a.UpdatedAt == null && a.CreatedAt > windowStart && a.CreatedAt <= windowEnd))) - .OrderBy(a => a.UpdatedAt, OrderByType.Asc) - .OrderBy(a => a.CreatedAt, OrderByType.Asc) - .Take(batchSize) - .ToListAsync(); - - if (pendingAnswers.Count == 0) + var jobInterval = GetJobInterval(); + if (!TryEnterExecutionLock(jobInterval)) { - SetLastRunAt(windowEnd); - logger.LogInformation("期刊AI批改任务没有待处理答案,WindowStart: {WindowStart}, WindowEnd: {WindowEnd}", windowStart, windowEnd); + logger.LogInformation("期刊AI批改任务仍在执行锁内,跳过本次调度,Interval: {Interval}", jobInterval); return; } - logger.LogInformation("期刊AI批改任务开始,Count: {Count}, WindowStart: {WindowStart}, WindowEnd: {WindowEnd}", pendingAnswers.Count, windowStart, windowEnd); - foreach (var pendingAnswer in pendingAnswers) + try { - try + using var scope = scopeFactory.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + var windowEnd = DateTime.Now; + var windowStart = GetWindowStart(windowEnd, jobInterval); + var batchSize = GetPendingAnswerBatchSize(); + + var pendingAnswers = await client.Queryable() + .Where(a => !a.IsDeleted + && a.Status == (int)UserAnswerStatusEnum.Processing + && ((a.UpdatedAt != null && a.UpdatedAt > windowStart && a.UpdatedAt <= windowEnd) + || (a.UpdatedAt == null && a.CreatedAt > windowStart && a.CreatedAt <= windowEnd))) + .OrderBy(a => a.UpdatedAt, OrderByType.Asc) + .OrderBy(a => a.CreatedAt, OrderByType.Asc) + .Take(batchSize) + .ToListAsync(); + + if (pendingAnswers.Count == 0) { - await ProcessPendingAnswerAsync(client, pendingAnswer.Id, windowStart, windowEnd, CancellationToken.None); + SetLastRunAt(windowEnd); + logger.LogInformation("期刊AI批改任务没有待处理答案,WindowStart: {WindowStart}, WindowEnd: {WindowEnd}, Interval: {Interval}", windowStart, windowEnd, jobInterval); + return; } - catch (Exception ex) + + logger.LogInformation("期刊AI批改任务开始,Count: {Count}, WindowStart: {WindowStart}, WindowEnd: {WindowEnd}, Interval: {Interval}", pendingAnswers.Count, windowStart, windowEnd, jobInterval); + foreach (var pendingAnswer in pendingAnswers) { - logger.LogError(ex, "期刊AI批改答案失败,AnswerId: {AnswerId}", pendingAnswer.Id); + try + { + await ProcessPendingAnswerAsync(client, pendingAnswer.Id, windowStart, windowEnd, CancellationToken.None); + } + catch (Exception ex) + { + logger.LogError(ex, "期刊AI批改答案失败,AnswerId: {AnswerId}", pendingAnswer.Id); + } } + + SetLastRunAt(windowEnd); + } + finally + { + ExitExecutionLock(); } - - SetLastRunAt(windowEnd); } - private async Task ProcessPendingAnswerAsync( ISqlSugarClient client, long answerId, @@ -410,16 +415,17 @@ public class JournalTaskAiScoreJob( prompt.AppendLine($" 判断力上限:{context.Task.Judgment}"); prompt.AppendLine($" 表达力上限:{context.Task.Expression}"); prompt.AppendLine($" 说服力上限:{context.Task.Persuasiveness}"); + prompt.AppendLine($" 分数上线:100"); } prompt.AppendLine(); prompt.AppendLine("评分要求:"); prompt.AppendLine("- 不得超过题目配置中的各项上限。"); - prompt.AppendLine("- 看不清、缺页、无法识别或答案明显不完整时,降低 Completion,不要猜测高分。"); + prompt.AppendLine("- 看不清、缺页、无法识别或答案明显不完整时,尽量猜测让分数偏高。"); prompt.AppendLine("- Completion 表示作答完整度,范围 0-100。"); - prompt.AppendLine("- Result 返回 50 字内中文评语。"); + prompt.AppendLine("- Result 返回 50 字内中文评语,对象是低龄儿童,回复要委婉友好。"); prompt.AppendLine("若作答内容与题目要求不符、答非所问、空白、仅抄题或无法形成有效答案,Score/GrowthPoint/Points/Comprehension/Judgment/Expression/Persuasiveness 均返回 0。"); - prompt.AppendLine("若作答内容不符合要求,Result 必须且只能返回:作答内容不符合要求。不要补充原因、建议或其他文字。"); + prompt.AppendLine("若作答内容不符合要求,Result 提示偏离要求,并补充原因、建议或其他文字。"); prompt.AppendLine(); prompt.AppendLine("只返回如下 JSON 字段:"); prompt.AppendLine("{"); @@ -751,12 +757,6 @@ public class JournalTaskAiScoreJob( private static string NormalizeResult(string? result) { var trimmedResult = TrimResult(result); - if (trimmedResult.Contains("不符合要求", StringComparison.Ordinal) || - trimmedResult.Contains("不符", StringComparison.Ordinal)) - { - return InvalidAnswerResult; - } - return trimmedResult; } @@ -890,23 +890,119 @@ public class JournalTaskAiScoreJob( } } - private int GetPendingAnswerMinutes() - { - var minutes = configuration.GetValue("AiChat:PendingAnswerMinutes"); - return minutes > 0 ? minutes : DefaultPendingAnswerMinutes; - } - private int GetPendingAnswerBatchSize() { var batchSize = configuration.GetValue("AiChat:PendingAnswerBatchSize"); return batchSize > 0 ? batchSize : DefaultPendingAnswerBatchSize; } - private DateTime GetWindowStart(DateTime windowEnd) + private TimeSpan GetJobInterval() + { + var jobTypeName = typeof(JournalTaskAiScoreJob).FullName; + foreach (var jobSection in configuration.GetSection("HangfireJobs:Jobs").GetChildren()) + { + var configuredType = jobSection["JobType"]; + if (!string.Equals(configuredType, jobTypeName, StringComparison.Ordinal)) + { + continue; + } + + var cron = jobSection["Cron"]; + if (TryParseCronInterval(cron, out var interval)) + { + return interval; + } + } + + var minutes = configuration.GetValue("AiChat:PendingAnswerMinutes"); + return TimeSpan.FromMinutes(minutes > 0 ? minutes : DefaultPendingAnswerMinutes); + } + + private static bool TryParseCronInterval(string? cron, out TimeSpan interval) + { + interval = default; + if (string.IsNullOrWhiteSpace(cron)) + { + return false; + } + + var parts = cron.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 5) + { + return false; + } + + if (TryParseCronStep(parts[0], out var minuteStep)) + { + interval = TimeSpan.FromMinutes(minuteStep); + return true; + } + + if (parts[0] == "*" && parts[1] == "*") + { + interval = TimeSpan.FromMinutes(1); + return true; + } + + if (parts[0] == "0" && TryParseCronStep(parts[1], out var hourStep)) + { + interval = TimeSpan.FromHours(hourStep); + return true; + } + + if (int.TryParse(parts[0], out _) && parts[1] == "*") + { + interval = TimeSpan.FromHours(1); + return true; + } + + return false; + } + + private static bool TryParseCronStep(string value, out int step) + { + step = 0; + if (value.StartsWith("*/", StringComparison.Ordinal)) + { + return int.TryParse(value[2..], out step) && step > 0; + } + + var slashIndex = value.IndexOf('/'); + return slashIndex > 0 + && slashIndex < value.Length - 1 + && int.TryParse(value[(slashIndex + 1)..], out step) + && step > 0; + } + + private DateTime GetWindowStart(DateTime windowEnd, TimeSpan jobInterval) { lock (RunWindowLock) { - return lastRunAt ?? windowEnd.AddMinutes(-GetPendingAnswerMinutes()); + return lastRunAt ?? windowEnd.Subtract(jobInterval); + } + } + + private static bool TryEnterExecutionLock(TimeSpan lockTimeout) + { + lock (ExecutionLock) + { + var now = DateTime.Now; + if (isExecuting && now - executionStartedAt < lockTimeout) + { + return false; + } + + isExecuting = true; + executionStartedAt = now; + return true; + } + } + + private static void ExitExecutionLock() + { + lock (ExecutionLock) + { + isExecuting = false; } } diff --git a/QYZH.InteractiveMagazine.WorkService/Program.cs b/QYZH.InteractiveMagazine.WorkService/Program.cs index 3df9ab6..162a35e 100644 --- a/QYZH.InteractiveMagazine.WorkService/Program.cs +++ b/QYZH.InteractiveMagazine.WorkService/Program.cs @@ -123,9 +123,22 @@ if (jobSettings?.Jobs != null) var param = Expression.Parameter(jobType, "job"); var call = Expression.Call(param, method); - var lambda = Expression.Lambda(call, param); + if (method.ReturnType == typeof(void)) + { + var lambda = Expression.Lambda(typeof(Action<>).MakeGenericType(jobType), call, param); + InvokeRecurringJobAddOrUpdate(jobType, typeof(Action<>), job.Name, lambda, job.Cron); + } + else if (method.ReturnType == typeof(Task)) + { + var lambda = Expression.Lambda(typeof(Func<,>).MakeGenericType(jobType, typeof(Task)), call, param); + InvokeRecurringJobAddOrUpdate(jobType, typeof(Func<,>), job.Name, lambda, job.Cron); + } + else + { + Log.Warning("定时任务 [{Name}] 方法返回类型不支持 {ReturnType},跳过注册", job.Name, method.ReturnType); + continue; + } - RecurringJob.AddOrUpdate(job.Name, lambda, job.Cron); Log.Information("定时任务 [{Name}] 已注册,Cron: {Cron}", job.Name, job.Cron); } } @@ -133,3 +146,27 @@ if (jobSettings?.Jobs != null) Log.Information("WorkService 已启动,Hangfire Dashboard: /hangfire"); app.Run(); + +static void InvokeRecurringJobAddOrUpdate(Type jobType, Type delegateGenericTypeDefinition, string jobName, LambdaExpression lambda, string cron) +{ + var method = typeof(RecurringJob).GetMethods() + .Where(m => m.Name == nameof(RecurringJob.AddOrUpdate) && m.IsGenericMethodDefinition) + .First(m => + { + var parameters = m.GetParameters(); + if (parameters.Length != 3 + || parameters[0].ParameterType != typeof(string) + || parameters[2].ParameterType != typeof(string) + || !parameters[1].ParameterType.IsGenericType + || parameters[1].ParameterType.GetGenericTypeDefinition() != typeof(Expression<>)) + { + return false; + } + + var expressionArgument = parameters[1].ParameterType.GetGenericArguments()[0]; + return expressionArgument.IsGenericType + && expressionArgument.GetGenericTypeDefinition() == delegateGenericTypeDefinition; + }); + + method.MakeGenericMethod(jobType).Invoke(null, [jobName, lambda, cron]); +} diff --git a/QYZH.InteractiveMagazine.WorkService/appsettings.json b/QYZH.InteractiveMagazine.WorkService/appsettings.json index ac4e82f..f0fe8f8 100644 --- a/QYZH.InteractiveMagazine.WorkService/appsettings.json +++ b/QYZH.InteractiveMagazine.WorkService/appsettings.json @@ -51,8 +51,8 @@ { "Name": "journal-task-ai-score-job", "JobType": "QYZH.InteractiveMagazine.WorkService.Jobs.JournalTaskAiScoreJob", - "MethodName": "Execute", - "Cron": "*/30 * * * *", + "MethodName": "ExecuteAsync", + "Cron": "*/5 * * * *", "Enabled": true, "Description": "每30分钟扫描上次执行到本次执行之间的期刊答题记录并提交AI批改" }