feat: 系统参数应用层与验证

This commit is contained in:
2025-12-02 12:53:06 +08:00
parent dd15a88ff4
commit 3b2b376787
18 changed files with 771 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
using MediatR;
using TakeoutSaaS.Application.App.SystemParameters.Dto;
namespace TakeoutSaaS.Application.App.SystemParameters.Commands;
/// <summary>
/// 创建系统参数命令。
/// </summary>
public sealed class CreateSystemParameterCommand : IRequest<SystemParameterDto>
{
/// <summary>
/// 参数键。
/// </summary>
public string Key { get; set; } = string.Empty;
/// <summary>
/// 参数值。
/// </summary>
public string Value { get; set; } = string.Empty;
/// <summary>
/// 描述。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 排序值。
/// </summary>
public int SortOrder { get; set; } = 100;
/// <summary>
/// 是否启用。
/// </summary>
public bool IsEnabled { get; set; } = true;
}

View File

@@ -0,0 +1,14 @@
using MediatR;
namespace TakeoutSaaS.Application.App.SystemParameters.Commands;
/// <summary>
/// 删除系统参数命令。
/// </summary>
public sealed record DeleteSystemParameterCommand : IRequest<bool>
{
/// <summary>
/// 参数 ID。
/// </summary>
public long ParameterId { get; init; }
}

View File

@@ -0,0 +1,40 @@
using MediatR;
using TakeoutSaaS.Application.App.SystemParameters.Dto;
namespace TakeoutSaaS.Application.App.SystemParameters.Commands;
/// <summary>
/// 更新系统参数命令。
/// </summary>
public sealed record UpdateSystemParameterCommand : IRequest<SystemParameterDto?>
{
/// <summary>
/// 参数 ID。
/// </summary>
public long ParameterId { get; init; }
/// <summary>
/// 参数键。
/// </summary>
public string Key { get; init; } = string.Empty;
/// <summary>
/// 参数值。
/// </summary>
public string Value { get; init; } = string.Empty;
/// <summary>
/// 描述。
/// </summary>
public string? Description { get; init; }
/// <summary>
/// 排序值。
/// </summary>
public int SortOrder { get; init; } = 100;
/// <summary>
/// 是否启用。
/// </summary>
public bool IsEnabled { get; init; } = true;
}

View File

@@ -0,0 +1,57 @@
using System.Text.Json.Serialization;
using TakeoutSaaS.Shared.Abstractions.Serialization;
namespace TakeoutSaaS.Application.App.SystemParameters.Dto;
/// <summary>
/// 系统参数 DTO。
/// </summary>
public sealed class SystemParameterDto
{
/// <summary>
/// 参数 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
/// <summary>
/// 租户 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long TenantId { get; init; }
/// <summary>
/// 参数键。
/// </summary>
public string Key { get; init; } = string.Empty;
/// <summary>
/// 参数值。
/// </summary>
public string Value { get; init; } = string.Empty;
/// <summary>
/// 描述。
/// </summary>
public string? Description { get; init; }
/// <summary>
/// 排序值。
/// </summary>
public int SortOrder { get; init; }
/// <summary>
/// 是否启用。
/// </summary>
public bool IsEnabled { get; init; }
/// <summary>
/// 创建时间。
/// </summary>
public DateTime CreatedAt { get; init; }
/// <summary>
/// 最近更新时间。
/// </summary>
public DateTime? UpdatedAt { get; init; }
}

View File

@@ -0,0 +1,64 @@
using MediatR;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.SystemParameters.Commands;
using TakeoutSaaS.Application.App.SystemParameters.Dto;
using TakeoutSaaS.Domain.SystemParameters.Entities;
using TakeoutSaaS.Domain.SystemParameters.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
namespace TakeoutSaaS.Application.App.SystemParameters.Handlers;
/// <summary>
/// 创建系统参数命令处理器。
/// </summary>
public sealed class CreateSystemParameterCommandHandler(
ISystemParameterRepository repository,
ILogger<CreateSystemParameterCommandHandler> logger)
: IRequestHandler<CreateSystemParameterCommand, SystemParameterDto>
{
private readonly ISystemParameterRepository _repository = repository;
private readonly ILogger<CreateSystemParameterCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<SystemParameterDto> Handle(CreateSystemParameterCommand request, CancellationToken cancellationToken)
{
// 1. 唯一性校验
var existing = await _repository.FindByKeyAsync(request.Key, cancellationToken);
if (existing != null)
{
throw new BusinessException(ErrorCodes.Conflict, "系统参数键已存在");
}
// 2. 构建实体
var parameter = new SystemParameter
{
Key = request.Key.Trim(),
Value = request.Value.Trim(),
Description = request.Description?.Trim(),
SortOrder = request.SortOrder,
IsEnabled = request.IsEnabled
};
// 3. 持久化
await _repository.AddAsync(parameter, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("创建系统参数 {Key}", parameter.Key);
// 4. 映射返回
return MapToDto(parameter);
}
private static SystemParameterDto MapToDto(SystemParameter parameter) => new()
{
Id = parameter.Id,
TenantId = parameter.TenantId,
Key = parameter.Key,
Value = parameter.Value,
Description = parameter.Description,
SortOrder = parameter.SortOrder,
IsEnabled = parameter.IsEnabled,
CreatedAt = parameter.CreatedAt,
UpdatedAt = parameter.UpdatedAt
};
}

View File

@@ -0,0 +1,33 @@
using MediatR;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.SystemParameters.Commands;
using TakeoutSaaS.Domain.SystemParameters.Repositories;
namespace TakeoutSaaS.Application.App.SystemParameters.Handlers;
/// <summary>
/// 删除系统参数命令处理器。
/// </summary>
public sealed class DeleteSystemParameterCommandHandler(
ISystemParameterRepository repository,
ILogger<DeleteSystemParameterCommandHandler> logger)
: IRequestHandler<DeleteSystemParameterCommand, bool>
{
private readonly ISystemParameterRepository _repository = repository;
private readonly ILogger<DeleteSystemParameterCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<bool> Handle(DeleteSystemParameterCommand request, CancellationToken cancellationToken)
{
var existing = await _repository.FindByIdAsync(request.ParameterId, cancellationToken);
if (existing == null)
{
return false;
}
await _repository.RemoveAsync(existing, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("删除系统参数 {Key}", existing.Key);
return true;
}
}

View File

@@ -0,0 +1,35 @@
using MediatR;
using TakeoutSaaS.Application.App.SystemParameters.Dto;
using TakeoutSaaS.Application.App.SystemParameters.Queries;
using TakeoutSaaS.Domain.SystemParameters.Repositories;
namespace TakeoutSaaS.Application.App.SystemParameters.Handlers;
/// <summary>
/// 获取系统参数详情查询处理器。
/// </summary>
public sealed class GetSystemParameterByIdQueryHandler(ISystemParameterRepository repository)
: IRequestHandler<GetSystemParameterByIdQuery, SystemParameterDto?>
{
private readonly ISystemParameterRepository _repository = repository;
/// <inheritdoc />
public async Task<SystemParameterDto?> Handle(GetSystemParameterByIdQuery request, CancellationToken cancellationToken)
{
var parameter = await _repository.FindByIdAsync(request.ParameterId, cancellationToken);
return parameter == null ? null : MapToDto(parameter);
}
private static SystemParameterDto MapToDto(Domain.SystemParameters.Entities.SystemParameter parameter) => new()
{
Id = parameter.Id,
TenantId = parameter.TenantId,
Key = parameter.Key,
Value = parameter.Value,
Description = parameter.Description,
SortOrder = parameter.SortOrder,
IsEnabled = parameter.IsEnabled,
CreatedAt = parameter.CreatedAt,
UpdatedAt = parameter.UpdatedAt
};
}

View File

@@ -0,0 +1,69 @@
using MediatR;
using TakeoutSaaS.Application.App.SystemParameters.Dto;
using TakeoutSaaS.Application.App.SystemParameters.Queries;
using TakeoutSaaS.Domain.SystemParameters.Repositories;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.App.SystemParameters.Handlers;
/// <summary>
/// 系统参数列表查询处理器。
/// </summary>
public sealed class SearchSystemParametersQueryHandler(ISystemParameterRepository repository)
: IRequestHandler<SearchSystemParametersQuery, PagedResult<SystemParameterDto>>
{
private readonly ISystemParameterRepository _repository = repository;
/// <inheritdoc />
public async Task<PagedResult<SystemParameterDto>> Handle(SearchSystemParametersQuery request, CancellationToken cancellationToken)
{
var parameters = await _repository.SearchAsync(request.Keyword, request.IsEnabled, cancellationToken);
var sorted = ApplySorting(parameters, request.SortBy, request.SortDescending);
var paged = sorted
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToList();
var items = paged.Select(MapToDto).ToList();
return new PagedResult<SystemParameterDto>(items, request.Page, request.PageSize, parameters.Count);
}
private static IOrderedEnumerable<Domain.SystemParameters.Entities.SystemParameter> ApplySorting(
IReadOnlyCollection<Domain.SystemParameters.Entities.SystemParameter> parameters,
string? sortBy,
bool sortDescending)
{
return sortBy?.ToLowerInvariant() switch
{
"key" => sortDescending
? parameters.OrderByDescending(x => x.Key)
: parameters.OrderBy(x => x.Key),
"sortorder" => sortDescending
? parameters.OrderByDescending(x => x.SortOrder)
: parameters.OrderBy(x => x.SortOrder),
"updatedat" => sortDescending
? parameters.OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt)
: parameters.OrderBy(x => x.UpdatedAt ?? x.CreatedAt),
"isenabled" => sortDescending
? parameters.OrderByDescending(x => x.IsEnabled)
: parameters.OrderBy(x => x.IsEnabled),
_ => sortDescending
? parameters.OrderByDescending(x => x.CreatedAt)
: parameters.OrderBy(x => x.CreatedAt)
};
}
private static SystemParameterDto MapToDto(Domain.SystemParameters.Entities.SystemParameter parameter) => new()
{
Id = parameter.Id,
TenantId = parameter.TenantId,
Key = parameter.Key,
Value = parameter.Value,
Description = parameter.Description,
SortOrder = parameter.SortOrder,
IsEnabled = parameter.IsEnabled,
CreatedAt = parameter.CreatedAt,
UpdatedAt = parameter.UpdatedAt
};
}

View File

@@ -0,0 +1,66 @@
using MediatR;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.SystemParameters.Commands;
using TakeoutSaaS.Application.App.SystemParameters.Dto;
using TakeoutSaaS.Domain.SystemParameters.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
namespace TakeoutSaaS.Application.App.SystemParameters.Handlers;
/// <summary>
/// 更新系统参数命令处理器。
/// </summary>
public sealed class UpdateSystemParameterCommandHandler(
ISystemParameterRepository repository,
ILogger<UpdateSystemParameterCommandHandler> logger)
: IRequestHandler<UpdateSystemParameterCommand, SystemParameterDto?>
{
private readonly ISystemParameterRepository _repository = repository;
private readonly ILogger<UpdateSystemParameterCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<SystemParameterDto?> Handle(UpdateSystemParameterCommand request, CancellationToken cancellationToken)
{
// 1. 读取已有参数
var existing = await _repository.FindByIdAsync(request.ParameterId, cancellationToken);
if (existing == null)
{
return null;
}
// 2. 唯一性校验
var duplicate = await _repository.FindByKeyAsync(request.Key, cancellationToken);
if (duplicate != null && duplicate.Id != existing.Id)
{
throw new BusinessException(ErrorCodes.Conflict, "系统参数键已存在");
}
// 3. 更新字段
existing.Key = request.Key.Trim();
existing.Value = request.Value.Trim();
existing.Description = request.Description?.Trim();
existing.SortOrder = request.SortOrder;
existing.IsEnabled = request.IsEnabled;
// 4. 持久化
await _repository.UpdateAsync(existing, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("更新系统参数 {Key}", existing.Key);
return MapToDto(existing);
}
private static SystemParameterDto MapToDto(Domain.SystemParameters.Entities.SystemParameter parameter) => new()
{
Id = parameter.Id,
TenantId = parameter.TenantId,
Key = parameter.Key,
Value = parameter.Value,
Description = parameter.Description,
SortOrder = parameter.SortOrder,
IsEnabled = parameter.IsEnabled,
CreatedAt = parameter.CreatedAt,
UpdatedAt = parameter.UpdatedAt
};
}

View File

@@ -0,0 +1,9 @@
using MediatR;
using TakeoutSaaS.Application.App.SystemParameters.Dto;
namespace TakeoutSaaS.Application.App.SystemParameters.Queries;
/// <summary>
/// 获取系统参数详情查询。
/// </summary>
public sealed record GetSystemParameterByIdQuery(long ParameterId) : IRequest<SystemParameterDto?>;

View File

@@ -0,0 +1,41 @@
using MediatR;
using TakeoutSaaS.Application.App.SystemParameters.Dto;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.App.SystemParameters.Queries;
/// <summary>
/// 系统参数列表查询。
/// </summary>
public sealed class SearchSystemParametersQuery : IRequest<PagedResult<SystemParameterDto>>
{
/// <summary>
/// 关键字(匹配 Key/Description
/// </summary>
public string? Keyword { get; init; }
/// <summary>
/// 启用状态过滤。
/// </summary>
public bool? IsEnabled { get; init; }
/// <summary>
/// 页码。
/// </summary>
public int Page { get; init; } = 1;
/// <summary>
/// 每页条数。
/// </summary>
public int PageSize { get; init; } = 20;
/// <summary>
/// 排序字段key/sortOrder/createdAt/updatedAt/isEnabled
/// </summary>
public string? SortBy { get; init; }
/// <summary>
/// 是否倒序。
/// </summary>
public bool SortDescending { get; init; } = true;
}

View File

@@ -0,0 +1,21 @@
using FluentValidation;
using TakeoutSaaS.Application.App.SystemParameters.Commands;
namespace TakeoutSaaS.Application.App.SystemParameters.Validators;
/// <summary>
/// 创建系统参数命令验证器。
/// </summary>
public sealed class CreateSystemParameterCommandValidator : AbstractValidator<CreateSystemParameterCommand>
{
/// <summary>
/// 初始化验证规则。
/// </summary>
public CreateSystemParameterCommandValidator()
{
RuleFor(x => x.Key).NotEmpty().MaximumLength(128);
RuleFor(x => x.Value).NotEmpty();
RuleFor(x => x.Description).MaximumLength(512);
RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
}
}

View File

@@ -0,0 +1,21 @@
using FluentValidation;
using TakeoutSaaS.Application.App.SystemParameters.Queries;
namespace TakeoutSaaS.Application.App.SystemParameters.Validators;
/// <summary>
/// 系统参数列表查询验证器。
/// </summary>
public sealed class SearchSystemParametersQueryValidator : AbstractValidator<SearchSystemParametersQuery>
{
/// <summary>
/// 初始化验证规则。
/// </summary>
public SearchSystemParametersQueryValidator()
{
RuleFor(x => x.Page).GreaterThan(0);
RuleFor(x => x.PageSize).InclusiveBetween(1, 200);
RuleFor(x => x.Keyword).MaximumLength(256);
RuleFor(x => x.SortBy).MaximumLength(64);
}
}

View File

@@ -0,0 +1,22 @@
using FluentValidation;
using TakeoutSaaS.Application.App.SystemParameters.Commands;
namespace TakeoutSaaS.Application.App.SystemParameters.Validators;
/// <summary>
/// 更新系统参数命令验证器。
/// </summary>
public sealed class UpdateSystemParameterCommandValidator : AbstractValidator<UpdateSystemParameterCommand>
{
/// <summary>
/// 初始化验证规则。
/// </summary>
public UpdateSystemParameterCommandValidator()
{
RuleFor(x => x.ParameterId).GreaterThan(0);
RuleFor(x => x.Key).NotEmpty().MaximumLength(128);
RuleFor(x => x.Value).NotEmpty();
RuleFor(x => x.Description).MaximumLength(512);
RuleFor(x => x.SortOrder).GreaterThanOrEqualTo(0);
}
}