Compare commits

..

2 Commits

Author SHA1 Message Date
glz
685a8aeaec Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-07-09 18:01:41 +08:00
glz
766be485d0 feat: 新增操作日志功能并优化定时任务配置
1.  新增OperationLogAttribute与OperationLogActionFilter,实现自动化操作日志记录
2.  新增OperationLogRecordInput输入模型,重构IOperationLogService日志接口
3.  为所有业务控制器接口添加操作日志注解
4.  调整期刊AI批改任务的执行周期与方法适配异步调用
5.  优化Hangfire定时任务注册逻辑,支持异步任务
6.  补充完善操作日志类型与目标类型枚举
2026-07-09 18:01:37 +08:00
25 changed files with 1001 additions and 92 deletions

View File

@ -21,6 +21,12 @@ public interface IOperationLogService : IBaseService<OperationLog>
/// <param name="ipAddress">IP地址可选</param> /// <param name="ipAddress">IP地址可选</param>
Task LogAsync(long operatorId, string operatorName, string actionType, string targetType, long targetId, string? targetName = null, string? detail = null, string? ipAddress = null); Task LogAsync(long operatorId, string operatorName, string actionType, string targetType, long targetId, string? targetName = null, string? detail = null, string? ipAddress = null);
/// <summary>
/// 记录操作日志
/// </summary>
/// <param name="input">操作日志记录输入</param>
Task LogAsync(OperationLogRecordInput input);
/// <summary> /// <summary>
/// 分页查询操作日志 /// 分页查询操作日志
/// </summary> /// </summary>

View File

@ -33,6 +33,7 @@ public static class DependencyInjectionExtensions
services.AddTransient<GlobalExceptionMiddleware>(); services.AddTransient<GlobalExceptionMiddleware>();
services.AddTransient<OperationLogMiddleware>(); services.AddTransient<OperationLogMiddleware>();
services.AddScoped<OperationLogActionFilter>();
} }
private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null) private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)

View File

@ -37,6 +37,7 @@ public static class InteractiveMagazineApiDefaultsExtensions
{ {
options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true; options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;
options.Filters.Add<ModelValidActionFilterAttribute>(); options.Filters.Add<ModelValidActionFilterAttribute>();
options.Filters.AddService<OperationLogActionFilter>();
}) })
.AddJsonOptions(options => .AddJsonOptions(options =>
{ {

View File

@ -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;
/// <summary>
/// 操作日志过滤器
/// </summary>
public class OperationLogActionFilter(
IOperationLogService operationLogService,
ILogger<OperationLogActionFilter> logger) : IAsyncActionFilter
{
private static readonly HashSet<string> SensitiveNames = new(StringComparer.OrdinalIgnoreCase)
{
"password",
"oldPassword",
"newPassword",
"token",
"secret",
"authorization"
};
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReferenceHandler = ReferenceHandler.IgnoreCycles
};
/// <summary>
/// 执行操作日志过滤器
/// </summary>
/// <param name="context">Action 执行上下文</param>
/// <param name="next">后续执行委托</param>
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var attribute = context.ActionDescriptor.EndpointMetadata
.OfType<OperationLogAttribute>()
.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<string, object?>
{
["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<string, object?>();
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<object?>()
.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;
}
}

View File

@ -0,0 +1,59 @@
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
/// <summary>
/// 操作日志标记
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class OperationLogAttribute : Attribute
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="actionType">操作类型</param>
/// <param name="targetType">目标类型</param>
public OperationLogAttribute(string actionType, string targetType)
{
ActionType = actionType;
TargetType = targetType;
}
/// <summary>
/// 操作类型
/// </summary>
public string ActionType { get; }
/// <summary>
/// 目标类型
/// </summary>
public string TargetType { get; }
/// <summary>
/// 目标Id路由键
/// </summary>
public string? TargetIdRouteKey { get; set; } = "id";
/// <summary>
/// 目标Id参数名
/// </summary>
public string? TargetIdArgumentName { get; set; }
/// <summary>
/// 目标名称参数名
/// </summary>
public string? TargetNameArgumentName { get; set; }
/// <summary>
/// 目标名称属性名
/// </summary>
public string? TargetNameProperty { get; set; }
/// <summary>
/// 使用当前操作人Id作为目标Id
/// </summary>
public bool UseOperatorAsTargetId { get; set; }
/// <summary>
/// 是否记录参数摘要
/// </summary>
public bool LogArguments { get; set; } = true;
}

View File

@ -5,6 +5,36 @@ namespace QYZH.InteractiveMagazine.Models.Dto;
/// </summary> /// </summary>
public static class OperationLogActionType public static class OperationLogActionType
{ {
/// <summary>
/// 新增
/// </summary>
public const string Create = "Create";
/// <summary>
/// 修改
/// </summary>
public const string Update = "Update";
/// <summary>
/// 删除
/// </summary>
public const string Delete = "Delete";
/// <summary>
/// 状态变更
/// </summary>
public const string StatusChange = "StatusChange";
/// <summary>
/// 分配
/// </summary>
public const string Assign = "Assign";
/// <summary>
/// 修改密码
/// </summary>
public const string ChangePassword = "ChangePassword";
/// <summary> /// <summary>
/// 手动增加积分 /// 手动增加积分
/// </summary> /// </summary>
@ -31,17 +61,163 @@ public static class OperationLogActionType
/// </summary> /// </summary>
public static class OperationLogTargetType public static class OperationLogTargetType
{ {
/// <summary>
/// 管理员
/// </summary>
public const string AdminUser = "AdminUser";
/// <summary>
/// 权限菜单
/// </summary>
public const string AdminMenu = "AdminMenu";
/// <summary>
/// 管理员角色
/// </summary>
public const string AdminRole = "AdminRole";
/// <summary> /// <summary>
/// 用户 /// 用户
/// </summary> /// </summary>
public const string User = "User"; public const string User = "User";
/// <summary>
/// 社区留言
/// </summary>
public const string CommunityMessage = "CommunityMessage";
/// <summary>
/// 勋章
/// </summary>
public const string Medal = "Medal";
/// <summary>
/// AI 基础提示词
/// </summary>
public const string AiBasePrompt = "AiBasePrompt";
/// <summary>
/// 签到配置
/// </summary>
public const string CheckInConfig = "CheckInConfig";
/// <summary>
/// Banner
/// </summary>
public const string Banner = "Banner";
/// <summary>
/// 素材
/// </summary>
public const string Material = "Material";
/// <summary>
/// 商品
/// </summary>
public const string Product = "Product";
/// <summary>
/// 宠物
/// </summary>
public const string Pet = "Pet";
/// <summary>
/// 宠物进化
/// </summary>
public const string PetEvolution = "PetEvolution";
/// <summary>
/// 宠物皮肤
/// </summary>
public const string PetSkin = "PetSkin";
/// <summary>
/// 宠物皮肤图片
/// </summary>
public const string PetSkinImage = "PetSkinImage";
/// <summary>
/// 用户期刊二维码
/// </summary>
public const string UserJournalQrCode = "UserJournalQrCode";
/// <summary>
/// 期刊
/// </summary>
public const string Journal = "Journal";
/// <summary>
/// 期刊目录
/// </summary>
public const string JournalCatalog = "JournalCatalog";
/// <summary>
/// 期刊书页
/// </summary>
public const string JournalPage = "JournalPage";
/// <summary>
/// 期刊书页任务
/// </summary>
public const string JournalPageTask = "JournalPageTask";
/// <summary>
/// 期刊任务答案
/// </summary>
public const string JournalTaskAnswer = "JournalTaskAnswer";
/// <summary> /// <summary>
/// 补偿任务 /// 补偿任务
/// </summary> /// </summary>
public const string CompensationTask = "CompensationTask"; public const string CompensationTask = "CompensationTask";
} }
/// <summary>
/// 操作日志记录输入
/// </summary>
public class OperationLogRecordInput
{
/// <summary>
/// 操作人Id
/// </summary>
public long OperatorId { get; set; }
/// <summary>
/// 操作人用户名
/// </summary>
public string OperatorName { get; set; } = string.Empty;
/// <summary>
/// 操作类型
/// </summary>
public string ActionType { get; set; } = string.Empty;
/// <summary>
/// 目标类型
/// </summary>
public string TargetType { get; set; } = string.Empty;
/// <summary>
/// 目标记录Id
/// </summary>
public long TargetId { get; set; }
/// <summary>
/// 目标名称
/// </summary>
public string? TargetName { get; set; }
/// <summary>
/// 操作详情 JSON
/// </summary>
public string? Detail { get; set; }
/// <summary>
/// IP地址
/// </summary>
public string? IpAddress { get; set; }
}
/// <summary> /// <summary>
/// 操作日志分页查询输入 /// 操作日志分页查询输入
/// </summary> /// </summary>

View File

@ -98,11 +98,16 @@ public class CompensationManageService(
UserId = task.UserId UserId = task.UserId
}); });
await operationLogService.LogAsync( await operationLogService.LogAsync(new OperationLogRecordInput
operatorId, operatorName, {
OperationLogActionType.CompensationRetry, OperatorId = operatorId,
OperationLogTargetType.CompensationTask, OperatorName = operatorName,
taskId, null, detail, ipAddress); ActionType = OperationLogActionType.CompensationRetry,
TargetType = OperationLogTargetType.CompensationTask,
TargetId = taskId,
Detail = detail,
IpAddress = ipAddress
});
logger.LogInformation("补偿任务手动重试成功TaskId: {TaskId}", taskId); logger.LogInformation("补偿任务手动重试成功TaskId: {TaskId}", taskId);
} }
@ -140,11 +145,16 @@ public class CompensationManageService(
UserId = task.UserId UserId = task.UserId
}); });
await operationLogService.LogAsync( await operationLogService.LogAsync(new OperationLogRecordInput
operatorId, operatorName, {
OperationLogActionType.CompensationResolve, OperatorId = operatorId,
OperationLogTargetType.CompensationTask, OperatorName = operatorName,
taskId, null, detail, ipAddress); ActionType = OperationLogActionType.CompensationResolve,
TargetType = OperationLogTargetType.CompensationTask,
TargetId = taskId,
Detail = detail,
IpAddress = ipAddress
});
logger.LogInformation("补偿任务标记已解决成功TaskId: {TaskId}", taskId); logger.LogInformation("补偿任务标记已解决成功TaskId: {TaskId}", taskId);
} }

View File

@ -24,23 +24,41 @@ public class OperationLogService(
/// 记录操作日志 /// 记录操作日志
/// </summary> /// </summary>
public async Task LogAsync(long operatorId, string operatorName, string actionType, string targetType, long targetId, string? targetName = null, string? detail = null, string? ipAddress = null) 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
});
}
/// <summary>
/// 记录操作日志
/// </summary>
public async Task LogAsync(OperationLogRecordInput input)
{ {
try try
{ {
var log = new OperationLog var log = new OperationLog
{ {
OperatorId = operatorId, OperatorId = input.OperatorId,
OperatorName = operatorName, OperatorName = input.OperatorName,
ActionType = actionType, ActionType = input.ActionType,
TargetType = targetType, TargetType = input.TargetType,
TargetId = targetId, TargetId = input.TargetId,
TargetName = targetName, TargetName = input.TargetName,
Detail = detail, Detail = input.Detail,
IpAddress = ipAddress, IpAddress = input.IpAddress,
IsDeleted = false, IsDeleted = false,
CreatedBy = operatorName, CreatedBy = input.OperatorName,
CreatedAt = DateTime.Now, CreatedAt = DateTime.Now,
UpdatedBy = operatorName, UpdatedBy = input.OperatorName,
UpdatedAt = DateTime.Now UpdatedAt = DateTime.Now
}; };
@ -48,12 +66,12 @@ public class OperationLogService(
logger.LogInformation( logger.LogInformation(
"记录操作日志Operator: {Operator}, Action: {Action}, Target: {TargetType}/{TargetId}", "记录操作日志Operator: {Operator}, Action: {Action}, Target: {TargetType}/{TargetId}",
operatorName, actionType, targetType, targetId); input.OperatorName, input.ActionType, input.TargetType, input.TargetId);
} }
catch (Exception ex) catch (Exception ex)
{ {
// 日志记录不应影响主业务流程 // 日志记录不应影响主业务流程
logger.LogError(ex, "记录操作日志失败Operator: {Operator}, Action: {Action}", operatorName, actionType); logger.LogError(ex, "记录操作日志失败Operator: {Operator}, Action: {Action}", input.OperatorName, input.ActionType);
} }
} }

View File

@ -252,11 +252,17 @@ public class UsersService(
RecordId = result.RecordId RecordId = result.RecordId
}); });
await operationLogService.LogAsync( await operationLogService.LogAsync(new OperationLogRecordInput
operatorId, operatorName, {
OperationLogActionType.ManualAddPoints, OperatorId = operatorId,
OperationLogTargetType.User, OperatorName = operatorName,
userId, user.Name, detail, ipAddress); ActionType = OperationLogActionType.ManualAddPoints,
TargetType = OperationLogTargetType.User,
TargetId = userId,
TargetName = user.Name,
Detail = detail,
IpAddress = ipAddress
});
return new ManualPointsOutput return new ManualPointsOutput
{ {
@ -302,11 +308,17 @@ public class UsersService(
RecordId = result.RecordId RecordId = result.RecordId
}); });
await operationLogService.LogAsync( await operationLogService.LogAsync(new OperationLogRecordInput
operatorId, operatorName, {
OperationLogActionType.ManualDeductPoints, OperatorId = operatorId,
OperationLogTargetType.User, OperatorName = operatorName,
userId, user.Name, detail, ipAddress); ActionType = OperationLogActionType.ManualDeductPoints,
TargetType = OperationLogTargetType.User,
TargetId = userId,
TargetName = user.Name,
Detail = detail,
IpAddress = ipAddress
});
return new ManualPointsOutput return new ManualPointsOutput
{ {

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
@ -76,6 +77,7 @@ public class AdminController : BaseController
/// </summary> /// </summary>
/// <param name="input">修改密码输入</param> /// <param name="input">修改密码输入</param>
/// <returns>修改密码结果</returns> /// <returns>修改密码结果</returns>
[OperationLog(OperationLogActionType.ChangePassword, OperationLogTargetType.AdminUser, UseOperatorAsTargetId = true, LogArguments = false)]
[HttpPost("changePassword")] [HttpPost("changePassword")]
public async Task<BaseResponse<object>> ChangePasswordAsync([FromBody] ChangePasswordInput input) public async Task<BaseResponse<object>> ChangePasswordAsync([FromBody] ChangePasswordInput input)
{ {
@ -94,6 +96,7 @@ public class AdminController : BaseController
/// </summary> /// </summary>
/// <param name="input">管理员输入</param> /// <param name="input">管理员输入</param>
/// <returns>创建的管理员信息</returns> /// <returns>创建的管理员信息</returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.AdminUser, TargetNameProperty = "UserName")]
[HttpPost("users")] [HttpPost("users")]
public async Task<BaseResponse<AdminUserOutput>> CreateUserAsync([FromBody] AdminUserInput input) public async Task<BaseResponse<AdminUserOutput>> CreateUserAsync([FromBody] AdminUserInput input)
{ {
@ -120,6 +123,7 @@ public class AdminController : BaseController
/// <param name="id">管理员ID</param> /// <param name="id">管理员ID</param>
/// <param name="input">管理员输入</param> /// <param name="input">管理员输入</param>
/// <returns>更新后的管理员信息</returns> /// <returns>更新后的管理员信息</returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.AdminUser, TargetNameProperty = "UserName")]
[HttpPut("users/{id}")] [HttpPut("users/{id}")]
public async Task<BaseResponse<AdminUserOutput>> UpdateUserAsync(long id, [FromBody] AdminUserInput input) public async Task<BaseResponse<AdminUserOutput>> UpdateUserAsync(long id, [FromBody] AdminUserInput input)
{ {
@ -145,6 +149,7 @@ public class AdminController : BaseController
/// </summary> /// </summary>
/// <param name="id">管理员ID</param> /// <param name="id">管理员ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.AdminUser)]
[HttpDelete("users/{id}")] [HttpDelete("users/{id}")]
public async Task<BaseResponse<object>> DeleteUserAsync(long id) public async Task<BaseResponse<object>> DeleteUserAsync(long id)
{ {
@ -220,6 +225,7 @@ public class AdminController : BaseController
/// </summary> /// </summary>
/// <param name="id">管理员ID</param> /// <param name="id">管理员ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.AdminUser)]
[HttpPut("users/{id}/status")] [HttpPut("users/{id}/status")]
public async Task<BaseResponse<object>> ToggleUserStatusAsync(long id) public async Task<BaseResponse<object>> ToggleUserStatusAsync(long id)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
@ -28,6 +29,7 @@ public class AiBasePromptController : BaseController
/// </summary> /// </summary>
/// <param name="input">Prompt配置信息</param> /// <param name="input">Prompt配置信息</param>
/// <returns>创建的Prompt配置信息</returns> /// <returns>创建的Prompt配置信息</returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.AiBasePrompt)]
[HttpPost] [HttpPost]
public async Task<BaseResponse<AiBasePromptOutput>> CreateAsync([FromBody] AiBasePromptInput input) public async Task<BaseResponse<AiBasePromptOutput>> CreateAsync([FromBody] AiBasePromptInput input)
{ {
@ -54,6 +56,7 @@ public class AiBasePromptController : BaseController
/// <param name="id">Prompt配置ID</param> /// <param name="id">Prompt配置ID</param>
/// <param name="input">Prompt配置信息</param> /// <param name="input">Prompt配置信息</param>
/// <returns>更新后的Prompt配置信息</returns> /// <returns>更新后的Prompt配置信息</returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.AiBasePrompt)]
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<BaseResponse<AiBasePromptOutput>> UpdateAsync(long id, [FromBody] AiBasePromptInput input) public async Task<BaseResponse<AiBasePromptOutput>> UpdateAsync(long id, [FromBody] AiBasePromptInput input)
{ {
@ -79,6 +82,7 @@ public class AiBasePromptController : BaseController
/// </summary> /// </summary>
/// <param name="id">Prompt配置ID</param> /// <param name="id">Prompt配置ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.AiBasePrompt)]
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<BaseResponse<object>> DeleteAsync(long id) public async Task<BaseResponse<object>> DeleteAsync(long id)
{ {
@ -178,6 +182,7 @@ public class AiBasePromptController : BaseController
/// </summary> /// </summary>
/// <param name="id">Prompt配置ID</param> /// <param name="id">Prompt配置ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.AiBasePrompt)]
[HttpPut("{id}/toggle-status")] [HttpPut("{id}/toggle-status")]
public async Task<BaseResponse<object>> ToggleStatusAsync(long id) public async Task<BaseResponse<object>> ToggleStatusAsync(long id)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Banner; using QYZH.InteractiveMagazine.Models.Dto.Banner;
@ -19,6 +20,7 @@ public class BannerController(IBannerService bannerService) : BaseController
/// </summary> /// </summary>
/// <param name="input">轮播图信息</param> /// <param name="input">轮播图信息</param>
/// <returns>创建后的轮播图信息</returns> /// <returns>创建后的轮播图信息</returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.Banner)]
[HttpPost] [HttpPost]
public async Task<BaseResponse<BannerOutput>> CreateAsync([FromBody] BannerInput input) public async Task<BaseResponse<BannerOutput>> CreateAsync([FromBody] BannerInput input)
{ {
@ -32,6 +34,7 @@ public class BannerController(IBannerService bannerService) : BaseController
/// <param name="id">轮播图ID</param> /// <param name="id">轮播图ID</param>
/// <param name="input">轮播图信息</param> /// <param name="input">轮播图信息</param>
/// <returns>更新后的轮播图信息</returns> /// <returns>更新后的轮播图信息</returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.Banner)]
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<BaseResponse<BannerOutput>> UpdateAsync(long id, [FromBody] BannerInput input) public async Task<BaseResponse<BannerOutput>> UpdateAsync(long id, [FromBody] BannerInput input)
{ {
@ -44,6 +47,7 @@ public class BannerController(IBannerService bannerService) : BaseController
/// </summary> /// </summary>
/// <param name="id">轮播图ID</param> /// <param name="id">轮播图ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.Banner)]
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<BaseResponse<object>> DeleteAsync(long id) public async Task<BaseResponse<object>> DeleteAsync(long id)
{ {
@ -80,6 +84,7 @@ public class BannerController(IBannerService bannerService) : BaseController
/// </summary> /// </summary>
/// <param name="id">轮播图ID</param> /// <param name="id">轮播图ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Banner)]
[HttpPut("{id}/status")] [HttpPut("{id}/status")]
public async Task<BaseResponse<bool>> UpdateStatusAsync(long id) public async Task<BaseResponse<bool>> UpdateStatusAsync(long id)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
@ -29,6 +30,7 @@ public class CheckInConfigController : BaseController
/// </summary> /// </summary>
/// <param name="input">签到配置信息</param> /// <param name="input">签到配置信息</param>
/// <returns>创建的签到配置信息</returns> /// <returns>创建的签到配置信息</returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.CheckInConfig)]
[HttpPost] [HttpPost]
public async Task<BaseResponse<CheckInConfigOutput>> CreateAsync([FromBody] CheckInConfigInput input) public async Task<BaseResponse<CheckInConfigOutput>> CreateAsync([FromBody] CheckInConfigInput input)
{ {
@ -55,6 +57,7 @@ public class CheckInConfigController : BaseController
/// <param name="id">签到配置ID</param> /// <param name="id">签到配置ID</param>
/// <param name="input">签到配置信息</param> /// <param name="input">签到配置信息</param>
/// <returns>更新后的签到配置信息</returns> /// <returns>更新后的签到配置信息</returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.CheckInConfig)]
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<BaseResponse<CheckInConfigOutput>> UpdateAsync(long id, [FromBody] CheckInConfigInput input) public async Task<BaseResponse<CheckInConfigOutput>> UpdateAsync(long id, [FromBody] CheckInConfigInput input)
{ {
@ -80,6 +83,7 @@ public class CheckInConfigController : BaseController
/// </summary> /// </summary>
/// <param name="id">签到配置ID</param> /// <param name="id">签到配置ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.CheckInConfig)]
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<BaseResponse<object>> DeleteAsync(long id) public async Task<BaseResponse<object>> DeleteAsync(long id)
{ {
@ -155,6 +159,7 @@ public class CheckInConfigController : BaseController
/// </summary> /// </summary>
/// <param name="id">签到配置ID</param> /// <param name="id">签到配置ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.CheckInConfig)]
[HttpPut("{id}/status")] [HttpPut("{id}/status")]
public async Task<BaseResponse<bool>> UpdateStatusAsync(long id) public async Task<BaseResponse<bool>> UpdateStatusAsync(long id)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
@ -72,6 +73,7 @@ public class CommunityMessageController : BaseController
/// <summary> /// <summary>
/// 删除消息 /// 删除消息
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.CommunityMessage)]
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<BaseResponse<object>> DeleteAsync(long id) public async Task<BaseResponse<object>> DeleteAsync(long id)
{ {
@ -95,6 +97,7 @@ public class CommunityMessageController : BaseController
/// <summary> /// <summary>
/// 冻结/解冻消息 /// 冻结/解冻消息
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.CommunityMessage)]
[HttpPut("{id}/freeze")] [HttpPut("{id}/freeze")]
public async Task<BaseResponse<object>> FreezeAsync(long id, [FromBody] AdminFreezeInput input) public async Task<BaseResponse<object>> FreezeAsync(long id, [FromBody] AdminFreezeInput input)
{ {
@ -118,6 +121,7 @@ public class CommunityMessageController : BaseController
/// <summary> /// <summary>
/// 设置/取消精选 /// 设置/取消精选
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.CommunityMessage)]
[HttpPut("{id}/featured")] [HttpPut("{id}/featured")]
public async Task<BaseResponse<object>> SetFeaturedAsync(long id, [FromBody] AdminSetFeaturedInput input) public async Task<BaseResponse<object>> SetFeaturedAsync(long id, [FromBody] AdminSetFeaturedInput input)
{ {
@ -141,6 +145,7 @@ public class CommunityMessageController : BaseController
/// <summary> /// <summary>
/// 设置排序权重 /// 设置排序权重
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.CommunityMessage)]
[HttpPut("{id}/sort")] [HttpPut("{id}/sort")]
public async Task<BaseResponse<object>> SetSortOrderAsync(long id, [FromBody] AdminSetSortOrderInput input) public async Task<BaseResponse<object>> SetSortOrderAsync(long id, [FromBody] AdminSetSortOrderInput input)
{ {
@ -164,6 +169,7 @@ public class CommunityMessageController : BaseController
/// <summary> /// <summary>
/// 批量发布消息 /// 批量发布消息
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.CommunityMessage, TargetIdRouteKey = null)]
[HttpPost("batch-publish")] [HttpPost("batch-publish")]
public async Task<BaseResponse<object>> BatchPublishAsync([FromBody] AdminBatchPublishInput input) public async Task<BaseResponse<object>> BatchPublishAsync([FromBody] AdminBatchPublishInput input)
{ {

View File

@ -6,6 +6,7 @@ using QYZH.InteractiveMagazine.Models.Enum;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MiniExcelLibs; using MiniExcelLibs;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.Infrastructure.OSS; using QYZH.InteractiveMagazine.Infrastructure.OSS;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
@ -76,6 +77,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// </summary> /// </summary>
/// <param name="dto">创建杂志</param> /// <param name="dto">创建杂志</param>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.Journal)]
[HttpPost, Route("journal/add")] [HttpPost, Route("journal/add")]
public async Task<BaseResponse<long>> AddAsync([FromBody] JournalAddDto dto) public async Task<BaseResponse<long>> AddAsync([FromBody] JournalAddDto dto)
{ {
@ -88,6 +90,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// </summary> /// </summary>
/// <param name="dto">书籍对象</param> /// <param name="dto">书籍对象</param>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.Journal, TargetIdArgumentName = "dto")]
[HttpPost, Route("journal/update")] [HttpPost, Route("journal/update")]
public async Task<BaseResponse> Edit([FromBody] JournalEditDto dto) public async Task<BaseResponse> Edit([FromBody] JournalEditDto dto)
{ {
@ -100,6 +103,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// </summary> /// </summary>
/// <param name="ids"></param> /// <param name="ids"></param>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.Journal, TargetIdRouteKey = null)]
[HttpPost, Route("journal/delete")] [HttpPost, Route("journal/delete")]
public async Task<BaseResponse<bool>> DeleteAsync([Required][FromBody] List<long> ids) public async Task<BaseResponse<bool>> DeleteAsync([Required][FromBody] List<long> ids)
{ {
@ -111,6 +115,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 书籍起始页 /// 书籍起始页
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.Journal)]
[HttpPost, Route("journal/startpage")] [HttpPost, Route("journal/startpage")]
public async Task<BaseResponse<bool>> StartPageAsync([FromQuery]long id, [FromQuery] int index) public async Task<BaseResponse<bool>> StartPageAsync([FromQuery]long id, [FromQuery] int index)
{ {
@ -122,6 +127,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 书籍归档 /// 书籍归档
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Journal)]
[HttpPost, Route("journal/rchive/{id:long}")] [HttpPost, Route("journal/rchive/{id:long}")]
public async Task<BaseResponse<bool>> Archive(long id) public async Task<BaseResponse<bool>> Archive(long id)
{ {
@ -133,6 +139,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 书籍废弃 /// 书籍废弃
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Journal)]
[HttpPost] [HttpPost]
[HttpPost, Route("journal/abandon/{id:long}")] [HttpPost, Route("journal/abandon/{id:long}")]
public async Task<BaseResponse<bool>> Abandon(long id) public async Task<BaseResponse<bool>> Abandon(long id)
@ -145,6 +152,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 书籍发布 /// 书籍发布
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Journal)]
[HttpPost, Route("journal/publish/{id:long}")] [HttpPost, Route("journal/publish/{id:long}")]
public async Task<BaseResponse<bool>> Publish(long id) public async Task<BaseResponse<bool>> Publish(long id)
{ {
@ -156,6 +164,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 书籍铺码 /// 书籍铺码
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.Journal)]
[HttpPost, Route("journal/printcode/{id:long}")] [HttpPost, Route("journal/printcode/{id:long}")]
public async Task<BaseResponse<DotMatrixOutput>> PrintCodeAsync(long id) public async Task<BaseResponse<DotMatrixOutput>> PrintCodeAsync(long id)
{ {
@ -168,6 +177,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// <param name="input"></param> /// <param name="input"></param>
/// <param name="file"></param> /// <param name="file"></param>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.Journal, TargetIdArgumentName = "input")]
[HttpPost, Route("journal/resultreport")] [HttpPost, Route("journal/resultreport")]
public async Task<BaseResponse<bool>> GenerateAsync([FromBody] DotMatrixNoteJournalReportInput input, [FromForm] IFormFile? file) public async Task<BaseResponse<bool>> GenerateAsync([FromBody] DotMatrixNoteJournalReportInput input, [FromForm] IFormFile? file)
{ {
@ -185,6 +195,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// </summary> /// </summary>
/// <param name="input"></param> /// <param name="input"></param>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.Journal)]
[HttpPost, Route("journal/import")] [HttpPost, Route("journal/import")]
public async Task<BaseResponse<long>> JournalImport(JournalImportDto input) public async Task<BaseResponse<long>> JournalImport(JournalImportDto input)
{ {
@ -266,6 +277,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// <param name="JournalId">file</param> /// <param name="JournalId">file</param>
/// <param name="file">file</param> /// <param name="file">file</param>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.JournalCatalog, TargetIdRouteKey = "JournalId")]
[HttpPost, Route("catalog/import/{JournalId}")] [HttpPost, Route("catalog/import/{JournalId}")]
public async Task<BaseResponse<bool>> ImportAsync(long JournalId, [FromForm(Name = "file")] IFormFile file) public async Task<BaseResponse<bool>> ImportAsync(long JournalId, [FromForm(Name = "file")] IFormFile file)
{ {
@ -283,6 +295,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// </summary> /// </summary>
/// <param name="input"></param> /// <param name="input"></param>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.JournalCatalog)]
[HttpPost, Route("catalog/add")] [HttpPost, Route("catalog/add")]
public async Task<BaseResponse<long>> CatalogAdd(JournalCatalogInput input) public async Task<BaseResponse<long>> CatalogAdd(JournalCatalogInput input)
{ {
@ -294,6 +307,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// </summary> /// </summary>
/// <param name="input"></param> /// <param name="input"></param>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalCatalog, TargetIdArgumentName = "input")]
[HttpPost, Route("catalog/update")] [HttpPost, Route("catalog/update")]
public async Task<BaseResponse<bool>> CatalogUpdate(JournalCatalogUpdateInput input) public async Task<BaseResponse<bool>> CatalogUpdate(JournalCatalogUpdateInput input)
{ {
@ -305,6 +319,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// </summary> /// </summary>
/// <param name="id"></param> /// <param name="id"></param>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.JournalCatalog)]
[HttpPost, Route("catalog/delete/{id:long}")] [HttpPost, Route("catalog/delete/{id:long}")]
public async Task<BaseResponse<bool>> CatalogDelete(long id) public async Task<BaseResponse<bool>> CatalogDelete(long id)
{ {
@ -318,6 +333,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// </summary> /// </summary>
/// <param name="input"></param> /// <param name="input"></param>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalCatalog, TargetIdArgumentName = "input")]
[HttpPost, Route("catalog/move")] [HttpPost, Route("catalog/move")]
public async Task<BaseResponse<bool>> CatalogMove(MoveInput input) public async Task<BaseResponse<bool>> CatalogMove(MoveInput input)
{ {
@ -333,6 +349,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 书页新增 /// 书页新增
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.JournalPage)]
[HttpPost, Route("page/add")] [HttpPost, Route("page/add")]
public async Task<BaseResponse<long>> PageAdd(JournalAddV2Input input) public async Task<BaseResponse<long>> PageAdd(JournalAddV2Input input)
{ {
@ -344,6 +361,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 书页修改 /// 书页修改
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalPage, TargetIdArgumentName = "input")]
[HttpPost, Route("page/update")] [HttpPost, Route("page/update")]
public async Task<BaseResponse<bool>> PageUpdate(PageLayoutInput input) public async Task<BaseResponse<bool>> PageUpdate(PageLayoutInput input)
{ {
@ -355,6 +373,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 自动铺码 /// 自动铺码
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalPage, TargetIdRouteKey = "JournalId")]
[HttpPost, Route("page/updatepageno/{JournalId:long}")] [HttpPost, Route("page/updatepageno/{JournalId:long}")]
public async Task<BaseResponse<bool>> PageUpdatePageNo([Required(ErrorMessage = "书籍编号不允许为空")] long JournalId) public async Task<BaseResponse<bool>> PageUpdatePageNo([Required(ErrorMessage = "书籍编号不允许为空")] long JournalId)
{ {
@ -418,6 +437,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 书页问题新增 /// 书页问题新增
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.JournalPageTask)]
[HttpPost, Route("page/task/add")] [HttpPost, Route("page/task/add")]
[ProducesResponseType(typeof(BaseResponse<long>), 200)] [ProducesResponseType(typeof(BaseResponse<long>), 200)]
public async Task<BaseResponse<long>> PageTaskAdd(JournalPageTaskAddInput input) public async Task<BaseResponse<long>> PageTaskAdd(JournalPageTaskAddInput input)
@ -430,6 +450,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 书页问题更新 /// 书页问题更新
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalPageTask, TargetIdArgumentName = "input")]
[HttpPost, Route("page/task/update")] [HttpPost, Route("page/task/update")]
[ProducesResponseType(typeof(BaseResponse<bool>), 200)] [ProducesResponseType(typeof(BaseResponse<bool>), 200)]
public async Task<BaseResponse<bool>> PageTaskUpdate(JournalPageTaskUpdateInput input) public async Task<BaseResponse<bool>> PageTaskUpdate(JournalPageTaskUpdateInput input)
@ -454,6 +475,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 书页问题删除 /// 书页问题删除
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.JournalPageTask)]
[HttpPost, Route("page/task/delete/{id:long}")] [HttpPost, Route("page/task/delete/{id:long}")]
public async Task<BaseResponse<bool>> PageTaskDelete(long id) public async Task<BaseResponse<bool>> PageTaskDelete(long id)
{ {
@ -465,6 +487,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 书页问题补充 /// 书页问题补充
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalPageTask, TargetIdArgumentName = "input")]
[HttpPost, Route("page/task/complement")] [HttpPost, Route("page/task/complement")]
public async Task<BaseResponse<bool>> ComplementAnsync(JournalPageTaskComplementInput input) public async Task<BaseResponse<bool>> ComplementAnsync(JournalPageTaskComplementInput input)
{ {
@ -476,6 +499,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 答案新增 /// 答案新增
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.JournalTaskAnswer)]
[HttpPost, Route("page/task/answer/add")] [HttpPost, Route("page/task/answer/add")]
public async Task<BaseResponse<long>> AnswerAdd(JournalTaskAnswerAddInput input) public async Task<BaseResponse<long>> AnswerAdd(JournalTaskAnswerAddInput input)
{ {
@ -487,6 +511,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 答案更新 /// 答案更新
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.JournalTaskAnswer, TargetIdArgumentName = "input")]
[HttpPost, Route("page/task/answer/update")] [HttpPost, Route("page/task/answer/update")]
public async Task<BaseResponse<bool>> AnswerUpdate(JournalTaskAnswerUpdateInput input) public async Task<BaseResponse<bool>> AnswerUpdate(JournalTaskAnswerUpdateInput input)
{ {
@ -498,6 +523,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers
/// 答案删除 /// 答案删除
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.JournalTaskAnswer)]
[HttpPost, Route("page/task/answer/delete/{id:long}")] [HttpPost, Route("page/task/answer/delete/{id:long}")]
public async Task<BaseResponse<bool>> AnswerDelete(long id) public async Task<BaseResponse<bool>> AnswerDelete(long id)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Material; using QYZH.InteractiveMagazine.Models.Dto.Material;
@ -19,6 +20,7 @@ public class MaterialController(IMaterialService materialService) : BaseControll
/// </summary> /// </summary>
/// <param name="input">资料信息</param> /// <param name="input">资料信息</param>
/// <returns>创建后的资料信息</returns> /// <returns>创建后的资料信息</returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.Material)]
[HttpPost] [HttpPost]
public async Task<BaseResponse<MaterialOutput>> CreateAsync([FromBody] MaterialInput input) public async Task<BaseResponse<MaterialOutput>> CreateAsync([FromBody] MaterialInput input)
{ {
@ -32,6 +34,7 @@ public class MaterialController(IMaterialService materialService) : BaseControll
/// <param name="id">资料ID</param> /// <param name="id">资料ID</param>
/// <param name="input">资料信息</param> /// <param name="input">资料信息</param>
/// <returns>更新后的资料信息</returns> /// <returns>更新后的资料信息</returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.Material)]
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<BaseResponse<MaterialOutput>> UpdateAsync(long id, [FromBody] MaterialInput input) public async Task<BaseResponse<MaterialOutput>> UpdateAsync(long id, [FromBody] MaterialInput input)
{ {
@ -44,6 +47,7 @@ public class MaterialController(IMaterialService materialService) : BaseControll
/// </summary> /// </summary>
/// <param name="id">资料ID</param> /// <param name="id">资料ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.Material)]
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<BaseResponse<object>> DeleteAsync(long id) public async Task<BaseResponse<object>> DeleteAsync(long id)
{ {
@ -80,6 +84,7 @@ public class MaterialController(IMaterialService materialService) : BaseControll
/// </summary> /// </summary>
/// <param name="id">资料ID</param> /// <param name="id">资料ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Material)]
[HttpPut("{id}/status")] [HttpPut("{id}/status")]
public async Task<BaseResponse<bool>> UpdateStatusAsync(long id) public async Task<BaseResponse<bool>> UpdateStatusAsync(long id)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
@ -28,6 +29,7 @@ public class MedalController : BaseController
/// </summary> /// </summary>
/// <param name="input">勋章信息</param> /// <param name="input">勋章信息</param>
/// <returns>创建的勋章信息</returns> /// <returns>创建的勋章信息</returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.Medal)]
[HttpPost] [HttpPost]
public async Task<BaseResponse<MedalOutput>> CreateAsync([FromBody] MedalInput input) public async Task<BaseResponse<MedalOutput>> CreateAsync([FromBody] MedalInput input)
{ {
@ -54,6 +56,7 @@ public class MedalController : BaseController
/// <param name="id">勋章ID</param> /// <param name="id">勋章ID</param>
/// <param name="input">勋章信息</param> /// <param name="input">勋章信息</param>
/// <returns>更新后的勋章信息</returns> /// <returns>更新后的勋章信息</returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.Medal)]
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<BaseResponse<MedalOutput>> UpdateAsync(long id, [FromBody] MedalInput input) public async Task<BaseResponse<MedalOutput>> UpdateAsync(long id, [FromBody] MedalInput input)
{ {
@ -79,6 +82,7 @@ public class MedalController : BaseController
/// </summary> /// </summary>
/// <param name="id">勋章ID</param> /// <param name="id">勋章ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.Medal)]
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<BaseResponse<object>> DeleteAsync(long id) public async Task<BaseResponse<object>> DeleteAsync(long id)
{ {
@ -155,6 +159,7 @@ public class MedalController : BaseController
/// <param name="id">勋章ID</param> /// <param name="id">勋章ID</param>
/// <param name="status">状态: 0=禁用, 1=启用</param> /// <param name="status">状态: 0=禁用, 1=启用</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Medal)]
[HttpPut("{id}/status")] [HttpPut("{id}/status")]
public async Task<BaseResponse<bool>> UpdateStatusAsync(long id) public async Task<BaseResponse<bool>> UpdateStatusAsync(long id)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
@ -17,6 +18,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService
/// <summary> /// <summary>
/// 创建菜单 /// 创建菜单
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.AdminMenu)]
[HttpPost("menus")] [HttpPost("menus")]
public async Task<BaseResponse<AdminMenuOutput>> CreateMenuAsync([FromBody] AdminMenuInput input) public async Task<BaseResponse<AdminMenuOutput>> CreateMenuAsync([FromBody] AdminMenuInput input)
{ {
@ -27,6 +29,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService
/// <summary> /// <summary>
/// 更新菜单 /// 更新菜单
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.AdminMenu)]
[HttpPut("menus/{id}")] [HttpPut("menus/{id}")]
public async Task<BaseResponse<AdminMenuOutput>> UpdateMenuAsync(long id, [FromBody] AdminMenuInput input) public async Task<BaseResponse<AdminMenuOutput>> UpdateMenuAsync(long id, [FromBody] AdminMenuInput input)
{ {
@ -37,6 +40,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService
/// <summary> /// <summary>
/// 删除菜单 /// 删除菜单
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.AdminMenu)]
[HttpDelete("menus/{id}")] [HttpDelete("menus/{id}")]
public async Task<BaseResponse<object>> DeleteMenuAsync(long id) public async Task<BaseResponse<object>> DeleteMenuAsync(long id)
{ {
@ -70,6 +74,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService
/// <summary> /// <summary>
/// 创建角色 /// 创建角色
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.AdminRole)]
[HttpPost("roles")] [HttpPost("roles")]
public async Task<BaseResponse<AdminRoleOutput>> CreateRoleAsync([FromBody] AdminRoleInput input) public async Task<BaseResponse<AdminRoleOutput>> CreateRoleAsync([FromBody] AdminRoleInput input)
{ {
@ -80,6 +85,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService
/// <summary> /// <summary>
/// 更新角色 /// 更新角色
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.AdminRole)]
[HttpPut("roles/{id}")] [HttpPut("roles/{id}")]
public async Task<BaseResponse<AdminRoleOutput>> UpdateRoleAsync(long id, [FromBody] AdminRoleInput input) public async Task<BaseResponse<AdminRoleOutput>> UpdateRoleAsync(long id, [FromBody] AdminRoleInput input)
{ {
@ -90,6 +96,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService
/// <summary> /// <summary>
/// 删除角色 /// 删除角色
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.AdminRole)]
[HttpDelete("roles/{id}")] [HttpDelete("roles/{id}")]
public async Task<BaseResponse<object>> DeleteRoleAsync(long id) public async Task<BaseResponse<object>> DeleteRoleAsync(long id)
{ {
@ -120,6 +127,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService
/// <summary> /// <summary>
/// 分配角色菜单 /// 分配角色菜单
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Assign, OperationLogTargetType.AdminRole, TargetIdRouteKey = "roleId")]
[HttpPut("roles/{roleId}/menus")] [HttpPut("roles/{roleId}/menus")]
public async Task<BaseResponse<object>> AssignRoleMenusAsync(long roleId, [FromBody] AssignRoleMenusInput input) public async Task<BaseResponse<object>> AssignRoleMenusAsync(long roleId, [FromBody] AssignRoleMenusInput input)
{ {
@ -130,6 +138,7 @@ public class PermissionController(IAdminPermissionService adminPermissionService
/// <summary> /// <summary>
/// 分配管理员角色 /// 分配管理员角色
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Assign, OperationLogTargetType.AdminUser, TargetIdRouteKey = "adminUserId")]
[HttpPut("users/{adminUserId}/roles")] [HttpPut("users/{adminUserId}/roles")]
public async Task<BaseResponse<object>> AssignAdminUserRolesAsync(long adminUserId, [FromBody] AssignAdminUserRolesInput input) public async Task<BaseResponse<object>> AssignAdminUserRolesAsync(long adminUserId, [FromBody] AssignAdminUserRolesInput input)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
@ -29,6 +30,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 创建宠物模板 /// 创建宠物模板
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.Pet)]
[HttpPost] [HttpPost]
public async Task<BaseResponse<PetTemplateOutput>> CreateTemplateAsync([FromBody] PetTemplateInput input) public async Task<BaseResponse<PetTemplateOutput>> CreateTemplateAsync([FromBody] PetTemplateInput input)
{ {
@ -52,6 +54,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 更新宠物模板 /// 更新宠物模板
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.Pet)]
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<BaseResponse<PetTemplateOutput>> UpdateTemplateAsync(long id, [FromBody] PetTemplateInput input) public async Task<BaseResponse<PetTemplateOutput>> UpdateTemplateAsync(long id, [FromBody] PetTemplateInput input)
{ {
@ -75,6 +78,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 删除宠物模板 /// 删除宠物模板
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.Pet)]
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<BaseResponse<object>> DeleteTemplateAsync(long id) public async Task<BaseResponse<object>> DeleteTemplateAsync(long id)
{ {
@ -144,6 +148,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 更新宠物模板状态(启用/禁用) /// 更新宠物模板状态(启用/禁用)
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Pet)]
[HttpPut("{id}/status")] [HttpPut("{id}/status")]
public async Task<BaseResponse<object>> UpdateTemplateStatusAsync(long id) public async Task<BaseResponse<object>> UpdateTemplateStatusAsync(long id)
{ {
@ -169,6 +174,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 创建进化阶段 /// 创建进化阶段
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.PetEvolution)]
[HttpPost("evolution")] [HttpPost("evolution")]
public async Task<BaseResponse<PetEvolutionOutput>> CreateEvolutionAsync([FromBody] PetEvolutionInput input) public async Task<BaseResponse<PetEvolutionOutput>> CreateEvolutionAsync([FromBody] PetEvolutionInput input)
{ {
@ -192,6 +198,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 更新进化阶段 /// 更新进化阶段
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.PetEvolution)]
[HttpPut("evolution/{id}")] [HttpPut("evolution/{id}")]
public async Task<BaseResponse<PetEvolutionOutput>> UpdateEvolutionAsync(long id, [FromBody] PetEvolutionInput input) public async Task<BaseResponse<PetEvolutionOutput>> UpdateEvolutionAsync(long id, [FromBody] PetEvolutionInput input)
{ {
@ -215,6 +222,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 删除进化阶段 /// 删除进化阶段
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.PetEvolution)]
[HttpDelete("evolution/{id}")] [HttpDelete("evolution/{id}")]
public async Task<BaseResponse<object>> DeleteEvolutionAsync(long id) public async Task<BaseResponse<object>> DeleteEvolutionAsync(long id)
{ {
@ -286,6 +294,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 创建皮肤 /// 创建皮肤
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.PetSkin)]
[HttpPost("skin")] [HttpPost("skin")]
public async Task<BaseResponse<PetSkinOutput>> CreateSkinAsync([FromBody] PetSkinInput input) public async Task<BaseResponse<PetSkinOutput>> CreateSkinAsync([FromBody] PetSkinInput input)
{ {
@ -309,6 +318,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 更新皮肤 /// 更新皮肤
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.PetSkin)]
[HttpPut("skin/{id}")] [HttpPut("skin/{id}")]
public async Task<BaseResponse<PetSkinOutput>> UpdateSkinAsync(long id, [FromBody] PetSkinInput input) public async Task<BaseResponse<PetSkinOutput>> UpdateSkinAsync(long id, [FromBody] PetSkinInput input)
{ {
@ -332,6 +342,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 删除皮肤 /// 删除皮肤
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.PetSkin)]
[HttpDelete("skin/{id}")] [HttpDelete("skin/{id}")]
public async Task<BaseResponse<object>> DeleteSkinAsync(long id) public async Task<BaseResponse<object>> DeleteSkinAsync(long id)
{ {
@ -403,6 +414,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 创建皮肤图片 /// 创建皮肤图片
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.PetSkinImage)]
[HttpPost("skin-image")] [HttpPost("skin-image")]
public async Task<BaseResponse<PetSkinImageOutput>> CreateSkinImageAsync([FromBody] PetSkinImageInput input) public async Task<BaseResponse<PetSkinImageOutput>> CreateSkinImageAsync([FromBody] PetSkinImageInput input)
{ {
@ -426,6 +438,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 更新皮肤图片 /// 更新皮肤图片
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.PetSkinImage)]
[HttpPut("skin-image/{id}")] [HttpPut("skin-image/{id}")]
public async Task<BaseResponse<PetSkinImageOutput>> UpdateSkinImageAsync(long id, [FromBody] PetSkinImageInput input) public async Task<BaseResponse<PetSkinImageOutput>> UpdateSkinImageAsync(long id, [FromBody] PetSkinImageInput input)
{ {
@ -449,6 +462,7 @@ public class PetManageController : BaseController
/// <summary> /// <summary>
/// 删除皮肤图片 /// 删除皮肤图片
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.PetSkinImage)]
[HttpDelete("skin-image/{id}")] [HttpDelete("skin-image/{id}")]
public async Task<BaseResponse<object>> DeleteSkinImageAsync(long id) public async Task<BaseResponse<object>> DeleteSkinImageAsync(long id)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
@ -28,6 +29,7 @@ public class ProductController : BaseController
/// </summary> /// </summary>
/// <param name="input">商品信息</param> /// <param name="input">商品信息</param>
/// <returns>创建的商品信息</returns> /// <returns>创建的商品信息</returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.Product)]
[HttpPost] [HttpPost]
public async Task<BaseResponse<ProductOutput>> CreateAsync([FromBody] ProductInput input) public async Task<BaseResponse<ProductOutput>> CreateAsync([FromBody] ProductInput input)
{ {
@ -54,6 +56,7 @@ public class ProductController : BaseController
/// <param name="id">商品ID</param> /// <param name="id">商品ID</param>
/// <param name="input">商品信息</param> /// <param name="input">商品信息</param>
/// <returns>更新后的商品信息</returns> /// <returns>更新后的商品信息</returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.Product)]
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<BaseResponse<ProductOutput>> UpdateAsync(long id, [FromBody] ProductInput input) public async Task<BaseResponse<ProductOutput>> UpdateAsync(long id, [FromBody] ProductInput input)
{ {
@ -79,6 +82,7 @@ public class ProductController : BaseController
/// </summary> /// </summary>
/// <param name="id">商品ID</param> /// <param name="id">商品ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.Product)]
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<BaseResponse<object>> DeleteAsync(long id) public async Task<BaseResponse<object>> DeleteAsync(long id)
{ {
@ -154,6 +158,7 @@ public class ProductController : BaseController
/// </summary> /// </summary>
/// <param name="id">商品ID</param> /// <param name="id">商品ID</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.Product)]
[HttpPut("{id}/status")] [HttpPut("{id}/status")]
public async Task<BaseResponse<object>> UpdateSaleStatusAsync(long id) public async Task<BaseResponse<object>> UpdateSaleStatusAsync(long id)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Models.Enum;
@ -18,6 +19,7 @@ public class UserJournalQrCodeController(IUserJournalService userJournalService)
/// </summary> /// </summary>
/// <param name="input">生成输入</param> /// <param name="input">生成输入</param>
/// <returns>二维码记录</returns> /// <returns>二维码记录</returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.UserJournalQrCode)]
[HttpPost("add")] [HttpPost("add")]
public async Task<BaseResponse<CreateUserJournalQrCodeOutput>> AddAsync([FromBody] CreateUserJournalQrCodeInput input) public async Task<BaseResponse<CreateUserJournalQrCodeOutput>> AddAsync([FromBody] CreateUserJournalQrCodeInput input)
{ {
@ -66,6 +68,7 @@ public class UserJournalQrCodeController(IUserJournalService userJournalService)
/// </summary> /// </summary>
/// <param name="input">删除输入</param> /// <param name="input">删除输入</param>
/// <returns>是否成功</returns> /// <returns>是否成功</returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.UserJournalQrCode, TargetIdRouteKey = null)]
[HttpPost("delete")] [HttpPost("delete")]
public async Task<BaseResponse<bool>> DeleteAsync([FromBody] DeleteUserJournalQrCodeInput input) public async Task<BaseResponse<bool>> DeleteAsync([FromBody] DeleteUserJournalQrCodeInput input)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
@ -82,6 +83,7 @@ public class UsersController : BaseController
/// <summary> /// <summary>
/// 更新用户状态 /// 更新用户状态
/// </summary> /// </summary>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.User)]
[HttpPut("{id}/status")] [HttpPut("{id}/status")]
public async Task<BaseResponse> UpdateStatus(long id) public async Task<BaseResponse> UpdateStatus(long id)
{ {

View File

@ -1,4 +1,3 @@
using Hangfire;
using QYZH.InteractiveMagazine.Infrastructure.OSS; using QYZH.InteractiveMagazine.Infrastructure.OSS;
using QYZH.InteractiveMagazine.Models.Entity; using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Models.Enum;
@ -27,67 +26,73 @@ public class JournalTaskAiScoreJob(
private const int DefaultAiScoreRetryDelayMilliseconds = 1000; private const int DefaultAiScoreRetryDelayMilliseconds = 1000;
private const int DefaultAiMaxConcurrency = 1; private const int DefaultAiMaxConcurrency = 1;
private const long DefaultMaxImageBytes = 10 * 1024 * 1024; private const long DefaultMaxImageBytes = 10 * 1024 * 1024;
private const string InvalidAnswerResult = "作答内容不符合要求";
private const string AiProcessingMessage = "AI批阅中请稍后"; private const string AiProcessingMessage = "AI批阅中请稍后";
private static readonly object AiSemaphoreLock = new(); private static readonly object AiSemaphoreLock = new();
private static readonly object RunWindowLock = new(); private static readonly object RunWindowLock = new();
private static readonly object ExecutionLock = new();
private static SemaphoreSlim? aiSemaphore; private static SemaphoreSlim? aiSemaphore;
private static int aiSemaphoreLimit; private static int aiSemaphoreLimit;
private static DateTime? lastRunAt; private static DateTime? lastRunAt;
private static bool isExecuting;
/// <summary> private static DateTime executionStartedAt;
/// 执行AI批改。
/// </summary>
[DisableConcurrentExecution(1800)]
public void Execute()
{
ExecuteAsync().GetAwaiter().GetResult();
}
/// <summary> /// <summary>
/// 异步执行AI批改。 /// 异步执行AI批改。
/// </summary> /// </summary>
public async Task ExecuteAsync() public async Task ExecuteAsync()
{ {
using var scope = scopeFactory.CreateScope(); var jobInterval = GetJobInterval();
var client = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>(); if (!TryEnterExecutionLock(jobInterval))
var windowEnd = DateTime.Now;
var windowStart = GetWindowStart(windowEnd);
var batchSize = GetPendingAnswerBatchSize();
var pendingAnswers = await client.Queryable<JournalPageTaskUserAnswer>()
.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)
{ {
SetLastRunAt(windowEnd); logger.LogInformation("期刊AI批改任务仍在执行锁内跳过本次调度Interval: {Interval}", jobInterval);
logger.LogInformation("期刊AI批改任务没有待处理答案WindowStart: {WindowStart}, WindowEnd: {WindowEnd}", windowStart, windowEnd);
return; return;
} }
logger.LogInformation("期刊AI批改任务开始Count: {Count}, WindowStart: {WindowStart}, WindowEnd: {WindowEnd}", pendingAnswers.Count, windowStart, windowEnd); try
foreach (var pendingAnswer in pendingAnswers)
{ {
try using var scope = scopeFactory.CreateScope();
var client = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
var windowEnd = DateTime.Now;
var windowStart = GetWindowStart(windowEnd, jobInterval);
var batchSize = GetPendingAnswerBatchSize();
var pendingAnswers = await client.Queryable<JournalPageTaskUserAnswer>()
.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( private async Task ProcessPendingAnswerAsync(
ISqlSugarClient client, ISqlSugarClient client,
long answerId, long answerId,
@ -410,16 +415,17 @@ public class JournalTaskAiScoreJob(
prompt.AppendLine($" 判断力上限:{context.Task.Judgment}"); prompt.AppendLine($" 判断力上限:{context.Task.Judgment}");
prompt.AppendLine($" 表达力上限:{context.Task.Expression}"); prompt.AppendLine($" 表达力上限:{context.Task.Expression}");
prompt.AppendLine($" 说服力上限:{context.Task.Persuasiveness}"); prompt.AppendLine($" 说服力上限:{context.Task.Persuasiveness}");
prompt.AppendLine($" 分数上线100");
} }
prompt.AppendLine(); prompt.AppendLine();
prompt.AppendLine("评分要求:"); prompt.AppendLine("评分要求:");
prompt.AppendLine("- 不得超过题目配置中的各项上限。"); prompt.AppendLine("- 不得超过题目配置中的各项上限。");
prompt.AppendLine("- 看不清、缺页、无法识别或答案明显不完整时,降低 Completion不要猜测高分。"); prompt.AppendLine("- 看不清、缺页、无法识别或答案明显不完整时,尽量猜测让分数偏高。");
prompt.AppendLine("- Completion 表示作答完整度,范围 0-100。"); 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("若作答内容与题目要求不符、答非所问、空白、仅抄题或无法形成有效答案Score/GrowthPoint/Points/Comprehension/Judgment/Expression/Persuasiveness 均返回 0。");
prompt.AppendLine("若作答内容不符合要求Result 必须且只能返回:作答内容不符合要求。不要补充原因、建议或其他文字。"); prompt.AppendLine("若作答内容不符合要求Result 提示偏离要求,并补充原因、建议或其他文字。");
prompt.AppendLine(); prompt.AppendLine();
prompt.AppendLine("只返回如下 JSON 字段:"); prompt.AppendLine("只返回如下 JSON 字段:");
prompt.AppendLine("{"); prompt.AppendLine("{");
@ -751,12 +757,6 @@ public class JournalTaskAiScoreJob(
private static string NormalizeResult(string? result) private static string NormalizeResult(string? result)
{ {
var trimmedResult = TrimResult(result); var trimmedResult = TrimResult(result);
if (trimmedResult.Contains("不符合要求", StringComparison.Ordinal) ||
trimmedResult.Contains("不符", StringComparison.Ordinal))
{
return InvalidAnswerResult;
}
return trimmedResult; return trimmedResult;
} }
@ -890,23 +890,119 @@ public class JournalTaskAiScoreJob(
} }
} }
private int GetPendingAnswerMinutes()
{
var minutes = configuration.GetValue<int>("AiChat:PendingAnswerMinutes");
return minutes > 0 ? minutes : DefaultPendingAnswerMinutes;
}
private int GetPendingAnswerBatchSize() private int GetPendingAnswerBatchSize()
{ {
var batchSize = configuration.GetValue<int>("AiChat:PendingAnswerBatchSize"); var batchSize = configuration.GetValue<int>("AiChat:PendingAnswerBatchSize");
return batchSize > 0 ? batchSize : DefaultPendingAnswerBatchSize; 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<int>("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) 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;
} }
} }

View File

@ -123,9 +123,22 @@ if (jobSettings?.Jobs != null)
var param = Expression.Parameter(jobType, "job"); var param = Expression.Parameter(jobType, "job");
var call = Expression.Call(param, method); var call = Expression.Call(param, method);
var lambda = Expression.Lambda<Action>(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); Log.Information("定时任务 [{Name}] 已注册Cron: {Cron}", job.Name, job.Cron);
} }
} }
@ -133,3 +146,27 @@ if (jobSettings?.Jobs != null)
Log.Information("WorkService 已启动Hangfire Dashboard: /hangfire"); Log.Information("WorkService 已启动Hangfire Dashboard: /hangfire");
app.Run(); 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]);
}

View File

@ -51,8 +51,8 @@
{ {
"Name": "journal-task-ai-score-job", "Name": "journal-task-ai-score-job",
"JobType": "QYZH.InteractiveMagazine.WorkService.Jobs.JournalTaskAiScoreJob", "JobType": "QYZH.InteractiveMagazine.WorkService.Jobs.JournalTaskAiScoreJob",
"MethodName": "Execute", "MethodName": "ExecuteAsync",
"Cron": "*/30 * * * *", "Cron": "*/5 * * * *",
"Enabled": true, "Enabled": true,
"Description": "每30分钟扫描上次执行到本次执行之间的期刊答题记录并提交AI批改" "Description": "每30分钟扫描上次执行到本次执行之间的期刊答题记录并提交AI批改"
} }