feat: 实现字典管理后端
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
namespace TakeoutSaaS.AdminApi.Contracts.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// 字典导出请求。
|
||||
/// </summary>
|
||||
public sealed record DictionaryExportRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出格式(csv/json)。
|
||||
/// </summary>
|
||||
public string? Format { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Contracts.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// 字典导入表单请求。
|
||||
/// </summary>
|
||||
public sealed record DictionaryImportFormRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 导入文件。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required IFormFile File { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 冲突解决模式(Skip/Overwrite/Append)。
|
||||
/// </summary>
|
||||
public string? ConflictMode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件格式(csv/json)。
|
||||
/// </summary>
|
||||
public string? Format { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
using TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.Dictionary.Caching;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存监控指标接口。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Roles = "PlatformAdmin")]
|
||||
[Route("api/admin/v{version:apiVersion}/dictionary/metrics")]
|
||||
public sealed class CacheMetricsController(
|
||||
CacheMetricsCollector metricsCollector,
|
||||
ICacheInvalidationLogRepository invalidationLogRepository)
|
||||
: BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取缓存统计信息。
|
||||
/// </summary>
|
||||
[HttpGet("cache-stats")]
|
||||
[ProducesResponseType(typeof(ApiResponse<CacheStatsSnapshot>), StatusCodes.Status200OK)]
|
||||
public ApiResponse<CacheStatsSnapshot> GetCacheStats([FromQuery] string? timeRange = "1h")
|
||||
{
|
||||
var window = timeRange?.ToLowerInvariant() switch
|
||||
{
|
||||
"24h" => TimeSpan.FromHours(24),
|
||||
"7d" => TimeSpan.FromDays(7),
|
||||
_ => TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
var snapshot = metricsCollector.GetSnapshot(window);
|
||||
return ApiResponse<CacheStatsSnapshot>.Ok(snapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存失效事件列表。
|
||||
/// </summary>
|
||||
[HttpGet("invalidation-events")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<CacheInvalidationLog>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<CacheInvalidationLog>>> GetInvalidationEvents(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
[FromQuery] DateTime? startDate = null,
|
||||
[FromQuery] DateTime? endDate = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var safePage = page <= 0 ? 1 : page;
|
||||
var safePageSize = pageSize <= 0 ? 20 : pageSize;
|
||||
|
||||
var (items, total) = await invalidationLogRepository.GetPagedAsync(
|
||||
safePage,
|
||||
safePageSize,
|
||||
startDate,
|
||||
endDate,
|
||||
cancellationToken);
|
||||
|
||||
var result = new PagedResult<CacheInvalidationLog>(items, safePage, safePageSize, total);
|
||||
return ApiResponse<PagedResult<CacheInvalidationLog>>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using TakeoutSaaS.AdminApi.Contracts.Requests;
|
||||
using TakeoutSaaS.Application.Dictionary.Contracts;
|
||||
using TakeoutSaaS.Application.Dictionary.Models;
|
||||
using TakeoutSaaS.Application.Dictionary.Services;
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 字典分组管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/dictionary/groups")]
|
||||
public sealed class DictionaryGroupsController(
|
||||
DictionaryCommandService commandService,
|
||||
DictionaryQueryService queryService,
|
||||
DictionaryImportExportService importExportService)
|
||||
: BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询字典分组。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("dictionary:group:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<DictionaryGroupDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<DictionaryGroupDto>>> List([FromQuery] DictionaryGroupQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await queryService.GetGroupsAsync(query, cancellationToken);
|
||||
return ApiResponse<PagedResult<DictionaryGroupDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典分组详情。
|
||||
/// </summary>
|
||||
[HttpGet("{groupId:long}")]
|
||||
[PermissionAuthorize("dictionary:group:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<DictionaryGroupDto>> Detail(long groupId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await queryService.GetGroupByIdAsync(groupId, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<DictionaryGroupDto>.Error(ErrorCodes.NotFound, "字典分组不存在")
|
||||
: ApiResponse<DictionaryGroupDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建字典分组。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("dictionary:group:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryGroupDto>> Create([FromBody] CreateDictionaryGroupRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await commandService.CreateGroupAsync(request, cancellationToken);
|
||||
return ApiResponse<DictionaryGroupDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新字典分组。
|
||||
/// </summary>
|
||||
[HttpPut("{groupId:long}")]
|
||||
[PermissionAuthorize("dictionary:group:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryGroupDto>> Update(long groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await commandService.UpdateGroupAsync(groupId, request, cancellationToken);
|
||||
return ApiResponse<DictionaryGroupDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除字典分组。
|
||||
/// </summary>
|
||||
[HttpDelete("{groupId:long}")]
|
||||
[PermissionAuthorize("dictionary:group:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long groupId, CancellationToken cancellationToken)
|
||||
{
|
||||
var success = await commandService.DeleteGroupAsync(groupId, cancellationToken);
|
||||
return success
|
||||
? ApiResponse.Success()
|
||||
: ApiResponse.Error(ErrorCodes.NotFound, "字典分组不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出字典分组数据。
|
||||
/// </summary>
|
||||
[HttpPost("{groupId:long}/export")]
|
||||
[PermissionAuthorize("dictionary:group:read")]
|
||||
[Produces("application/octet-stream")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Export(long groupId, [FromBody] DictionaryExportRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var format = NormalizeFormat(request.Format);
|
||||
await using var stream = new MemoryStream();
|
||||
|
||||
if (format == "json")
|
||||
{
|
||||
await importExportService.ExportToJsonAsync(groupId, stream, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await importExportService.ExportToCsvAsync(groupId, stream, cancellationToken);
|
||||
}
|
||||
|
||||
var extension = format == "json" ? "json" : "csv";
|
||||
var fileName = $"dictionary_{groupId}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.{extension}";
|
||||
Response.Headers[HeaderNames.ContentDisposition] = new ContentDispositionHeaderValue("attachment")
|
||||
{
|
||||
FileName = fileName,
|
||||
FileNameStar = fileName
|
||||
}.ToString();
|
||||
|
||||
var contentType = format == "json" ? MediaTypeNames.Application.Json : "text/csv";
|
||||
return File(stream.ToArray(), contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入字典分组数据。
|
||||
/// </summary>
|
||||
[HttpPost("{groupId:long}/import")]
|
||||
[PermissionAuthorize("dictionary:item:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryImportResultDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryImportResultDto>> Import(
|
||||
long groupId,
|
||||
[FromForm] DictionaryImportFormRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.File.Length > 10 * 1024 * 1024)
|
||||
{
|
||||
return ApiResponse<DictionaryImportResultDto>.Error(ErrorCodes.BadRequest, "导入文件不能超过 10MB");
|
||||
}
|
||||
|
||||
var format = NormalizeFormat(request.Format);
|
||||
var conflictMode = ParseConflictMode(request.ConflictMode);
|
||||
|
||||
await using var stream = request.File.OpenReadStream();
|
||||
var importRequest = new DictionaryImportRequest
|
||||
{
|
||||
GroupId = groupId,
|
||||
FileName = request.File.FileName,
|
||||
FileSize = request.File.Length,
|
||||
ConflictMode = conflictMode,
|
||||
FileStream = stream
|
||||
};
|
||||
|
||||
var result = format == "json"
|
||||
? await importExportService.ImportFromJsonAsync(importRequest, cancellationToken)
|
||||
: await importExportService.ImportFromCsvAsync(importRequest, cancellationToken);
|
||||
|
||||
return ApiResponse<DictionaryImportResultDto>.Ok(result);
|
||||
}
|
||||
|
||||
private static string NormalizeFormat(string? format)
|
||||
{
|
||||
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "json";
|
||||
}
|
||||
|
||||
return "csv";
|
||||
}
|
||||
|
||||
private static ConflictResolutionMode ParseConflictMode(string? conflictMode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(conflictMode))
|
||||
{
|
||||
return ConflictResolutionMode.Skip;
|
||||
}
|
||||
|
||||
return Enum.TryParse<ConflictResolutionMode>(conflictMode, ignoreCase: true, out var mode)
|
||||
? mode
|
||||
: ConflictResolutionMode.Skip;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.Dictionary.Contracts;
|
||||
using TakeoutSaaS.Application.Dictionary.Models;
|
||||
using TakeoutSaaS.Application.Dictionary.Services;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 字典项管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/dictionary/groups/{groupId:long}/items")]
|
||||
public sealed class DictionaryItemsController(
|
||||
DictionaryCommandService commandService,
|
||||
DictionaryQueryService queryService)
|
||||
: BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询字典项列表。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("dictionary:group:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<DictionaryItemDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<DictionaryItemDto>>> List(long groupId, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await queryService.GetItemsByGroupIdAsync(groupId, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<DictionaryItemDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建字典项。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[PermissionAuthorize("dictionary:item:create")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryItemDto>> Create(long groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
request.GroupId = groupId;
|
||||
var result = await commandService.CreateItemAsync(request, cancellationToken);
|
||||
return ApiResponse<DictionaryItemDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新字典项。
|
||||
/// </summary>
|
||||
[HttpPut("{itemId:long}")]
|
||||
[PermissionAuthorize("dictionary:item:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<DictionaryItemDto>> Update(long groupId, long itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = groupId;
|
||||
var result = await commandService.UpdateItemAsync(itemId, request, cancellationToken);
|
||||
return ApiResponse<DictionaryItemDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除字典项。
|
||||
/// </summary>
|
||||
[HttpDelete("{itemId:long}")]
|
||||
[PermissionAuthorize("dictionary:item:delete")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Delete(long groupId, long itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = groupId;
|
||||
var success = await commandService.DeleteItemAsync(itemId, cancellationToken);
|
||||
return success
|
||||
? ApiResponse.Success()
|
||||
: ApiResponse.Error(ErrorCodes.NotFound, "字典项不存在");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TakeoutSaaS.Application.Dictionary.Contracts;
|
||||
using TakeoutSaaS.Application.Dictionary.Models;
|
||||
using TakeoutSaaS.Application.Dictionary.Services;
|
||||
using TakeoutSaaS.Module.Authorization.Attributes;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 租户字典覆盖配置管理。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/admin/v{version:apiVersion}/dictionary/overrides")]
|
||||
public sealed class DictionaryOverridesController(
|
||||
DictionaryOverrideService overrideService,
|
||||
ITenantProvider tenantProvider)
|
||||
: BaseApiController
|
||||
{
|
||||
private const string TenantIdHeaderName = "X-Tenant-Id";
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前租户的覆盖配置列表。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[PermissionAuthorize("dictionary:override:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<OverrideConfigDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<OverrideConfigDto>>> List(CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<IReadOnlyList<OverrideConfigDto>>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var result = await overrideService.GetOverridesAsync(tenantId, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<OverrideConfigDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定字典分组的覆盖配置。
|
||||
/// </summary>
|
||||
[HttpGet("{groupCode}")]
|
||||
[PermissionAuthorize("dictionary:override:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<OverrideConfigDto>> Detail(string groupCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<OverrideConfigDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var result = await overrideService.GetOverrideAsync(tenantId, groupCode, cancellationToken);
|
||||
return result == null
|
||||
? ApiResponse<OverrideConfigDto>.Error(ErrorCodes.NotFound, "覆盖配置不存在")
|
||||
: ApiResponse<OverrideConfigDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用覆盖模式。
|
||||
/// </summary>
|
||||
[HttpPost("{groupCode}/enable")]
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<OverrideConfigDto>> Enable(string groupCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<OverrideConfigDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var result = await overrideService.EnableOverrideAsync(tenantId, groupCode, cancellationToken);
|
||||
return ApiResponse<OverrideConfigDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用覆盖模式。
|
||||
/// </summary>
|
||||
[HttpPost("{groupCode}/disable")]
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<object>> Disable(string groupCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<object>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var success = await overrideService.DisableOverrideAsync(tenantId, groupCode, cancellationToken);
|
||||
return success
|
||||
? ApiResponse.Success()
|
||||
: ApiResponse.Error(ErrorCodes.NotFound, "覆盖配置不存在");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新隐藏的系统字典项。
|
||||
/// </summary>
|
||||
[HttpPut("{groupCode}/hidden-items")]
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<OverrideConfigDto>> UpdateHiddenItems(
|
||||
string groupCode,
|
||||
[FromBody] DictionaryOverrideHiddenItemsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<OverrideConfigDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var result = await overrideService.UpdateHiddenItemsAsync(tenantId, groupCode, request.HiddenItemIds, cancellationToken);
|
||||
return ApiResponse<OverrideConfigDto>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新自定义排序。
|
||||
/// </summary>
|
||||
[HttpPut("{groupCode}/sort-order")]
|
||||
[PermissionAuthorize("dictionary:override:update")]
|
||||
[ProducesResponseType(typeof(ApiResponse<OverrideConfigDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<OverrideConfigDto>> UpdateSortOrder(
|
||||
string groupCode,
|
||||
[FromBody] DictionaryOverrideSortOrderRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var headerError = EnsureTenantHeader<OverrideConfigDto>();
|
||||
if (headerError != null)
|
||||
{
|
||||
return headerError;
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var result = await overrideService.UpdateCustomSortOrderAsync(tenantId, groupCode, request.SortOrder, cancellationToken);
|
||||
return ApiResponse<OverrideConfigDto>.Ok(result);
|
||||
}
|
||||
|
||||
private ApiResponse<T>? EnsureTenantHeader<T>()
|
||||
{
|
||||
if (!Request.Headers.TryGetValue(TenantIdHeaderName, out var tenantHeader) || string.IsNullOrWhiteSpace(tenantHeader))
|
||||
{
|
||||
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"缺少租户标识,请在请求头 {TenantIdHeaderName} 指定租户");
|
||||
}
|
||||
|
||||
if (!long.TryParse(tenantHeader.FirstOrDefault(), out _))
|
||||
{
|
||||
return ApiResponse<T>.Error(StatusCodes.Status400BadRequest, $"租户标识无效,请在请求头 {TenantIdHeaderName} 指定正确的租户 ID");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,15 @@
|
||||
"SlidingExpiration": "00:30:00"
|
||||
}
|
||||
},
|
||||
"CacheWarmup": {
|
||||
"DictionaryCodes": [
|
||||
"order_status",
|
||||
"payment_method",
|
||||
"shipping_method",
|
||||
"product_category",
|
||||
"user_role"
|
||||
]
|
||||
},
|
||||
"Tenancy": {
|
||||
"TenantIdHeaderName": "X-Tenant-Id",
|
||||
"TenantCodeHeaderName": "X-Tenant-Code",
|
||||
|
||||
@@ -66,6 +66,15 @@
|
||||
"SlidingExpiration": "00:30:00"
|
||||
}
|
||||
},
|
||||
"CacheWarmup": {
|
||||
"DictionaryCodes": [
|
||||
"order_status",
|
||||
"payment_method",
|
||||
"shipping_method",
|
||||
"product_category",
|
||||
"user_role"
|
||||
]
|
||||
},
|
||||
"Tenancy": {
|
||||
"TenantIdHeaderName": "X-Tenant-Id",
|
||||
"TenantCodeHeaderName": "X-Tenant-Code",
|
||||
|
||||
@@ -156,6 +156,8 @@
|
||||
"dictionary:item:create",
|
||||
"dictionary:item:update",
|
||||
"dictionary:item:delete",
|
||||
"dictionary:override:read",
|
||||
"dictionary:override:update",
|
||||
"system-parameter:create",
|
||||
"system-parameter:read",
|
||||
"system-parameter:update",
|
||||
@@ -265,6 +267,8 @@
|
||||
"dictionary:item:create",
|
||||
"dictionary:item:update",
|
||||
"dictionary:item:delete",
|
||||
"dictionary:override:read",
|
||||
"dictionary:override:update",
|
||||
"system-parameter:read"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using TakeoutSaaS.Application.Dictionary.Contracts;
|
||||
using TakeoutSaaS.Application.Dictionary.Models;
|
||||
using TakeoutSaaS.Application.Dictionary.Services;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||
using TakeoutSaaS.Shared.Web.Api;
|
||||
|
||||
namespace TakeoutSaaS.UserApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 字典查询接口。
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[Route("api/user/v{version:apiVersion}/dictionary")]
|
||||
public sealed class DictionaryController(DictionaryQueryService queryService) : BaseApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取指定字典分组的合并结果。
|
||||
/// </summary>
|
||||
[HttpGet("{code}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<DictionaryItemDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<DictionaryItemDto>>> GetByCode(string code, CancellationToken cancellationToken)
|
||||
{
|
||||
Response.Headers[HeaderNames.CacheControl] = "max-age=1800";
|
||||
var result = await queryService.GetMergedDictionaryAsync(code, cancellationToken);
|
||||
return ApiResponse<IReadOnlyList<DictionaryItemDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取字典分组。
|
||||
/// </summary>
|
||||
[HttpPost("batch")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>> BatchGet(
|
||||
[FromBody] DictionaryBatchQueryRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Codes.Count > 20)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "最多支持 20 个字典编码");
|
||||
}
|
||||
|
||||
var result = await queryService.BatchGetDictionariesAsync(request.Codes, cancellationToken);
|
||||
return ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>.Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
using Serilog;
|
||||
using TakeoutSaaS.Infrastructure.Identity.Extensions;
|
||||
using TakeoutSaaS.Module.Dictionary.Extensions;
|
||||
using TakeoutSaaS.Module.Tenancy.Extensions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
using TakeoutSaaS.Shared.Kernel.Ids;
|
||||
@@ -44,6 +46,9 @@ if (isDevelopment)
|
||||
|
||||
// 4. 注册多租户与健康检查
|
||||
builder.Services.AddTenantResolution(builder.Configuration);
|
||||
builder.Services.AddJwtAuthentication(builder.Configuration);
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddDictionaryModule(builder.Configuration);
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
// 5. 配置 OpenTelemetry 采集
|
||||
@@ -114,6 +119,8 @@ var app = builder.Build();
|
||||
app.UseCors("UserApiCors");
|
||||
app.UseTenantResolution();
|
||||
app.UseSharedWebCore();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSharedSwagger();
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user