chore: 提交现有修改
This commit is contained in:
@@ -17,21 +17,16 @@ namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
/// <summary>
|
||||
/// 管理后台认证接口
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
///
|
||||
/// </remarks>
|
||||
/// <param name="authService"></param>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[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>
|
||||
/// 登录获取 Token
|
||||
@@ -41,7 +36,7 @@ public sealed class AuthController : BaseApiController
|
||||
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -53,7 +48,7 @@ public sealed class AuthController : BaseApiController
|
||||
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -72,7 +67,7 @@ public sealed class AuthController : BaseApiController
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,21 +12,16 @@ namespace TakeoutSaaS.AdminApi.Controllers;
|
||||
/// <summary>
|
||||
/// 参数字典管理。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 初始化字典控制器。
|
||||
/// </remarks>
|
||||
/// <param name="dictionaryAppService">字典服务</param>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[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>
|
||||
/// 查询字典分组。
|
||||
@@ -36,7 +31,7 @@ public sealed class DictionaryController : BaseApiController
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<DictionaryGroupDto>>), StatusCodes.Status200OK)]
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -48,7 +43,7 @@ public sealed class DictionaryController : BaseApiController
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -60,7 +55,7 @@ public sealed class DictionaryController : BaseApiController
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -72,7 +67,7 @@ public sealed class DictionaryController : BaseApiController
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<object>> DeleteGroup(long groupId, CancellationToken cancellationToken)
|
||||
{
|
||||
await _dictionaryAppService.DeleteGroupAsync(groupId, cancellationToken);
|
||||
await dictionaryAppService.DeleteGroupAsync(groupId, cancellationToken);
|
||||
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)
|
||||
{
|
||||
request.GroupId = groupId;
|
||||
var item = await _dictionaryAppService.CreateItemAsync(request, cancellationToken);
|
||||
var item = await dictionaryAppService.CreateItemAsync(request, cancellationToken);
|
||||
return ApiResponse<DictionaryItemDto>.Ok(item);
|
||||
}
|
||||
|
||||
@@ -97,7 +92,7 @@ public sealed class DictionaryController : BaseApiController
|
||||
[ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)]
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -109,7 +104,7 @@ public sealed class DictionaryController : BaseApiController
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<object>> DeleteItem(long itemId, CancellationToken cancellationToken)
|
||||
{
|
||||
await _dictionaryAppService.DeleteItemAsync(itemId, cancellationToken);
|
||||
await dictionaryAppService.DeleteItemAsync(itemId, cancellationToken);
|
||||
return ApiResponse.Success();
|
||||
}
|
||||
|
||||
@@ -120,7 +115,7 @@ public sealed class DictionaryController : BaseApiController
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>>), StatusCodes.Status200OK)]
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,21 +10,16 @@ namespace TakeoutSaaS.MiniApi.Controllers;
|
||||
/// <summary>
|
||||
/// 小程序登录认证
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 小程序登录认证
|
||||
/// </remarks>
|
||||
/// <param name="authService"></param>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[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>
|
||||
/// 微信登录
|
||||
@@ -34,7 +29,7 @@ public sealed class AuthController : BaseApiController
|
||||
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -46,7 +41,7 @@ public sealed class AuthController : BaseApiController
|
||||
[ProducesResponseType(typeof(ApiResponse<TokenResponse>), StatusCodes.Status200OK)]
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,21 +16,16 @@ namespace TakeoutSaaS.MiniApi.Controllers;
|
||||
/// <summary>
|
||||
/// 当前用户信息
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
///
|
||||
/// </remarks>
|
||||
/// <param name="authService"></param>
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize]
|
||||
[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>
|
||||
/// 获取用户档案
|
||||
@@ -46,7 +41,7 @@ public sealed class MeController : BaseApiController
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,31 +15,19 @@ namespace TakeoutSaaS.Application.Dictionary.Services;
|
||||
/// <summary>
|
||||
/// 参数字典应用服务实现。
|
||||
/// </summary>
|
||||
public sealed class DictionaryAppService : IDictionaryAppService
|
||||
public sealed class DictionaryAppService(
|
||||
IDictionaryRepository repository,
|
||||
IDictionaryCache cache,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<DictionaryAppService> logger) : IDictionaryAppService
|
||||
{
|
||||
private readonly IDictionaryRepository _repository;
|
||||
private readonly IDictionaryCache _cache;
|
||||
private readonly ITenantProvider _tenantProvider;
|
||||
private readonly ILogger<DictionaryAppService> _logger;
|
||||
|
||||
public DictionaryAppService(
|
||||
IDictionaryRepository repository,
|
||||
IDictionaryCache cache,
|
||||
ITenantProvider tenantProvider,
|
||||
ILogger<DictionaryAppService> logger)
|
||||
{
|
||||
_repository = repository;
|
||||
_cache = cache;
|
||||
_tenantProvider = tenantProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<DictionaryGroupDto> CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedCode = NormalizeCode(request.Code);
|
||||
var targetTenant = ResolveTargetTenant(request.Scope);
|
||||
|
||||
var existing = await _repository.FindGroupByCodeAsync(normalizedCode, cancellationToken);
|
||||
var existing = await repository.FindGroupByCodeAsync(normalizedCode, cancellationToken);
|
||||
if (existing != null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, $"字典分组编码 {normalizedCode} 已存在");
|
||||
@@ -56,9 +44,9 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
IsEnabled = true
|
||||
};
|
||||
|
||||
await _repository.AddGroupAsync(group, cancellationToken);
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("创建字典分组:{Code}({Scope})", group.Code, group.Scope);
|
||||
await repository.AddGroupAsync(group, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("创建字典分组:{Code}({Scope})", group.Code, group.Scope);
|
||||
return MapGroup(group, includeItems: false);
|
||||
}
|
||||
|
||||
@@ -71,9 +59,9 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
group.Description = request.Description?.Trim();
|
||||
group.IsEnabled = request.IsEnabled;
|
||||
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
await InvalidateCacheAsync(group, cancellationToken);
|
||||
_logger.LogInformation("更新字典分组:{GroupId}", group.Id);
|
||||
logger.LogInformation("更新字典分组:{GroupId}", group.Id);
|
||||
return MapGroup(group, includeItems: false);
|
||||
}
|
||||
|
||||
@@ -82,19 +70,19 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
var group = await RequireGroupAsync(groupId, cancellationToken);
|
||||
EnsureScopePermission(group.Scope);
|
||||
|
||||
await _repository.RemoveGroupAsync(group, cancellationToken);
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
await repository.RemoveGroupAsync(group, cancellationToken);
|
||||
await repository.SaveChangesAsync(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)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var scope = ResolveScopeForQuery(request.Scope, tenantId);
|
||||
EnsureScopePermission(scope);
|
||||
|
||||
var groups = await _repository.SearchGroupsAsync(scope, cancellationToken);
|
||||
var groups = await repository.SearchGroupsAsync(scope, cancellationToken);
|
||||
var includeItems = request.IncludeItems;
|
||||
var result = new List<DictionaryGroupDto>(groups.Count);
|
||||
|
||||
@@ -103,7 +91,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
IReadOnlyList<DictionaryItemDto> items = Array.Empty<DictionaryItemDto>();
|
||||
if (includeItems)
|
||||
{
|
||||
var itemEntities = await _repository.GetItemsByGroupIdAsync(group.Id, cancellationToken);
|
||||
var itemEntities = await repository.GetItemsByGroupIdAsync(group.Id, cancellationToken);
|
||||
items = itemEntities.Select(MapItem).ToList();
|
||||
}
|
||||
|
||||
@@ -131,10 +119,10 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
IsEnabled = request.IsEnabled
|
||||
};
|
||||
|
||||
await _repository.AddItemAsync(item, cancellationToken);
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
await repository.AddItemAsync(item, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
await InvalidateCacheAsync(group, cancellationToken);
|
||||
_logger.LogInformation("新增字典项:{ItemId}", item.Id);
|
||||
logger.LogInformation("新增字典项:{ItemId}", item.Id);
|
||||
return MapItem(item);
|
||||
}
|
||||
|
||||
@@ -150,9 +138,9 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
item.IsDefault = request.IsDefault;
|
||||
item.IsEnabled = request.IsEnabled;
|
||||
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
await InvalidateCacheAsync(group, cancellationToken);
|
||||
_logger.LogInformation("更新字典项:{ItemId}", item.Id);
|
||||
logger.LogInformation("更新字典项:{ItemId}", item.Id);
|
||||
return MapItem(item);
|
||||
}
|
||||
|
||||
@@ -162,10 +150,10 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
|
||||
EnsureScopePermission(group.Scope);
|
||||
|
||||
await _repository.RemoveItemAsync(item, cancellationToken);
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
await repository.RemoveItemAsync(item, cancellationToken);
|
||||
await repository.SaveChangesAsync(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)
|
||||
@@ -181,7 +169,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
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);
|
||||
|
||||
foreach (var code in normalizedCodes)
|
||||
@@ -202,7 +190,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
|
||||
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)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "字典分组不存在");
|
||||
@@ -213,7 +201,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
|
||||
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)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "字典项不存在");
|
||||
@@ -224,7 +212,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
|
||||
private long ResolveTargetTenant(DictionaryScope scope)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (scope == DictionaryScope.System)
|
||||
{
|
||||
EnsurePlatformTenant(tenantId);
|
||||
@@ -253,7 +241,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
|
||||
private void EnsureScopePermission(DictionaryScope scope)
|
||||
{
|
||||
var tenantId = _tenantProvider.GetCurrentTenantId();
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (scope == DictionaryScope.System && tenantId != 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
|
||||
@@ -270,7 +258,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
|
||||
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)
|
||||
{
|
||||
return;
|
||||
@@ -281,20 +269,20 @@ public sealed class DictionaryAppService : IDictionaryAppService
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
.Where(item => item.IsEnabled && (item.Group?.IsEnabled ?? true))
|
||||
.Select(MapItem)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.ToList();
|
||||
|
||||
await _cache.SetAsync(tenantId, code, items, cancellationToken);
|
||||
await cache.SetAsync(tenantId, code, items, cancellationToken);
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,30 +5,25 @@ namespace TakeoutSaaS.Application.Sms.Contracts;
|
||||
/// <summary>
|
||||
/// 发送验证码请求。
|
||||
/// </summary>
|
||||
public sealed class SendVerificationCodeRequest
|
||||
/// <remarks>
|
||||
/// 创建发送请求。
|
||||
/// </remarks>
|
||||
public sealed class SendVerificationCodeRequest(string phoneNumber, string scene, SmsProviderKind? provider = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建发送请求。
|
||||
/// </summary>
|
||||
public SendVerificationCodeRequest(string phoneNumber, string scene, SmsProviderKind? provider = null)
|
||||
{
|
||||
PhoneNumber = phoneNumber;
|
||||
Scene = scene;
|
||||
Provider = provider;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 手机号(支持 +86 前缀或纯 11 位)。
|
||||
/// </summary>
|
||||
public string PhoneNumber { get; }
|
||||
public string PhoneNumber { get; } = phoneNumber;
|
||||
|
||||
/// <summary>
|
||||
/// 业务场景(如 login/register/reset)。
|
||||
/// </summary>
|
||||
public string Scene { get; }
|
||||
public string Scene { get; } = scene;
|
||||
|
||||
/// <summary>
|
||||
/// 指定服务商,未指定则使用默认配置。
|
||||
/// </summary>
|
||||
public SmsProviderKind? Provider { get; }
|
||||
public SmsProviderKind? Provider { get; } = provider;
|
||||
}
|
||||
|
||||
@@ -3,30 +3,25 @@ namespace TakeoutSaaS.Application.Sms.Contracts;
|
||||
/// <summary>
|
||||
/// 校验验证码请求。
|
||||
/// </summary>
|
||||
public sealed class VerifyVerificationCodeRequest
|
||||
/// <remarks>
|
||||
/// 创建校验请求。
|
||||
/// </remarks>
|
||||
public sealed class VerifyVerificationCodeRequest(string phoneNumber, string scene, string code)
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建校验请求。
|
||||
/// </summary>
|
||||
public VerifyVerificationCodeRequest(string phoneNumber, string scene, string code)
|
||||
{
|
||||
PhoneNumber = phoneNumber;
|
||||
Scene = scene;
|
||||
Code = code;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 手机号。
|
||||
/// </summary>
|
||||
public string PhoneNumber { get; }
|
||||
public string PhoneNumber { get; } = phoneNumber;
|
||||
|
||||
/// <summary>
|
||||
/// 业务场景。
|
||||
/// </summary>
|
||||
public string Scene { get; }
|
||||
public string Scene { get; } = scene;
|
||||
|
||||
/// <summary>
|
||||
/// 填写的验证码。
|
||||
/// </summary>
|
||||
public string Code { get; }
|
||||
public string Code { get; } = code;
|
||||
}
|
||||
|
||||
@@ -5,42 +5,35 @@ namespace TakeoutSaaS.Application.Storage.Contracts;
|
||||
/// <summary>
|
||||
/// 直传凭证请求模型。
|
||||
/// </summary>
|
||||
public sealed class DirectUploadRequest
|
||||
/// <remarks>
|
||||
/// 创建直传请求。
|
||||
/// </remarks>
|
||||
public sealed class DirectUploadRequest(UploadFileType fileType, string fileName, string contentType, long contentLength, string? requestOrigin)
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建直传请求。
|
||||
/// </summary>
|
||||
public DirectUploadRequest(UploadFileType fileType, string fileName, string contentType, long contentLength, string? requestOrigin)
|
||||
{
|
||||
FileType = fileType;
|
||||
FileName = fileName;
|
||||
ContentType = contentType;
|
||||
ContentLength = contentLength;
|
||||
RequestOrigin = requestOrigin;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 文件类型。
|
||||
/// </summary>
|
||||
public UploadFileType FileType { get; }
|
||||
public UploadFileType FileType { get; } = fileType;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名。
|
||||
/// </summary>
|
||||
public string FileName { get; }
|
||||
public string FileName { get; } = fileName;
|
||||
|
||||
/// <summary>
|
||||
/// 内容类型。
|
||||
/// </summary>
|
||||
public string ContentType { get; }
|
||||
public string ContentType { get; } = contentType;
|
||||
|
||||
/// <summary>
|
||||
/// 文件长度。
|
||||
/// </summary>
|
||||
public long ContentLength { get; }
|
||||
public long ContentLength { get; } = contentLength;
|
||||
|
||||
/// <summary>
|
||||
/// 请求来源(Origin/Referer)。
|
||||
/// </summary>
|
||||
public string? RequestOrigin { get; }
|
||||
public string? RequestOrigin { get; } = requestOrigin;
|
||||
}
|
||||
|
||||
@@ -6,54 +6,46 @@ namespace TakeoutSaaS.Application.Storage.Contracts;
|
||||
/// <summary>
|
||||
/// 上传文件请求模型。
|
||||
/// </summary>
|
||||
public sealed class UploadFileRequest
|
||||
/// <remarks>
|
||||
/// 创建上传文件请求。
|
||||
/// </remarks>
|
||||
public sealed class UploadFileRequest(
|
||||
UploadFileType fileType,
|
||||
Stream content,
|
||||
string fileName,
|
||||
string contentType,
|
||||
long contentLength,
|
||||
string? requestOrigin)
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建上传文件请求。
|
||||
/// </summary>
|
||||
public UploadFileRequest(
|
||||
UploadFileType fileType,
|
||||
Stream content,
|
||||
string fileName,
|
||||
string contentType,
|
||||
long contentLength,
|
||||
string? requestOrigin)
|
||||
{
|
||||
FileType = fileType;
|
||||
Content = content;
|
||||
FileName = fileName;
|
||||
ContentType = contentType;
|
||||
ContentLength = contentLength;
|
||||
RequestOrigin = requestOrigin;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 文件分类。
|
||||
/// </summary>
|
||||
public UploadFileType FileType { get; }
|
||||
public UploadFileType FileType { get; } = fileType;
|
||||
|
||||
/// <summary>
|
||||
/// 文件流。
|
||||
/// </summary>
|
||||
public Stream Content { get; }
|
||||
public Stream Content { get; } = content;
|
||||
|
||||
/// <summary>
|
||||
/// 原始文件名。
|
||||
/// </summary>
|
||||
public string FileName { get; }
|
||||
public string FileName { get; } = fileName;
|
||||
|
||||
/// <summary>
|
||||
/// 内容类型。
|
||||
/// </summary>
|
||||
public string ContentType { get; }
|
||||
public string ContentType { get; } = contentType;
|
||||
|
||||
/// <summary>
|
||||
/// 文件大小。
|
||||
/// </summary>
|
||||
public long ContentLength { get; }
|
||||
public long ContentLength { get; } = contentLength;
|
||||
|
||||
/// <summary>
|
||||
/// 请求来源(Origin/Referer)。
|
||||
/// </summary>
|
||||
public string? RequestOrigin { get; }
|
||||
public string? RequestOrigin { get; } = requestOrigin;
|
||||
}
|
||||
|
||||
@@ -3,16 +3,11 @@ namespace TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
/// <summary>
|
||||
/// 业务异常(用于可预期的业务校验错误)。
|
||||
/// </summary>
|
||||
public class BusinessException : Exception
|
||||
public class BusinessException(int errorCode, string message) : Exception(message)
|
||||
{
|
||||
/// <summary>
|
||||
/// 业务错误码。
|
||||
/// </summary>
|
||||
public int ErrorCode { get; }
|
||||
|
||||
public BusinessException(int errorCode, string message) : base(message)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
}
|
||||
public int ErrorCode { get; } = errorCode;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,11 @@ namespace TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
/// <summary>
|
||||
/// 验证异常(用于聚合验证错误信息)。
|
||||
/// </summary>
|
||||
public class ValidationException : Exception
|
||||
public class ValidationException(IDictionary<string, string[]> errors) : Exception("一个或多个验证错误")
|
||||
{
|
||||
/// <summary>
|
||||
/// 字段/属性的错误集合。
|
||||
/// </summary>
|
||||
public IDictionary<string, string[]> Errors { get; }
|
||||
|
||||
public ValidationException(IDictionary<string, string[]> errors)
|
||||
: base("一个或多个验证错误")
|
||||
{
|
||||
Errors = errors;
|
||||
}
|
||||
public IDictionary<string, string[]> Errors { get; } = errors;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,42 +6,33 @@ namespace TakeoutSaaS.Shared.Abstractions.Results;
|
||||
/// 分页结果包装,携带列表与总条数等元数据。
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public IReadOnlyList<T> Items { get; }
|
||||
public IReadOnlyList<T> Items { get; } = items;
|
||||
|
||||
/// <summary>
|
||||
/// 当前页码,从 1 开始。
|
||||
/// </summary>
|
||||
public int Page { get; }
|
||||
public int Page { get; } = page;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; }
|
||||
public int PageSize { get; } = pageSize;
|
||||
|
||||
/// <summary>
|
||||
/// 总条数。
|
||||
/// </summary>
|
||||
public int TotalCount { get; }
|
||||
public int TotalCount { get; } = totalCount;
|
||||
|
||||
/// <summary>
|
||||
/// 总页数。
|
||||
/// </summary>
|
||||
public int TotalPages { get; }
|
||||
|
||||
/// <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);
|
||||
}
|
||||
public int TotalPages { get; } = pageSize == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||
}
|
||||
|
||||
@@ -3,40 +3,33 @@ namespace TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
/// <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>
|
||||
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>
|
||||
/// 当前租户 ID,未解析时为 Guid.Empty。
|
||||
/// </summary>
|
||||
public long TenantId { get; }
|
||||
public long TenantId { get; } = tenantId;
|
||||
|
||||
/// <summary>
|
||||
/// 当前租户编码(例如子域名或业务编码),可为空。
|
||||
/// </summary>
|
||||
public string? TenantCode { get; }
|
||||
public string? TenantCode { get; } = tenantCode;
|
||||
|
||||
/// <summary>
|
||||
/// 租户解析来源(Header、Host、Token 等)。
|
||||
/// </summary>
|
||||
public string Source { get; }
|
||||
public string Source { get; } = source;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已成功解析到租户。
|
||||
|
||||
@@ -8,7 +8,12 @@ namespace TakeoutSaaS.Shared.Kernel.Ids;
|
||||
/// <summary>
|
||||
/// 基于雪花算法的长整型 ID 生成器。
|
||||
/// </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 int WorkerIdBits = 5;
|
||||
@@ -23,23 +28,12 @@ public sealed class SnowflakeIdGenerator : IIdGenerator
|
||||
private const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits;
|
||||
private const long SequenceMask = -1L ^ (-1L << SequenceBits);
|
||||
|
||||
private readonly long _workerId;
|
||||
private readonly long _datacenterId;
|
||||
private readonly long _workerId = Normalize(workerId, MaxWorkerId, nameof(workerId));
|
||||
private readonly long _datacenterId = Normalize(datacenterId, MaxDatacenterId, nameof(datacenterId));
|
||||
private long _lastTimestamp = -1L;
|
||||
private long _sequence;
|
||||
private long _sequence = RandomNumberGenerator.GetInt32(0, (int)SequenceMask);
|
||||
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 />
|
||||
public long NextId()
|
||||
|
||||
@@ -11,22 +11,11 @@ namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||
/// <summary>
|
||||
/// 统一 TraceId/CorrelationId,贯穿日志与响应。
|
||||
/// </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 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)
|
||||
{
|
||||
var traceId = ResolveTraceId(context);
|
||||
@@ -39,14 +28,14 @@ public sealed class CorrelationIdMiddleware
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
using (_logger.BeginScope(new Dictionary<string, object>
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
["TraceId"] = traceId
|
||||
}))
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
await next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -67,7 +56,7 @@ public sealed class CorrelationIdMiddleware
|
||||
return requestId;
|
||||
}
|
||||
|
||||
return _idGenerator.NextId().ToString();
|
||||
return idGenerator.NextId().ToString();
|
||||
}
|
||||
|
||||
private static bool TryGetHeader(HttpContext context, string headerName, out string value)
|
||||
|
||||
@@ -14,34 +14,23 @@ namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||
/// <summary>
|
||||
/// 全局异常处理中间件,将异常统一映射为 ApiResponse。
|
||||
/// </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()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger, IHostEnvironment environment)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
await next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "未处理异常:{Message}", ex.Message);
|
||||
logger.LogError(ex, "未处理异常:{Message}", ex.Message);
|
||||
await HandleExceptionAsync(context, ex);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +39,7 @@ public sealed class ExceptionHandlingMiddleware
|
||||
{
|
||||
var (statusCode, response) = BuildErrorResponse(exception);
|
||||
|
||||
if (_environment.IsDevelopment())
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
response = response with
|
||||
{
|
||||
|
||||
@@ -9,29 +9,21 @@ namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||
/// <summary>
|
||||
/// 基础请求日志(方法、路径、耗时、状态码、TraceId)。
|
||||
/// </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)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
await next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
var traceId = TraceContext.TraceId ?? context.TraceIdentifier;
|
||||
_logger.LogInformation(
|
||||
logger.LogInformation(
|
||||
"HTTP {Method} {Path} => {StatusCode} ({Elapsed} ms) TraceId:{TraceId}",
|
||||
context.Request.Method,
|
||||
context.Request.Path,
|
||||
|
||||
@@ -7,24 +7,19 @@ namespace TakeoutSaaS.Shared.Web.Security;
|
||||
/// <summary>
|
||||
/// 基于 HttpContext 的当前用户访问器。
|
||||
/// </summary>
|
||||
public sealed class HttpContextCurrentUserAccessor : ICurrentUserAccessor
|
||||
/// <remarks>
|
||||
/// 初始化访问器。
|
||||
/// </remarks>
|
||||
public sealed class HttpContextCurrentUserAccessor(IHttpContextAccessor httpContextAccessor) : ICurrentUserAccessor
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化访问器。
|
||||
/// </summary>
|
||||
public HttpContextCurrentUserAccessor(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public long UserId
|
||||
{
|
||||
get
|
||||
{
|
||||
var principal = _httpContextAccessor.HttpContext?.User;
|
||||
var principal = httpContextAccessor.HttpContext?.User;
|
||||
if (principal == null || !principal.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
return 0;
|
||||
|
||||
@@ -9,22 +9,15 @@ namespace TakeoutSaaS.Shared.Web.Swagger;
|
||||
/// <summary>
|
||||
/// 根据 API 版本动态注册 Swagger 文档。
|
||||
/// </summary>
|
||||
internal sealed class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
internal sealed class ConfigureSwaggerOptions(
|
||||
IApiVersionDescriptionProvider provider,
|
||||
IOptions<SwaggerDocumentSettings> settings) : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private readonly IApiVersionDescriptionProvider _provider;
|
||||
private readonly SwaggerDocumentSettings _settings;
|
||||
|
||||
public ConfigureSwaggerOptions(
|
||||
IApiVersionDescriptionProvider provider,
|
||||
IOptions<SwaggerDocumentSettings> settings)
|
||||
{
|
||||
_provider = provider;
|
||||
_settings = settings.Value;
|
||||
}
|
||||
private readonly SwaggerDocumentSettings _settings = settings.Value;
|
||||
|
||||
public void Configure(SwaggerGenOptions options)
|
||||
{
|
||||
foreach (var description in _provider.ApiVersionDescriptions)
|
||||
foreach (var description in provider.ApiVersionDescriptions)
|
||||
{
|
||||
var info = new OpenApiInfo
|
||||
{
|
||||
|
||||
@@ -16,42 +16,34 @@ namespace TakeoutSaaS.Infrastructure.App.Persistence;
|
||||
/// <summary>
|
||||
/// 业务数据种子,确保默认租户与基础字典可重复执行。
|
||||
/// </summary>
|
||||
public sealed class AppDataSeeder : IHostedService
|
||||
/// <remarks>
|
||||
/// 初始化种子服务。
|
||||
/// </remarks>
|
||||
public sealed class AppDataSeeder(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<AppDataSeeder> logger,
|
||||
IOptions<AppSeedOptions> options) : IHostedService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<AppDataSeeder> _logger;
|
||||
private readonly AppSeedOptions _options;
|
||||
private readonly AppSeedOptions _options = options.Value;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化种子服务。
|
||||
/// </summary>
|
||||
public AppDataSeeder(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<AppDataSeeder> logger,
|
||||
IOptions<AppSeedOptions> options)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_options.Enabled)
|
||||
{
|
||||
_logger.LogInformation("AppSeed 未启用,跳过业务数据初始化");
|
||||
logger.LogInformation("AppSeed 未启用,跳过业务数据初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var appDbContext = scope.ServiceProvider.GetRequiredService<TakeoutAppDbContext>();
|
||||
var dictionaryDbContext = scope.ServiceProvider.GetRequiredService<DictionaryDbContext>();
|
||||
|
||||
var defaultTenantId = await EnsureDefaultTenantAsync(appDbContext, cancellationToken);
|
||||
await EnsureDictionarySeedsAsync(dictionaryDbContext, defaultTenantId, cancellationToken);
|
||||
|
||||
_logger.LogInformation("AppSeed 完成业务数据初始化");
|
||||
logger.LogInformation("AppSeed 完成业务数据初始化");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -65,7 +57,7 @@ public sealed class AppDataSeeder : IHostedService
|
||||
var tenantOptions = _options.DefaultTenant;
|
||||
if (tenantOptions == null || string.IsNullOrWhiteSpace(tenantOptions.Code) || string.IsNullOrWhiteSpace(tenantOptions.Name))
|
||||
{
|
||||
_logger.LogInformation("AppSeed 未配置默认租户,跳过租户种子");
|
||||
logger.LogInformation("AppSeed 未配置默认租户,跳过租户种子");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -89,7 +81,7 @@ public sealed class AppDataSeeder : IHostedService
|
||||
|
||||
await dbContext.Tenants.AddAsync(tenant, cancellationToken);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("AppSeed 已创建默认租户 {TenantCode}", code);
|
||||
logger.LogInformation("AppSeed 已创建默认租户 {TenantCode}", code);
|
||||
return tenant.Id;
|
||||
}
|
||||
|
||||
@@ -129,11 +121,11 @@ public sealed class AppDataSeeder : IHostedService
|
||||
{
|
||||
dbContext.Tenants.Update(existingTenant);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("AppSeed 已更新默认租户 {TenantCode}", code);
|
||||
logger.LogInformation("AppSeed 已更新默认租户 {TenantCode}", code);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("AppSeed 默认租户 {TenantCode} 已存在且无需更新", code);
|
||||
logger.LogInformation("AppSeed 默认租户 {TenantCode} 已存在且无需更新", code);
|
||||
}
|
||||
|
||||
return existingTenant.Id;
|
||||
@@ -149,7 +141,7 @@ public sealed class AppDataSeeder : IHostedService
|
||||
|
||||
if (!hasDictionaryGroups)
|
||||
{
|
||||
_logger.LogInformation("AppSeed 未配置基础字典,跳过字典种子");
|
||||
logger.LogInformation("AppSeed 未配置基础字典,跳过字典种子");
|
||||
}
|
||||
|
||||
if (hasDictionaryGroups)
|
||||
@@ -158,7 +150,7 @@ public sealed class AppDataSeeder : IHostedService
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(groupOptions.Code) || string.IsNullOrWhiteSpace(groupOptions.Name))
|
||||
{
|
||||
_logger.LogWarning("AppSeed 跳过字典分组,Code 或 Name 为空");
|
||||
logger.LogWarning("AppSeed 跳过字典分组,Code 或 Name 为空");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -183,7 +175,7 @@ public sealed class AppDataSeeder : IHostedService
|
||||
};
|
||||
|
||||
await dbContext.DictionaryGroups.AddAsync(group, cancellationToken);
|
||||
_logger.LogInformation("AppSeed 创建字典分组 {GroupCode} (Tenant: {TenantId})", code, tenantId);
|
||||
logger.LogInformation("AppSeed 创建字典分组 {GroupCode} (Tenant: {TenantId})", code, tenantId);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -236,7 +228,7 @@ public sealed class AppDataSeeder : IHostedService
|
||||
|
||||
if (systemParameters.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("AppSeed 未配置系统参数,跳过系统参数种子");
|
||||
logger.LogInformation("AppSeed 未配置系统参数,跳过系统参数种子");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -246,7 +238,7 @@ public sealed class AppDataSeeder : IHostedService
|
||||
|
||||
if (!grouped.Any())
|
||||
{
|
||||
_logger.LogInformation("AppSeed 系统参数配置为空,跳过系统参数种子");
|
||||
logger.LogInformation("AppSeed 系统参数配置为空,跳过系统参数种子");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -271,7 +263,7 @@ public sealed class AppDataSeeder : IHostedService
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
@@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||
/// <summary>
|
||||
/// 配送聚合的 EF Core 仓储实现。
|
||||
/// </summary>
|
||||
public sealed class EfDeliveryRepository : IDeliveryRepository
|
||||
/// <remarks>
|
||||
/// 初始化仓储。
|
||||
/// </remarks>
|
||||
public sealed class EfDeliveryRepository(TakeoutAppDbContext context) : IDeliveryRepository
|
||||
{
|
||||
private readonly TakeoutAppDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化仓储。
|
||||
/// </summary>
|
||||
public EfDeliveryRepository(TakeoutAppDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DeliveryOrder?> FindByIdAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.DeliveryOrders
|
||||
return context.DeliveryOrders
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.Id == deliveryOrderId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -34,7 +29,7 @@ public sealed class EfDeliveryRepository : IDeliveryRepository
|
||||
/// <inheritdoc />
|
||||
public Task<DeliveryOrder?> FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.DeliveryOrders
|
||||
return context.DeliveryOrders
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.OrderId == orderId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -43,7 +38,7 @@ public sealed class EfDeliveryRepository : IDeliveryRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DeliveryEvent>> GetEventsAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var events = await _context.DeliveryEvents
|
||||
var events = await context.DeliveryEvents
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.DeliveryOrderId == deliveryOrderId)
|
||||
.OrderBy(x => x.CreatedAt)
|
||||
@@ -55,25 +50,25 @@ public sealed class EfDeliveryRepository : IDeliveryRepository
|
||||
/// <inheritdoc />
|
||||
public Task AddDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.DeliveryOrders.AddAsync(deliveryOrder, cancellationToken).AsTask();
|
||||
return context.DeliveryOrders.AddAsync(deliveryOrder, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddEventAsync(DeliveryEvent deliveryEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.DeliveryEvents.AddAsync(deliveryEvent, cancellationToken).AsTask();
|
||||
return context.DeliveryEvents.AddAsync(deliveryEvent, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.SaveChangesAsync(cancellationToken);
|
||||
return context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DeliveryOrder>> SearchAsync(long tenantId, DeliveryStatus? status, long? orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.DeliveryOrders
|
||||
var query = context.DeliveryOrders
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId);
|
||||
|
||||
@@ -95,23 +90,23 @@ public sealed class EfDeliveryRepository : IDeliveryRepository
|
||||
/// <inheritdoc />
|
||||
public Task UpdateDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.DeliveryOrders.Update(deliveryOrder);
|
||||
context.DeliveryOrders.Update(deliveryOrder);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
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)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
@@ -120,6 +115,6 @@ public sealed class EfDeliveryRepository : IDeliveryRepository
|
||||
return;
|
||||
}
|
||||
|
||||
_context.DeliveryOrders.Remove(existing);
|
||||
context.DeliveryOrders.Remove(existing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||
/// <summary>
|
||||
/// 商户聚合的 EF Core 仓储实现。
|
||||
/// </summary>
|
||||
public sealed class EfMerchantRepository : IMerchantRepository
|
||||
/// <remarks>
|
||||
/// 初始化仓储。
|
||||
/// </remarks>
|
||||
public sealed class EfMerchantRepository(TakeoutAppDbContext context) : IMerchantRepository
|
||||
{
|
||||
private readonly TakeoutAppDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化仓储。
|
||||
/// </summary>
|
||||
public EfMerchantRepository(TakeoutAppDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Merchant?> FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.Merchants
|
||||
return context.Merchants
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.Id == merchantId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -34,7 +29,7 @@ public sealed class EfMerchantRepository : IMerchantRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Merchant>> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Merchants
|
||||
var query = context.Merchants
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId);
|
||||
|
||||
@@ -51,7 +46,7 @@ public sealed class EfMerchantRepository : IMerchantRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<MerchantStaff>> GetStaffAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var staffs = await _context.MerchantStaff
|
||||
var staffs = await context.MerchantStaff
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId)
|
||||
.OrderBy(x => x.Name)
|
||||
@@ -63,7 +58,7 @@ public sealed class EfMerchantRepository : IMerchantRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<MerchantContract>> GetContractsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var contracts = await _context.MerchantContracts
|
||||
var contracts = await context.MerchantContracts
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
@@ -75,7 +70,7 @@ public sealed class EfMerchantRepository : IMerchantRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<MerchantDocument>> GetDocumentsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var documents = await _context.MerchantDocuments
|
||||
var documents = await context.MerchantDocuments
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.MerchantId == merchantId)
|
||||
.OrderBy(x => x.CreatedAt)
|
||||
@@ -87,44 +82,44 @@ public sealed class EfMerchantRepository : IMerchantRepository
|
||||
/// <inheritdoc />
|
||||
public Task AddMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.Merchants.AddAsync(merchant, cancellationToken).AsTask();
|
||||
return context.Merchants.AddAsync(merchant, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddStaffAsync(MerchantStaff staff, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.MerchantStaff.AddAsync(staff, cancellationToken).AsTask();
|
||||
return context.MerchantStaff.AddAsync(staff, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddContractAsync(MerchantContract contract, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.MerchantContracts.AddAsync(contract, cancellationToken).AsTask();
|
||||
return context.MerchantContracts.AddAsync(contract, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddDocumentAsync(MerchantDocument document, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.MerchantDocuments.AddAsync(document, cancellationToken).AsTask();
|
||||
return context.MerchantDocuments.AddAsync(document, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.SaveChangesAsync(cancellationToken);
|
||||
return context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.Merchants.Update(merchant);
|
||||
context.Merchants.Update(merchant);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
@@ -133,6 +128,6 @@ public sealed class EfMerchantRepository : IMerchantRepository
|
||||
return;
|
||||
}
|
||||
|
||||
_context.Merchants.Remove(existing);
|
||||
context.Merchants.Remove(existing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,22 +11,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||
/// <summary>
|
||||
/// 订单聚合的 EF Core 仓储实现。
|
||||
/// </summary>
|
||||
public sealed class EfOrderRepository : IOrderRepository
|
||||
/// <remarks>
|
||||
/// 初始化仓储。
|
||||
/// </remarks>
|
||||
public sealed class EfOrderRepository(TakeoutAppDbContext context) : IOrderRepository
|
||||
{
|
||||
private readonly TakeoutAppDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化仓储。
|
||||
/// </summary>
|
||||
public EfOrderRepository(TakeoutAppDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Order?> FindByIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.Orders
|
||||
return context.Orders
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.Id == orderId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -35,7 +30,7 @@ public sealed class EfOrderRepository : IOrderRepository
|
||||
/// <inheritdoc />
|
||||
public Task<Order?> FindByOrderNoAsync(string orderNo, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.Orders
|
||||
return context.Orders
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.OrderNo == orderNo)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -44,7 +39,7 @@ public sealed class EfOrderRepository : IOrderRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Order>> SearchAsync(long tenantId, OrderStatus? status, PaymentStatus? paymentStatus, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Orders
|
||||
var query = context.Orders
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId);
|
||||
|
||||
@@ -68,7 +63,7 @@ public sealed class EfOrderRepository : IOrderRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<OrderItem>> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = await _context.OrderItems
|
||||
var items = await context.OrderItems
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.OrderId == orderId)
|
||||
.OrderBy(x => x.Id)
|
||||
@@ -80,7 +75,7 @@ public sealed class EfOrderRepository : IOrderRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<OrderStatusHistory>> GetStatusHistoryAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var histories = await _context.OrderStatusHistories
|
||||
var histories = await context.OrderStatusHistories
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.OrderId == orderId)
|
||||
.OrderBy(x => x.CreatedAt)
|
||||
@@ -92,7 +87,7 @@ public sealed class EfOrderRepository : IOrderRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<RefundRequest>> GetRefundsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var refunds = await _context.RefundRequests
|
||||
var refunds = await context.RefundRequests
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.OrderId == orderId)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
@@ -104,68 +99,68 @@ public sealed class EfOrderRepository : IOrderRepository
|
||||
/// <inheritdoc />
|
||||
public Task AddOrderAsync(Order order, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.Orders.AddAsync(order, cancellationToken).AsTask();
|
||||
return context.Orders.AddAsync(order, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddItemsAsync(IEnumerable<OrderItem> items, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.OrderItems.AddRangeAsync(items, cancellationToken);
|
||||
return context.OrderItems.AddRangeAsync(items, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddStatusHistoryAsync(OrderStatusHistory history, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.OrderStatusHistories.AddAsync(history, cancellationToken).AsTask();
|
||||
return context.OrderStatusHistories.AddAsync(history, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddRefundAsync(RefundRequest refund, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.RefundRequests.AddAsync(refund, cancellationToken).AsTask();
|
||||
return context.RefundRequests.AddAsync(refund, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.SaveChangesAsync(cancellationToken);
|
||||
return context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateOrderAsync(Order order, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.Orders.Update(order);
|
||||
context.Orders.Update(order);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
.ToListAsync(cancellationToken);
|
||||
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)
|
||||
.ToListAsync(cancellationToken);
|
||||
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)
|
||||
.ToListAsync(cancellationToken);
|
||||
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)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (existing == null)
|
||||
@@ -173,6 +168,6 @@ public sealed class EfOrderRepository : IOrderRepository
|
||||
return;
|
||||
}
|
||||
|
||||
_context.Orders.Remove(existing);
|
||||
context.Orders.Remove(existing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||
/// <summary>
|
||||
/// 支付记录的 EF Core 仓储实现。
|
||||
/// </summary>
|
||||
public sealed class EfPaymentRepository : IPaymentRepository
|
||||
/// <remarks>
|
||||
/// 初始化仓储。
|
||||
/// </remarks>
|
||||
public sealed class EfPaymentRepository(TakeoutAppDbContext context) : IPaymentRepository
|
||||
{
|
||||
private readonly TakeoutAppDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化仓储。
|
||||
/// </summary>
|
||||
public EfPaymentRepository(TakeoutAppDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PaymentRecord?> FindByIdAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.PaymentRecords
|
||||
return context.PaymentRecords
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.Id == paymentId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -34,7 +29,7 @@ public sealed class EfPaymentRepository : IPaymentRepository
|
||||
/// <inheritdoc />
|
||||
public Task<PaymentRecord?> FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.PaymentRecords
|
||||
return context.PaymentRecords
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.OrderId == orderId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -43,7 +38,7 @@ public sealed class EfPaymentRepository : IPaymentRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<PaymentRefundRecord>> GetRefundsAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var refunds = await _context.PaymentRefundRecords
|
||||
var refunds = await context.PaymentRefundRecords
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.PaymentRecordId == paymentId)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
@@ -55,19 +50,19 @@ public sealed class EfPaymentRepository : IPaymentRepository
|
||||
/// <inheritdoc />
|
||||
public Task AddPaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.PaymentRecords.AddAsync(payment, cancellationToken).AsTask();
|
||||
return context.PaymentRecords.AddAsync(payment, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddRefundAsync(PaymentRefundRecord refund, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.PaymentRefundRecords.AddAsync(refund, cancellationToken).AsTask();
|
||||
return context.PaymentRefundRecords.AddAsync(refund, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<PaymentRecord>> SearchAsync(long tenantId, PaymentStatus? status, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.PaymentRecords
|
||||
var query = context.PaymentRecords
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId);
|
||||
|
||||
@@ -84,28 +79,28 @@ public sealed class EfPaymentRepository : IPaymentRepository
|
||||
/// <inheritdoc />
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.SaveChangesAsync(cancellationToken);
|
||||
return context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdatePaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.PaymentRecords.Update(payment);
|
||||
context.PaymentRecords.Update(payment);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
.ToListAsync(cancellationToken);
|
||||
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)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (existing == null)
|
||||
@@ -113,6 +108,6 @@ public sealed class EfPaymentRepository : IPaymentRepository
|
||||
return;
|
||||
}
|
||||
|
||||
_context.PaymentRecords.Remove(existing);
|
||||
context.PaymentRecords.Remove(existing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||
/// <summary>
|
||||
/// 商品聚合的 EF Core 仓储实现。
|
||||
/// </summary>
|
||||
public sealed class EfProductRepository : IProductRepository
|
||||
/// <remarks>
|
||||
/// 初始化仓储。
|
||||
/// </remarks>
|
||||
public sealed class EfProductRepository(TakeoutAppDbContext context) : IProductRepository
|
||||
{
|
||||
private readonly TakeoutAppDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化仓储。
|
||||
/// </summary>
|
||||
public EfProductRepository(TakeoutAppDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Product?> FindByIdAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.Products
|
||||
return context.Products
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.Id == productId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -34,7 +29,7 @@ public sealed class EfProductRepository : IProductRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Product>> SearchAsync(long tenantId, long? categoryId, ProductStatus? status, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Products
|
||||
var query = context.Products
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId);
|
||||
|
||||
@@ -58,7 +53,7 @@ public sealed class EfProductRepository : IProductRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ProductCategory>> GetCategoriesAsync(long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var categories = await _context.ProductCategories
|
||||
var categories = await context.ProductCategories
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
@@ -70,7 +65,7 @@ public sealed class EfProductRepository : IProductRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ProductSku>> GetSkusAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var skus = await _context.ProductSkus
|
||||
var skus = await context.ProductSkus
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.ProductId == productId)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
@@ -82,7 +77,7 @@ public sealed class EfProductRepository : IProductRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ProductAddonGroup>> GetAddonGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var groups = await _context.ProductAddonGroups
|
||||
var groups = await context.ProductAddonGroups
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.ProductId == productId)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
@@ -94,7 +89,7 @@ public sealed class EfProductRepository : IProductRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ProductAddonOption>> GetAddonOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var groupIds = await _context.ProductAddonGroups
|
||||
var groupIds = await context.ProductAddonGroups
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.ProductId == productId)
|
||||
.Select(x => x.Id)
|
||||
@@ -105,7 +100,7 @@ public sealed class EfProductRepository : IProductRepository
|
||||
return Array.Empty<ProductAddonOption>();
|
||||
}
|
||||
|
||||
var options = await _context.ProductAddonOptions
|
||||
var options = await context.ProductAddonOptions
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && groupIds.Contains(x.AddonGroupId))
|
||||
.OrderBy(x => x.SortOrder)
|
||||
@@ -117,7 +112,7 @@ public sealed class EfProductRepository : IProductRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ProductAttributeGroup>> GetAttributeGroupsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var groups = await _context.ProductAttributeGroups
|
||||
var groups = await context.ProductAttributeGroups
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.ProductId == productId)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
@@ -129,7 +124,7 @@ public sealed class EfProductRepository : IProductRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ProductAttributeOption>> GetAttributeOptionsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var groupIds = await _context.ProductAttributeGroups
|
||||
var groupIds = await context.ProductAttributeGroups
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.ProductId == productId)
|
||||
.Select(x => x.Id)
|
||||
@@ -140,7 +135,7 @@ public sealed class EfProductRepository : IProductRepository
|
||||
return Array.Empty<ProductAttributeOption>();
|
||||
}
|
||||
|
||||
var options = await _context.ProductAttributeOptions
|
||||
var options = await context.ProductAttributeOptions
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && groupIds.Contains(x.AttributeGroupId))
|
||||
.OrderBy(x => x.SortOrder)
|
||||
@@ -152,7 +147,7 @@ public sealed class EfProductRepository : IProductRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ProductMediaAsset>> GetMediaAssetsAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var assets = await _context.ProductMediaAssets
|
||||
var assets = await context.ProductMediaAssets
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.ProductId == productId)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
@@ -164,7 +159,7 @@ public sealed class EfProductRepository : IProductRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ProductPricingRule>> GetPricingRulesAsync(long productId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var rules = await _context.ProductPricingRules
|
||||
var rules = await context.ProductPricingRules
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.ProductId == productId)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
@@ -176,59 +171,59 @@ public sealed class EfProductRepository : IProductRepository
|
||||
/// <inheritdoc />
|
||||
public Task AddCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.ProductCategories.AddAsync(category, cancellationToken).AsTask();
|
||||
return context.ProductCategories.AddAsync(category, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddProductAsync(Product product, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.Products.AddAsync(product, cancellationToken).AsTask();
|
||||
return context.Products.AddAsync(product, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddSkusAsync(IEnumerable<ProductSku> skus, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.ProductSkus.AddRangeAsync(skus, cancellationToken);
|
||||
return context.ProductSkus.AddRangeAsync(skus, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddAddonGroupsAsync(IEnumerable<ProductAddonGroup> groups, IEnumerable<ProductAddonOption> options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var addGroupsTask = _context.ProductAddonGroups.AddRangeAsync(groups, cancellationToken);
|
||||
var addOptionsTask = _context.ProductAddonOptions.AddRangeAsync(options, cancellationToken);
|
||||
var addGroupsTask = context.ProductAddonGroups.AddRangeAsync(groups, cancellationToken);
|
||||
var addOptionsTask = context.ProductAddonOptions.AddRangeAsync(options, cancellationToken);
|
||||
return Task.WhenAll(addGroupsTask, addOptionsTask);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddAttributeGroupsAsync(IEnumerable<ProductAttributeGroup> groups, IEnumerable<ProductAttributeOption> options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var addGroupsTask = _context.ProductAttributeGroups.AddRangeAsync(groups, cancellationToken);
|
||||
var addOptionsTask = _context.ProductAttributeOptions.AddRangeAsync(options, cancellationToken);
|
||||
var addGroupsTask = context.ProductAttributeGroups.AddRangeAsync(groups, cancellationToken);
|
||||
var addOptionsTask = context.ProductAttributeOptions.AddRangeAsync(options, cancellationToken);
|
||||
return Task.WhenAll(addGroupsTask, addOptionsTask);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddMediaAssetsAsync(IEnumerable<ProductMediaAsset> assets, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.ProductMediaAssets.AddRangeAsync(assets, cancellationToken);
|
||||
return context.ProductMediaAssets.AddRangeAsync(assets, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddPricingRulesAsync(IEnumerable<ProductPricingRule> rules, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.ProductPricingRules.AddRangeAsync(rules, cancellationToken);
|
||||
return context.ProductPricingRules.AddRangeAsync(rules, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.SaveChangesAsync(cancellationToken);
|
||||
return context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateProductAsync(Product product, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.Products.Update(product);
|
||||
context.Products.Update(product);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -241,7 +236,7 @@ public sealed class EfProductRepository : IProductRepository
|
||||
await RemoveAddonGroupsAsync(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)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
@@ -250,20 +245,20 @@ public sealed class EfProductRepository : IProductRepository
|
||||
return;
|
||||
}
|
||||
|
||||
_context.Products.Remove(existing);
|
||||
context.Products.Remove(existing);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateCategoryAsync(ProductCategory category, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.ProductCategories.Update(category);
|
||||
context.ProductCategories.Update(category);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
@@ -272,13 +267,13 @@ public sealed class EfProductRepository : IProductRepository
|
||||
return;
|
||||
}
|
||||
|
||||
_context.ProductCategories.Remove(existing);
|
||||
context.ProductCategories.Remove(existing);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -287,13 +282,13 @@ public sealed class EfProductRepository : IProductRepository
|
||||
return;
|
||||
}
|
||||
|
||||
_context.ProductSkus.RemoveRange(skus);
|
||||
context.ProductSkus.RemoveRange(skus);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
.Select(x => x.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
@@ -303,29 +298,29 @@ public sealed class EfProductRepository : IProductRepository
|
||||
return;
|
||||
}
|
||||
|
||||
var options = await _context.ProductAddonOptions
|
||||
var options = await context.ProductAddonOptions
|
||||
.Where(x => x.TenantId == tenantId && groupIds.Contains(x.AddonGroupId))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
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))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (groups.Count > 0)
|
||||
{
|
||||
_context.ProductAddonGroups.RemoveRange(groups);
|
||||
context.ProductAddonGroups.RemoveRange(groups);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
.Select(x => x.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
@@ -335,29 +330,29 @@ public sealed class EfProductRepository : IProductRepository
|
||||
return;
|
||||
}
|
||||
|
||||
var options = await _context.ProductAttributeOptions
|
||||
var options = await context.ProductAttributeOptions
|
||||
.Where(x => x.TenantId == tenantId && groupIds.Contains(x.AttributeGroupId))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
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))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (groups.Count > 0)
|
||||
{
|
||||
_context.ProductAttributeGroups.RemoveRange(groups);
|
||||
context.ProductAttributeGroups.RemoveRange(groups);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -366,13 +361,13 @@ public sealed class EfProductRepository : IProductRepository
|
||||
return;
|
||||
}
|
||||
|
||||
_context.ProductMediaAssets.RemoveRange(assets);
|
||||
context.ProductMediaAssets.RemoveRange(assets);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -381,6 +376,6 @@ public sealed class EfProductRepository : IProductRepository
|
||||
return;
|
||||
}
|
||||
|
||||
_context.ProductPricingRules.RemoveRange(rules);
|
||||
context.ProductPricingRules.RemoveRange(rules);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,22 +10,17 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||
/// <summary>
|
||||
/// 门店聚合的 EF Core 仓储实现。
|
||||
/// </summary>
|
||||
public sealed class EfStoreRepository : IStoreRepository
|
||||
/// <remarks>
|
||||
/// 初始化仓储。
|
||||
/// </remarks>
|
||||
public sealed class EfStoreRepository(TakeoutAppDbContext context) : IStoreRepository
|
||||
{
|
||||
private readonly TakeoutAppDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化仓储。
|
||||
/// </summary>
|
||||
public EfStoreRepository(TakeoutAppDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Store?> FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.Stores
|
||||
return context.Stores
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.Id == storeId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -34,7 +29,7 @@ public sealed class EfStoreRepository : IStoreRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Store>> SearchAsync(long tenantId, StoreStatus? status, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _context.Stores
|
||||
var query = context.Stores
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId);
|
||||
|
||||
@@ -53,7 +48,7 @@ public sealed class EfStoreRepository : IStoreRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreBusinessHour>> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var hours = await _context.StoreBusinessHours
|
||||
var hours = await context.StoreBusinessHours
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.StoreId == storeId)
|
||||
.OrderBy(x => x.DayOfWeek)
|
||||
@@ -66,7 +61,7 @@ public sealed class EfStoreRepository : IStoreRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreDeliveryZone>> GetDeliveryZonesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var zones = await _context.StoreDeliveryZones
|
||||
var zones = await context.StoreDeliveryZones
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.StoreId == storeId)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
@@ -78,7 +73,7 @@ public sealed class EfStoreRepository : IStoreRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreHoliday>> GetHolidaysAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var holidays = await _context.StoreHolidays
|
||||
var holidays = await context.StoreHolidays
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.StoreId == storeId)
|
||||
.OrderBy(x => x.Date)
|
||||
@@ -90,7 +85,7 @@ public sealed class EfStoreRepository : IStoreRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreTableArea>> GetTableAreasAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var areas = await _context.StoreTableAreas
|
||||
var areas = await context.StoreTableAreas
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.StoreId == storeId)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
@@ -102,7 +97,7 @@ public sealed class EfStoreRepository : IStoreRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreTable>> GetTablesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tables = await _context.StoreTables
|
||||
var tables = await context.StoreTables
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.StoreId == storeId)
|
||||
.OrderBy(x => x.TableCode)
|
||||
@@ -114,7 +109,7 @@ public sealed class EfStoreRepository : IStoreRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreEmployeeShift>> GetShiftsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var shifts = await _context.StoreEmployeeShifts
|
||||
var shifts = await context.StoreEmployeeShifts
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.StoreId == storeId)
|
||||
.OrderBy(x => x.ShiftDate)
|
||||
@@ -127,62 +122,62 @@ public sealed class EfStoreRepository : IStoreRepository
|
||||
/// <inheritdoc />
|
||||
public Task AddStoreAsync(Store store, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.Stores.AddAsync(store, cancellationToken).AsTask();
|
||||
return context.Stores.AddAsync(store, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddBusinessHoursAsync(IEnumerable<StoreBusinessHour> hours, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.StoreBusinessHours.AddRangeAsync(hours, cancellationToken);
|
||||
return context.StoreBusinessHours.AddRangeAsync(hours, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddDeliveryZonesAsync(IEnumerable<StoreDeliveryZone> zones, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.StoreDeliveryZones.AddRangeAsync(zones, cancellationToken);
|
||||
return context.StoreDeliveryZones.AddRangeAsync(zones, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddHolidaysAsync(IEnumerable<StoreHoliday> holidays, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.StoreHolidays.AddRangeAsync(holidays, cancellationToken);
|
||||
return context.StoreHolidays.AddRangeAsync(holidays, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddTableAreasAsync(IEnumerable<StoreTableArea> areas, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.StoreTableAreas.AddRangeAsync(areas, cancellationToken);
|
||||
return context.StoreTableAreas.AddRangeAsync(areas, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddTablesAsync(IEnumerable<StoreTable> tables, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.StoreTables.AddRangeAsync(tables, cancellationToken);
|
||||
return context.StoreTables.AddRangeAsync(tables, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddShiftsAsync(IEnumerable<StoreEmployeeShift> shifts, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.StoreEmployeeShifts.AddRangeAsync(shifts, cancellationToken);
|
||||
return context.StoreEmployeeShifts.AddRangeAsync(shifts, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _context.SaveChangesAsync(cancellationToken);
|
||||
return context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateStoreAsync(Store store, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.Stores.Update(store);
|
||||
context.Stores.Update(store);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
@@ -191,6 +186,6 @@ public sealed class EfStoreRepository : IStoreRepository
|
||||
return;
|
||||
}
|
||||
|
||||
_context.Stores.Remove(existing);
|
||||
context.Stores.Remove(existing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,24 +10,18 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories;
|
||||
/// <summary>
|
||||
/// EF Core 字典仓储实现。
|
||||
/// </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)
|
||||
=> _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)
|
||||
=> _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)
|
||||
{
|
||||
var query = _context.DictionaryGroups.AsNoTracking();
|
||||
var query = context.DictionaryGroups.AsNoTracking();
|
||||
if (scope.HasValue)
|
||||
{
|
||||
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)
|
||||
{
|
||||
_context.DictionaryGroups.Add(group);
|
||||
context.DictionaryGroups.Add(group);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task RemoveGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.DictionaryGroups.Remove(group);
|
||||
context.DictionaryGroups.Remove(group);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return await _context.DictionaryItems
|
||||
return await context.DictionaryItems
|
||||
.AsNoTracking()
|
||||
.Where(item => item.GroupId == groupId)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
@@ -64,18 +58,18 @@ public sealed class EfDictionaryRepository : IDictionaryRepository
|
||||
|
||||
public Task AddItemAsync(DictionaryItem item, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.DictionaryItems.Add(item);
|
||||
context.DictionaryItems.Add(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task RemoveItemAsync(DictionaryItem item, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_context.DictionaryItems.Remove(item);
|
||||
context.DictionaryItems.Remove(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -90,7 +84,7 @@ public sealed class EfDictionaryRepository : IDictionaryRepository
|
||||
return Array.Empty<DictionaryItem>();
|
||||
}
|
||||
|
||||
var query = _context.DictionaryItems
|
||||
var query = context.DictionaryItems
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Include(item => item.Group)
|
||||
|
||||
@@ -10,22 +10,15 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Services;
|
||||
/// <summary>
|
||||
/// 基于 IDistributedCache 的字典缓存实现。
|
||||
/// </summary>
|
||||
public sealed class DistributedDictionaryCache : IDictionaryCache
|
||||
public sealed class DistributedDictionaryCache(IDistributedCache cache, IOptions<DictionaryCacheOptions> options) : IDictionaryCache
|
||||
{
|
||||
private readonly IDistributedCache _cache;
|
||||
private readonly DictionaryCacheOptions _options;
|
||||
private readonly DictionaryCacheOptions _options = options.Value;
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return null;
|
||||
@@ -42,13 +35,13 @@ public sealed class DistributedDictionaryCache : IDictionaryCache
|
||||
{
|
||||
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)
|
||||
{
|
||||
var cacheKey = BuildKey(tenantId, code);
|
||||
return _cache.RemoveAsync(cacheKey, cancellationToken);
|
||||
return cache.RemoveAsync(cacheKey, cancellationToken);
|
||||
}
|
||||
|
||||
private static string BuildKey(long tenantId, string code)
|
||||
|
||||
@@ -10,18 +10,12 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
/// <summary>
|
||||
/// EF Core 后台用户仓储实现。
|
||||
/// </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)
|
||||
=> _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)
|
||||
=> _dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
||||
=> dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -10,24 +10,18 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
/// <summary>
|
||||
/// EF Core 小程序用户仓储实现。
|
||||
/// </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)
|
||||
=> _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)
|
||||
=> _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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
user = new MiniUser
|
||||
@@ -39,7 +33,7 @@ public sealed class EfMiniUserRepository : IMiniUserRepository
|
||||
Avatar = avatar,
|
||||
TenantId = tenantId
|
||||
};
|
||||
_dbContext.MiniUsers.Add(user);
|
||||
dbContext.MiniUsers.Add(user);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -48,7 +42,7 @@ public sealed class EfMiniUserRepository : IMiniUserRepository
|
||||
user.Avatar = avatar ?? user.Avatar;
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,21 +13,14 @@ namespace TakeoutSaaS.Infrastructure.Identity.Services;
|
||||
/// <summary>
|
||||
/// Redis 登录限流实现。
|
||||
/// </summary>
|
||||
public sealed class RedisLoginRateLimiter : ILoginRateLimiter
|
||||
public sealed class RedisLoginRateLimiter(IDistributedCache cache, IOptions<LoginRateLimitOptions> options) : ILoginRateLimiter
|
||||
{
|
||||
private readonly IDistributedCache _cache;
|
||||
private readonly LoginRateLimitOptions _options;
|
||||
|
||||
public RedisLoginRateLimiter(IDistributedCache cache, IOptions<LoginRateLimitOptions> options)
|
||||
{
|
||||
_cache = cache;
|
||||
_options = options.Value;
|
||||
}
|
||||
private readonly LoginRateLimitOptions _options = options.Value;
|
||||
|
||||
public async Task EnsureAllowedAsync(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
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);
|
||||
if (count >= _options.MaxAttempts)
|
||||
{
|
||||
@@ -35,7 +28,7 @@ public sealed class RedisLoginRateLimiter : ILoginRateLimiter
|
||||
}
|
||||
|
||||
count++;
|
||||
await _cache.SetStringAsync(
|
||||
await cache.SetStringAsync(
|
||||
cacheKey,
|
||||
count.ToString(),
|
||||
new DistributedCacheEntryOptions
|
||||
@@ -46,7 +39,7 @@ public sealed class RedisLoginRateLimiter : ILoginRateLimiter
|
||||
}
|
||||
|
||||
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}";
|
||||
}
|
||||
|
||||
@@ -14,17 +14,10 @@ namespace TakeoutSaaS.Infrastructure.Identity.Services;
|
||||
/// <summary>
|
||||
/// Redis 刷新令牌存储。
|
||||
/// </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 readonly IDistributedCache _cache;
|
||||
private readonly RefreshTokenStoreOptions _options;
|
||||
|
||||
public RedisRefreshTokenStore(IDistributedCache cache, IOptions<RefreshTokenStoreOptions> options)
|
||||
{
|
||||
_cache = cache;
|
||||
_options = options.Value;
|
||||
}
|
||||
private readonly RefreshTokenStoreOptions _options = options.Value;
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
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)
|
||||
? null
|
||||
: JsonSerializer.Deserialize<RefreshTokenDescriptor>(json, JsonOptions);
|
||||
@@ -56,7 +49,7 @@ public sealed class RedisRefreshTokenStore : IRefreshTokenStore
|
||||
|
||||
var updated = descriptor with { Revoked = true };
|
||||
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}";
|
||||
|
||||
@@ -15,21 +15,14 @@ namespace TakeoutSaaS.Infrastructure.Identity.Services;
|
||||
/// <summary>
|
||||
/// 微信 code2Session 实现
|
||||
/// </summary>
|
||||
public sealed class WeChatAuthService : IWeChatAuthService
|
||||
public sealed class WeChatAuthService(HttpClient httpClient, IOptions<WeChatMiniOptions> options) : IWeChatAuthService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly WeChatMiniOptions _options;
|
||||
|
||||
public WeChatAuthService(HttpClient httpClient, IOptions<WeChatMiniOptions> options)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_options = options.Value;
|
||||
}
|
||||
private readonly WeChatMiniOptions _options = options.Value;
|
||||
|
||||
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";
|
||||
using var response = await _httpClient.GetAsync(requestUri, cancellationToken);
|
||||
using var response = await httpClient.GetAsync(requestUri, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<WeChatSessionResponse>(cancellationToken: cancellationToken);
|
||||
|
||||
@@ -3,40 +3,34 @@ namespace TakeoutSaaS.Module.Storage.Models;
|
||||
/// <summary>
|
||||
/// 直传(预签名上传)请求参数。
|
||||
/// </summary>
|
||||
public sealed class StorageDirectUploadRequest
|
||||
/// <remarks>
|
||||
/// 初始化请求。
|
||||
/// </remarks>
|
||||
/// <param name="objectKey">对象键。</param>
|
||||
/// <param name="contentType">内容类型。</param>
|
||||
/// <param name="contentLength">内容长度。</param>
|
||||
/// <param name="expires">签名有效期。</param>
|
||||
public sealed class StorageDirectUploadRequest(string objectKey, string contentType, long contentLength, TimeSpan expires)
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化请求。
|
||||
/// </summary>
|
||||
/// <param name="objectKey">对象键。</param>
|
||||
/// <param name="contentType">内容类型。</param>
|
||||
/// <param name="contentLength">内容长度。</param>
|
||||
/// <param name="expires">签名有效期。</param>
|
||||
public StorageDirectUploadRequest(string objectKey, string contentType, long contentLength, TimeSpan expires)
|
||||
{
|
||||
ObjectKey = objectKey;
|
||||
ContentType = contentType;
|
||||
ContentLength = contentLength;
|
||||
Expires = expires;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 目标对象键。
|
||||
/// </summary>
|
||||
public string ObjectKey { get; }
|
||||
public string ObjectKey { get; } = objectKey;
|
||||
|
||||
/// <summary>
|
||||
/// 内容类型。
|
||||
/// </summary>
|
||||
public string ContentType { get; }
|
||||
public string ContentType { get; } = contentType;
|
||||
|
||||
/// <summary>
|
||||
/// 内容长度。
|
||||
/// </summary>
|
||||
public long ContentLength { get; }
|
||||
public long ContentLength { get; } = contentLength;
|
||||
|
||||
/// <summary>
|
||||
/// 签名有效期。
|
||||
/// </summary>
|
||||
public TimeSpan Expires { get; }
|
||||
public TimeSpan Expires { get; } = expires;
|
||||
}
|
||||
|
||||
@@ -6,70 +6,60 @@ namespace TakeoutSaaS.Module.Storage.Models;
|
||||
/// <summary>
|
||||
/// 对象存储上传请求参数。
|
||||
/// </summary>
|
||||
public sealed class StorageUploadRequest
|
||||
/// <remarks>
|
||||
/// 初始化上传请求。
|
||||
/// </remarks>
|
||||
/// <param name="objectKey">对象键(含路径)。</param>
|
||||
/// <param name="content">文件流。</param>
|
||||
/// <param name="contentType">内容类型。</param>
|
||||
/// <param name="contentLength">内容长度。</param>
|
||||
/// <param name="generateSignedUrl">是否返回签名访问链接。</param>
|
||||
/// <param name="signedUrlExpires">签名有效期。</param>
|
||||
/// <param name="metadata">附加元数据。</param>
|
||||
public sealed class StorageUploadRequest(
|
||||
string objectKey,
|
||||
Stream content,
|
||||
string contentType,
|
||||
long contentLength,
|
||||
bool generateSignedUrl,
|
||||
TimeSpan signedUrlExpires,
|
||||
IDictionary<string, string>? metadata = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化上传请求。
|
||||
/// </summary>
|
||||
/// <param name="objectKey">对象键(含路径)。</param>
|
||||
/// <param name="content">文件流。</param>
|
||||
/// <param name="contentType">内容类型。</param>
|
||||
/// <param name="contentLength">内容长度。</param>
|
||||
/// <param name="generateSignedUrl">是否返回签名访问链接。</param>
|
||||
/// <param name="signedUrlExpires">签名有效期。</param>
|
||||
/// <param name="metadata">附加元数据。</param>
|
||||
public StorageUploadRequest(
|
||||
string objectKey,
|
||||
Stream content,
|
||||
string contentType,
|
||||
long contentLength,
|
||||
bool generateSignedUrl,
|
||||
TimeSpan signedUrlExpires,
|
||||
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>
|
||||
public string ObjectKey { get; }
|
||||
public string ObjectKey { get; } = objectKey;
|
||||
|
||||
/// <summary>
|
||||
/// 文件流。
|
||||
/// </summary>
|
||||
public Stream Content { get; }
|
||||
public Stream Content { get; } = content;
|
||||
|
||||
/// <summary>
|
||||
/// 内容类型。
|
||||
/// </summary>
|
||||
public string ContentType { get; }
|
||||
public string ContentType { get; } = contentType;
|
||||
|
||||
/// <summary>
|
||||
/// 内容长度。
|
||||
/// </summary>
|
||||
public long ContentLength { get; }
|
||||
public long ContentLength { get; } = contentLength;
|
||||
|
||||
/// <summary>
|
||||
/// 是否需要签名访问链接。
|
||||
/// </summary>
|
||||
public bool GenerateSignedUrl { get; }
|
||||
public bool GenerateSignedUrl { get; } = generateSignedUrl;
|
||||
|
||||
/// <summary>
|
||||
/// 签名有效期。
|
||||
/// </summary>
|
||||
public TimeSpan SignedUrlExpires { get; }
|
||||
public TimeSpan SignedUrlExpires { get; } = signedUrlExpires;
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
@@ -5,20 +5,15 @@ namespace TakeoutSaaS.Module.Tenancy;
|
||||
/// <summary>
|
||||
/// 默认租户提供者:基于租户上下文访问器暴露当前租户 ID。
|
||||
/// </summary>
|
||||
public sealed class TenantProvider : ITenantProvider
|
||||
/// <remarks>
|
||||
/// 初始化租户提供者。
|
||||
/// </remarks>
|
||||
/// <param name="tenantContextAccessor">租户上下文访问器</param>
|
||||
public sealed class TenantProvider(ITenantContextAccessor tenantContextAccessor) : ITenantProvider
|
||||
{
|
||||
private readonly ITenantContextAccessor _tenantContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化租户提供者。
|
||||
/// </summary>
|
||||
/// <param name="tenantContextAccessor">租户上下文访问器</param>
|
||||
public TenantProvider(ITenantContextAccessor tenantContextAccessor)
|
||||
{
|
||||
_tenantContextAccessor = tenantContextAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public long GetCurrentTenantId()
|
||||
=> _tenantContextAccessor.Current?.TenantId ?? 0;
|
||||
=> tenantContextAccessor.Current?.TenantId ?? 0;
|
||||
}
|
||||
|
||||
@@ -11,54 +11,43 @@ namespace TakeoutSaaS.Module.Tenancy;
|
||||
/// <summary>
|
||||
/// 多租户解析中间件:支持 Header、域名与 Token Claim 的优先级解析。
|
||||
/// </summary>
|
||||
public sealed class TenantResolutionMiddleware
|
||||
/// <remarks>
|
||||
/// 初始化中间件。
|
||||
/// </remarks>
|
||||
public sealed class TenantResolutionMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<TenantResolutionMiddleware> logger,
|
||||
ITenantContextAccessor tenantContextAccessor,
|
||||
IOptionsMonitor<TenantResolutionOptions> optionsMonitor)
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<TenantResolutionMiddleware> _logger;
|
||||
private readonly ITenantContextAccessor _tenantContextAccessor;
|
||||
private readonly IOptionsMonitor<TenantResolutionOptions> _optionsMonitor;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化中间件。
|
||||
/// </summary>
|
||||
public TenantResolutionMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<TenantResolutionMiddleware> logger,
|
||||
ITenantContextAccessor tenantContextAccessor,
|
||||
IOptionsMonitor<TenantResolutionOptions> optionsMonitor)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
_tenantContextAccessor = tenantContextAccessor;
|
||||
_optionsMonitor = optionsMonitor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析租户并将上下文注入请求。
|
||||
/// </summary>
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var options = _optionsMonitor.CurrentValue ?? new TenantResolutionOptions();
|
||||
var options = optionsMonitor.CurrentValue ?? new TenantResolutionOptions();
|
||||
if (ShouldSkip(context.Request.Path, options))
|
||||
{
|
||||
await _next(context);
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var tenantContext = ResolveTenant(context, options);
|
||||
_tenantContextAccessor.Current = tenantContext;
|
||||
tenantContextAccessor.Current = tenantContext;
|
||||
context.Items[TenantConstants.HttpContextItemKey] = tenantContext;
|
||||
|
||||
if (!tenantContext.IsResolved)
|
||||
{
|
||||
_logger.LogDebug("未能解析租户:{Path}", context.Request.Path);
|
||||
logger.LogDebug("未能解析租户:{Path}", context.Request.Path);
|
||||
|
||||
if (options.ThrowIfUnresolved)
|
||||
{
|
||||
var response = ApiResponse.Error(ErrorCodes.BadRequest, "缺少租户标识");
|
||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
await context.Response.WriteAsJsonAsync(response, cancellationToken: context.RequestAborted);
|
||||
_tenantContextAccessor.Current = null;
|
||||
tenantContextAccessor.Current = null;
|
||||
context.Items.Remove(TenantConstants.HttpContextItemKey);
|
||||
return;
|
||||
}
|
||||
@@ -66,11 +55,11 @@ public sealed class TenantResolutionMiddleware
|
||||
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
await next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_tenantContextAccessor.Current = null;
|
||||
tenantContextAccessor.Current = null;
|
||||
context.Items.Remove(TenantConstants.HttpContextItemKey);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user