feat: 实现字典管理后端

This commit is contained in:
2025-12-30 19:38:13 +08:00
parent a427b0f22a
commit dc9f6136d6
83 changed files with 6901 additions and 50 deletions

View File

@@ -0,0 +1,12 @@
namespace TakeoutSaaS.AdminApi.Contracts.Requests;
/// <summary>
/// 字典导出请求。
/// </summary>
public sealed record DictionaryExportRequest
{
/// <summary>
/// 导出格式csv/json
/// </summary>
public string? Format { get; init; }
}

View File

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

View File

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

View File

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

View File

@@ -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, "字典项不存在");
}
}

View File

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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"
]
},

View File

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

View File

@@ -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();