chore: 提交现有修改

This commit is contained in:
2025-12-02 12:11:25 +08:00
parent 541b75ecd8
commit 5332c87d9d
37 changed files with 429 additions and 677 deletions

View File

@@ -17,21 +17,16 @@ namespace TakeoutSaaS.AdminApi.Controllers;
/// <summary> /// <summary>
/// 管理后台认证接口 /// 管理后台认证接口
/// </summary> /// </summary>
/// <remarks>
///
/// </remarks>
/// <param name="authService"></param>
[ApiVersion("1.0")] [ApiVersion("1.0")]
[Authorize] [Authorize]
[Route("api/admin/v{version:apiVersion}/auth")] [Route("api/admin/v{version:apiVersion}/auth")]
public sealed class AuthController : BaseApiController public sealed class AuthController(IAdminAuthService authService) : BaseApiController
{ {
private readonly IAdminAuthService _authService;
/// <summary>
///
/// </summary>
/// <param name="authService"></param>
public AuthController(IAdminAuthService authService)
{
_authService = authService;
}
/// <summary> /// <summary>
/// 登录获取 Token /// 登录获取 Token
@@ -41,7 +36,7 @@ public sealed class AuthController : BaseApiController
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TokenResponse>> Login([FromBody] AdminLoginRequest request, CancellationToken cancellationToken) public async Task<ApiResponse<TokenResponse>> Login([FromBody] AdminLoginRequest request, CancellationToken cancellationToken)
{ {
var response = await _authService.LoginAsync(request, cancellationToken); var response = await authService.LoginAsync(request, cancellationToken);
return ApiResponse<TokenResponse>.Ok(response); return ApiResponse<TokenResponse>.Ok(response);
} }
@@ -53,7 +48,7 @@ public sealed class AuthController : BaseApiController
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TokenResponse>> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) public async Task<ApiResponse<TokenResponse>> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken)
{ {
var response = await _authService.RefreshTokenAsync(request, cancellationToken); var response = await authService.RefreshTokenAsync(request, cancellationToken);
return ApiResponse<TokenResponse>.Ok(response); return ApiResponse<TokenResponse>.Ok(response);
} }
@@ -72,7 +67,7 @@ public sealed class AuthController : BaseApiController
return ApiResponse<CurrentUserProfile>.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识"); return ApiResponse<CurrentUserProfile>.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识");
} }
var profile = await _authService.GetProfileAsync(userId, cancellationToken); var profile = await authService.GetProfileAsync(userId, cancellationToken);
return ApiResponse<CurrentUserProfile>.Ok(profile); return ApiResponse<CurrentUserProfile>.Ok(profile);
} }
} }

View File

@@ -12,21 +12,16 @@ namespace TakeoutSaaS.AdminApi.Controllers;
/// <summary> /// <summary>
/// 参数字典管理。 /// 参数字典管理。
/// </summary> /// </summary>
/// <remarks>
/// 初始化字典控制器。
/// </remarks>
/// <param name="dictionaryAppService">字典服务</param>
[ApiVersion("1.0")] [ApiVersion("1.0")]
[Authorize] [Authorize]
[Route("api/admin/v{version:apiVersion}/dictionaries")] [Route("api/admin/v{version:apiVersion}/dictionaries")]
public sealed class DictionaryController : BaseApiController public sealed class DictionaryController(IDictionaryAppService dictionaryAppService) : BaseApiController
{ {
private readonly IDictionaryAppService _dictionaryAppService;
/// <summary>
/// 初始化字典控制器。
/// </summary>
/// <param name="dictionaryAppService">字典服务</param>
public DictionaryController(IDictionaryAppService dictionaryAppService)
{
_dictionaryAppService = dictionaryAppService;
}
/// <summary> /// <summary>
/// 查询字典分组。 /// 查询字典分组。
@@ -36,7 +31,7 @@ public sealed class DictionaryController : BaseApiController
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<DictionaryGroupDto>>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<IReadOnlyList<DictionaryGroupDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<DictionaryGroupDto>>> GetGroups([FromQuery] DictionaryGroupQuery query, CancellationToken cancellationToken) public async Task<ApiResponse<IReadOnlyList<DictionaryGroupDto>>> GetGroups([FromQuery] DictionaryGroupQuery query, CancellationToken cancellationToken)
{ {
var groups = await _dictionaryAppService.SearchGroupsAsync(query, cancellationToken); var groups = await dictionaryAppService.SearchGroupsAsync(query, cancellationToken);
return ApiResponse<IReadOnlyList<DictionaryGroupDto>>.Ok(groups); return ApiResponse<IReadOnlyList<DictionaryGroupDto>>.Ok(groups);
} }
@@ -48,7 +43,7 @@ public sealed class DictionaryController : BaseApiController
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<DictionaryGroupDto>> CreateGroup([FromBody] CreateDictionaryGroupRequest request, CancellationToken cancellationToken) public async Task<ApiResponse<DictionaryGroupDto>> CreateGroup([FromBody] CreateDictionaryGroupRequest request, CancellationToken cancellationToken)
{ {
var group = await _dictionaryAppService.CreateGroupAsync(request, cancellationToken); var group = await dictionaryAppService.CreateGroupAsync(request, cancellationToken);
return ApiResponse<DictionaryGroupDto>.Ok(group); return ApiResponse<DictionaryGroupDto>.Ok(group);
} }
@@ -60,7 +55,7 @@ public sealed class DictionaryController : BaseApiController
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<DictionaryGroupDto>> UpdateGroup(long groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken) public async Task<ApiResponse<DictionaryGroupDto>> UpdateGroup(long groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken)
{ {
var group = await _dictionaryAppService.UpdateGroupAsync(groupId, request, cancellationToken); var group = await dictionaryAppService.UpdateGroupAsync(groupId, request, cancellationToken);
return ApiResponse<DictionaryGroupDto>.Ok(group); return ApiResponse<DictionaryGroupDto>.Ok(group);
} }
@@ -72,7 +67,7 @@ public sealed class DictionaryController : BaseApiController
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
public async Task<ApiResponse<object>> DeleteGroup(long groupId, CancellationToken cancellationToken) public async Task<ApiResponse<object>> DeleteGroup(long groupId, CancellationToken cancellationToken)
{ {
await _dictionaryAppService.DeleteGroupAsync(groupId, cancellationToken); await dictionaryAppService.DeleteGroupAsync(groupId, cancellationToken);
return ApiResponse.Success(); return ApiResponse.Success();
} }
@@ -85,7 +80,7 @@ public sealed class DictionaryController : BaseApiController
public async Task<ApiResponse<DictionaryItemDto>> CreateItem(long groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken) public async Task<ApiResponse<DictionaryItemDto>> CreateItem(long groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken)
{ {
request.GroupId = groupId; request.GroupId = groupId;
var item = await _dictionaryAppService.CreateItemAsync(request, cancellationToken); var item = await dictionaryAppService.CreateItemAsync(request, cancellationToken);
return ApiResponse<DictionaryItemDto>.Ok(item); return ApiResponse<DictionaryItemDto>.Ok(item);
} }
@@ -97,7 +92,7 @@ public sealed class DictionaryController : BaseApiController
[ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<DictionaryItemDto>> UpdateItem(long itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken) public async Task<ApiResponse<DictionaryItemDto>> UpdateItem(long itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken)
{ {
var item = await _dictionaryAppService.UpdateItemAsync(itemId, request, cancellationToken); var item = await dictionaryAppService.UpdateItemAsync(itemId, request, cancellationToken);
return ApiResponse<DictionaryItemDto>.Ok(item); return ApiResponse<DictionaryItemDto>.Ok(item);
} }
@@ -109,7 +104,7 @@ public sealed class DictionaryController : BaseApiController
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
public async Task<ApiResponse<object>> DeleteItem(long itemId, CancellationToken cancellationToken) public async Task<ApiResponse<object>> DeleteItem(long itemId, CancellationToken cancellationToken)
{ {
await _dictionaryAppService.DeleteItemAsync(itemId, cancellationToken); await dictionaryAppService.DeleteItemAsync(itemId, cancellationToken);
return ApiResponse.Success(); return ApiResponse.Success();
} }
@@ -120,7 +115,7 @@ public sealed class DictionaryController : BaseApiController
[ProducesResponseType(typeof(ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>> BatchGet([FromBody] DictionaryBatchQueryRequest request, CancellationToken cancellationToken) public async Task<ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>> BatchGet([FromBody] DictionaryBatchQueryRequest request, CancellationToken cancellationToken)
{ {
var dictionaries = await _dictionaryAppService.GetCachedItemsAsync(request, cancellationToken); var dictionaries = await dictionaryAppService.GetCachedItemsAsync(request, cancellationToken);
return ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>.Ok(dictionaries); return ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>.Ok(dictionaries);
} }
} }

View File

@@ -10,21 +10,16 @@ namespace TakeoutSaaS.MiniApi.Controllers;
/// <summary> /// <summary>
/// 小程序登录认证 /// 小程序登录认证
/// </summary> /// </summary>
/// <remarks>
/// 小程序登录认证
/// </remarks>
/// <param name="authService"></param>
[ApiVersion("1.0")] [ApiVersion("1.0")]
[Authorize] [Authorize]
[Route("api/mini/v{version:apiVersion}/auth")] [Route("api/mini/v{version:apiVersion}/auth")]
public sealed class AuthController : BaseApiController public sealed class AuthController(IMiniAuthService authService) : BaseApiController
{ {
private readonly IMiniAuthService _authService;
/// <summary>
/// 小程序登录认证
/// </summary>
/// <param name="authService"></param>
public AuthController(IMiniAuthService authService)
{
_authService = authService;
}
/// <summary> /// <summary>
/// 微信登录 /// 微信登录
@@ -34,7 +29,7 @@ public sealed class AuthController : BaseApiController
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TokenResponse>> LoginWithWeChat([FromBody] WeChatLoginRequest request, CancellationToken cancellationToken) public async Task<ApiResponse<TokenResponse>> LoginWithWeChat([FromBody] WeChatLoginRequest request, CancellationToken cancellationToken)
{ {
var response = await _authService.LoginWithWeChatAsync(request, cancellationToken); var response = await authService.LoginWithWeChatAsync(request, cancellationToken);
return ApiResponse<TokenResponse>.Ok(response); return ApiResponse<TokenResponse>.Ok(response);
} }
@@ -46,7 +41,7 @@ public sealed class AuthController : BaseApiController
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
public async Task<ApiResponse<TokenResponse>> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) public async Task<ApiResponse<TokenResponse>> RefreshToken([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken)
{ {
var response = await _authService.RefreshTokenAsync(request, cancellationToken); var response = await authService.RefreshTokenAsync(request, cancellationToken);
return ApiResponse<TokenResponse>.Ok(response); return ApiResponse<TokenResponse>.Ok(response);
} }
} }

View File

@@ -16,21 +16,16 @@ namespace TakeoutSaaS.MiniApi.Controllers;
/// <summary> /// <summary>
/// 当前用户信息 /// 当前用户信息
/// </summary> /// </summary>
/// <remarks>
///
/// </remarks>
/// <param name="authService"></param>
[ApiVersion("1.0")] [ApiVersion("1.0")]
[Authorize] [Authorize]
[Route("api/mini/v{version:apiVersion}/me")] [Route("api/mini/v{version:apiVersion}/me")]
public sealed class MeController : BaseApiController public sealed class MeController(IMiniAuthService authService) : BaseApiController
{ {
private readonly IMiniAuthService _authService;
/// <summary>
///
/// </summary>
/// <param name="authService"></param>
public MeController(IMiniAuthService authService)
{
_authService = authService;
}
/// <summary> /// <summary>
/// 获取用户档案 /// 获取用户档案
@@ -46,7 +41,7 @@ public sealed class MeController : BaseApiController
return ApiResponse<CurrentUserProfile>.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识"); return ApiResponse<CurrentUserProfile>.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识");
} }
var profile = await _authService.GetProfileAsync(userId, cancellationToken); var profile = await authService.GetProfileAsync(userId, cancellationToken);
return ApiResponse<CurrentUserProfile>.Ok(profile); return ApiResponse<CurrentUserProfile>.Ok(profile);
} }
} }

View File

@@ -15,31 +15,19 @@ namespace TakeoutSaaS.Application.Dictionary.Services;
/// <summary> /// <summary>
/// 参数字典应用服务实现。 /// 参数字典应用服务实现。
/// </summary> /// </summary>
public sealed class DictionaryAppService : IDictionaryAppService public sealed class DictionaryAppService(
{
private readonly IDictionaryRepository _repository;
private readonly IDictionaryCache _cache;
private readonly ITenantProvider _tenantProvider;
private readonly ILogger<DictionaryAppService> _logger;
public DictionaryAppService(
IDictionaryRepository repository, IDictionaryRepository repository,
IDictionaryCache cache, IDictionaryCache cache,
ITenantProvider tenantProvider, ITenantProvider tenantProvider,
ILogger<DictionaryAppService> logger) ILogger<DictionaryAppService> logger) : IDictionaryAppService
{ {
_repository = repository;
_cache = cache;
_tenantProvider = tenantProvider;
_logger = logger;
}
public async Task<DictionaryGroupDto> CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default) public async Task<DictionaryGroupDto> CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default)
{ {
var normalizedCode = NormalizeCode(request.Code); var normalizedCode = NormalizeCode(request.Code);
var targetTenant = ResolveTargetTenant(request.Scope); var targetTenant = ResolveTargetTenant(request.Scope);
var existing = await _repository.FindGroupByCodeAsync(normalizedCode, cancellationToken); var existing = await repository.FindGroupByCodeAsync(normalizedCode, cancellationToken);
if (existing != null) if (existing != null)
{ {
throw new BusinessException(ErrorCodes.Conflict, $"字典分组编码 {normalizedCode} 已存在"); throw new BusinessException(ErrorCodes.Conflict, $"字典分组编码 {normalizedCode} 已存在");
@@ -56,9 +44,9 @@ public sealed class DictionaryAppService : IDictionaryAppService
IsEnabled = true IsEnabled = true
}; };
await _repository.AddGroupAsync(group, cancellationToken); await repository.AddGroupAsync(group, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken); await repository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("创建字典分组:{Code}({Scope})", group.Code, group.Scope); logger.LogInformation("创建字典分组:{Code}({Scope})", group.Code, group.Scope);
return MapGroup(group, includeItems: false); return MapGroup(group, includeItems: false);
} }
@@ -71,9 +59,9 @@ public sealed class DictionaryAppService : IDictionaryAppService
group.Description = request.Description?.Trim(); group.Description = request.Description?.Trim();
group.IsEnabled = request.IsEnabled; group.IsEnabled = request.IsEnabled;
await _repository.SaveChangesAsync(cancellationToken); await repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken); await InvalidateCacheAsync(group, cancellationToken);
_logger.LogInformation("更新字典分组:{GroupId}", group.Id); logger.LogInformation("更新字典分组:{GroupId}", group.Id);
return MapGroup(group, includeItems: false); return MapGroup(group, includeItems: false);
} }
@@ -82,19 +70,19 @@ public sealed class DictionaryAppService : IDictionaryAppService
var group = await RequireGroupAsync(groupId, cancellationToken); var group = await RequireGroupAsync(groupId, cancellationToken);
EnsureScopePermission(group.Scope); EnsureScopePermission(group.Scope);
await _repository.RemoveGroupAsync(group, cancellationToken); await repository.RemoveGroupAsync(group, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken); await repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken); await InvalidateCacheAsync(group, cancellationToken);
_logger.LogInformation("删除字典分组:{GroupId}", group.Id); logger.LogInformation("删除字典分组:{GroupId}", group.Id);
} }
public async Task<IReadOnlyList<DictionaryGroupDto>> SearchGroupsAsync(DictionaryGroupQuery request, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<DictionaryGroupDto>> SearchGroupsAsync(DictionaryGroupQuery request, CancellationToken cancellationToken = default)
{ {
var tenantId = _tenantProvider.GetCurrentTenantId(); var tenantId = tenantProvider.GetCurrentTenantId();
var scope = ResolveScopeForQuery(request.Scope, tenantId); var scope = ResolveScopeForQuery(request.Scope, tenantId);
EnsureScopePermission(scope); EnsureScopePermission(scope);
var groups = await _repository.SearchGroupsAsync(scope, cancellationToken); var groups = await repository.SearchGroupsAsync(scope, cancellationToken);
var includeItems = request.IncludeItems; var includeItems = request.IncludeItems;
var result = new List<DictionaryGroupDto>(groups.Count); var result = new List<DictionaryGroupDto>(groups.Count);
@@ -103,7 +91,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
IReadOnlyList<DictionaryItemDto> items = Array.Empty<DictionaryItemDto>(); IReadOnlyList<DictionaryItemDto> items = Array.Empty<DictionaryItemDto>();
if (includeItems) if (includeItems)
{ {
var itemEntities = await _repository.GetItemsByGroupIdAsync(group.Id, cancellationToken); var itemEntities = await repository.GetItemsByGroupIdAsync(group.Id, cancellationToken);
items = itemEntities.Select(MapItem).ToList(); items = itemEntities.Select(MapItem).ToList();
} }
@@ -131,10 +119,10 @@ public sealed class DictionaryAppService : IDictionaryAppService
IsEnabled = request.IsEnabled IsEnabled = request.IsEnabled
}; };
await _repository.AddItemAsync(item, cancellationToken); await repository.AddItemAsync(item, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken); await repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken); await InvalidateCacheAsync(group, cancellationToken);
_logger.LogInformation("新增字典项:{ItemId}", item.Id); logger.LogInformation("新增字典项:{ItemId}", item.Id);
return MapItem(item); return MapItem(item);
} }
@@ -150,9 +138,9 @@ public sealed class DictionaryAppService : IDictionaryAppService
item.IsDefault = request.IsDefault; item.IsDefault = request.IsDefault;
item.IsEnabled = request.IsEnabled; item.IsEnabled = request.IsEnabled;
await _repository.SaveChangesAsync(cancellationToken); await repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken); await InvalidateCacheAsync(group, cancellationToken);
_logger.LogInformation("更新字典项:{ItemId}", item.Id); logger.LogInformation("更新字典项:{ItemId}", item.Id);
return MapItem(item); return MapItem(item);
} }
@@ -162,10 +150,10 @@ public sealed class DictionaryAppService : IDictionaryAppService
var group = await RequireGroupAsync(item.GroupId, cancellationToken); var group = await RequireGroupAsync(item.GroupId, cancellationToken);
EnsureScopePermission(group.Scope); EnsureScopePermission(group.Scope);
await _repository.RemoveItemAsync(item, cancellationToken); await repository.RemoveItemAsync(item, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken); await repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken); await InvalidateCacheAsync(group, cancellationToken);
_logger.LogInformation("删除字典项:{ItemId}", item.Id); logger.LogInformation("删除字典项:{ItemId}", item.Id);
} }
public async Task<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>> GetCachedItemsAsync(DictionaryBatchQueryRequest request, CancellationToken cancellationToken = default) public async Task<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>> GetCachedItemsAsync(DictionaryBatchQueryRequest request, CancellationToken cancellationToken = default)
@@ -181,7 +169,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
return new Dictionary<string, IReadOnlyList<DictionaryItemDto>>(StringComparer.OrdinalIgnoreCase); return new Dictionary<string, IReadOnlyList<DictionaryItemDto>>(StringComparer.OrdinalIgnoreCase);
} }
var tenantId = _tenantProvider.GetCurrentTenantId(); var tenantId = tenantProvider.GetCurrentTenantId();
var result = new Dictionary<string, IReadOnlyList<DictionaryItemDto>>(StringComparer.OrdinalIgnoreCase); var result = new Dictionary<string, IReadOnlyList<DictionaryItemDto>>(StringComparer.OrdinalIgnoreCase);
foreach (var code in normalizedCodes) foreach (var code in normalizedCodes)
@@ -202,7 +190,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
private async Task<DictionaryGroup> RequireGroupAsync(long groupId, CancellationToken cancellationToken) private async Task<DictionaryGroup> RequireGroupAsync(long groupId, CancellationToken cancellationToken)
{ {
var group = await _repository.FindGroupByIdAsync(groupId, cancellationToken); var group = await repository.FindGroupByIdAsync(groupId, cancellationToken);
if (group == null) if (group == null)
{ {
throw new BusinessException(ErrorCodes.NotFound, "字典分组不存在"); throw new BusinessException(ErrorCodes.NotFound, "字典分组不存在");
@@ -213,7 +201,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
private async Task<DictionaryItem> RequireItemAsync(long itemId, CancellationToken cancellationToken) private async Task<DictionaryItem> RequireItemAsync(long itemId, CancellationToken cancellationToken)
{ {
var item = await _repository.FindItemByIdAsync(itemId, cancellationToken); var item = await repository.FindItemByIdAsync(itemId, cancellationToken);
if (item == null) if (item == null)
{ {
throw new BusinessException(ErrorCodes.NotFound, "字典项不存在"); throw new BusinessException(ErrorCodes.NotFound, "字典项不存在");
@@ -224,7 +212,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
private long ResolveTargetTenant(DictionaryScope scope) private long ResolveTargetTenant(DictionaryScope scope)
{ {
var tenantId = _tenantProvider.GetCurrentTenantId(); var tenantId = tenantProvider.GetCurrentTenantId();
if (scope == DictionaryScope.System) if (scope == DictionaryScope.System)
{ {
EnsurePlatformTenant(tenantId); EnsurePlatformTenant(tenantId);
@@ -253,7 +241,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
private void EnsureScopePermission(DictionaryScope scope) private void EnsureScopePermission(DictionaryScope scope)
{ {
var tenantId = _tenantProvider.GetCurrentTenantId(); var tenantId = tenantProvider.GetCurrentTenantId();
if (scope == DictionaryScope.System && tenantId != 0) if (scope == DictionaryScope.System && tenantId != 0)
{ {
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
@@ -270,7 +258,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
private async Task InvalidateCacheAsync(DictionaryGroup group, CancellationToken cancellationToken) private async Task InvalidateCacheAsync(DictionaryGroup group, CancellationToken cancellationToken)
{ {
await _cache.RemoveAsync(group.TenantId, group.Code, cancellationToken); await cache.RemoveAsync(group.TenantId, group.Code, cancellationToken);
if (group.Scope == DictionaryScope.Business) if (group.Scope == DictionaryScope.Business)
{ {
return; return;
@@ -281,20 +269,20 @@ public sealed class DictionaryAppService : IDictionaryAppService
private async Task<IReadOnlyList<DictionaryItemDto>> GetOrLoadCacheAsync(long tenantId, string code, CancellationToken cancellationToken) private async Task<IReadOnlyList<DictionaryItemDto>> GetOrLoadCacheAsync(long tenantId, string code, CancellationToken cancellationToken)
{ {
var cached = await _cache.GetAsync(tenantId, code, cancellationToken); var cached = await cache.GetAsync(tenantId, code, cancellationToken);
if (cached != null) if (cached != null)
{ {
return cached; return cached;
} }
var entities = await _repository.GetItemsByCodesAsync(new[] { code }, tenantId, includeSystem: false, cancellationToken); var entities = await repository.GetItemsByCodesAsync(new[] { code }, tenantId, includeSystem: false, cancellationToken);
var items = entities var items = entities
.Where(item => item.IsEnabled && (item.Group?.IsEnabled ?? true)) .Where(item => item.IsEnabled && (item.Group?.IsEnabled ?? true))
.Select(MapItem) .Select(MapItem)
.OrderBy(item => item.SortOrder) .OrderBy(item => item.SortOrder)
.ToList(); .ToList();
await _cache.SetAsync(tenantId, code, items, cancellationToken); await cache.SetAsync(tenantId, code, items, cancellationToken);
return items; return items;
} }

View File

@@ -5,30 +5,25 @@ namespace TakeoutSaaS.Application.Sms.Contracts;
/// <summary> /// <summary>
/// 发送验证码请求。 /// 发送验证码请求。
/// </summary> /// </summary>
public sealed class SendVerificationCodeRequest /// <remarks>
{
/// <summary>
/// 创建发送请求。 /// 创建发送请求。
/// </summary> /// </remarks>
public SendVerificationCodeRequest(string phoneNumber, string scene, SmsProviderKind? provider = null) public sealed class SendVerificationCodeRequest(string phoneNumber, string scene, SmsProviderKind? provider = null)
{ {
PhoneNumber = phoneNumber;
Scene = scene;
Provider = provider;
}
/// <summary> /// <summary>
/// 手机号(支持 +86 前缀或纯 11 位)。 /// 手机号(支持 +86 前缀或纯 11 位)。
/// </summary> /// </summary>
public string PhoneNumber { get; } public string PhoneNumber { get; } = phoneNumber;
/// <summary> /// <summary>
/// 业务场景(如 login/register/reset /// 业务场景(如 login/register/reset
/// </summary> /// </summary>
public string Scene { get; } public string Scene { get; } = scene;
/// <summary> /// <summary>
/// 指定服务商,未指定则使用默认配置。 /// 指定服务商,未指定则使用默认配置。
/// </summary> /// </summary>
public SmsProviderKind? Provider { get; } public SmsProviderKind? Provider { get; } = provider;
} }

View File

@@ -3,30 +3,25 @@ namespace TakeoutSaaS.Application.Sms.Contracts;
/// <summary> /// <summary>
/// 校验验证码请求。 /// 校验验证码请求。
/// </summary> /// </summary>
public sealed class VerifyVerificationCodeRequest /// <remarks>
{
/// <summary>
/// 创建校验请求。 /// 创建校验请求。
/// </summary> /// </remarks>
public VerifyVerificationCodeRequest(string phoneNumber, string scene, string code) public sealed class VerifyVerificationCodeRequest(string phoneNumber, string scene, string code)
{ {
PhoneNumber = phoneNumber;
Scene = scene;
Code = code;
}
/// <summary> /// <summary>
/// 手机号。 /// 手机号。
/// </summary> /// </summary>
public string PhoneNumber { get; } public string PhoneNumber { get; } = phoneNumber;
/// <summary> /// <summary>
/// 业务场景。 /// 业务场景。
/// </summary> /// </summary>
public string Scene { get; } public string Scene { get; } = scene;
/// <summary> /// <summary>
/// 填写的验证码。 /// 填写的验证码。
/// </summary> /// </summary>
public string Code { get; } public string Code { get; } = code;
} }

View File

@@ -5,42 +5,35 @@ namespace TakeoutSaaS.Application.Storage.Contracts;
/// <summary> /// <summary>
/// 直传凭证请求模型。 /// 直传凭证请求模型。
/// </summary> /// </summary>
public sealed class DirectUploadRequest /// <remarks>
{
/// <summary>
/// 创建直传请求。 /// 创建直传请求。
/// </summary> /// </remarks>
public DirectUploadRequest(UploadFileType fileType, string fileName, string contentType, long contentLength, string? requestOrigin) public sealed class DirectUploadRequest(UploadFileType fileType, string fileName, string contentType, long contentLength, string? requestOrigin)
{ {
FileType = fileType;
FileName = fileName;
ContentType = contentType;
ContentLength = contentLength;
RequestOrigin = requestOrigin;
}
/// <summary> /// <summary>
/// 文件类型。 /// 文件类型。
/// </summary> /// </summary>
public UploadFileType FileType { get; } public UploadFileType FileType { get; } = fileType;
/// <summary> /// <summary>
/// 文件名。 /// 文件名。
/// </summary> /// </summary>
public string FileName { get; } public string FileName { get; } = fileName;
/// <summary> /// <summary>
/// 内容类型。 /// 内容类型。
/// </summary> /// </summary>
public string ContentType { get; } public string ContentType { get; } = contentType;
/// <summary> /// <summary>
/// 文件长度。 /// 文件长度。
/// </summary> /// </summary>
public long ContentLength { get; } public long ContentLength { get; } = contentLength;
/// <summary> /// <summary>
/// 请求来源Origin/Referer /// 请求来源Origin/Referer
/// </summary> /// </summary>
public string? RequestOrigin { get; } public string? RequestOrigin { get; } = requestOrigin;
} }

View File

@@ -6,12 +6,10 @@ namespace TakeoutSaaS.Application.Storage.Contracts;
/// <summary> /// <summary>
/// 上传文件请求模型。 /// 上传文件请求模型。
/// </summary> /// </summary>
public sealed class UploadFileRequest /// <remarks>
{
/// <summary>
/// 创建上传文件请求。 /// 创建上传文件请求。
/// </summary> /// </remarks>
public UploadFileRequest( public sealed class UploadFileRequest(
UploadFileType fileType, UploadFileType fileType,
Stream content, Stream content,
string fileName, string fileName,
@@ -19,41 +17,35 @@ public sealed class UploadFileRequest
long contentLength, long contentLength,
string? requestOrigin) string? requestOrigin)
{ {
FileType = fileType;
Content = content;
FileName = fileName;
ContentType = contentType;
ContentLength = contentLength;
RequestOrigin = requestOrigin;
}
/// <summary> /// <summary>
/// 文件分类。 /// 文件分类。
/// </summary> /// </summary>
public UploadFileType FileType { get; } public UploadFileType FileType { get; } = fileType;
/// <summary> /// <summary>
/// 文件流。 /// 文件流。
/// </summary> /// </summary>
public Stream Content { get; } public Stream Content { get; } = content;
/// <summary> /// <summary>
/// 原始文件名。 /// 原始文件名。
/// </summary> /// </summary>
public string FileName { get; } public string FileName { get; } = fileName;
/// <summary> /// <summary>
/// 内容类型。 /// 内容类型。
/// </summary> /// </summary>
public string ContentType { get; } public string ContentType { get; } = contentType;
/// <summary> /// <summary>
/// 文件大小。 /// 文件大小。
/// </summary> /// </summary>
public long ContentLength { get; } public long ContentLength { get; } = contentLength;
/// <summary> /// <summary>
/// 请求来源Origin/Referer /// 请求来源Origin/Referer
/// </summary> /// </summary>
public string? RequestOrigin { get; } public string? RequestOrigin { get; } = requestOrigin;
} }

View File

@@ -3,16 +3,11 @@ namespace TakeoutSaaS.Shared.Abstractions.Exceptions;
/// <summary> /// <summary>
/// 业务异常(用于可预期的业务校验错误)。 /// 业务异常(用于可预期的业务校验错误)。
/// </summary> /// </summary>
public class BusinessException : Exception public class BusinessException(int errorCode, string message) : Exception(message)
{ {
/// <summary> /// <summary>
/// 业务错误码。 /// 业务错误码。
/// </summary> /// </summary>
public int ErrorCode { get; } public int ErrorCode { get; } = errorCode;
public BusinessException(int errorCode, string message) : base(message)
{
ErrorCode = errorCode;
}
} }

View File

@@ -6,17 +6,11 @@ namespace TakeoutSaaS.Shared.Abstractions.Exceptions;
/// <summary> /// <summary>
/// 验证异常(用于聚合验证错误信息)。 /// 验证异常(用于聚合验证错误信息)。
/// </summary> /// </summary>
public class ValidationException : Exception public class ValidationException(IDictionary<string, string[]> errors) : Exception("一个或多个验证错误")
{ {
/// <summary> /// <summary>
/// 字段/属性的错误集合。 /// 字段/属性的错误集合。
/// </summary> /// </summary>
public IDictionary<string, string[]> Errors { get; } public IDictionary<string, string[]> Errors { get; } = errors;
public ValidationException(IDictionary<string, string[]> errors)
: base("一个或多个验证错误")
{
Errors = errors;
}
} }

View File

@@ -6,42 +6,33 @@ namespace TakeoutSaaS.Shared.Abstractions.Results;
/// 分页结果包装,携带列表与总条数等元数据。 /// 分页结果包装,携带列表与总条数等元数据。
/// </summary> /// </summary>
/// <typeparam name="T">数据类型。</typeparam> /// <typeparam name="T">数据类型。</typeparam>
public sealed class PagedResult<T> /// <remarks>
/// 初始化分页结果。
/// </remarks>
public sealed class PagedResult<T>(IReadOnlyList<T> items, int page, int pageSize, int totalCount)
{ {
/// <summary> /// <summary>
/// 数据列表。 /// 数据列表。
/// </summary> /// </summary>
public IReadOnlyList<T> Items { get; } public IReadOnlyList<T> Items { get; } = items;
/// <summary> /// <summary>
/// 当前页码,从 1 开始。 /// 当前页码,从 1 开始。
/// </summary> /// </summary>
public int Page { get; } public int Page { get; } = page;
/// <summary> /// <summary>
/// 每页条数。 /// 每页条数。
/// </summary> /// </summary>
public int PageSize { get; } public int PageSize { get; } = pageSize;
/// <summary> /// <summary>
/// 总条数。 /// 总条数。
/// </summary> /// </summary>
public int TotalCount { get; } public int TotalCount { get; } = totalCount;
/// <summary> /// <summary>
/// 总页数。 /// 总页数。
/// </summary> /// </summary>
public int TotalPages { get; } public int TotalPages { get; } = pageSize == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);
/// <summary>
/// 初始化分页结果。
/// </summary>
public PagedResult(IReadOnlyList<T> items, int page, int pageSize, int totalCount)
{
Items = items;
Page = page;
PageSize = pageSize;
TotalCount = totalCount;
TotalPages = pageSize == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);
}
} }

View File

@@ -3,40 +3,33 @@ namespace TakeoutSaaS.Shared.Abstractions.Tenancy;
/// <summary> /// <summary>
/// 租户上下文:封装当前请求解析得到的租户标识、编号及解析来源。 /// 租户上下文:封装当前请求解析得到的租户标识、编号及解析来源。
/// </summary> /// </summary>
public sealed class TenantContext /// <remarks>
/// 初始化租户上下文。
/// </remarks>
/// <param name="tenantId">租户 ID</param>
/// <param name="tenantCode">租户编码(可选)</param>
/// <param name="source">解析来源</param>
public sealed class TenantContext(long tenantId, string? tenantCode, string source)
{ {
/// <summary> /// <summary>
/// 未解析到租户时的默认上下文。 /// 未解析到租户时的默认上下文。
/// </summary> /// </summary>
public static TenantContext Empty { get; } = new(0, null, "unresolved"); public static TenantContext Empty { get; } = new(0, null, "unresolved");
/// <summary>
/// 初始化租户上下文。
/// </summary>
/// <param name="tenantId">租户 ID</param>
/// <param name="tenantCode">租户编码(可选)</param>
/// <param name="source">解析来源</param>
public TenantContext(long tenantId, string? tenantCode, string source)
{
TenantId = tenantId;
TenantCode = tenantCode;
Source = source;
}
/// <summary> /// <summary>
/// 当前租户 ID未解析时为 Guid.Empty。 /// 当前租户 ID未解析时为 Guid.Empty。
/// </summary> /// </summary>
public long TenantId { get; } public long TenantId { get; } = tenantId;
/// <summary> /// <summary>
/// 当前租户编码(例如子域名或业务编码),可为空。 /// 当前租户编码(例如子域名或业务编码),可为空。
/// </summary> /// </summary>
public string? TenantCode { get; } public string? TenantCode { get; } = tenantCode;
/// <summary> /// <summary>
/// 租户解析来源Header、Host、Token 等)。 /// 租户解析来源Header、Host、Token 等)。
/// </summary> /// </summary>
public string Source { get; } public string Source { get; } = source;
/// <summary> /// <summary>
/// 是否已成功解析到租户。 /// 是否已成功解析到租户。

View File

@@ -8,7 +8,12 @@ namespace TakeoutSaaS.Shared.Kernel.Ids;
/// <summary> /// <summary>
/// 基于雪花算法的长整型 ID 生成器。 /// 基于雪花算法的长整型 ID 生成器。
/// </summary> /// </summary>
public sealed class SnowflakeIdGenerator : IIdGenerator /// <remarks>
/// 初始化生成器。
/// </remarks>
/// <param name="workerId">工作节点 ID。</param>
/// <param name="datacenterId">机房 ID。</param>
public sealed class SnowflakeIdGenerator(long workerId = 0, long datacenterId = 0) : IIdGenerator
{ {
private const long Twepoch = 1577836800000L; // 2020-01-01 UTC private const long Twepoch = 1577836800000L; // 2020-01-01 UTC
private const int WorkerIdBits = 5; private const int WorkerIdBits = 5;
@@ -23,23 +28,12 @@ public sealed class SnowflakeIdGenerator : IIdGenerator
private const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits; private const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits;
private const long SequenceMask = -1L ^ (-1L << SequenceBits); private const long SequenceMask = -1L ^ (-1L << SequenceBits);
private readonly long _workerId; private readonly long _workerId = Normalize(workerId, MaxWorkerId, nameof(workerId));
private readonly long _datacenterId; private readonly long _datacenterId = Normalize(datacenterId, MaxDatacenterId, nameof(datacenterId));
private long _lastTimestamp = -1L; private long _lastTimestamp = -1L;
private long _sequence; private long _sequence = RandomNumberGenerator.GetInt32(0, (int)SequenceMask);
private readonly object _syncRoot = new(); private readonly object _syncRoot = new();
/// <summary>
/// 初始化生成器。
/// </summary>
/// <param name="workerId">工作节点 ID。</param>
/// <param name="datacenterId">机房 ID。</param>
public SnowflakeIdGenerator(long workerId = 0, long datacenterId = 0)
{
_workerId = Normalize(workerId, MaxWorkerId, nameof(workerId));
_datacenterId = Normalize(datacenterId, MaxDatacenterId, nameof(datacenterId));
_sequence = RandomNumberGenerator.GetInt32(0, (int)SequenceMask);
}
/// <inheritdoc /> /// <inheritdoc />
public long NextId() public long NextId()

View File

@@ -11,22 +11,11 @@ namespace TakeoutSaaS.Shared.Web.Middleware;
/// <summary> /// <summary>
/// 统一 TraceId/CorrelationId贯穿日志与响应。 /// 统一 TraceId/CorrelationId贯穿日志与响应。
/// </summary> /// </summary>
public sealed class CorrelationIdMiddleware public sealed class CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger, IIdGenerator idGenerator)
{ {
private const string TraceHeader = "X-Trace-Id"; private const string TraceHeader = "X-Trace-Id";
private const string RequestHeader = "X-Request-Id"; private const string RequestHeader = "X-Request-Id";
private readonly RequestDelegate _next;
private readonly ILogger<CorrelationIdMiddleware> _logger;
private readonly IIdGenerator _idGenerator;
public CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger, IIdGenerator idGenerator)
{
_next = next;
_logger = logger;
_idGenerator = idGenerator;
}
public async Task InvokeAsync(HttpContext context) public async Task InvokeAsync(HttpContext context)
{ {
var traceId = ResolveTraceId(context); var traceId = ResolveTraceId(context);
@@ -39,14 +28,14 @@ public sealed class CorrelationIdMiddleware
return Task.CompletedTask; return Task.CompletedTask;
}); });
using (_logger.BeginScope(new Dictionary<string, object> using (logger.BeginScope(new Dictionary<string, object>
{ {
["TraceId"] = traceId ["TraceId"] = traceId
})) }))
{ {
try try
{ {
await _next(context); await next(context);
} }
finally finally
{ {
@@ -67,7 +56,7 @@ public sealed class CorrelationIdMiddleware
return requestId; return requestId;
} }
return _idGenerator.NextId().ToString(); return idGenerator.NextId().ToString();
} }
private static bool TryGetHeader(HttpContext context, string headerName, out string value) private static bool TryGetHeader(HttpContext context, string headerName, out string value)

View File

@@ -14,34 +14,23 @@ namespace TakeoutSaaS.Shared.Web.Middleware;
/// <summary> /// <summary>
/// 全局异常处理中间件,将异常统一映射为 ApiResponse。 /// 全局异常处理中间件,将异常统一映射为 ApiResponse。
/// </summary> /// </summary>
public sealed class ExceptionHandlingMiddleware public sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger, IHostEnvironment environment)
{ {
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
private readonly IHostEnvironment _environment;
private static readonly JsonSerializerOptions SerializerOptions = new() private static readonly JsonSerializerOptions SerializerOptions = new()
{ {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
}; };
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger, IHostEnvironment environment)
{
_next = next;
_logger = logger;
_environment = environment;
}
public async Task InvokeAsync(HttpContext context) public async Task InvokeAsync(HttpContext context)
{ {
try try
{ {
await _next(context); await next(context);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "未处理异常:{Message}", ex.Message); logger.LogError(ex, "未处理异常:{Message}", ex.Message);
await HandleExceptionAsync(context, ex); await HandleExceptionAsync(context, ex);
} }
} }
@@ -50,7 +39,7 @@ public sealed class ExceptionHandlingMiddleware
{ {
var (statusCode, response) = BuildErrorResponse(exception); var (statusCode, response) = BuildErrorResponse(exception);
if (_environment.IsDevelopment()) if (environment.IsDevelopment())
{ {
response = response with response = response with
{ {

View File

@@ -9,29 +9,21 @@ namespace TakeoutSaaS.Shared.Web.Middleware;
/// <summary> /// <summary>
/// 基础请求日志方法、路径、耗时、状态码、TraceId /// 基础请求日志方法、路径、耗时、状态码、TraceId
/// </summary> /// </summary>
public sealed class RequestLoggingMiddleware public sealed class RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{ {
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context) public async Task InvokeAsync(HttpContext context)
{ {
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
try try
{ {
await _next(context); await next(context);
} }
finally finally
{ {
stopwatch.Stop(); stopwatch.Stop();
var traceId = TraceContext.TraceId ?? context.TraceIdentifier; var traceId = TraceContext.TraceId ?? context.TraceIdentifier;
_logger.LogInformation( logger.LogInformation(
"HTTP {Method} {Path} => {StatusCode} ({Elapsed} ms) TraceId:{TraceId}", "HTTP {Method} {Path} => {StatusCode} ({Elapsed} ms) TraceId:{TraceId}",
context.Request.Method, context.Request.Method,
context.Request.Path, context.Request.Path,

View File

@@ -7,24 +7,19 @@ namespace TakeoutSaaS.Shared.Web.Security;
/// <summary> /// <summary>
/// 基于 HttpContext 的当前用户访问器。 /// 基于 HttpContext 的当前用户访问器。
/// </summary> /// </summary>
public sealed class HttpContextCurrentUserAccessor : ICurrentUserAccessor /// <remarks>
{
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// 初始化访问器。 /// 初始化访问器。
/// </summary> /// </remarks>
public HttpContextCurrentUserAccessor(IHttpContextAccessor httpContextAccessor) public sealed class HttpContextCurrentUserAccessor(IHttpContextAccessor httpContextAccessor) : ICurrentUserAccessor
{ {
_httpContextAccessor = httpContextAccessor;
}
/// <inheritdoc /> /// <inheritdoc />
public long UserId public long UserId
{ {
get get
{ {
var principal = _httpContextAccessor.HttpContext?.User; var principal = httpContextAccessor.HttpContext?.User;
if (principal == null || !principal.Identity?.IsAuthenticated == true) if (principal == null || !principal.Identity?.IsAuthenticated == true)
{ {
return 0; return 0;

View File

@@ -9,22 +9,15 @@ namespace TakeoutSaaS.Shared.Web.Swagger;
/// <summary> /// <summary>
/// 根据 API 版本动态注册 Swagger 文档。 /// 根据 API 版本动态注册 Swagger 文档。
/// </summary> /// </summary>
internal sealed class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions> internal sealed class ConfigureSwaggerOptions(
{
private readonly IApiVersionDescriptionProvider _provider;
private readonly SwaggerDocumentSettings _settings;
public ConfigureSwaggerOptions(
IApiVersionDescriptionProvider provider, IApiVersionDescriptionProvider provider,
IOptions<SwaggerDocumentSettings> settings) IOptions<SwaggerDocumentSettings> settings) : IConfigureOptions<SwaggerGenOptions>
{ {
_provider = provider; private readonly SwaggerDocumentSettings _settings = settings.Value;
_settings = settings.Value;
}
public void Configure(SwaggerGenOptions options) public void Configure(SwaggerGenOptions options)
{ {
foreach (var description in _provider.ApiVersionDescriptions) foreach (var description in provider.ApiVersionDescriptions)
{ {
var info = new OpenApiInfo var info = new OpenApiInfo
{ {

View File

@@ -16,42 +16,34 @@ namespace TakeoutSaaS.Infrastructure.App.Persistence;
/// <summary> /// <summary>
/// 业务数据种子,确保默认租户与基础字典可重复执行。 /// 业务数据种子,确保默认租户与基础字典可重复执行。
/// </summary> /// </summary>
public sealed class AppDataSeeder : IHostedService /// <remarks>
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<AppDataSeeder> _logger;
private readonly AppSeedOptions _options;
/// <summary>
/// 初始化种子服务。 /// 初始化种子服务。
/// </summary> /// </remarks>
public AppDataSeeder( public sealed class AppDataSeeder(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ILogger<AppDataSeeder> logger, ILogger<AppDataSeeder> logger,
IOptions<AppSeedOptions> options) IOptions<AppSeedOptions> options) : IHostedService
{ {
_serviceProvider = serviceProvider; private readonly AppSeedOptions _options = options.Value;
_logger = logger;
_options = options.Value;
}
/// <inheritdoc /> /// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
if (!_options.Enabled) if (!_options.Enabled)
{ {
_logger.LogInformation("AppSeed 未启用,跳过业务数据初始化"); logger.LogInformation("AppSeed 未启用,跳过业务数据初始化");
return; return;
} }
using var scope = _serviceProvider.CreateScope(); using var scope = serviceProvider.CreateScope();
var appDbContext = scope.ServiceProvider.GetRequiredService<TakeoutAppDbContext>(); var appDbContext = scope.ServiceProvider.GetRequiredService<TakeoutAppDbContext>();
var dictionaryDbContext = scope.ServiceProvider.GetRequiredService<DictionaryDbContext>(); var dictionaryDbContext = scope.ServiceProvider.GetRequiredService<DictionaryDbContext>();
var defaultTenantId = await EnsureDefaultTenantAsync(appDbContext, cancellationToken); var defaultTenantId = await EnsureDefaultTenantAsync(appDbContext, cancellationToken);
await EnsureDictionarySeedsAsync(dictionaryDbContext, defaultTenantId, cancellationToken); await EnsureDictionarySeedsAsync(dictionaryDbContext, defaultTenantId, cancellationToken);
_logger.LogInformation("AppSeed 完成业务数据初始化"); logger.LogInformation("AppSeed 完成业务数据初始化");
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -65,7 +57,7 @@ public sealed class AppDataSeeder : IHostedService
var tenantOptions = _options.DefaultTenant; var tenantOptions = _options.DefaultTenant;
if (tenantOptions == null || string.IsNullOrWhiteSpace(tenantOptions.Code) || string.IsNullOrWhiteSpace(tenantOptions.Name)) if (tenantOptions == null || string.IsNullOrWhiteSpace(tenantOptions.Code) || string.IsNullOrWhiteSpace(tenantOptions.Name))
{ {
_logger.LogInformation("AppSeed 未配置默认租户,跳过租户种子"); logger.LogInformation("AppSeed 未配置默认租户,跳过租户种子");
return null; return null;
} }
@@ -89,7 +81,7 @@ public sealed class AppDataSeeder : IHostedService
await dbContext.Tenants.AddAsync(tenant, cancellationToken); await dbContext.Tenants.AddAsync(tenant, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation("AppSeed 已创建默认租户 {TenantCode}", code); logger.LogInformation("AppSeed 已创建默认租户 {TenantCode}", code);
return tenant.Id; return tenant.Id;
} }
@@ -129,11 +121,11 @@ public sealed class AppDataSeeder : IHostedService
{ {
dbContext.Tenants.Update(existingTenant); dbContext.Tenants.Update(existingTenant);
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation("AppSeed 已更新默认租户 {TenantCode}", code); logger.LogInformation("AppSeed 已更新默认租户 {TenantCode}", code);
} }
else else
{ {
_logger.LogInformation("AppSeed 默认租户 {TenantCode} 已存在且无需更新", code); logger.LogInformation("AppSeed 默认租户 {TenantCode} 已存在且无需更新", code);
} }
return existingTenant.Id; return existingTenant.Id;
@@ -149,7 +141,7 @@ public sealed class AppDataSeeder : IHostedService
if (!hasDictionaryGroups) if (!hasDictionaryGroups)
{ {
_logger.LogInformation("AppSeed 未配置基础字典,跳过字典种子"); logger.LogInformation("AppSeed 未配置基础字典,跳过字典种子");
} }
if (hasDictionaryGroups) if (hasDictionaryGroups)
@@ -158,7 +150,7 @@ public sealed class AppDataSeeder : IHostedService
{ {
if (string.IsNullOrWhiteSpace(groupOptions.Code) || string.IsNullOrWhiteSpace(groupOptions.Name)) if (string.IsNullOrWhiteSpace(groupOptions.Code) || string.IsNullOrWhiteSpace(groupOptions.Name))
{ {
_logger.LogWarning("AppSeed 跳过字典分组Code 或 Name 为空"); logger.LogWarning("AppSeed 跳过字典分组Code 或 Name 为空");
continue; continue;
} }
@@ -183,7 +175,7 @@ public sealed class AppDataSeeder : IHostedService
}; };
await dbContext.DictionaryGroups.AddAsync(group, cancellationToken); await dbContext.DictionaryGroups.AddAsync(group, cancellationToken);
_logger.LogInformation("AppSeed 创建字典分组 {GroupCode} (Tenant: {TenantId})", code, tenantId); logger.LogInformation("AppSeed 创建字典分组 {GroupCode} (Tenant: {TenantId})", code, tenantId);
} }
else else
{ {
@@ -236,7 +228,7 @@ public sealed class AppDataSeeder : IHostedService
if (systemParameters.Count == 0) if (systemParameters.Count == 0)
{ {
_logger.LogInformation("AppSeed 未配置系统参数,跳过系统参数种子"); logger.LogInformation("AppSeed 未配置系统参数,跳过系统参数种子");
return; return;
} }
@@ -246,7 +238,7 @@ public sealed class AppDataSeeder : IHostedService
if (!grouped.Any()) if (!grouped.Any())
{ {
_logger.LogInformation("AppSeed 系统参数配置为空,跳过系统参数种子"); logger.LogInformation("AppSeed 系统参数配置为空,跳过系统参数种子");
return; return;
} }
@@ -271,7 +263,7 @@ public sealed class AppDataSeeder : IHostedService
}; };
await dbContext.DictionaryGroups.AddAsync(dictionaryGroup, cancellationToken); await dbContext.DictionaryGroups.AddAsync(dictionaryGroup, cancellationToken);
_logger.LogInformation("AppSeed 创建系统参数分组 (Tenant: {TenantId})", tenantId); logger.LogInformation("AppSeed 创建系统参数分组 (Tenant: {TenantId})", tenantId);
} }
var seedItems = group.Select(x => new DictionarySeedItemOptions var seedItems = group.Select(x => new DictionarySeedItemOptions

View File

@@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
/// <summary> /// <summary>
/// 配送聚合的 EF Core 仓储实现。 /// 配送聚合的 EF Core 仓储实现。
/// </summary> /// </summary>
public sealed class EfDeliveryRepository : IDeliveryRepository /// <remarks>
{
private readonly TakeoutAppDbContext _context;
/// <summary>
/// 初始化仓储。 /// 初始化仓储。
/// </summary> /// </remarks>
public EfDeliveryRepository(TakeoutAppDbContext context) public sealed class EfDeliveryRepository(TakeoutAppDbContext context) : IDeliveryRepository
{ {
_context = context;
}
/// <inheritdoc /> /// <inheritdoc />
public Task<DeliveryOrder?> FindByIdAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default) public Task<DeliveryOrder?> FindByIdAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default)
{ {
return _context.DeliveryOrders return context.DeliveryOrders
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.Id == deliveryOrderId) .Where(x => x.TenantId == tenantId && x.Id == deliveryOrderId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -34,7 +29,7 @@ public sealed class EfDeliveryRepository : IDeliveryRepository
/// <inheritdoc /> /// <inheritdoc />
public Task<DeliveryOrder?> FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) public Task<DeliveryOrder?> FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
{ {
return _context.DeliveryOrders return context.DeliveryOrders
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.OrderId == orderId) .Where(x => x.TenantId == tenantId && x.OrderId == orderId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -43,7 +38,7 @@ public sealed class EfDeliveryRepository : IDeliveryRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<DeliveryEvent>> GetEventsAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<DeliveryEvent>> GetEventsAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default)
{ {
var events = await _context.DeliveryEvents var events = await context.DeliveryEvents
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.DeliveryOrderId == deliveryOrderId) .Where(x => x.TenantId == tenantId && x.DeliveryOrderId == deliveryOrderId)
.OrderBy(x => x.CreatedAt) .OrderBy(x => x.CreatedAt)
@@ -55,25 +50,25 @@ public sealed class EfDeliveryRepository : IDeliveryRepository
/// <inheritdoc /> /// <inheritdoc />
public Task AddDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default) public Task AddDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default)
{ {
return _context.DeliveryOrders.AddAsync(deliveryOrder, cancellationToken).AsTask(); return context.DeliveryOrders.AddAsync(deliveryOrder, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddEventAsync(DeliveryEvent deliveryEvent, CancellationToken cancellationToken = default) public Task AddEventAsync(DeliveryEvent deliveryEvent, CancellationToken cancellationToken = default)
{ {
return _context.DeliveryEvents.AddAsync(deliveryEvent, cancellationToken).AsTask(); return context.DeliveryEvents.AddAsync(deliveryEvent, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task SaveChangesAsync(CancellationToken cancellationToken = default) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{ {
return _context.SaveChangesAsync(cancellationToken); return context.SaveChangesAsync(cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<DeliveryOrder>> SearchAsync(long tenantId, DeliveryStatus? status, long? orderId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<DeliveryOrder>> SearchAsync(long tenantId, DeliveryStatus? status, long? orderId, CancellationToken cancellationToken = default)
{ {
var query = _context.DeliveryOrders var query = context.DeliveryOrders
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId); .Where(x => x.TenantId == tenantId);
@@ -95,23 +90,23 @@ public sealed class EfDeliveryRepository : IDeliveryRepository
/// <inheritdoc /> /// <inheritdoc />
public Task UpdateDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default) public Task UpdateDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default)
{ {
_context.DeliveryOrders.Update(deliveryOrder); context.DeliveryOrders.Update(deliveryOrder);
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task DeleteDeliveryOrderAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default) public async Task DeleteDeliveryOrderAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default)
{ {
var events = await _context.DeliveryEvents var events = await context.DeliveryEvents
.Where(x => x.TenantId == tenantId && x.DeliveryOrderId == deliveryOrderId) .Where(x => x.TenantId == tenantId && x.DeliveryOrderId == deliveryOrderId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (events.Count > 0) if (events.Count > 0)
{ {
_context.DeliveryEvents.RemoveRange(events); context.DeliveryEvents.RemoveRange(events);
} }
var existing = await _context.DeliveryOrders var existing = await context.DeliveryOrders
.Where(x => x.TenantId == tenantId && x.Id == deliveryOrderId) .Where(x => x.TenantId == tenantId && x.Id == deliveryOrderId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -120,6 +115,6 @@ public sealed class EfDeliveryRepository : IDeliveryRepository
return; return;
} }
_context.DeliveryOrders.Remove(existing); context.DeliveryOrders.Remove(existing);
} }
} }

View File

@@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
/// <summary> /// <summary>
/// 商户聚合的 EF Core 仓储实现。 /// 商户聚合的 EF Core 仓储实现。
/// </summary> /// </summary>
public sealed class EfMerchantRepository : IMerchantRepository /// <remarks>
{
private readonly TakeoutAppDbContext _context;
/// <summary>
/// 初始化仓储。 /// 初始化仓储。
/// </summary> /// </remarks>
public EfMerchantRepository(TakeoutAppDbContext context) public sealed class EfMerchantRepository(TakeoutAppDbContext context) : IMerchantRepository
{ {
_context = context;
}
/// <inheritdoc /> /// <inheritdoc />
public Task<Merchant?> FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) public Task<Merchant?> FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default)
{ {
return _context.Merchants return context.Merchants
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.Id == merchantId) .Where(x => x.TenantId == tenantId && x.Id == merchantId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -34,7 +29,7 @@ public sealed class EfMerchantRepository : IMerchantRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<Merchant>> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<Merchant>> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default)
{ {
var query = _context.Merchants var query = context.Merchants
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId); .Where(x => x.TenantId == tenantId);
@@ -51,7 +46,7 @@ public sealed class EfMerchantRepository : IMerchantRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<MerchantStaff>> GetStaffAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<MerchantStaff>> GetStaffAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default)
{ {
var staffs = await _context.MerchantStaff var staffs = await context.MerchantStaff
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId) .Where(x => x.TenantId == tenantId && x.MerchantId == merchantId)
.OrderBy(x => x.Name) .OrderBy(x => x.Name)
@@ -63,7 +58,7 @@ public sealed class EfMerchantRepository : IMerchantRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<MerchantContract>> GetContractsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<MerchantContract>> GetContractsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default)
{ {
var contracts = await _context.MerchantContracts var contracts = await context.MerchantContracts
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId) .Where(x => x.TenantId == tenantId && x.MerchantId == merchantId)
.OrderByDescending(x => x.CreatedAt) .OrderByDescending(x => x.CreatedAt)
@@ -75,7 +70,7 @@ public sealed class EfMerchantRepository : IMerchantRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<MerchantDocument>> GetDocumentsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<MerchantDocument>> GetDocumentsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default)
{ {
var documents = await _context.MerchantDocuments var documents = await context.MerchantDocuments
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId) .Where(x => x.TenantId == tenantId && x.MerchantId == merchantId)
.OrderBy(x => x.CreatedAt) .OrderBy(x => x.CreatedAt)
@@ -87,44 +82,44 @@ public sealed class EfMerchantRepository : IMerchantRepository
/// <inheritdoc /> /// <inheritdoc />
public Task AddMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default) public Task AddMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default)
{ {
return _context.Merchants.AddAsync(merchant, cancellationToken).AsTask(); return context.Merchants.AddAsync(merchant, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddStaffAsync(MerchantStaff staff, CancellationToken cancellationToken = default) public Task AddStaffAsync(MerchantStaff staff, CancellationToken cancellationToken = default)
{ {
return _context.MerchantStaff.AddAsync(staff, cancellationToken).AsTask(); return context.MerchantStaff.AddAsync(staff, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddContractAsync(MerchantContract contract, CancellationToken cancellationToken = default) public Task AddContractAsync(MerchantContract contract, CancellationToken cancellationToken = default)
{ {
return _context.MerchantContracts.AddAsync(contract, cancellationToken).AsTask(); return context.MerchantContracts.AddAsync(contract, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddDocumentAsync(MerchantDocument document, CancellationToken cancellationToken = default) public Task AddDocumentAsync(MerchantDocument document, CancellationToken cancellationToken = default)
{ {
return _context.MerchantDocuments.AddAsync(document, cancellationToken).AsTask(); return context.MerchantDocuments.AddAsync(document, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task SaveChangesAsync(CancellationToken cancellationToken = default) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{ {
return _context.SaveChangesAsync(cancellationToken); return context.SaveChangesAsync(cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task UpdateMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default) public Task UpdateMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default)
{ {
_context.Merchants.Update(merchant); context.Merchants.Update(merchant);
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task DeleteMerchantAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default) public async Task DeleteMerchantAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default)
{ {
var existing = await _context.Merchants var existing = await context.Merchants
.Where(x => x.TenantId == tenantId && x.Id == merchantId) .Where(x => x.TenantId == tenantId && x.Id == merchantId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -133,6 +128,6 @@ public sealed class EfMerchantRepository : IMerchantRepository
return; return;
} }
_context.Merchants.Remove(existing); context.Merchants.Remove(existing);
} }
} }

View File

@@ -11,22 +11,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
/// <summary> /// <summary>
/// 订单聚合的 EF Core 仓储实现。 /// 订单聚合的 EF Core 仓储实现。
/// </summary> /// </summary>
public sealed class EfOrderRepository : IOrderRepository /// <remarks>
{
private readonly TakeoutAppDbContext _context;
/// <summary>
/// 初始化仓储。 /// 初始化仓储。
/// </summary> /// </remarks>
public EfOrderRepository(TakeoutAppDbContext context) public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepository
{ {
_context = context;
}
/// <inheritdoc /> /// <inheritdoc />
public Task<Order?> FindByIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) public Task<Order?> FindByIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
{ {
return _context.Orders return context.Orders
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.Id == orderId) .Where(x => x.TenantId == tenantId && x.Id == orderId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -35,7 +30,7 @@ public sealed class EfOrderRepository : IOrderRepository
/// <inheritdoc /> /// <inheritdoc />
public Task<Order?> FindByOrderNoAsync(string orderNo, long tenantId, CancellationToken cancellationToken = default) public Task<Order?> FindByOrderNoAsync(string orderNo, long tenantId, CancellationToken cancellationToken = default)
{ {
return _context.Orders return context.Orders
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.OrderNo == orderNo) .Where(x => x.TenantId == tenantId && x.OrderNo == orderNo)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -44,7 +39,7 @@ public sealed class EfOrderRepository : IOrderRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<Order>> SearchAsync(long tenantId, OrderStatus? status, PaymentStatus? paymentStatus, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<Order>> SearchAsync(long tenantId, OrderStatus? status, PaymentStatus? paymentStatus, CancellationToken cancellationToken = default)
{ {
var query = _context.Orders var query = context.Orders
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId); .Where(x => x.TenantId == tenantId);
@@ -68,7 +63,7 @@ public sealed class EfOrderRepository : IOrderRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<OrderItem>> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<OrderItem>> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
{ {
var items = await _context.OrderItems var items = await context.OrderItems
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.OrderId == orderId) .Where(x => x.TenantId == tenantId && x.OrderId == orderId)
.OrderBy(x => x.Id) .OrderBy(x => x.Id)
@@ -80,7 +75,7 @@ public sealed class EfOrderRepository : IOrderRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<OrderStatusHistory>> GetStatusHistoryAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<OrderStatusHistory>> GetStatusHistoryAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
{ {
var histories = await _context.OrderStatusHistories var histories = await context.OrderStatusHistories
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.OrderId == orderId) .Where(x => x.TenantId == tenantId && x.OrderId == orderId)
.OrderBy(x => x.CreatedAt) .OrderBy(x => x.CreatedAt)
@@ -92,7 +87,7 @@ public sealed class EfOrderRepository : IOrderRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<RefundRequest>> GetRefundsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<RefundRequest>> GetRefundsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
{ {
var refunds = await _context.RefundRequests var refunds = await context.RefundRequests
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.OrderId == orderId) .Where(x => x.TenantId == tenantId && x.OrderId == orderId)
.OrderByDescending(x => x.CreatedAt) .OrderByDescending(x => x.CreatedAt)
@@ -104,68 +99,68 @@ public sealed class EfOrderRepository : IOrderRepository
/// <inheritdoc /> /// <inheritdoc />
public Task AddOrderAsync(Order order, CancellationToken cancellationToken = default) public Task AddOrderAsync(Order order, CancellationToken cancellationToken = default)
{ {
return _context.Orders.AddAsync(order, cancellationToken).AsTask(); return context.Orders.AddAsync(order, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddItemsAsync(IEnumerable<OrderItem> items, CancellationToken cancellationToken = default) public Task AddItemsAsync(IEnumerable<OrderItem> items, CancellationToken cancellationToken = default)
{ {
return _context.OrderItems.AddRangeAsync(items, cancellationToken); return context.OrderItems.AddRangeAsync(items, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddStatusHistoryAsync(OrderStatusHistory history, CancellationToken cancellationToken = default) public Task AddStatusHistoryAsync(OrderStatusHistory history, CancellationToken cancellationToken = default)
{ {
return _context.OrderStatusHistories.AddAsync(history, cancellationToken).AsTask(); return context.OrderStatusHistories.AddAsync(history, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddRefundAsync(RefundRequest refund, CancellationToken cancellationToken = default) public Task AddRefundAsync(RefundRequest refund, CancellationToken cancellationToken = default)
{ {
return _context.RefundRequests.AddAsync(refund, cancellationToken).AsTask(); return context.RefundRequests.AddAsync(refund, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task SaveChangesAsync(CancellationToken cancellationToken = default) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{ {
return _context.SaveChangesAsync(cancellationToken); return context.SaveChangesAsync(cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task UpdateOrderAsync(Order order, CancellationToken cancellationToken = default) public Task UpdateOrderAsync(Order order, CancellationToken cancellationToken = default)
{ {
_context.Orders.Update(order); context.Orders.Update(order);
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task DeleteOrderAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) public async Task DeleteOrderAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
{ {
var items = await _context.OrderItems var items = await context.OrderItems
.Where(x => x.TenantId == tenantId && x.OrderId == orderId) .Where(x => x.TenantId == tenantId && x.OrderId == orderId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (items.Count > 0) if (items.Count > 0)
{ {
_context.OrderItems.RemoveRange(items); context.OrderItems.RemoveRange(items);
} }
var histories = await _context.OrderStatusHistories var histories = await context.OrderStatusHistories
.Where(x => x.TenantId == tenantId && x.OrderId == orderId) .Where(x => x.TenantId == tenantId && x.OrderId == orderId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (histories.Count > 0) if (histories.Count > 0)
{ {
_context.OrderStatusHistories.RemoveRange(histories); context.OrderStatusHistories.RemoveRange(histories);
} }
var refunds = await _context.RefundRequests var refunds = await context.RefundRequests
.Where(x => x.TenantId == tenantId && x.OrderId == orderId) .Where(x => x.TenantId == tenantId && x.OrderId == orderId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (refunds.Count > 0) if (refunds.Count > 0)
{ {
_context.RefundRequests.RemoveRange(refunds); context.RefundRequests.RemoveRange(refunds);
} }
var existing = await _context.Orders var existing = await context.Orders
.Where(x => x.TenantId == tenantId && x.Id == orderId) .Where(x => x.TenantId == tenantId && x.Id == orderId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
if (existing == null) if (existing == null)
@@ -173,6 +168,6 @@ public sealed class EfOrderRepository : IOrderRepository
return; return;
} }
_context.Orders.Remove(existing); context.Orders.Remove(existing);
} }
} }

View File

@@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
/// <summary> /// <summary>
/// 支付记录的 EF Core 仓储实现。 /// 支付记录的 EF Core 仓储实现。
/// </summary> /// </summary>
public sealed class EfPaymentRepository : IPaymentRepository /// <remarks>
{
private readonly TakeoutAppDbContext _context;
/// <summary>
/// 初始化仓储。 /// 初始化仓储。
/// </summary> /// </remarks>
public EfPaymentRepository(TakeoutAppDbContext context) public sealed class EfPaymentRepository(TakeoutAppDbContext context) : IPaymentRepository
{ {
_context = context;
}
/// <inheritdoc /> /// <inheritdoc />
public Task<PaymentRecord?> FindByIdAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default) public Task<PaymentRecord?> FindByIdAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default)
{ {
return _context.PaymentRecords return context.PaymentRecords
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.Id == paymentId) .Where(x => x.TenantId == tenantId && x.Id == paymentId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -34,7 +29,7 @@ public sealed class EfPaymentRepository : IPaymentRepository
/// <inheritdoc /> /// <inheritdoc />
public Task<PaymentRecord?> FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default) public Task<PaymentRecord?> FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
{ {
return _context.PaymentRecords return context.PaymentRecords
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.OrderId == orderId) .Where(x => x.TenantId == tenantId && x.OrderId == orderId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -43,7 +38,7 @@ public sealed class EfPaymentRepository : IPaymentRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<PaymentRefundRecord>> GetRefundsAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<PaymentRefundRecord>> GetRefundsAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default)
{ {
var refunds = await _context.PaymentRefundRecords var refunds = await context.PaymentRefundRecords
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.PaymentRecordId == paymentId) .Where(x => x.TenantId == tenantId && x.PaymentRecordId == paymentId)
.OrderByDescending(x => x.CreatedAt) .OrderByDescending(x => x.CreatedAt)
@@ -55,19 +50,19 @@ public sealed class EfPaymentRepository : IPaymentRepository
/// <inheritdoc /> /// <inheritdoc />
public Task AddPaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default) public Task AddPaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default)
{ {
return _context.PaymentRecords.AddAsync(payment, cancellationToken).AsTask(); return context.PaymentRecords.AddAsync(payment, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddRefundAsync(PaymentRefundRecord refund, CancellationToken cancellationToken = default) public Task AddRefundAsync(PaymentRefundRecord refund, CancellationToken cancellationToken = default)
{ {
return _context.PaymentRefundRecords.AddAsync(refund, cancellationToken).AsTask(); return context.PaymentRefundRecords.AddAsync(refund, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<PaymentRecord>> SearchAsync(long tenantId, PaymentStatus? status, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<PaymentRecord>> SearchAsync(long tenantId, PaymentStatus? status, CancellationToken cancellationToken = default)
{ {
var query = _context.PaymentRecords var query = context.PaymentRecords
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId); .Where(x => x.TenantId == tenantId);
@@ -84,28 +79,28 @@ public sealed class EfPaymentRepository : IPaymentRepository
/// <inheritdoc /> /// <inheritdoc />
public Task SaveChangesAsync(CancellationToken cancellationToken = default) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{ {
return _context.SaveChangesAsync(cancellationToken); return context.SaveChangesAsync(cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task UpdatePaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default) public Task UpdatePaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default)
{ {
_context.PaymentRecords.Update(payment); context.PaymentRecords.Update(payment);
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task DeletePaymentAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default) public async Task DeletePaymentAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default)
{ {
var refunds = await _context.PaymentRefundRecords var refunds = await context.PaymentRefundRecords
.Where(x => x.TenantId == tenantId && x.PaymentRecordId == paymentId) .Where(x => x.TenantId == tenantId && x.PaymentRecordId == paymentId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (refunds.Count > 0) if (refunds.Count > 0)
{ {
_context.PaymentRefundRecords.RemoveRange(refunds); context.PaymentRefundRecords.RemoveRange(refunds);
} }
var existing = await _context.PaymentRecords var existing = await context.PaymentRecords
.Where(x => x.TenantId == tenantId && x.Id == paymentId) .Where(x => x.TenantId == tenantId && x.Id == paymentId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
if (existing == null) if (existing == null)
@@ -113,6 +108,6 @@ public sealed class EfPaymentRepository : IPaymentRepository
return; return;
} }
_context.PaymentRecords.Remove(existing); context.PaymentRecords.Remove(existing);
} }
} }

View File

@@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
/// <summary> /// <summary>
/// 商品聚合的 EF Core 仓储实现。 /// 商品聚合的 EF Core 仓储实现。
/// </summary> /// </summary>
public sealed class EfProductRepository : IProductRepository /// <remarks>
{
private readonly TakeoutAppDbContext _context;
/// <summary>
/// 初始化仓储。 /// 初始化仓储。
/// </summary> /// </remarks>
public EfProductRepository(TakeoutAppDbContext context) public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductRepository
{ {
_context = context;
}
/// <inheritdoc /> /// <inheritdoc />
public Task<Product?> FindByIdAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public Task<Product?> FindByIdAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
return _context.Products return context.Products
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.Id == productId) .Where(x => x.TenantId == tenantId && x.Id == productId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -34,7 +29,7 @@ public sealed class EfProductRepository : IProductRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<Product>> SearchAsync(long tenantId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<Product>> SearchAsync(long tenantId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default)
{ {
var query = _context.Products var query = context.Products
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId); .Where(x => x.TenantId == tenantId);
@@ -58,7 +53,7 @@ public sealed class EfProductRepository : IProductRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<ProductCategory>> GetCategoriesAsync(long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<ProductCategory>> GetCategoriesAsync(long tenantId, CancellationToken cancellationToken = default)
{ {
var categories = await _context.ProductCategories var categories = await context.ProductCategories
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId) .Where(x => x.TenantId == tenantId)
.OrderBy(x => x.SortOrder) .OrderBy(x => x.SortOrder)
@@ -70,7 +65,7 @@ public sealed class EfProductRepository : IProductRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<ProductSku>> GetSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<ProductSku>> GetSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
var skus = await _context.ProductSkus var skus = await context.ProductSkus
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.ProductId == productId) .Where(x => x.TenantId == tenantId && x.ProductId == productId)
.OrderBy(x => x.SortOrder) .OrderBy(x => x.SortOrder)
@@ -82,7 +77,7 @@ public sealed class EfProductRepository : IProductRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<ProductAddonGroup>> GetAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<ProductAddonGroup>> GetAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
var groups = await _context.ProductAddonGroups var groups = await context.ProductAddonGroups
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.ProductId == productId) .Where(x => x.TenantId == tenantId && x.ProductId == productId)
.OrderBy(x => x.SortOrder) .OrderBy(x => x.SortOrder)
@@ -94,7 +89,7 @@ public sealed class EfProductRepository : IProductRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<ProductAddonOption>> GetAddonOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<ProductAddonOption>> GetAddonOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
var groupIds = await _context.ProductAddonGroups var groupIds = await context.ProductAddonGroups
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.ProductId == productId) .Where(x => x.TenantId == tenantId && x.ProductId == productId)
.Select(x => x.Id) .Select(x => x.Id)
@@ -105,7 +100,7 @@ public sealed class EfProductRepository : IProductRepository
return Array.Empty<ProductAddonOption>(); return Array.Empty<ProductAddonOption>();
} }
var options = await _context.ProductAddonOptions var options = await context.ProductAddonOptions
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && groupIds.Contains(x.AddonGroupId)) .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AddonGroupId))
.OrderBy(x => x.SortOrder) .OrderBy(x => x.SortOrder)
@@ -117,7 +112,7 @@ public sealed class EfProductRepository : IProductRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<ProductAttributeGroup>> GetAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<ProductAttributeGroup>> GetAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
var groups = await _context.ProductAttributeGroups var groups = await context.ProductAttributeGroups
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.ProductId == productId) .Where(x => x.TenantId == tenantId && x.ProductId == productId)
.OrderBy(x => x.SortOrder) .OrderBy(x => x.SortOrder)
@@ -129,7 +124,7 @@ public sealed class EfProductRepository : IProductRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<ProductAttributeOption>> GetAttributeOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<ProductAttributeOption>> GetAttributeOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
var groupIds = await _context.ProductAttributeGroups var groupIds = await context.ProductAttributeGroups
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.ProductId == productId) .Where(x => x.TenantId == tenantId && x.ProductId == productId)
.Select(x => x.Id) .Select(x => x.Id)
@@ -140,7 +135,7 @@ public sealed class EfProductRepository : IProductRepository
return Array.Empty<ProductAttributeOption>(); return Array.Empty<ProductAttributeOption>();
} }
var options = await _context.ProductAttributeOptions var options = await context.ProductAttributeOptions
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && groupIds.Contains(x.AttributeGroupId)) .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AttributeGroupId))
.OrderBy(x => x.SortOrder) .OrderBy(x => x.SortOrder)
@@ -152,7 +147,7 @@ public sealed class EfProductRepository : IProductRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<ProductMediaAsset>> GetMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<ProductMediaAsset>> GetMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
var assets = await _context.ProductMediaAssets var assets = await context.ProductMediaAssets
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.ProductId == productId) .Where(x => x.TenantId == tenantId && x.ProductId == productId)
.OrderBy(x => x.SortOrder) .OrderBy(x => x.SortOrder)
@@ -164,7 +159,7 @@ public sealed class EfProductRepository : IProductRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<ProductPricingRule>> GetPricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<ProductPricingRule>> GetPricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
var rules = await _context.ProductPricingRules var rules = await context.ProductPricingRules
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.ProductId == productId) .Where(x => x.TenantId == tenantId && x.ProductId == productId)
.OrderBy(x => x.SortOrder) .OrderBy(x => x.SortOrder)
@@ -176,59 +171,59 @@ public sealed class EfProductRepository : IProductRepository
/// <inheritdoc /> /// <inheritdoc />
public Task AddCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default) public Task AddCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default)
{ {
return _context.ProductCategories.AddAsync(category, cancellationToken).AsTask(); return context.ProductCategories.AddAsync(category, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddProductAsync(Product product, CancellationToken cancellationToken = default) public Task AddProductAsync(Product product, CancellationToken cancellationToken = default)
{ {
return _context.Products.AddAsync(product, cancellationToken).AsTask(); return context.Products.AddAsync(product, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddSkusAsync(IEnumerable<ProductSku> skus, CancellationToken cancellationToken = default) public Task AddSkusAsync(IEnumerable<ProductSku> skus, CancellationToken cancellationToken = default)
{ {
return _context.ProductSkus.AddRangeAsync(skus, cancellationToken); return context.ProductSkus.AddRangeAsync(skus, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddAddonGroupsAsync(IEnumerable<ProductAddonGroup> groups, IEnumerable<ProductAddonOption> options, CancellationToken cancellationToken = default) public Task AddAddonGroupsAsync(IEnumerable<ProductAddonGroup> groups, IEnumerable<ProductAddonOption> options, CancellationToken cancellationToken = default)
{ {
var addGroupsTask = _context.ProductAddonGroups.AddRangeAsync(groups, cancellationToken); var addGroupsTask = context.ProductAddonGroups.AddRangeAsync(groups, cancellationToken);
var addOptionsTask = _context.ProductAddonOptions.AddRangeAsync(options, cancellationToken); var addOptionsTask = context.ProductAddonOptions.AddRangeAsync(options, cancellationToken);
return Task.WhenAll(addGroupsTask, addOptionsTask); return Task.WhenAll(addGroupsTask, addOptionsTask);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddAttributeGroupsAsync(IEnumerable<ProductAttributeGroup> groups, IEnumerable<ProductAttributeOption> options, CancellationToken cancellationToken = default) public Task AddAttributeGroupsAsync(IEnumerable<ProductAttributeGroup> groups, IEnumerable<ProductAttributeOption> options, CancellationToken cancellationToken = default)
{ {
var addGroupsTask = _context.ProductAttributeGroups.AddRangeAsync(groups, cancellationToken); var addGroupsTask = context.ProductAttributeGroups.AddRangeAsync(groups, cancellationToken);
var addOptionsTask = _context.ProductAttributeOptions.AddRangeAsync(options, cancellationToken); var addOptionsTask = context.ProductAttributeOptions.AddRangeAsync(options, cancellationToken);
return Task.WhenAll(addGroupsTask, addOptionsTask); return Task.WhenAll(addGroupsTask, addOptionsTask);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddMediaAssetsAsync(IEnumerable<ProductMediaAsset> assets, CancellationToken cancellationToken = default) public Task AddMediaAssetsAsync(IEnumerable<ProductMediaAsset> assets, CancellationToken cancellationToken = default)
{ {
return _context.ProductMediaAssets.AddRangeAsync(assets, cancellationToken); return context.ProductMediaAssets.AddRangeAsync(assets, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddPricingRulesAsync(IEnumerable<ProductPricingRule> rules, CancellationToken cancellationToken = default) public Task AddPricingRulesAsync(IEnumerable<ProductPricingRule> rules, CancellationToken cancellationToken = default)
{ {
return _context.ProductPricingRules.AddRangeAsync(rules, cancellationToken); return context.ProductPricingRules.AddRangeAsync(rules, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task SaveChangesAsync(CancellationToken cancellationToken = default) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{ {
return _context.SaveChangesAsync(cancellationToken); return context.SaveChangesAsync(cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task UpdateProductAsync(Product product, CancellationToken cancellationToken = default) public Task UpdateProductAsync(Product product, CancellationToken cancellationToken = default)
{ {
_context.Products.Update(product); context.Products.Update(product);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -241,7 +236,7 @@ public sealed class EfProductRepository : IProductRepository
await RemoveAddonGroupsAsync(productId, tenantId, cancellationToken); await RemoveAddonGroupsAsync(productId, tenantId, cancellationToken);
await RemoveSkusAsync(productId, tenantId, cancellationToken); await RemoveSkusAsync(productId, tenantId, cancellationToken);
var existing = await _context.Products var existing = await context.Products
.Where(x => x.TenantId == tenantId && x.Id == productId) .Where(x => x.TenantId == tenantId && x.Id == productId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -250,20 +245,20 @@ public sealed class EfProductRepository : IProductRepository
return; return;
} }
_context.Products.Remove(existing); context.Products.Remove(existing);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task UpdateCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default) public Task UpdateCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default)
{ {
_context.ProductCategories.Update(category); context.ProductCategories.Update(category);
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task DeleteCategoryAsync(long categoryId, long tenantId, CancellationToken cancellationToken = default) public async Task DeleteCategoryAsync(long categoryId, long tenantId, CancellationToken cancellationToken = default)
{ {
var existing = await _context.ProductCategories var existing = await context.ProductCategories
.Where(x => x.TenantId == tenantId && x.Id == categoryId) .Where(x => x.TenantId == tenantId && x.Id == categoryId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -272,13 +267,13 @@ public sealed class EfProductRepository : IProductRepository
return; return;
} }
_context.ProductCategories.Remove(existing); context.ProductCategories.Remove(existing);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task RemoveSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public async Task RemoveSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
var skus = await _context.ProductSkus var skus = await context.ProductSkus
.Where(x => x.TenantId == tenantId && x.ProductId == productId) .Where(x => x.TenantId == tenantId && x.ProductId == productId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -287,13 +282,13 @@ public sealed class EfProductRepository : IProductRepository
return; return;
} }
_context.ProductSkus.RemoveRange(skus); context.ProductSkus.RemoveRange(skus);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task RemoveAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public async Task RemoveAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
var groupIds = await _context.ProductAddonGroups var groupIds = await context.ProductAddonGroups
.Where(x => x.TenantId == tenantId && x.ProductId == productId) .Where(x => x.TenantId == tenantId && x.ProductId == productId)
.Select(x => x.Id) .Select(x => x.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -303,29 +298,29 @@ public sealed class EfProductRepository : IProductRepository
return; return;
} }
var options = await _context.ProductAddonOptions var options = await context.ProductAddonOptions
.Where(x => x.TenantId == tenantId && groupIds.Contains(x.AddonGroupId)) .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AddonGroupId))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (options.Count > 0) if (options.Count > 0)
{ {
_context.ProductAddonOptions.RemoveRange(options); context.ProductAddonOptions.RemoveRange(options);
} }
var groups = await _context.ProductAddonGroups var groups = await context.ProductAddonGroups
.Where(x => groupIds.Contains(x.Id)) .Where(x => groupIds.Contains(x.Id))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (groups.Count > 0) if (groups.Count > 0)
{ {
_context.ProductAddonGroups.RemoveRange(groups); context.ProductAddonGroups.RemoveRange(groups);
} }
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task RemoveAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public async Task RemoveAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
var groupIds = await _context.ProductAttributeGroups var groupIds = await context.ProductAttributeGroups
.Where(x => x.TenantId == tenantId && x.ProductId == productId) .Where(x => x.TenantId == tenantId && x.ProductId == productId)
.Select(x => x.Id) .Select(x => x.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -335,29 +330,29 @@ public sealed class EfProductRepository : IProductRepository
return; return;
} }
var options = await _context.ProductAttributeOptions var options = await context.ProductAttributeOptions
.Where(x => x.TenantId == tenantId && groupIds.Contains(x.AttributeGroupId)) .Where(x => x.TenantId == tenantId && groupIds.Contains(x.AttributeGroupId))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (options.Count > 0) if (options.Count > 0)
{ {
_context.ProductAttributeOptions.RemoveRange(options); context.ProductAttributeOptions.RemoveRange(options);
} }
var groups = await _context.ProductAttributeGroups var groups = await context.ProductAttributeGroups
.Where(x => groupIds.Contains(x.Id)) .Where(x => groupIds.Contains(x.Id))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (groups.Count > 0) if (groups.Count > 0)
{ {
_context.ProductAttributeGroups.RemoveRange(groups); context.ProductAttributeGroups.RemoveRange(groups);
} }
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task RemoveMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public async Task RemoveMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
var assets = await _context.ProductMediaAssets var assets = await context.ProductMediaAssets
.Where(x => x.TenantId == tenantId && x.ProductId == productId) .Where(x => x.TenantId == tenantId && x.ProductId == productId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -366,13 +361,13 @@ public sealed class EfProductRepository : IProductRepository
return; return;
} }
_context.ProductMediaAssets.RemoveRange(assets); context.ProductMediaAssets.RemoveRange(assets);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task RemovePricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default) public async Task RemovePricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
{ {
var rules = await _context.ProductPricingRules var rules = await context.ProductPricingRules
.Where(x => x.TenantId == tenantId && x.ProductId == productId) .Where(x => x.TenantId == tenantId && x.ProductId == productId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -381,6 +376,6 @@ public sealed class EfProductRepository : IProductRepository
return; return;
} }
_context.ProductPricingRules.RemoveRange(rules); context.ProductPricingRules.RemoveRange(rules);
} }
} }

View File

@@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
/// <summary> /// <summary>
/// 门店聚合的 EF Core 仓储实现。 /// 门店聚合的 EF Core 仓储实现。
/// </summary> /// </summary>
public sealed class EfStoreRepository : IStoreRepository /// <remarks>
{
private readonly TakeoutAppDbContext _context;
/// <summary>
/// 初始化仓储。 /// 初始化仓储。
/// </summary> /// </remarks>
public EfStoreRepository(TakeoutAppDbContext context) public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepository
{ {
_context = context;
}
/// <inheritdoc /> /// <inheritdoc />
public Task<Store?> FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) public Task<Store?> FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
{ {
return _context.Stores return context.Stores
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.Id == storeId) .Where(x => x.TenantId == tenantId && x.Id == storeId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -34,7 +29,7 @@ public sealed class EfStoreRepository : IStoreRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<Store>> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<Store>> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default)
{ {
var query = _context.Stores var query = context.Stores
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId); .Where(x => x.TenantId == tenantId);
@@ -53,7 +48,7 @@ public sealed class EfStoreRepository : IStoreRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<StoreBusinessHour>> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<StoreBusinessHour>> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
{ {
var hours = await _context.StoreBusinessHours var hours = await context.StoreBusinessHours
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.StoreId == storeId) .Where(x => x.TenantId == tenantId && x.StoreId == storeId)
.OrderBy(x => x.DayOfWeek) .OrderBy(x => x.DayOfWeek)
@@ -66,7 +61,7 @@ public sealed class EfStoreRepository : IStoreRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<StoreDeliveryZone>> GetDeliveryZonesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<StoreDeliveryZone>> GetDeliveryZonesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
{ {
var zones = await _context.StoreDeliveryZones var zones = await context.StoreDeliveryZones
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.StoreId == storeId) .Where(x => x.TenantId == tenantId && x.StoreId == storeId)
.OrderBy(x => x.SortOrder) .OrderBy(x => x.SortOrder)
@@ -78,7 +73,7 @@ public sealed class EfStoreRepository : IStoreRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<StoreHoliday>> GetHolidaysAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<StoreHoliday>> GetHolidaysAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
{ {
var holidays = await _context.StoreHolidays var holidays = await context.StoreHolidays
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.StoreId == storeId) .Where(x => x.TenantId == tenantId && x.StoreId == storeId)
.OrderBy(x => x.Date) .OrderBy(x => x.Date)
@@ -90,7 +85,7 @@ public sealed class EfStoreRepository : IStoreRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<StoreTableArea>> GetTableAreasAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<StoreTableArea>> GetTableAreasAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
{ {
var areas = await _context.StoreTableAreas var areas = await context.StoreTableAreas
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.StoreId == storeId) .Where(x => x.TenantId == tenantId && x.StoreId == storeId)
.OrderBy(x => x.SortOrder) .OrderBy(x => x.SortOrder)
@@ -102,7 +97,7 @@ public sealed class EfStoreRepository : IStoreRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<StoreTable>> GetTablesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<StoreTable>> GetTablesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
{ {
var tables = await _context.StoreTables var tables = await context.StoreTables
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.StoreId == storeId) .Where(x => x.TenantId == tenantId && x.StoreId == storeId)
.OrderBy(x => x.TableCode) .OrderBy(x => x.TableCode)
@@ -114,7 +109,7 @@ public sealed class EfStoreRepository : IStoreRepository
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<StoreEmployeeShift>> GetShiftsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<StoreEmployeeShift>> GetShiftsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
{ {
var shifts = await _context.StoreEmployeeShifts var shifts = await context.StoreEmployeeShifts
.AsNoTracking() .AsNoTracking()
.Where(x => x.TenantId == tenantId && x.StoreId == storeId) .Where(x => x.TenantId == tenantId && x.StoreId == storeId)
.OrderBy(x => x.ShiftDate) .OrderBy(x => x.ShiftDate)
@@ -127,62 +122,62 @@ public sealed class EfStoreRepository : IStoreRepository
/// <inheritdoc /> /// <inheritdoc />
public Task AddStoreAsync(Store store, CancellationToken cancellationToken = default) public Task AddStoreAsync(Store store, CancellationToken cancellationToken = default)
{ {
return _context.Stores.AddAsync(store, cancellationToken).AsTask(); return context.Stores.AddAsync(store, cancellationToken).AsTask();
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddBusinessHoursAsync(IEnumerable<StoreBusinessHour> hours, CancellationToken cancellationToken = default) public Task AddBusinessHoursAsync(IEnumerable<StoreBusinessHour> hours, CancellationToken cancellationToken = default)
{ {
return _context.StoreBusinessHours.AddRangeAsync(hours, cancellationToken); return context.StoreBusinessHours.AddRangeAsync(hours, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddDeliveryZonesAsync(IEnumerable<StoreDeliveryZone> zones, CancellationToken cancellationToken = default) public Task AddDeliveryZonesAsync(IEnumerable<StoreDeliveryZone> zones, CancellationToken cancellationToken = default)
{ {
return _context.StoreDeliveryZones.AddRangeAsync(zones, cancellationToken); return context.StoreDeliveryZones.AddRangeAsync(zones, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddHolidaysAsync(IEnumerable<StoreHoliday> holidays, CancellationToken cancellationToken = default) public Task AddHolidaysAsync(IEnumerable<StoreHoliday> holidays, CancellationToken cancellationToken = default)
{ {
return _context.StoreHolidays.AddRangeAsync(holidays, cancellationToken); return context.StoreHolidays.AddRangeAsync(holidays, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddTableAreasAsync(IEnumerable<StoreTableArea> areas, CancellationToken cancellationToken = default) public Task AddTableAreasAsync(IEnumerable<StoreTableArea> areas, CancellationToken cancellationToken = default)
{ {
return _context.StoreTableAreas.AddRangeAsync(areas, cancellationToken); return context.StoreTableAreas.AddRangeAsync(areas, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddTablesAsync(IEnumerable<StoreTable> tables, CancellationToken cancellationToken = default) public Task AddTablesAsync(IEnumerable<StoreTable> tables, CancellationToken cancellationToken = default)
{ {
return _context.StoreTables.AddRangeAsync(tables, cancellationToken); return context.StoreTables.AddRangeAsync(tables, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task AddShiftsAsync(IEnumerable<StoreEmployeeShift> shifts, CancellationToken cancellationToken = default) public Task AddShiftsAsync(IEnumerable<StoreEmployeeShift> shifts, CancellationToken cancellationToken = default)
{ {
return _context.StoreEmployeeShifts.AddRangeAsync(shifts, cancellationToken); return context.StoreEmployeeShifts.AddRangeAsync(shifts, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task SaveChangesAsync(CancellationToken cancellationToken = default) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{ {
return _context.SaveChangesAsync(cancellationToken); return context.SaveChangesAsync(cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public Task UpdateStoreAsync(Store store, CancellationToken cancellationToken = default) public Task UpdateStoreAsync(Store store, CancellationToken cancellationToken = default)
{ {
_context.Stores.Update(store); context.Stores.Update(store);
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task DeleteStoreAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) public async Task DeleteStoreAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
{ {
var existing = await _context.Stores var existing = await context.Stores
.Where(x => x.TenantId == tenantId && x.Id == storeId) .Where(x => x.TenantId == tenantId && x.Id == storeId)
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
@@ -191,6 +186,6 @@ public sealed class EfStoreRepository : IStoreRepository
return; return;
} }
_context.Stores.Remove(existing); context.Stores.Remove(existing);
} }
} }

View File

@@ -10,24 +10,18 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories;
/// <summary> /// <summary>
/// EF Core 字典仓储实现。 /// EF Core 字典仓储实现。
/// </summary> /// </summary>
public sealed class EfDictionaryRepository : IDictionaryRepository public sealed class EfDictionaryRepository(DictionaryDbContext context) : IDictionaryRepository
{ {
private readonly DictionaryDbContext _context;
public EfDictionaryRepository(DictionaryDbContext context)
{
_context = context;
}
public Task<DictionaryGroup?> FindGroupByIdAsync(long id, CancellationToken cancellationToken = default) public Task<DictionaryGroup?> FindGroupByIdAsync(long id, CancellationToken cancellationToken = default)
=> _context.DictionaryGroups.FirstOrDefaultAsync(group => group.Id == id, cancellationToken); => context.DictionaryGroups.FirstOrDefaultAsync(group => group.Id == id, cancellationToken);
public Task<DictionaryGroup?> FindGroupByCodeAsync(string code, CancellationToken cancellationToken = default) public Task<DictionaryGroup?> FindGroupByCodeAsync(string code, CancellationToken cancellationToken = default)
=> _context.DictionaryGroups.FirstOrDefaultAsync(group => group.Code == code, cancellationToken); => context.DictionaryGroups.FirstOrDefaultAsync(group => group.Code == code, cancellationToken);
public async Task<IReadOnlyList<DictionaryGroup>> SearchGroupsAsync(DictionaryScope? scope, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<DictionaryGroup>> SearchGroupsAsync(DictionaryScope? scope, CancellationToken cancellationToken = default)
{ {
var query = _context.DictionaryGroups.AsNoTracking(); var query = context.DictionaryGroups.AsNoTracking();
if (scope.HasValue) if (scope.HasValue)
{ {
query = query.Where(group => group.Scope == scope.Value); query = query.Where(group => group.Scope == scope.Value);
@@ -40,22 +34,22 @@ public sealed class EfDictionaryRepository : IDictionaryRepository
public Task AddGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default) public Task AddGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default)
{ {
_context.DictionaryGroups.Add(group); context.DictionaryGroups.Add(group);
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task RemoveGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default) public Task RemoveGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default)
{ {
_context.DictionaryGroups.Remove(group); context.DictionaryGroups.Remove(group);
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<DictionaryItem?> FindItemByIdAsync(long id, CancellationToken cancellationToken = default) public Task<DictionaryItem?> FindItemByIdAsync(long id, CancellationToken cancellationToken = default)
=> _context.DictionaryItems.FirstOrDefaultAsync(item => item.Id == id, cancellationToken); => context.DictionaryItems.FirstOrDefaultAsync(item => item.Id == id, cancellationToken);
public async Task<IReadOnlyList<DictionaryItem>> GetItemsByGroupIdAsync(long groupId, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<DictionaryItem>> GetItemsByGroupIdAsync(long groupId, CancellationToken cancellationToken = default)
{ {
return await _context.DictionaryItems return await context.DictionaryItems
.AsNoTracking() .AsNoTracking()
.Where(item => item.GroupId == groupId) .Where(item => item.GroupId == groupId)
.OrderBy(item => item.SortOrder) .OrderBy(item => item.SortOrder)
@@ -64,18 +58,18 @@ public sealed class EfDictionaryRepository : IDictionaryRepository
public Task AddItemAsync(DictionaryItem item, CancellationToken cancellationToken = default) public Task AddItemAsync(DictionaryItem item, CancellationToken cancellationToken = default)
{ {
_context.DictionaryItems.Add(item); context.DictionaryItems.Add(item);
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task RemoveItemAsync(DictionaryItem item, CancellationToken cancellationToken = default) public Task RemoveItemAsync(DictionaryItem item, CancellationToken cancellationToken = default)
{ {
_context.DictionaryItems.Remove(item); context.DictionaryItems.Remove(item);
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task SaveChangesAsync(CancellationToken cancellationToken = default) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> _context.SaveChangesAsync(cancellationToken); => context.SaveChangesAsync(cancellationToken);
public async Task<IReadOnlyList<DictionaryItem>> GetItemsByCodesAsync(IEnumerable<string> codes, long tenantId, bool includeSystem, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<DictionaryItem>> GetItemsByCodesAsync(IEnumerable<string> codes, long tenantId, bool includeSystem, CancellationToken cancellationToken = default)
{ {
@@ -90,7 +84,7 @@ public sealed class EfDictionaryRepository : IDictionaryRepository
return Array.Empty<DictionaryItem>(); return Array.Empty<DictionaryItem>();
} }
var query = _context.DictionaryItems var query = context.DictionaryItems
.AsNoTracking() .AsNoTracking()
.IgnoreQueryFilters() .IgnoreQueryFilters()
.Include(item => item.Group) .Include(item => item.Group)

View File

@@ -10,22 +10,15 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Services;
/// <summary> /// <summary>
/// 基于 IDistributedCache 的字典缓存实现。 /// 基于 IDistributedCache 的字典缓存实现。
/// </summary> /// </summary>
public sealed class DistributedDictionaryCache : IDictionaryCache public sealed class DistributedDictionaryCache(IDistributedCache cache, IOptions<DictionaryCacheOptions> options) : IDictionaryCache
{ {
private readonly IDistributedCache _cache; private readonly DictionaryCacheOptions _options = options.Value;
private readonly DictionaryCacheOptions _options;
private readonly JsonSerializerOptions _serializerOptions = new(JsonSerializerDefaults.Web); private readonly JsonSerializerOptions _serializerOptions = new(JsonSerializerDefaults.Web);
public DistributedDictionaryCache(IDistributedCache cache, IOptions<DictionaryCacheOptions> options)
{
_cache = cache;
_options = options.Value;
}
public async Task<IReadOnlyList<DictionaryItemDto>?> GetAsync(long tenantId, string code, CancellationToken cancellationToken = default) public async Task<IReadOnlyList<DictionaryItemDto>?> GetAsync(long tenantId, string code, CancellationToken cancellationToken = default)
{ {
var cacheKey = BuildKey(tenantId, code); var cacheKey = BuildKey(tenantId, code);
var payload = await _cache.GetAsync(cacheKey, cancellationToken); var payload = await cache.GetAsync(cacheKey, cancellationToken);
if (payload == null || payload.Length == 0) if (payload == null || payload.Length == 0)
{ {
return null; return null;
@@ -42,13 +35,13 @@ public sealed class DistributedDictionaryCache : IDictionaryCache
{ {
SlidingExpiration = _options.SlidingExpiration SlidingExpiration = _options.SlidingExpiration
}; };
return _cache.SetAsync(cacheKey, payload, options, cancellationToken); return cache.SetAsync(cacheKey, payload, options, cancellationToken);
} }
public Task RemoveAsync(long tenantId, string code, CancellationToken cancellationToken = default) public Task RemoveAsync(long tenantId, string code, CancellationToken cancellationToken = default)
{ {
var cacheKey = BuildKey(tenantId, code); var cacheKey = BuildKey(tenantId, code);
return _cache.RemoveAsync(cacheKey, cancellationToken); return cache.RemoveAsync(cacheKey, cancellationToken);
} }
private static string BuildKey(long tenantId, string code) private static string BuildKey(long tenantId, string code)

View File

@@ -10,18 +10,12 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// <summary> /// <summary>
/// EF Core 后台用户仓储实现。 /// EF Core 后台用户仓储实现。
/// </summary> /// </summary>
public sealed class EfIdentityUserRepository : IIdentityUserRepository public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIdentityUserRepository
{ {
private readonly IdentityDbContext _dbContext;
public EfIdentityUserRepository(IdentityDbContext dbContext)
{
_dbContext = dbContext;
}
public Task<IdentityUser?> FindByAccountAsync(string account, CancellationToken cancellationToken = default) public Task<IdentityUser?> FindByAccountAsync(string account, CancellationToken cancellationToken = default)
=> _dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Account == account, cancellationToken); => dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Account == account, cancellationToken);
public Task<IdentityUser?> FindByIdAsync(long userId, CancellationToken cancellationToken = default) public Task<IdentityUser?> FindByIdAsync(long userId, CancellationToken cancellationToken = default)
=> _dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); => dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
} }

View File

@@ -10,24 +10,18 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// <summary> /// <summary>
/// EF Core 小程序用户仓储实现。 /// EF Core 小程序用户仓储实现。
/// </summary> /// </summary>
public sealed class EfMiniUserRepository : IMiniUserRepository public sealed class EfMiniUserRepository(IdentityDbContext dbContext) : IMiniUserRepository
{ {
private readonly IdentityDbContext _dbContext;
public EfMiniUserRepository(IdentityDbContext dbContext)
{
_dbContext = dbContext;
}
public Task<MiniUser?> FindByOpenIdAsync(string openId, CancellationToken cancellationToken = default) public Task<MiniUser?> FindByOpenIdAsync(string openId, CancellationToken cancellationToken = default)
=> _dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); => dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken);
public Task<MiniUser?> FindByIdAsync(long id, CancellationToken cancellationToken = default) public Task<MiniUser?> FindByIdAsync(long id, CancellationToken cancellationToken = default)
=> _dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken); => dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
public async Task<MiniUser> CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken = default) public async Task<MiniUser> CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken = default)
{ {
var user = await _dbContext.MiniUsers.FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); var user = await dbContext.MiniUsers.FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken);
if (user == null) if (user == null)
{ {
user = new MiniUser user = new MiniUser
@@ -39,7 +33,7 @@ public sealed class EfMiniUserRepository : IMiniUserRepository
Avatar = avatar, Avatar = avatar,
TenantId = tenantId TenantId = tenantId
}; };
_dbContext.MiniUsers.Add(user); dbContext.MiniUsers.Add(user);
} }
else else
{ {
@@ -48,7 +42,7 @@ public sealed class EfMiniUserRepository : IMiniUserRepository
user.Avatar = avatar ?? user.Avatar; user.Avatar = avatar ?? user.Avatar;
} }
await _dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
return user; return user;
} }
} }

View File

@@ -13,21 +13,14 @@ namespace TakeoutSaaS.Infrastructure.Identity.Services;
/// <summary> /// <summary>
/// Redis 登录限流实现。 /// Redis 登录限流实现。
/// </summary> /// </summary>
public sealed class RedisLoginRateLimiter : ILoginRateLimiter public sealed class RedisLoginRateLimiter(IDistributedCache cache, IOptions<LoginRateLimitOptions> options) : ILoginRateLimiter
{ {
private readonly IDistributedCache _cache; private readonly LoginRateLimitOptions _options = options.Value;
private readonly LoginRateLimitOptions _options;
public RedisLoginRateLimiter(IDistributedCache cache, IOptions<LoginRateLimitOptions> options)
{
_cache = cache;
_options = options.Value;
}
public async Task EnsureAllowedAsync(string key, CancellationToken cancellationToken = default) public async Task EnsureAllowedAsync(string key, CancellationToken cancellationToken = default)
{ {
var cacheKey = BuildKey(key); var cacheKey = BuildKey(key);
var current = await _cache.GetStringAsync(cacheKey, cancellationToken); var current = await cache.GetStringAsync(cacheKey, cancellationToken);
var count = string.IsNullOrWhiteSpace(current) ? 0 : int.Parse(current); var count = string.IsNullOrWhiteSpace(current) ? 0 : int.Parse(current);
if (count >= _options.MaxAttempts) if (count >= _options.MaxAttempts)
{ {
@@ -35,7 +28,7 @@ public sealed class RedisLoginRateLimiter : ILoginRateLimiter
} }
count++; count++;
await _cache.SetStringAsync( await cache.SetStringAsync(
cacheKey, cacheKey,
count.ToString(), count.ToString(),
new DistributedCacheEntryOptions new DistributedCacheEntryOptions
@@ -46,7 +39,7 @@ public sealed class RedisLoginRateLimiter : ILoginRateLimiter
} }
public Task ResetAsync(string key, CancellationToken cancellationToken = default) public Task ResetAsync(string key, CancellationToken cancellationToken = default)
=> _cache.RemoveAsync(BuildKey(key), cancellationToken); => cache.RemoveAsync(BuildKey(key), cancellationToken);
private static string BuildKey(string key) => $"identity:login:{key}"; private static string BuildKey(string key) => $"identity:login:{key}";
} }

View File

@@ -14,17 +14,10 @@ namespace TakeoutSaaS.Infrastructure.Identity.Services;
/// <summary> /// <summary>
/// Redis 刷新令牌存储。 /// Redis 刷新令牌存储。
/// </summary> /// </summary>
public sealed class RedisRefreshTokenStore : IRefreshTokenStore public sealed class RedisRefreshTokenStore(IDistributedCache cache, IOptions<RefreshTokenStoreOptions> options) : IRefreshTokenStore
{ {
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
private readonly IDistributedCache _cache; private readonly RefreshTokenStoreOptions _options = options.Value;
private readonly RefreshTokenStoreOptions _options;
public RedisRefreshTokenStore(IDistributedCache cache, IOptions<RefreshTokenStoreOptions> options)
{
_cache = cache;
_options = options.Value;
}
public async Task<RefreshTokenDescriptor> IssueAsync(long userId, DateTime expiresAt, CancellationToken cancellationToken = default) public async Task<RefreshTokenDescriptor> IssueAsync(long userId, DateTime expiresAt, CancellationToken cancellationToken = default)
{ {
@@ -33,14 +26,14 @@ public sealed class RedisRefreshTokenStore : IRefreshTokenStore
var key = BuildKey(token); var key = BuildKey(token);
var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = expiresAt }; var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = expiresAt };
await _cache.SetStringAsync(key, JsonSerializer.Serialize(descriptor, JsonOptions), entryOptions, cancellationToken); await cache.SetStringAsync(key, JsonSerializer.Serialize(descriptor, JsonOptions), entryOptions, cancellationToken);
return descriptor; return descriptor;
} }
public async Task<RefreshTokenDescriptor?> GetAsync(string refreshToken, CancellationToken cancellationToken = default) public async Task<RefreshTokenDescriptor?> GetAsync(string refreshToken, CancellationToken cancellationToken = default)
{ {
var json = await _cache.GetStringAsync(BuildKey(refreshToken), cancellationToken); var json = await cache.GetStringAsync(BuildKey(refreshToken), cancellationToken);
return string.IsNullOrWhiteSpace(json) return string.IsNullOrWhiteSpace(json)
? null ? null
: JsonSerializer.Deserialize<RefreshTokenDescriptor>(json, JsonOptions); : JsonSerializer.Deserialize<RefreshTokenDescriptor>(json, JsonOptions);
@@ -56,7 +49,7 @@ public sealed class RedisRefreshTokenStore : IRefreshTokenStore
var updated = descriptor with { Revoked = true }; var updated = descriptor with { Revoked = true };
var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = updated.ExpiresAt }; var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = updated.ExpiresAt };
await _cache.SetStringAsync(BuildKey(refreshToken), JsonSerializer.Serialize(updated, JsonOptions), entryOptions, cancellationToken); await cache.SetStringAsync(BuildKey(refreshToken), JsonSerializer.Serialize(updated, JsonOptions), entryOptions, cancellationToken);
} }
private string BuildKey(string token) => $"{_options.Prefix}{token}"; private string BuildKey(string token) => $"{_options.Prefix}{token}";

View File

@@ -15,21 +15,14 @@ namespace TakeoutSaaS.Infrastructure.Identity.Services;
/// <summary> /// <summary>
/// 微信 code2Session 实现 /// 微信 code2Session 实现
/// </summary> /// </summary>
public sealed class WeChatAuthService : IWeChatAuthService public sealed class WeChatAuthService(HttpClient httpClient, IOptions<WeChatMiniOptions> options) : IWeChatAuthService
{ {
private readonly HttpClient _httpClient; private readonly WeChatMiniOptions _options = options.Value;
private readonly WeChatMiniOptions _options;
public WeChatAuthService(HttpClient httpClient, IOptions<WeChatMiniOptions> options)
{
_httpClient = httpClient;
_options = options.Value;
}
public async Task<WeChatSessionInfo> Code2SessionAsync(string code, CancellationToken cancellationToken = default) public async Task<WeChatSessionInfo> Code2SessionAsync(string code, CancellationToken cancellationToken = default)
{ {
var requestUri = $"sns/jscode2session?appid={Uri.EscapeDataString(_options.AppId)}&secret={Uri.EscapeDataString(_options.Secret)}&js_code={Uri.EscapeDataString(code)}&grant_type=authorization_code"; var requestUri = $"sns/jscode2session?appid={Uri.EscapeDataString(_options.AppId)}&secret={Uri.EscapeDataString(_options.Secret)}&js_code={Uri.EscapeDataString(code)}&grant_type=authorization_code";
using var response = await _httpClient.GetAsync(requestUri, cancellationToken); using var response = await httpClient.GetAsync(requestUri, cancellationToken);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<WeChatSessionResponse>(cancellationToken: cancellationToken); var payload = await response.Content.ReadFromJsonAsync<WeChatSessionResponse>(cancellationToken: cancellationToken);

View File

@@ -3,40 +3,34 @@ namespace TakeoutSaaS.Module.Storage.Models;
/// <summary> /// <summary>
/// 直传(预签名上传)请求参数。 /// 直传(预签名上传)请求参数。
/// </summary> /// </summary>
public sealed class StorageDirectUploadRequest /// <remarks>
{
/// <summary>
/// 初始化请求。 /// 初始化请求。
/// </summary> /// </remarks>
/// <param name="objectKey">对象键。</param> /// <param name="objectKey">对象键。</param>
/// <param name="contentType">内容类型。</param> /// <param name="contentType">内容类型。</param>
/// <param name="contentLength">内容长度。</param> /// <param name="contentLength">内容长度。</param>
/// <param name="expires">签名有效期。</param> /// <param name="expires">签名有效期。</param>
public StorageDirectUploadRequest(string objectKey, string contentType, long contentLength, TimeSpan expires) public sealed class StorageDirectUploadRequest(string objectKey, string contentType, long contentLength, TimeSpan expires)
{ {
ObjectKey = objectKey;
ContentType = contentType;
ContentLength = contentLength;
Expires = expires;
}
/// <summary> /// <summary>
/// 目标对象键。 /// 目标对象键。
/// </summary> /// </summary>
public string ObjectKey { get; } public string ObjectKey { get; } = objectKey;
/// <summary> /// <summary>
/// 内容类型。 /// 内容类型。
/// </summary> /// </summary>
public string ContentType { get; } public string ContentType { get; } = contentType;
/// <summary> /// <summary>
/// 内容长度。 /// 内容长度。
/// </summary> /// </summary>
public long ContentLength { get; } public long ContentLength { get; } = contentLength;
/// <summary> /// <summary>
/// 签名有效期。 /// 签名有效期。
/// </summary> /// </summary>
public TimeSpan Expires { get; } public TimeSpan Expires { get; } = expires;
} }

View File

@@ -6,11 +6,9 @@ namespace TakeoutSaaS.Module.Storage.Models;
/// <summary> /// <summary>
/// 对象存储上传请求参数。 /// 对象存储上传请求参数。
/// </summary> /// </summary>
public sealed class StorageUploadRequest /// <remarks>
{
/// <summary>
/// 初始化上传请求。 /// 初始化上传请求。
/// </summary> /// </remarks>
/// <param name="objectKey">对象键(含路径)。</param> /// <param name="objectKey">对象键(含路径)。</param>
/// <param name="content">文件流。</param> /// <param name="content">文件流。</param>
/// <param name="contentType">内容类型。</param> /// <param name="contentType">内容类型。</param>
@@ -18,7 +16,7 @@ public sealed class StorageUploadRequest
/// <param name="generateSignedUrl">是否返回签名访问链接。</param> /// <param name="generateSignedUrl">是否返回签名访问链接。</param>
/// <param name="signedUrlExpires">签名有效期。</param> /// <param name="signedUrlExpires">签名有效期。</param>
/// <param name="metadata">附加元数据。</param> /// <param name="metadata">附加元数据。</param>
public StorageUploadRequest( public sealed class StorageUploadRequest(
string objectKey, string objectKey,
Stream content, Stream content,
string contentType, string contentType,
@@ -27,49 +25,41 @@ public sealed class StorageUploadRequest
TimeSpan signedUrlExpires, TimeSpan signedUrlExpires,
IDictionary<string, string>? metadata = null) IDictionary<string, string>? metadata = null)
{ {
ObjectKey = objectKey;
Content = content;
ContentType = contentType;
ContentLength = contentLength;
GenerateSignedUrl = generateSignedUrl;
SignedUrlExpires = signedUrlExpires;
Metadata = metadata == null
? new Dictionary<string, string>()
: new Dictionary<string, string>(metadata);
}
/// <summary> /// <summary>
/// 对象键。 /// 对象键。
/// </summary> /// </summary>
public string ObjectKey { get; } public string ObjectKey { get; } = objectKey;
/// <summary> /// <summary>
/// 文件流。 /// 文件流。
/// </summary> /// </summary>
public Stream Content { get; } public Stream Content { get; } = content;
/// <summary> /// <summary>
/// 内容类型。 /// 内容类型。
/// </summary> /// </summary>
public string ContentType { get; } public string ContentType { get; } = contentType;
/// <summary> /// <summary>
/// 内容长度。 /// 内容长度。
/// </summary> /// </summary>
public long ContentLength { get; } public long ContentLength { get; } = contentLength;
/// <summary> /// <summary>
/// 是否需要签名访问链接。 /// 是否需要签名访问链接。
/// </summary> /// </summary>
public bool GenerateSignedUrl { get; } public bool GenerateSignedUrl { get; } = generateSignedUrl;
/// <summary> /// <summary>
/// 签名有效期。 /// 签名有效期。
/// </summary> /// </summary>
public TimeSpan SignedUrlExpires { get; } public TimeSpan SignedUrlExpires { get; } = signedUrlExpires;
/// <summary> /// <summary>
/// 元数据集合。 /// 元数据集合。
/// </summary> /// </summary>
public IReadOnlyDictionary<string, string> Metadata { get; } public IReadOnlyDictionary<string, string> Metadata { get; } = metadata == null
? new Dictionary<string, string>()
: new Dictionary<string, string>(metadata);
} }

View File

@@ -5,20 +5,15 @@ namespace TakeoutSaaS.Module.Tenancy;
/// <summary> /// <summary>
/// 默认租户提供者:基于租户上下文访问器暴露当前租户 ID。 /// 默认租户提供者:基于租户上下文访问器暴露当前租户 ID。
/// </summary> /// </summary>
public sealed class TenantProvider : ITenantProvider /// <remarks>
{
private readonly ITenantContextAccessor _tenantContextAccessor;
/// <summary>
/// 初始化租户提供者。 /// 初始化租户提供者。
/// </summary> /// </remarks>
/// <param name="tenantContextAccessor">租户上下文访问器</param> /// <param name="tenantContextAccessor">租户上下文访问器</param>
public TenantProvider(ITenantContextAccessor tenantContextAccessor) public sealed class TenantProvider(ITenantContextAccessor tenantContextAccessor) : ITenantProvider
{ {
_tenantContextAccessor = tenantContextAccessor;
}
/// <inheritdoc /> /// <inheritdoc />
public long GetCurrentTenantId() public long GetCurrentTenantId()
=> _tenantContextAccessor.Current?.TenantId ?? 0; => tenantContextAccessor.Current?.TenantId ?? 0;
} }

View File

@@ -11,54 +11,43 @@ namespace TakeoutSaaS.Module.Tenancy;
/// <summary> /// <summary>
/// 多租户解析中间件:支持 Header、域名与 Token Claim 的优先级解析。 /// 多租户解析中间件:支持 Header、域名与 Token Claim 的优先级解析。
/// </summary> /// </summary>
public sealed class TenantResolutionMiddleware /// <remarks>
{
private readonly RequestDelegate _next;
private readonly ILogger<TenantResolutionMiddleware> _logger;
private readonly ITenantContextAccessor _tenantContextAccessor;
private readonly IOptionsMonitor<TenantResolutionOptions> _optionsMonitor;
/// <summary>
/// 初始化中间件。 /// 初始化中间件。
/// </summary> /// </remarks>
public TenantResolutionMiddleware( public sealed class TenantResolutionMiddleware(
RequestDelegate next, RequestDelegate next,
ILogger<TenantResolutionMiddleware> logger, ILogger<TenantResolutionMiddleware> logger,
ITenantContextAccessor tenantContextAccessor, ITenantContextAccessor tenantContextAccessor,
IOptionsMonitor<TenantResolutionOptions> optionsMonitor) IOptionsMonitor<TenantResolutionOptions> optionsMonitor)
{ {
_next = next;
_logger = logger;
_tenantContextAccessor = tenantContextAccessor;
_optionsMonitor = optionsMonitor;
}
/// <summary> /// <summary>
/// 解析租户并将上下文注入请求。 /// 解析租户并将上下文注入请求。
/// </summary> /// </summary>
public async Task InvokeAsync(HttpContext context) public async Task InvokeAsync(HttpContext context)
{ {
var options = _optionsMonitor.CurrentValue ?? new TenantResolutionOptions(); var options = optionsMonitor.CurrentValue ?? new TenantResolutionOptions();
if (ShouldSkip(context.Request.Path, options)) if (ShouldSkip(context.Request.Path, options))
{ {
await _next(context); await next(context);
return; return;
} }
var tenantContext = ResolveTenant(context, options); var tenantContext = ResolveTenant(context, options);
_tenantContextAccessor.Current = tenantContext; tenantContextAccessor.Current = tenantContext;
context.Items[TenantConstants.HttpContextItemKey] = tenantContext; context.Items[TenantConstants.HttpContextItemKey] = tenantContext;
if (!tenantContext.IsResolved) if (!tenantContext.IsResolved)
{ {
_logger.LogDebug("未能解析租户:{Path}", context.Request.Path); logger.LogDebug("未能解析租户:{Path}", context.Request.Path);
if (options.ThrowIfUnresolved) if (options.ThrowIfUnresolved)
{ {
var response = ApiResponse.Error(ErrorCodes.BadRequest, "缺少租户标识"); var response = ApiResponse.Error(ErrorCodes.BadRequest, "缺少租户标识");
context.Response.StatusCode = StatusCodes.Status400BadRequest; context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsJsonAsync(response, cancellationToken: context.RequestAborted); await context.Response.WriteAsJsonAsync(response, cancellationToken: context.RequestAborted);
_tenantContextAccessor.Current = null; tenantContextAccessor.Current = null;
context.Items.Remove(TenantConstants.HttpContextItemKey); context.Items.Remove(TenantConstants.HttpContextItemKey);
return; return;
} }
@@ -66,11 +55,11 @@ public sealed class TenantResolutionMiddleware
try try
{ {
await _next(context); await next(context);
} }
finally finally
{ {
_tenantContextAccessor.Current = null; tenantContextAccessor.Current = null;
context.Items.Remove(TenantConstants.HttpContextItemKey); context.Items.Remove(TenantConstants.HttpContextItemKey);
} }
} }