feat: migrate snowflake ids and refresh migrations

This commit is contained in:
2025-12-02 09:04:37 +08:00
parent 462e15abbb
commit 148475fa43
174 changed files with 8020 additions and 34278 deletions

View File

@@ -67,7 +67,7 @@ public sealed class AuthController : BaseApiController
public async Task<ApiResponse<CurrentUserProfile>> GetProfile(CancellationToken cancellationToken)
{
var userId = User.GetUserId();
if (userId == Guid.Empty)
if (userId == 0)
{
return ApiResponse<CurrentUserProfile>.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识");
}

View File

@@ -55,10 +55,10 @@ public sealed class DictionaryController : BaseApiController
/// <summary>
/// 更新字典分组。
/// </summary>
[HttpPut("{groupId:guid}")]
[HttpPut("{groupId:long}")]
[PermissionAuthorize("dictionary:group:update")]
[ProducesResponseType(typeof(ApiResponse<DictionaryGroupDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<DictionaryGroupDto>> UpdateGroup(Guid groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken)
public async Task<ApiResponse<DictionaryGroupDto>> UpdateGroup(long groupId, [FromBody] UpdateDictionaryGroupRequest request, CancellationToken cancellationToken)
{
var group = await _dictionaryAppService.UpdateGroupAsync(groupId, request, cancellationToken);
return ApiResponse<DictionaryGroupDto>.Ok(group);
@@ -67,10 +67,10 @@ public sealed class DictionaryController : BaseApiController
/// <summary>
/// 删除字典分组。
/// </summary>
[HttpDelete("{groupId:guid}")]
[HttpDelete("{groupId:long}")]
[PermissionAuthorize("dictionary:group:delete")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
public async Task<ApiResponse<object>> DeleteGroup(Guid groupId, CancellationToken cancellationToken)
public async Task<ApiResponse<object>> DeleteGroup(long groupId, CancellationToken cancellationToken)
{
await _dictionaryAppService.DeleteGroupAsync(groupId, cancellationToken);
return ApiResponse.Success();
@@ -79,10 +79,10 @@ public sealed class DictionaryController : BaseApiController
/// <summary>
/// 创建字典项。
/// </summary>
[HttpPost("{groupId:guid}/items")]
[HttpPost("{groupId:long}/items")]
[PermissionAuthorize("dictionary:item:create")]
[ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<DictionaryItemDto>> CreateItem(Guid groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken)
public async Task<ApiResponse<DictionaryItemDto>> CreateItem(long groupId, [FromBody] CreateDictionaryItemRequest request, CancellationToken cancellationToken)
{
request.GroupId = groupId;
var item = await _dictionaryAppService.CreateItemAsync(request, cancellationToken);
@@ -92,10 +92,10 @@ public sealed class DictionaryController : BaseApiController
/// <summary>
/// 更新字典项。
/// </summary>
[HttpPut("items/{itemId:guid}")]
[HttpPut("items/{itemId:long}")]
[PermissionAuthorize("dictionary:item:update")]
[ProducesResponseType(typeof(ApiResponse<DictionaryItemDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<DictionaryItemDto>> UpdateItem(Guid itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken)
public async Task<ApiResponse<DictionaryItemDto>> UpdateItem(long itemId, [FromBody] UpdateDictionaryItemRequest request, CancellationToken cancellationToken)
{
var item = await _dictionaryAppService.UpdateItemAsync(itemId, request, cancellationToken);
return ApiResponse<DictionaryItemDto>.Ok(item);
@@ -104,10 +104,10 @@ public sealed class DictionaryController : BaseApiController
/// <summary>
/// 删除字典项。
/// </summary>
[HttpDelete("items/{itemId:guid}")]
[HttpDelete("items/{itemId:long}")]
[PermissionAuthorize("dictionary:item:delete")]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
public async Task<ApiResponse<object>> DeleteItem(Guid itemId, CancellationToken cancellationToken)
public async Task<ApiResponse<object>> DeleteItem(long itemId, CancellationToken cancellationToken)
{
await _dictionaryAppService.DeleteItemAsync(itemId, cancellationToken);
return ApiResponse.Success();

View File

@@ -0,0 +1,72 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using TakeoutSaaS.Application.App.Merchants.Commands;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Domain.Merchants.Enums;
using TakeoutSaaS.Module.Authorization.Attributes;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Web.Api;
namespace TakeoutSaaS.AdminApi.Controllers;
/// <summary>
/// 商户管理。
/// </summary>
[ApiVersion("1.0")]
[Authorize]
[Route("api/admin/v{version:apiVersion}/merchants")]
public sealed class MerchantsController : BaseApiController
{
private readonly IMediator _mediator;
/// <summary>
/// 初始化控制器。
/// </summary>
public MerchantsController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
/// 创建商户。
/// </summary>
[HttpPost]
[PermissionAuthorize("merchant:create")]
[ProducesResponseType(typeof(ApiResponse<MerchantDto>), StatusCodes.Status200OK)]
public async Task<ApiResponse<MerchantDto>> Create([FromBody] CreateMerchantCommand command, CancellationToken cancellationToken)
{
var result = await _mediator.Send(command, cancellationToken);
return ApiResponse<MerchantDto>.Ok(result);
}
/// <summary>
/// 查询商户列表。
/// </summary>
[HttpGet]
[PermissionAuthorize("merchant:read")]
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<MerchantDto>>), StatusCodes.Status200OK)]
public async Task<ApiResponse<IReadOnlyList<MerchantDto>>> List([FromQuery] MerchantStatus? status, CancellationToken cancellationToken)
{
var result = await _mediator.Send(new SearchMerchantsQuery { Status = status }, cancellationToken);
return ApiResponse<IReadOnlyList<MerchantDto>>.Ok(result);
}
/// <summary>
/// 获取商户详情。
/// </summary>
[HttpGet("{merchantId:long}")]
[PermissionAuthorize("merchant:read")]
[ProducesResponseType(typeof(ApiResponse<MerchantDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
public async Task<ApiResponse<MerchantDto>> Detail(long merchantId, CancellationToken cancellationToken)
{
var result = await _mediator.Send(new GetMerchantByIdQuery { MerchantId = merchantId }, cancellationToken);
return result == null
? ApiResponse<MerchantDto>.Error(ErrorCodes.NotFound, "商户不存在")
: ApiResponse<MerchantDto>.Ok(result);
}
}

View File

@@ -6,10 +6,12 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using TakeoutSaaS.Application.App.Extensions;
using TakeoutSaaS.Application.Identity.Extensions;
using TakeoutSaaS.Application.Messaging.Extensions;
using TakeoutSaaS.Application.Sms.Extensions;
using TakeoutSaaS.Application.Storage.Extensions;
using TakeoutSaaS.Infrastructure.App.Extensions;
using TakeoutSaaS.Infrastructure.Identity.Extensions;
using TakeoutSaaS.Module.Authorization.Extensions;
using TakeoutSaaS.Module.Dictionary.Extensions;
@@ -40,6 +42,8 @@ builder.Services.AddSharedSwagger(options =>
});
builder.Services.AddIdentityApplication();
builder.Services.AddIdentityInfrastructure(builder.Configuration, enableAdminSeed: true);
builder.Services.AddAppInfrastructure(builder.Configuration);
builder.Services.AddAppApplication();
builder.Services.AddJwtAuthentication(builder.Configuration);
builder.Services.AddAuthorization();
builder.Services.AddPermissionAuthorization();

View File

@@ -8,6 +8,10 @@
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Web\TakeoutSaaS.Shared.Web.csproj" />

View File

@@ -149,5 +149,39 @@
"WorkerCount": 5,
"DashboardEnabled": false,
"DashboardPath": "/hangfire"
},
"App": {
"Seed": {
"Enabled": true,
"DefaultTenant": {
"TenantId": 1000000000001,
"Code": "demo",
"Name": "Demo租户",
"ShortName": "Demo",
"ContactName": "DemoAdmin",
"ContactPhone": "13800000000"
},
"DictionaryGroups": [
{
"Code": "order_status",
"Name": "订单状态",
"Scope": "Business",
"Items": [
{ "Key": "pending", "Value": "待支付", "SortOrder": 10 },
{ "Key": "paid", "Value": "已支付", "SortOrder": 20 },
{ "Key": "finished", "Value": "已完成", "SortOrder": 30 }
]
},
{
"Code": "store_tags",
"Name": "门店标签",
"Scope": "Business",
"Items": [
{ "Key": "hot", "Value": "热门", "SortOrder": 10 },
{ "Key": "new", "Value": "新店", "SortOrder": 20 }
]
}
]
}
}
}

View File

@@ -41,7 +41,7 @@ public sealed class MeController : BaseApiController
public async Task<ApiResponse<CurrentUserProfile>> Get(CancellationToken cancellationToken)
{
var userId = User.GetUserId();
if (userId == Guid.Empty)
if (userId == 0)
{
return ApiResponse<CurrentUserProfile>.Error(ErrorCodes.Unauthorized, "Token 缺少有效的用户标识");
}

View File

@@ -0,0 +1,23 @@
using System.Reflection;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
namespace TakeoutSaaS.Application.App.Extensions;
/// <summary>
/// 业务应用层服务注册。
/// </summary>
public static class AppApplicationServiceCollectionExtensions
{
/// <summary>
/// 注册业务应用层MediatR 处理器等)。
/// </summary>
/// <param name="services">服务集合。</param>
/// <returns>服务集合。</returns>
public static IServiceCollection AddAppApplication(this IServiceCollection services)
{
services.AddMediatR(Assembly.GetExecutingAssembly());
return services;
}
}

View File

@@ -0,0 +1,53 @@
using MediatR;
using System.ComponentModel.DataAnnotations;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Domain.Merchants.Enums;
namespace TakeoutSaaS.Application.App.Merchants.Commands;
/// <summary>
/// 创建商户命令。
/// </summary>
public sealed class CreateMerchantCommand : IRequest<MerchantDto>
{
/// <summary>
/// 品牌名称。
/// </summary>
[Required, MaxLength(128)]
public string BrandName { get; init; } = string.Empty;
/// <summary>
/// 品牌简称。
/// </summary>
[MaxLength(64)]
public string? BrandAlias { get; init; }
/// <summary>
/// 品牌 Logo。
/// </summary>
[MaxLength(256)]
public string? LogoUrl { get; init; }
/// <summary>
/// 品类。
/// </summary>
[MaxLength(64)]
public string? Category { get; init; }
/// <summary>
/// 联系电话。
/// </summary>
[Required, MaxLength(32)]
public string ContactPhone { get; init; } = string.Empty;
/// <summary>
/// 联系邮箱。
/// </summary>
[MaxLength(128)]
public string? ContactEmail { get; init; }
/// <summary>
/// 状态,可用于直接设为审核通过等场景。
/// </summary>
public MerchantStatus Status { get; init; } = MerchantStatus.Pending;
}

View File

@@ -0,0 +1,63 @@
using System.Text.Json.Serialization;
using TakeoutSaaS.Domain.Merchants.Enums;
using TakeoutSaaS.Shared.Abstractions.Serialization;
namespace TakeoutSaaS.Application.App.Merchants.Dto;
/// <summary>
/// 商户 DTO。
/// </summary>
public sealed class MerchantDto
{
/// <summary>
/// 商户 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
/// <summary>
/// 租户 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long TenantId { get; init; }
/// <summary>
/// 品牌名称。
/// </summary>
public string BrandName { get; init; } = string.Empty;
/// <summary>
/// 品牌简称。
/// </summary>
public string? BrandAlias { get; init; }
/// <summary>
/// 品牌 Logo。
/// </summary>
public string? LogoUrl { get; init; }
/// <summary>
/// 品类。
/// </summary>
public string? Category { get; init; }
/// <summary>
/// 联系电话。
/// </summary>
public string ContactPhone { get; init; } = string.Empty;
/// <summary>
/// 联系邮箱。
/// </summary>
public string? ContactEmail { get; init; }
/// <summary>
/// 入驻状态。
/// </summary>
public MerchantStatus Status { get; init; }
/// <summary>
/// 入驻时间。
/// </summary>
public DateTime? JoinedAt { get; init; }
}

View File

@@ -0,0 +1,54 @@
using MediatR;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.Merchants.Commands;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Domain.Merchants.Entities;
using TakeoutSaaS.Domain.Merchants.Repositories;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// <summary>
/// 创建商户命令处理器。
/// </summary>
public sealed class CreateMerchantCommandHandler(IMerchantRepository merchantRepository, ILogger<CreateMerchantCommandHandler> logger)
: IRequestHandler<CreateMerchantCommand, MerchantDto>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ILogger<CreateMerchantCommandHandler> _logger = logger;
/// <inheritdoc />
public async Task<MerchantDto> Handle(CreateMerchantCommand request, CancellationToken cancellationToken)
{
var merchant = new Merchant
{
BrandName = request.BrandName.Trim(),
BrandAlias = request.BrandAlias?.Trim(),
LogoUrl = request.LogoUrl?.Trim(),
Category = request.Category?.Trim(),
ContactPhone = request.ContactPhone.Trim(),
ContactEmail = request.ContactEmail?.Trim(),
Status = request.Status,
JoinedAt = DateTime.UtcNow
};
await _merchantRepository.AddMerchantAsync(merchant, cancellationToken);
await _merchantRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("创建商户 {MerchantId} - {BrandName}", merchant.Id, merchant.BrandName);
return MapToDto(merchant);
}
private static MerchantDto MapToDto(Merchant merchant) => new()
{
Id = merchant.Id,
TenantId = merchant.TenantId,
BrandName = merchant.BrandName,
BrandAlias = merchant.BrandAlias,
LogoUrl = merchant.LogoUrl,
Category = merchant.Category,
ContactPhone = merchant.ContactPhone,
ContactEmail = merchant.ContactEmail,
Status = merchant.Status,
JoinedAt = merchant.JoinedAt
};
}

View File

@@ -0,0 +1,42 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// <summary>
/// 获取商户详情查询处理器。
/// </summary>
public sealed class GetMerchantByIdQueryHandler(IMerchantRepository merchantRepository, ITenantProvider tenantProvider)
: IRequestHandler<GetMerchantByIdQuery, MerchantDto?>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
public async Task<MerchantDto?> Handle(GetMerchantByIdQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var merchant = await _merchantRepository.FindByIdAsync(request.MerchantId, tenantId, cancellationToken);
if (merchant == null)
{
return null;
}
return new MerchantDto
{
Id = merchant.Id,
TenantId = merchant.TenantId,
BrandName = merchant.BrandName,
BrandAlias = merchant.BrandAlias,
LogoUrl = merchant.LogoUrl,
Category = merchant.Category,
ContactPhone = merchant.ContactPhone,
ContactEmail = merchant.ContactEmail,
Status = merchant.Status,
JoinedAt = merchant.JoinedAt
};
}
}

View File

@@ -0,0 +1,42 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// <summary>
/// 商户列表查询处理器。
/// </summary>
public sealed class SearchMerchantsQueryHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider)
: IRequestHandler<SearchMerchantsQuery, IReadOnlyList<MerchantDto>>
{
private readonly IMerchantRepository _merchantRepository = merchantRepository;
private readonly ITenantProvider _tenantProvider = tenantProvider;
/// <inheritdoc />
public async Task<IReadOnlyList<MerchantDto>> Handle(SearchMerchantsQuery request, CancellationToken cancellationToken)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var merchants = await _merchantRepository.SearchAsync(tenantId, request.Status, cancellationToken);
return merchants
.Select(merchant => new MerchantDto
{
Id = merchant.Id,
TenantId = merchant.TenantId,
BrandName = merchant.BrandName,
BrandAlias = merchant.BrandAlias,
LogoUrl = merchant.LogoUrl,
Category = merchant.Category,
ContactPhone = merchant.ContactPhone,
ContactEmail = merchant.ContactEmail,
Status = merchant.Status,
JoinedAt = merchant.JoinedAt
})
.ToList();
}
}

View File

@@ -0,0 +1,15 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
namespace TakeoutSaaS.Application.App.Merchants.Queries;
/// <summary>
/// 按 ID 获取商户。
/// </summary>
public sealed class GetMerchantByIdQuery : IRequest<MerchantDto?>
{
/// <summary>
/// 商户 ID。
/// </summary>
public long MerchantId { get; init; }
}

View File

@@ -0,0 +1,16 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Domain.Merchants.Enums;
namespace TakeoutSaaS.Application.App.Merchants.Queries;
/// <summary>
/// 搜索商户列表。
/// </summary>
public sealed class SearchMerchantsQuery : IRequest<IReadOnlyList<MerchantDto>>
{
/// <summary>
/// 按状态过滤。
/// </summary>
public MerchantStatus? Status { get; init; }
}

View File

@@ -10,17 +10,17 @@ public interface IDictionaryAppService
{
Task<DictionaryGroupDto> CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default);
Task<DictionaryGroupDto> UpdateGroupAsync(Guid groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default);
Task<DictionaryGroupDto> UpdateGroupAsync(long groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default);
Task DeleteGroupAsync(Guid groupId, CancellationToken cancellationToken = default);
Task DeleteGroupAsync(long groupId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<DictionaryGroupDto>> SearchGroupsAsync(DictionaryGroupQuery request, CancellationToken cancellationToken = default);
Task<DictionaryItemDto> CreateItemAsync(CreateDictionaryItemRequest request, CancellationToken cancellationToken = default);
Task<DictionaryItemDto> UpdateItemAsync(Guid itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default);
Task<DictionaryItemDto> UpdateItemAsync(long itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default);
Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken = default);
Task DeleteItemAsync(long itemId, CancellationToken cancellationToken = default);
Task<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>> GetCachedItemsAsync(DictionaryBatchQueryRequest request, CancellationToken cancellationToken = default);
}

View File

@@ -13,15 +13,15 @@ public interface IDictionaryCache
/// <summary>
/// 获取缓存。
/// </summary>
Task<IReadOnlyList<DictionaryItemDto>?> GetAsync(Guid tenantId, string code, CancellationToken cancellationToken = default);
Task<IReadOnlyList<DictionaryItemDto>?> GetAsync(long tenantId, string code, CancellationToken cancellationToken = default);
/// <summary>
/// 写入缓存。
/// </summary>
Task SetAsync(Guid tenantId, string code, IReadOnlyList<DictionaryItemDto> items, CancellationToken cancellationToken = default);
Task SetAsync(long tenantId, string code, IReadOnlyList<DictionaryItemDto> items, CancellationToken cancellationToken = default);
/// <summary>
/// 移除缓存。
/// </summary>
Task RemoveAsync(Guid tenantId, string code, CancellationToken cancellationToken = default);
Task RemoveAsync(long tenantId, string code, CancellationToken cancellationToken = default);
}

View File

@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
using TakeoutSaaS.Shared.Abstractions.Serialization;
using System.ComponentModel.DataAnnotations;
namespace TakeoutSaaS.Application.Dictionary.Contracts;
@@ -11,7 +13,8 @@ public sealed class CreateDictionaryItemRequest
/// 所属分组 ID。
/// </summary>
[Required]
public Guid GroupId { get; set; }
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long GroupId { get; set; }
/// <summary>
/// 字典项键。

View File

@@ -1,5 +1,8 @@
using TakeoutSaaS.Domain.Dictionary.Enums;
using System.Text.Json.Serialization;
using TakeoutSaaS.Shared.Abstractions.Serialization;
namespace TakeoutSaaS.Application.Dictionary.Models;
/// <summary>
@@ -7,7 +10,8 @@ namespace TakeoutSaaS.Application.Dictionary.Models;
/// </summary>
public sealed class DictionaryGroupDto
{
public Guid Id { get; init; }
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
public string Code { get; init; } = string.Empty;

View File

@@ -1,3 +1,6 @@
using System.Text.Json.Serialization;
using TakeoutSaaS.Shared.Abstractions.Serialization;
namespace TakeoutSaaS.Application.Dictionary.Models;
/// <summary>
@@ -5,9 +8,11 @@ namespace TakeoutSaaS.Application.Dictionary.Models;
/// </summary>
public sealed class DictionaryItemDto
{
public Guid Id { get; init; }
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
public Guid GroupId { get; init; }
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long GroupId { get; init; }
public string Key { get; init; } = string.Empty;

View File

@@ -47,7 +47,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
var group = new DictionaryGroup
{
Id = Guid.NewGuid(),
Id = 0,
TenantId = targetTenant,
Code = normalizedCode,
Name = request.Name.Trim(),
@@ -62,7 +62,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
return MapGroup(group, includeItems: false);
}
public async Task<DictionaryGroupDto> UpdateGroupAsync(Guid groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default)
public async Task<DictionaryGroupDto> UpdateGroupAsync(long groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default)
{
var group = await RequireGroupAsync(groupId, cancellationToken);
EnsureScopePermission(group.Scope);
@@ -77,7 +77,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
return MapGroup(group, includeItems: false);
}
public async Task DeleteGroupAsync(Guid groupId, CancellationToken cancellationToken = default)
public async Task DeleteGroupAsync(long groupId, CancellationToken cancellationToken = default)
{
var group = await RequireGroupAsync(groupId, cancellationToken);
EnsureScopePermission(group.Scope);
@@ -120,7 +120,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
var item = new DictionaryItem
{
Id = Guid.NewGuid(),
Id = 0,
TenantId = group.TenantId,
GroupId = group.Id,
Key = request.Key.Trim(),
@@ -138,7 +138,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
return MapItem(item);
}
public async Task<DictionaryItemDto> UpdateItemAsync(Guid itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default)
public async Task<DictionaryItemDto> UpdateItemAsync(long itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default)
{
var item = await RequireItemAsync(itemId, cancellationToken);
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
@@ -156,7 +156,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
return MapItem(item);
}
public async Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken = default)
public async Task DeleteItemAsync(long itemId, CancellationToken cancellationToken = default)
{
var item = await RequireItemAsync(itemId, cancellationToken);
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
@@ -186,8 +186,8 @@ public sealed class DictionaryAppService : IDictionaryAppService
foreach (var code in normalizedCodes)
{
var systemItems = await GetOrLoadCacheAsync(Guid.Empty, code, cancellationToken);
if (tenantId == Guid.Empty)
var systemItems = await GetOrLoadCacheAsync(0, code, cancellationToken);
if (tenantId == 0)
{
result[code] = systemItems;
continue;
@@ -200,7 +200,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
return result;
}
private async Task<DictionaryGroup> RequireGroupAsync(Guid groupId, CancellationToken cancellationToken)
private async Task<DictionaryGroup> RequireGroupAsync(long groupId, CancellationToken cancellationToken)
{
var group = await _repository.FindGroupByIdAsync(groupId, cancellationToken);
if (group == null)
@@ -211,7 +211,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
return group;
}
private async Task<DictionaryItem> RequireItemAsync(Guid itemId, CancellationToken cancellationToken)
private async Task<DictionaryItem> RequireItemAsync(long itemId, CancellationToken cancellationToken)
{
var item = await _repository.FindItemByIdAsync(itemId, cancellationToken);
if (item == null)
@@ -222,16 +222,16 @@ public sealed class DictionaryAppService : IDictionaryAppService
return item;
}
private Guid ResolveTargetTenant(DictionaryScope scope)
private long ResolveTargetTenant(DictionaryScope scope)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
if (scope == DictionaryScope.System)
{
EnsurePlatformTenant(tenantId);
return Guid.Empty;
return 0;
}
if (tenantId == Guid.Empty)
if (tenantId == 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "业务参数需指定租户");
}
@@ -241,28 +241,28 @@ public sealed class DictionaryAppService : IDictionaryAppService
private static string NormalizeCode(string code) => code.Trim().ToLowerInvariant();
private static DictionaryScope ResolveScopeForQuery(DictionaryScope? requestedScope, Guid tenantId)
private static DictionaryScope ResolveScopeForQuery(DictionaryScope? requestedScope, long tenantId)
{
if (requestedScope.HasValue)
{
return requestedScope.Value;
}
return tenantId == Guid.Empty ? DictionaryScope.System : DictionaryScope.Business;
return tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business;
}
private void EnsureScopePermission(DictionaryScope scope)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
if (scope == DictionaryScope.System && tenantId != Guid.Empty)
if (scope == DictionaryScope.System && tenantId != 0)
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
}
}
private void EnsurePlatformTenant(Guid tenantId)
private void EnsurePlatformTenant(long tenantId)
{
if (tenantId != Guid.Empty)
if (tenantId != 0)
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
}
@@ -279,7 +279,7 @@ public sealed class DictionaryAppService : IDictionaryAppService
// 系统参数更新需要逐租户重新合并,由调用方在下一次请求时重新加载
}
private async Task<IReadOnlyList<DictionaryItemDto>> GetOrLoadCacheAsync(Guid tenantId, string code, CancellationToken cancellationToken)
private async Task<IReadOnlyList<DictionaryItemDto>> GetOrLoadCacheAsync(long tenantId, string code, CancellationToken cancellationToken)
{
var cached = await _cache.GetAsync(tenantId, code, cancellationToken);
if (cached != null)

View File

@@ -12,5 +12,5 @@ public interface IAdminAuthService
{
Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default);
Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default);
Task<CurrentUserProfile> GetProfileAsync(Guid userId, CancellationToken cancellationToken = default);
Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default);
}

View File

@@ -12,5 +12,5 @@ public interface IMiniAuthService
{
Task<TokenResponse> LoginWithWeChatAsync(WeChatLoginRequest request, CancellationToken cancellationToken = default);
Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default);
Task<CurrentUserProfile> GetProfileAsync(Guid userId, CancellationToken cancellationToken = default);
Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default);
}

View File

@@ -10,7 +10,7 @@ namespace TakeoutSaaS.Application.Identity.Abstractions;
/// </summary>
public interface IRefreshTokenStore
{
Task<RefreshTokenDescriptor> IssueAsync(Guid userId, DateTime expiresAt, CancellationToken cancellationToken = default);
Task<RefreshTokenDescriptor> IssueAsync(long userId, DateTime expiresAt, CancellationToken cancellationToken = default);
Task<RefreshTokenDescriptor?> GetAsync(string refreshToken, CancellationToken cancellationToken = default);
Task RevokeAsync(string refreshToken, CancellationToken cancellationToken = default);
}

View File

@@ -8,7 +8,7 @@ public sealed class CurrentUserProfile
/// <summary>
/// 用户 ID。
/// </summary>
public Guid UserId { get; init; }
public long UserId { get; init; }
/// <summary>
/// 登录账号。
@@ -23,12 +23,12 @@ public sealed class CurrentUserProfile
/// <summary>
/// 所属租户 ID。
/// </summary>
public Guid TenantId { get; init; }
public long TenantId { get; init; }
/// <summary>
/// 所属商户 ID平台管理员为空
/// </summary>
public Guid? MerchantId { get; init; }
public long? MerchantId { get; init; }
/// <summary>
/// 角色集合。

View File

@@ -9,6 +9,6 @@ namespace TakeoutSaaS.Application.Identity.Models;
/// <param name="Revoked">是否已撤销</param>
public sealed record RefreshTokenDescriptor(
string Token,
Guid UserId,
long UserId,
DateTime ExpiresAt,
bool Revoked);

View File

@@ -77,7 +77,7 @@ public sealed class AdminAuthService(
/// <param name="cancellationToken">取消令牌</param>
/// <returns>用户档案</returns>
/// <exception cref="BusinessException">用户不存在时抛出</exception>
public async Task<CurrentUserProfile> GetProfileAsync(Guid userId, CancellationToken cancellationToken = default)
public async Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default)
{
var user = await userRepository.FindByIdAsync(userId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");

View File

@@ -44,7 +44,7 @@ public sealed class MiniAuthService(
// 3. 获取当前租户 ID多租户支持
var tenantId = tenantProvider.GetCurrentTenantId();
if (tenantId == Guid.Empty)
if (tenantId == 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
}
@@ -95,7 +95,7 @@ public sealed class MiniAuthService(
/// <param name="cancellationToken">取消令牌</param>
/// <returns>用户档案</returns>
/// <exception cref="BusinessException">用户不存在时抛出</exception>
public async Task<CurrentUserProfile> GetProfileAsync(Guid userId, CancellationToken cancellationToken = default)
public async Task<CurrentUserProfile> GetProfileAsync(long userId, CancellationToken cancellationToken = default)
{
var user = await miniUserRepository.FindByIdAsync(userId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
@@ -113,7 +113,7 @@ public sealed class MiniAuthService(
/// <param name="tenantId">租户 ID</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>用户实体和是否为新用户的元组</returns>
private async Task<(MiniUser user, bool isNew)> GetOrBindMiniUserAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken)
private async Task<(MiniUser user, bool isNew)> GetOrBindMiniUserAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken)
{
// 检查用户是否已存在
var existing = await miniUserRepository.FindByOpenIdAsync(openId, cancellationToken);

View File

@@ -8,7 +8,7 @@ public sealed class OrderCreatedEvent
/// <summary>
/// 订单标识。
/// </summary>
public Guid OrderId { get; init; }
public long OrderId { get; init; }
/// <summary>
/// 订单编号。
@@ -23,7 +23,7 @@ public sealed class OrderCreatedEvent
/// <summary>
/// 所属租户。
/// </summary>
public Guid TenantId { get; init; }
public long TenantId { get; init; }
/// <summary>
/// 创建时间UTC

View File

@@ -8,7 +8,7 @@ public sealed class PaymentSucceededEvent
/// <summary>
/// 订单标识。
/// </summary>
public Guid OrderId { get; init; }
public long OrderId { get; init; }
/// <summary>
/// 支付流水号。
@@ -23,7 +23,7 @@ public sealed class PaymentSucceededEvent
/// <summary>
/// 所属租户。
/// </summary>
public Guid TenantId { get; init; }
public long TenantId { get; init; }
/// <summary>
/// 支付时间UTC

View File

@@ -44,7 +44,7 @@ public sealed class VerificationCodeService(
var codeOptions = codeOptionsMonitor.CurrentValue;
var templateCode = ResolveTemplate(request.Scene, smsOptions);
var phone = NormalizePhoneNumber(request.PhoneNumber);
var tenantKey = tenantProvider.GetCurrentTenantId() == Guid.Empty ? "platform" : tenantProvider.GetCurrentTenantId().ToString("N");
var tenantKey = tenantProvider.GetCurrentTenantId() == 0 ? "platform" : tenantProvider.GetCurrentTenantId().ToString();
var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}";
var cooldownKey = $"{cacheKey}:cooldown";
@@ -90,7 +90,7 @@ public sealed class VerificationCodeService(
var codeOptions = codeOptionsMonitor.CurrentValue;
var phone = NormalizePhoneNumber(request.PhoneNumber);
var tenantKey = tenantProvider.GetCurrentTenantId() == Guid.Empty ? "platform" : tenantProvider.GetCurrentTenantId().ToString("N");
var tenantKey = tenantProvider.GetCurrentTenantId() == 0 ? "platform" : tenantProvider.GetCurrentTenantId().ToString();
var cacheKey = $"{codeOptions.CachePrefix}:{tenantKey}:{request.Scene}:{phone}";
var cachedCode = await cache.GetStringAsync(cacheKey, cancellationToken).ConfigureAwait(false);

View File

@@ -212,7 +212,7 @@ public sealed class FileStorageService(
private string BuildObjectKey(UploadFileType type, string extension)
{
var tenantId = tenantProvider.GetCurrentTenantId();
var tenantSegment = tenantId == Guid.Empty ? "platform" : tenantId.ToString("N");
var tenantSegment = tenantId == 0 ? "platform" : tenantId.ToString();
var folder = type.ToFolderName();
var now = DateTime.UtcNow;
var fileName = $"{Guid.NewGuid():N}{extension}";

View File

@@ -8,6 +8,7 @@
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Sms\TakeoutSaaS.Module.Sms.csproj" />

View File

@@ -23,15 +23,15 @@ public abstract class AuditableEntityBase : EntityBase, IAuditableEntity
/// <summary>
/// 创建人用户标识,匿名或系统操作时为 null。
/// </summary>
public Guid? CreatedBy { get; set; }
public long? CreatedBy { get; set; }
/// <summary>
/// 最后更新人用户标识,匿名或系统操作时为 null。
/// </summary>
public Guid? UpdatedBy { get; set; }
public long? UpdatedBy { get; set; }
/// <summary>
/// 删除人用户标识(软删除),未删除时为 null。
/// </summary>
public Guid? DeletedBy { get; set; }
public long? DeletedBy { get; set; }
}

View File

@@ -8,5 +8,5 @@ public abstract class EntityBase
/// <summary>
/// 实体唯一标识。
/// </summary>
public Guid Id { get; set; }
public long Id { get; set; }
}

View File

@@ -23,15 +23,15 @@ public interface IAuditableEntity : ISoftDeleteEntity
/// <summary>
/// 创建人用户标识,匿名或系统操作时为 null。
/// </summary>
Guid? CreatedBy { get; set; }
long? CreatedBy { get; set; }
/// <summary>
/// 最后更新人用户标识,匿名或系统操作时为 null。
/// </summary>
Guid? UpdatedBy { get; set; }
long? UpdatedBy { get; set; }
/// <summary>
/// 删除人用户标识(软删除),未删除时为 null。
/// </summary>
Guid? DeletedBy { get; set; }
long? DeletedBy { get; set; }
}

View File

@@ -8,5 +8,5 @@ public interface IMultiTenantEntity
/// <summary>
/// 所属租户 ID。
/// </summary>
Guid TenantId { get; set; }
long TenantId { get; set; }
}

View File

@@ -8,5 +8,5 @@ public abstract class MultiTenantEntityBase : AuditableEntityBase, IMultiTenantE
/// <summary>
/// 所属租户 ID。
/// </summary>
public Guid TenantId { get; set; }
public long TenantId { get; set; }
}

View File

@@ -0,0 +1,13 @@
namespace TakeoutSaaS.Shared.Abstractions.Ids;
/// <summary>
/// 雪花 ID 生成器接口。
/// </summary>
public interface IIdGenerator
{
/// <summary>
/// 生成下一个唯一长整型 ID。
/// </summary>
/// <returns>雪花 ID。</returns>
long NextId();
}

View File

@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
namespace TakeoutSaaS.Shared.Abstractions.Ids;
/// <summary>
/// 雪花 ID 生成器配置。
/// </summary>
public sealed class IdGeneratorOptions
{
/// <summary>
/// 配置节名称。
/// </summary>
public const string SectionName = "IdGenerator";
/// <summary>
/// 工作节点标识0-31。
/// </summary>
[Range(0, 31)]
public int WorkerId { get; set; }
/// <summary>
/// 机房标识0-31。
/// </summary>
[Range(0, 31)]
public int DatacenterId { get; set; }
}

View File

@@ -99,6 +99,65 @@ public sealed record ApiResponse<T>
return TraceContext.TraceId;
}
return Activity.Current?.Id ?? Guid.NewGuid().ToString("N");
if (!string.IsNullOrWhiteSpace(TraceContext.TraceId))
{
return TraceContext.TraceId;
}
if (Activity.Current?.Id is { } id && !string.IsNullOrWhiteSpace(id))
{
return id;
}
return IdFallbackGenerator.Instance.NextId().ToString();
}
}
internal sealed class IdFallbackGenerator
{
private static readonly Lazy<IdFallbackGenerator> Lazy = new(() => new IdFallbackGenerator());
public static IdFallbackGenerator Instance => Lazy.Value;
private readonly object _sync = new();
private long _lastTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
private long _sequence;
private IdFallbackGenerator()
{
}
public long NextId()
{
lock (_sync)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (timestamp == _lastTimestamp)
{
_sequence = (_sequence + 1) & 4095;
if (_sequence == 0)
{
timestamp = WaitNextMillis(_lastTimestamp);
}
}
else
{
_sequence = 0;
}
_lastTimestamp = timestamp;
return ((timestamp - 1577836800000L) << 22) | _sequence;
}
}
private static long WaitNextMillis(long lastTimestamp)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
while (timestamp <= lastTimestamp)
{
Thread.SpinWait(100);
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
return timestamp;
}
}

View File

@@ -8,7 +8,7 @@ public interface ICurrentUserAccessor
/// <summary>
/// 当前用户 ID未登录时为 Guid.Empty。
/// </summary>
Guid UserId { get; }
long UserId { get; }
/// <summary>
/// 是否已登录。

View File

@@ -0,0 +1,52 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace TakeoutSaaS.Shared.Abstractions.Serialization;
/// <summary>
/// 将 long 类型的雪花 ID 以字符串形式序列化/反序列化,避免前端精度丢失。
/// </summary>
public sealed class SnowflakeIdJsonConverter : JsonConverter<long>
{
/// <inheritdoc />
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.Number => reader.GetInt64(),
JsonTokenType.String when long.TryParse(reader.GetString(), out var value) => value,
JsonTokenType.Null => 0,
_ => throw new JsonException("无法解析雪花 ID")
};
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
{
writer.WriteStringValue(value == 0 ? "0" : value.ToString());
}
}
/// <summary>
/// 可空雪花 ID 转换器。
/// </summary>
public sealed class NullableSnowflakeIdJsonConverter : JsonConverter<long?>
{
/// <inheritdoc />
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.Number => reader.GetInt64(),
JsonTokenType.String when long.TryParse(reader.GetString(), out var value) => value,
JsonTokenType.Null => null,
_ => throw new JsonException("无法解析雪花 ID")
};
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.HasValue ? value.Value.ToString() : null);
}
}

View File

@@ -8,5 +8,5 @@ public interface ITenantProvider
/// <summary>
/// 获取当前租户 ID未解析时返回 Guid.Empty。
/// </summary>
Guid GetCurrentTenantId();
long GetCurrentTenantId();
}

View File

@@ -8,7 +8,7 @@ public sealed class TenantContext
/// <summary>
/// 未解析到租户时的默认上下文。
/// </summary>
public static TenantContext Empty { get; } = new(Guid.Empty, null, "unresolved");
public static TenantContext Empty { get; } = new(0, null, "unresolved");
/// <summary>
/// 初始化租户上下文。
@@ -16,7 +16,7 @@ public sealed class TenantContext
/// <param name="tenantId">租户 ID</param>
/// <param name="tenantCode">租户编码(可选)</param>
/// <param name="source">解析来源</param>
public TenantContext(Guid tenantId, string? tenantCode, string source)
public TenantContext(long tenantId, string? tenantCode, string source)
{
TenantId = tenantId;
TenantCode = tenantCode;
@@ -26,7 +26,7 @@ public sealed class TenantContext
/// <summary>
/// 当前租户 ID未解析时为 Guid.Empty。
/// </summary>
public Guid TenantId { get; }
public long TenantId { get; }
/// <summary>
/// 当前租户编码(例如子域名或业务编码),可为空。
@@ -41,5 +41,5 @@ public sealed class TenantContext
/// <summary>
/// 是否已成功解析到租户。
/// </summary>
public bool IsResolved => TenantId != Guid.Empty;
public bool IsResolved => TenantId != 0;
}

View File

@@ -0,0 +1,111 @@
using System.Diagnostics;
using System.Security.Cryptography;
using System.Threading;
using TakeoutSaaS.Shared.Abstractions.Ids;
namespace TakeoutSaaS.Shared.Kernel.Ids;
/// <summary>
/// 基于雪花算法的长整型 ID 生成器。
/// </summary>
public sealed class SnowflakeIdGenerator : IIdGenerator
{
private const long Twepoch = 1577836800000L; // 2020-01-01 UTC
private const int WorkerIdBits = 5;
private const int DatacenterIdBits = 5;
private const int SequenceBits = 12;
private const long MaxWorkerId = -1L ^ (-1L << WorkerIdBits);
private const long MaxDatacenterId = -1L ^ (-1L << DatacenterIdBits);
private const int WorkerIdShift = SequenceBits;
private const int DatacenterIdShift = SequenceBits + WorkerIdBits;
private const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits;
private const long SequenceMask = -1L ^ (-1L << SequenceBits);
private readonly long _workerId;
private readonly long _datacenterId;
private long _lastTimestamp = -1L;
private long _sequence;
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()
{
lock (_syncRoot)
{
var timestamp = CurrentTimeMillis();
if (timestamp < _lastTimestamp)
{
// 时钟回拨时等待到下一毫秒。
var wait = _lastTimestamp - timestamp;
Thread.Sleep(TimeSpan.FromMilliseconds(wait));
timestamp = CurrentTimeMillis();
if (timestamp < _lastTimestamp)
{
throw new InvalidOperationException($"系统时钟回拨 {_lastTimestamp - timestamp} 毫秒,无法生成 ID。");
}
}
if (_lastTimestamp == timestamp)
{
_sequence = (_sequence + 1) & SequenceMask;
if (_sequence == 0)
{
timestamp = WaitNextMillis(_lastTimestamp);
}
}
else
{
_sequence = 0;
}
_lastTimestamp = timestamp;
var id = ((timestamp - Twepoch) << TimestampLeftShift)
| (_datacenterId << DatacenterIdShift)
| (_workerId << WorkerIdShift)
| _sequence;
Debug.Assert(id > 0);
return id;
}
}
private static long WaitNextMillis(long lastTimestamp)
{
var timestamp = CurrentTimeMillis();
while (timestamp <= lastTimestamp)
{
Thread.SpinWait(50);
timestamp = CurrentTimeMillis();
}
return timestamp;
}
private static long CurrentTimeMillis() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
private static long Normalize(long value, long max, string name)
{
if (value < 0 || value > max)
{
throw new ArgumentOutOfRangeException(name, value, $"取值范围 0~{max}");
}
return value;
}
}

View File

@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Shared.Abstractions.Diagnostics;
using TakeoutSaaS.Shared.Abstractions.Ids;
namespace TakeoutSaaS.Shared.Web.Middleware;
@@ -17,11 +18,13 @@ public sealed class CorrelationIdMiddleware
private readonly RequestDelegate _next;
private readonly ILogger<CorrelationIdMiddleware> _logger;
private readonly IIdGenerator _idGenerator;
public CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger)
public CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger, IIdGenerator idGenerator)
{
_next = next;
_logger = logger;
_idGenerator = idGenerator;
}
public async Task InvokeAsync(HttpContext context)
@@ -52,7 +55,7 @@ public sealed class CorrelationIdMiddleware
}
}
private static string ResolveTraceId(HttpContext context)
private string ResolveTraceId(HttpContext context)
{
if (TryGetHeader(context, TraceHeader, out var traceId))
{
@@ -64,7 +67,7 @@ public sealed class CorrelationIdMiddleware
return requestId;
}
return Guid.NewGuid().ToString("N");
return _idGenerator.NextId().ToString();
}
private static bool TryGetHeader(HttpContext context, string headerName, out string value)

View File

@@ -9,20 +9,20 @@ namespace TakeoutSaaS.Shared.Web.Security;
public static class ClaimsPrincipalExtensions
{
/// <summary>
/// 获取当前用户 Id不存在时返回 Guid.Empty
/// 获取当前用户 Id不存在时返回 0
/// </summary>
public static Guid GetUserId(this ClaimsPrincipal? principal)
public static long GetUserId(this ClaimsPrincipal? principal)
{
if (principal == null)
{
return Guid.Empty;
return 0;
}
var identifier = principal.FindFirstValue(ClaimTypes.NameIdentifier)
?? principal.FindFirstValue("sub");
return Guid.TryParse(identifier, out var userId)
return long.TryParse(identifier, out var userId)
? userId
: Guid.Empty;
: 0;
}
}

View File

@@ -20,23 +20,23 @@ public sealed class HttpContextCurrentUserAccessor : ICurrentUserAccessor
}
/// <inheritdoc />
public Guid UserId
public long UserId
{
get
{
var principal = _httpContextAccessor.HttpContext?.User;
if (principal == null || !principal.Identity?.IsAuthenticated == true)
{
return Guid.Empty;
return 0;
}
var identifier = principal.FindFirstValue(ClaimTypes.NameIdentifier)
?? principal.FindFirstValue("sub");
return Guid.TryParse(identifier, out var id) ? id : Guid.Empty;
return long.TryParse(identifier, out var id) ? id : 0;
}
}
/// <inheritdoc />
public bool IsAuthenticated => UserId != Guid.Empty;
public bool IsAuthenticated => UserId != 0;
}

View File

@@ -11,7 +11,7 @@ public sealed class MetricAlertRule : MultiTenantEntityBase
/// <summary>
/// 关联指标。
/// </summary>
public Guid MetricDefinitionId { get; set; }
public long MetricDefinitionId { get; set; }
/// <summary>
/// 触发条件 JSON。

View File

@@ -10,7 +10,7 @@ public sealed class MetricSnapshot : MultiTenantEntityBase
/// <summary>
/// 指标定义 ID。
/// </summary>
public Guid MetricDefinitionId { get; set; }
public long MetricDefinitionId { get; set; }
/// <summary>
/// 维度键JSON

View File

@@ -11,7 +11,7 @@ public sealed class Coupon : MultiTenantEntityBase
/// <summary>
/// 模板标识。
/// </summary>
public Guid CouponTemplateId { get; set; }
public long CouponTemplateId { get; set; }
/// <summary>
/// 券码或序列号。
@@ -21,12 +21,12 @@ public sealed class Coupon : MultiTenantEntityBase
/// <summary>
/// 归属用户。
/// </summary>
public Guid UserId { get; set; }
public long UserId { get; set; }
/// <summary>
/// 订单 ID已使用时记录
/// </summary>
public Guid? OrderId { get; set; }
public long? OrderId { get; set; }
/// <summary>
/// 状态。

View File

@@ -11,7 +11,7 @@ public sealed class ChatMessage : MultiTenantEntityBase
/// <summary>
/// 会话标识。
/// </summary>
public Guid ChatSessionId { get; set; }
public long ChatSessionId { get; set; }
/// <summary>
/// 发送方类型。
@@ -21,7 +21,7 @@ public sealed class ChatMessage : MultiTenantEntityBase
/// <summary>
/// 发送方用户 ID。
/// </summary>
public Guid? SenderUserId { get; set; }
public long? SenderUserId { get; set; }
/// <summary>
/// 消息内容。

View File

@@ -16,17 +16,17 @@ public sealed class ChatSession : MultiTenantEntityBase
/// <summary>
/// 顾客用户 ID。
/// </summary>
public Guid CustomerUserId { get; set; }
public long CustomerUserId { get; set; }
/// <summary>
/// 当前客服员工 ID。
/// </summary>
public Guid? AgentUserId { get; set; }
public long? AgentUserId { get; set; }
/// <summary>
/// 所属门店(可空为平台)。
/// </summary>
public Guid? StoreId { get; set; }
public long? StoreId { get; set; }
/// <summary>
/// 会话状态。

View File

@@ -16,12 +16,12 @@ public sealed class SupportTicket : MultiTenantEntityBase
/// <summary>
/// 客户用户 ID。
/// </summary>
public Guid CustomerUserId { get; set; }
public long CustomerUserId { get; set; }
/// <summary>
/// 关联订单(如有)。
/// </summary>
public Guid? OrderId { get; set; }
public long? OrderId { get; set; }
/// <summary>
/// 工单主题。
@@ -46,7 +46,7 @@ public sealed class SupportTicket : MultiTenantEntityBase
/// <summary>
/// 指派的客服。
/// </summary>
public Guid? AssignedAgentId { get; set; }
public long? AssignedAgentId { get; set; }
/// <summary>
/// 关闭时间。

View File

@@ -10,12 +10,12 @@ public sealed class TicketComment : MultiTenantEntityBase
/// <summary>
/// 工单标识。
/// </summary>
public Guid SupportTicketId { get; set; }
public long SupportTicketId { get; set; }
/// <summary>
/// 评论人 ID。
/// </summary>
public Guid? AuthorUserId { get; set; }
public long? AuthorUserId { get; set; }
/// <summary>
/// 评论内容。

View File

@@ -11,7 +11,7 @@ public sealed class DeliveryEvent : MultiTenantEntityBase
/// <summary>
/// 配送单标识。
/// </summary>
public Guid DeliveryOrderId { get; set; }
public long DeliveryOrderId { get; set; }
/// <summary>
/// 事件类型。

View File

@@ -8,7 +8,7 @@ namespace TakeoutSaaS.Domain.Deliveries.Entities;
/// </summary>
public sealed class DeliveryOrder : MultiTenantEntityBase
{
public Guid OrderId { get; set; }
public long OrderId { get; set; }
/// <summary>
/// 配送服务商。

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TakeoutSaaS.Domain.Deliveries.Entities;
namespace TakeoutSaaS.Domain.Deliveries.Repositories;
/// <summary>
/// 配送聚合仓储契约。
/// </summary>
public interface IDeliveryRepository
{
/// <summary>
/// 依据标识获取配送单。
/// </summary>
Task<DeliveryOrder?> FindByIdAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 依据订单标识获取配送单。
/// </summary>
Task<DeliveryOrder?> FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取配送事件轨迹。
/// </summary>
Task<IReadOnlyList<DeliveryEvent>> GetEventsAsync(long deliveryOrderId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 新增配送单。
/// </summary>
Task AddDeliveryOrderAsync(DeliveryOrder deliveryOrder, CancellationToken cancellationToken = default);
/// <summary>
/// 新增配送事件。
/// </summary>
Task AddEventAsync(DeliveryEvent deliveryEvent, CancellationToken cancellationToken = default);
/// <summary>
/// 持久化变更。
/// </summary>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -10,7 +10,7 @@ public sealed class DictionaryItem : MultiTenantEntityBase
/// <summary>
/// 关联分组 ID。
/// </summary>
public Guid GroupId { get; set; }
public long GroupId { get; set; }
/// <summary>
/// 字典项键。

View File

@@ -14,7 +14,7 @@ public interface IDictionaryRepository
/// <summary>
/// 依据 ID 获取分组。
/// </summary>
Task<DictionaryGroup?> FindGroupByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<DictionaryGroup?> FindGroupByIdAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// 依据编码获取分组。
@@ -39,17 +39,17 @@ public interface IDictionaryRepository
/// <summary>
/// 依据 ID 获取字典项。
/// </summary>
Task<DictionaryItem?> FindItemByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<DictionaryItem?> FindItemByIdAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// 获取某分组下的所有字典项。
/// </summary>
Task<IReadOnlyList<DictionaryItem>> GetItemsByGroupIdAsync(Guid groupId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<DictionaryItem>> GetItemsByGroupIdAsync(long groupId, CancellationToken cancellationToken = default);
/// <summary>
/// 按分组编码集合获取字典项(可包含系统参数)。
/// </summary>
Task<IReadOnlyList<DictionaryItem>> GetItemsByCodesAsync(IEnumerable<string> codes, Guid tenantId, bool includeSystem, CancellationToken cancellationToken = default);
Task<IReadOnlyList<DictionaryItem>> GetItemsByCodesAsync(IEnumerable<string> codes, long tenantId, bool includeSystem, CancellationToken cancellationToken = default);
/// <summary>
/// 新增字典项。

View File

@@ -11,17 +11,17 @@ public sealed class AffiliateOrder : MultiTenantEntityBase
/// <summary>
/// 推广人标识。
/// </summary>
public Guid AffiliatePartnerId { get; set; }
public long AffiliatePartnerId { get; set; }
/// <summary>
/// 关联订单。
/// </summary>
public Guid OrderId { get; set; }
public long OrderId { get; set; }
/// <summary>
/// 用户 ID。
/// </summary>
public Guid BuyerUserId { get; set; }
public long BuyerUserId { get; set; }
/// <summary>
/// 订单金额。

View File

@@ -11,7 +11,7 @@ public sealed class AffiliatePartner : MultiTenantEntityBase
/// <summary>
/// 用户 ID如绑定平台账号
/// </summary>
public Guid? UserId { get; set; }
public long? UserId { get; set; }
/// <summary>
/// 昵称或渠道名称。

View File

@@ -11,7 +11,7 @@ public sealed class AffiliatePayout : MultiTenantEntityBase
/// <summary>
/// 合作伙伴标识。
/// </summary>
public Guid AffiliatePartnerId { get; set; }
public long AffiliatePartnerId { get; set; }
/// <summary>
/// 结算周期描述。

View File

@@ -10,12 +10,12 @@ public sealed class CheckInRecord : MultiTenantEntityBase
/// <summary>
/// 活动标识。
/// </summary>
public Guid CheckInCampaignId { get; set; }
public long CheckInCampaignId { get; set; }
/// <summary>
/// 用户标识。
/// </summary>
public Guid UserId { get; set; }
public long UserId { get; set; }
/// <summary>
/// 签到日期(本地)。

View File

@@ -10,12 +10,12 @@ public sealed class CommunityComment : MultiTenantEntityBase
/// <summary>
/// 动态标识。
/// </summary>
public Guid PostId { get; set; }
public long PostId { get; set; }
/// <summary>
/// 评论人。
/// </summary>
public Guid AuthorUserId { get; set; }
public long AuthorUserId { get; set; }
/// <summary>
/// 评论内容。
@@ -25,7 +25,7 @@ public sealed class CommunityComment : MultiTenantEntityBase
/// <summary>
/// 父级评论 ID。
/// </summary>
public Guid? ParentId { get; set; }
public long? ParentId { get; set; }
/// <summary>
/// 状态。

View File

@@ -11,7 +11,7 @@ public sealed class CommunityPost : MultiTenantEntityBase
/// <summary>
/// 作者用户 ID。
/// </summary>
public Guid AuthorUserId { get; set; }
public long AuthorUserId { get; set; }
/// <summary>
/// 标题。

View File

@@ -11,12 +11,12 @@ public sealed class CommunityReaction : MultiTenantEntityBase
/// <summary>
/// 动态 ID。
/// </summary>
public Guid PostId { get; set; }
public long PostId { get; set; }
/// <summary>
/// 用户 ID。
/// </summary>
public Guid UserId { get; set; }
public long UserId { get; set; }
/// <summary>
/// 反应类型。

View File

@@ -11,12 +11,12 @@ public sealed class GroupOrder : MultiTenantEntityBase
/// <summary>
/// 门店标识。
/// </summary>
public Guid StoreId { get; set; }
public long StoreId { get; set; }
/// <summary>
/// 关联商品或套餐。
/// </summary>
public Guid ProductId { get; set; }
public long ProductId { get; set; }
/// <summary>
/// 拼单编号。
@@ -26,7 +26,7 @@ public sealed class GroupOrder : MultiTenantEntityBase
/// <summary>
/// 团长用户 ID。
/// </summary>
public Guid LeaderUserId { get; set; }
public long LeaderUserId { get; set; }
/// <summary>
/// 成团需要的人数。

View File

@@ -11,17 +11,17 @@ public sealed class GroupParticipant : MultiTenantEntityBase
/// <summary>
/// 拼单活动标识。
/// </summary>
public Guid GroupOrderId { get; set; }
public long GroupOrderId { get; set; }
/// <summary>
/// 对应订单标识。
/// </summary>
public Guid OrderId { get; set; }
public long OrderId { get; set; }
/// <summary>
/// 用户标识。
/// </summary>
public Guid UserId { get; set; }
public long UserId { get; set; }
/// <summary>
/// 参与状态。

View File

@@ -25,7 +25,7 @@ public sealed class IdentityUser : MultiTenantEntityBase
/// <summary>
/// 所属商户(平台管理员为空)。
/// </summary>
public Guid? MerchantId { get; set; }
public long? MerchantId { get; set; }
/// <summary>
/// 角色集合。

View File

@@ -18,5 +18,5 @@ public interface IIdentityUserRepository
/// <summary>
/// 根据 ID 获取后台用户。
/// </summary>
Task<IdentityUser?> FindByIdAsync(Guid userId, CancellationToken cancellationToken = default);
Task<IdentityUser?> FindByIdAsync(long userId, CancellationToken cancellationToken = default);
}

View File

@@ -21,7 +21,7 @@ public interface IMiniUserRepository
/// <param name="id">用户 ID</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>小程序用户,如果不存在则返回 null</returns>
Task<MiniUser?> FindByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<MiniUser?> FindByIdAsync(long id, CancellationToken cancellationToken = default);
/// <summary>
/// 创建或更新小程序用户(如果 OpenId 已存在则更新,否则创建)。
@@ -33,5 +33,5 @@ public interface IMiniUserRepository
/// <param name="tenantId">租户 ID</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>创建或更新后的小程序用户</returns>
Task<MiniUser> CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken = default);
Task<MiniUser> CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken = default);
}

View File

@@ -11,7 +11,7 @@ public sealed class InventoryAdjustment : MultiTenantEntityBase
/// <summary>
/// 对应的库存记录标识。
/// </summary>
public Guid InventoryItemId { get; set; }
public long InventoryItemId { get; set; }
/// <summary>
/// 调整类型。
@@ -31,7 +31,7 @@ public sealed class InventoryAdjustment : MultiTenantEntityBase
/// <summary>
/// 操作人标识。
/// </summary>
public Guid? OperatorId { get; set; }
public long? OperatorId { get; set; }
/// <summary>
/// 发生时间。

View File

@@ -10,12 +10,12 @@ public sealed class InventoryBatch : MultiTenantEntityBase
/// <summary>
/// 门店标识。
/// </summary>
public Guid StoreId { get; set; }
public long StoreId { get; set; }
/// <summary>
/// SKU 标识。
/// </summary>
public Guid ProductSkuId { get; set; }
public long ProductSkuId { get; set; }
/// <summary>
/// 批次编号。

View File

@@ -10,12 +10,12 @@ public sealed class InventoryItem : MultiTenantEntityBase
/// <summary>
/// 门店标识。
/// </summary>
public Guid StoreId { get; set; }
public long StoreId { get; set; }
/// <summary>
/// SKU 标识。
/// </summary>
public Guid ProductSkuId { get; set; }
public long ProductSkuId { get; set; }
/// <summary>
/// 批次编号,可为空表示混批。

View File

@@ -10,7 +10,7 @@ public sealed class MemberGrowthLog : MultiTenantEntityBase
/// <summary>
/// 会员标识。
/// </summary>
public Guid MemberId { get; set; }
public long MemberId { get; set; }
/// <summary>
/// 变动数量。

View File

@@ -11,7 +11,7 @@ public sealed class MemberPointLedger : MultiTenantEntityBase
/// <summary>
/// 会员标识。
/// </summary>
public Guid MemberId { get; set; }
public long MemberId { get; set; }
/// <summary>
/// 变动数量,可为负值。
@@ -31,7 +31,7 @@ public sealed class MemberPointLedger : MultiTenantEntityBase
/// <summary>
/// 来源 ID订单、活动等
/// </summary>
public Guid? SourceId { get; set; }
public long? SourceId { get; set; }
/// <summary>
/// 发生时间。

View File

@@ -11,7 +11,7 @@ public sealed class MemberProfile : MultiTenantEntityBase
/// <summary>
/// 用户标识。
/// </summary>
public Guid UserId { get; set; }
public long UserId { get; set; }
/// <summary>
/// 手机号。
@@ -31,7 +31,7 @@ public sealed class MemberProfile : MultiTenantEntityBase
/// <summary>
/// 当前会员等级 ID。
/// </summary>
public Guid? MemberTierId { get; set; }
public long? MemberTierId { get; set; }
/// <summary>
/// 会员状态。

View File

@@ -11,7 +11,7 @@ public sealed class MerchantContract : MultiTenantEntityBase
/// <summary>
/// 所属商户标识。
/// </summary>
public Guid MerchantId { get; set; }
public long MerchantId { get; set; }
/// <summary>
/// 合同编号。

View File

@@ -11,7 +11,7 @@ public sealed class MerchantDocument : MultiTenantEntityBase
/// <summary>
/// 所属商户标识。
/// </summary>
public Guid MerchantId { get; set; }
public long MerchantId { get; set; }
/// <summary>
/// 证照类型。

View File

@@ -11,12 +11,12 @@ public sealed class MerchantStaff : MultiTenantEntityBase
/// <summary>
/// 所属商户标识。
/// </summary>
public Guid MerchantId { get; set; }
public long MerchantId { get; set; }
/// <summary>
/// 可选的关联门店 ID。
/// </summary>
public Guid? StoreId { get; set; }
public long? StoreId { get; set; }
/// <summary>
/// 员工姓名。
@@ -36,7 +36,7 @@ public sealed class MerchantStaff : MultiTenantEntityBase
/// <summary>
/// 登录账号 ID指向统一身份体系
/// </summary>
public Guid? IdentityUserId { get; set; }
public long? IdentityUserId { get; set; }
/// <summary>
/// 员工角色类型。

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TakeoutSaaS.Domain.Merchants.Entities;
using TakeoutSaaS.Domain.Merchants.Enums;
namespace TakeoutSaaS.Domain.Merchants.Repositories;
/// <summary>
/// 商户聚合仓储契约,提供基础 CRUD 与查询能力。
/// </summary>
public interface IMerchantRepository
{
/// <summary>
/// 依据标识获取商户。
/// </summary>
Task<Merchant?> FindByIdAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 按状态筛选商户列表。
/// </summary>
Task<IReadOnlyList<Merchant>> SearchAsync(long tenantId, MerchantStatus? status, CancellationToken cancellationToken = default);
/// <summary>
/// 获取指定商户的员工列表。
/// </summary>
Task<IReadOnlyList<MerchantStaff>> GetStaffAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取指定商户的合同列表。
/// </summary>
Task<IReadOnlyList<MerchantContract>> GetContractsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取指定商户的资质文件列表。
/// </summary>
Task<IReadOnlyList<MerchantDocument>> GetDocumentsAsync(long merchantId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 新增商户主体。
/// </summary>
Task AddMerchantAsync(Merchant merchant, CancellationToken cancellationToken = default);
/// <summary>
/// 新增商户员工。
/// </summary>
Task AddStaffAsync(MerchantStaff staff, CancellationToken cancellationToken = default);
/// <summary>
/// 新增商户合同。
/// </summary>
Task AddContractAsync(MerchantContract contract, CancellationToken cancellationToken = default);
/// <summary>
/// 新增商户资质文件。
/// </summary>
Task AddDocumentAsync(MerchantDocument document, CancellationToken cancellationToken = default);
/// <summary>
/// 持久化变更。
/// </summary>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -10,7 +10,7 @@ public sealed class MapLocation : MultiTenantEntityBase
/// <summary>
/// 关联门店 ID可空表示独立 POI。
/// </summary>
public Guid? StoreId { get; set; }
public long? StoreId { get; set; }
/// <summary>
/// 名称。

View File

@@ -11,12 +11,12 @@ public sealed class NavigationRequest : MultiTenantEntityBase
/// <summary>
/// 用户 ID。
/// </summary>
public Guid UserId { get; set; }
public long UserId { get; set; }
/// <summary>
/// 门店 ID。
/// </summary>
public Guid StoreId { get; set; }
public long StoreId { get; set; }
/// <summary>
/// 来源通道小程序、H5 等)。

View File

@@ -11,17 +11,17 @@ public sealed class CartItem : MultiTenantEntityBase
/// <summary>
/// 所属购物车标识。
/// </summary>
public Guid ShoppingCartId { get; set; }
public long ShoppingCartId { get; set; }
/// <summary>
/// 商品或 SKU 标识。
/// </summary>
public Guid ProductId { get; set; }
public long ProductId { get; set; }
/// <summary>
/// SKU 标识。
/// </summary>
public Guid? ProductSkuId { get; set; }
public long? ProductSkuId { get; set; }
/// <summary>
/// 商品名称快照。

View File

@@ -10,7 +10,7 @@ public sealed class CartItemAddon : MultiTenantEntityBase
/// <summary>
/// 所属购物车条目。
/// </summary>
public Guid CartItemId { get; set; }
public long CartItemId { get; set; }
/// <summary>
/// 选项名称。
@@ -25,5 +25,5 @@ public sealed class CartItemAddon : MultiTenantEntityBase
/// <summary>
/// 选项 ID可对应 ProductAddonOption
/// </summary>
public Guid? OptionId { get; set; }
public long? OptionId { get; set; }
}

View File

@@ -11,12 +11,12 @@ public sealed class CheckoutSession : MultiTenantEntityBase
/// <summary>
/// 用户标识。
/// </summary>
public Guid UserId { get; set; }
public long UserId { get; set; }
/// <summary>
/// 门店标识。
/// </summary>
public Guid StoreId { get; set; }
public long StoreId { get; set; }
/// <summary>
/// 会话 Token。

View File

@@ -11,12 +11,12 @@ public sealed class ShoppingCart : MultiTenantEntityBase
/// <summary>
/// 用户标识。
/// </summary>
public Guid UserId { get; set; }
public long UserId { get; set; }
/// <summary>
/// 门店标识。
/// </summary>
public Guid StoreId { get; set; }
public long StoreId { get; set; }
/// <summary>
/// 购物车状态,包含正常/锁定。

View File

@@ -17,7 +17,7 @@ public sealed class Order : MultiTenantEntityBase
/// <summary>
/// 门店。
/// </summary>
public Guid StoreId { get; set; }
public long StoreId { get; set; }
/// <summary>
/// 下单渠道。
@@ -62,7 +62,7 @@ public sealed class Order : MultiTenantEntityBase
/// <summary>
/// 预约 ID。
/// </summary>
public Guid? ReservationId { get; set; }
public long? ReservationId { get; set; }
/// <summary>
/// 商品总额。

View File

@@ -10,12 +10,12 @@ public sealed class OrderItem : MultiTenantEntityBase
/// <summary>
/// 订单 ID。
/// </summary>
public Guid OrderId { get; set; }
public long OrderId { get; set; }
/// <summary>
/// 商品 ID。
/// </summary>
public Guid ProductId { get; set; }
public long ProductId { get; set; }
/// <summary>
/// 商品名称。

View File

@@ -11,7 +11,7 @@ public sealed class OrderStatusHistory : MultiTenantEntityBase
/// <summary>
/// 订单标识。
/// </summary>
public Guid OrderId { get; set; }
public long OrderId { get; set; }
/// <summary>
/// 变更后的状态。
@@ -21,7 +21,7 @@ public sealed class OrderStatusHistory : MultiTenantEntityBase
/// <summary>
/// 操作人标识(可为空表示系统)。
/// </summary>
public Guid? OperatorId { get; set; }
public long? OperatorId { get; set; }
/// <summary>
/// 备注信息。

View File

@@ -11,7 +11,7 @@ public sealed class RefundRequest : MultiTenantEntityBase
/// <summary>
/// 关联订单标识。
/// </summary>
public Guid OrderId { get; set; }
public long OrderId { get; set; }
/// <summary>
/// 退款单号。

View File

@@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TakeoutSaaS.Domain.Orders.Entities;
using TakeoutSaaS.Domain.Orders.Enums;
using TakeoutSaaS.Domain.Payments.Enums;
namespace TakeoutSaaS.Domain.Orders.Repositories;
/// <summary>
/// 订单聚合仓储契约。
/// </summary>
public interface IOrderRepository
{
/// <summary>
/// 依据标识获取订单。
/// </summary>
Task<Order?> FindByIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 依据订单号获取订单。
/// </summary>
Task<Order?> FindByOrderNoAsync(string orderNo, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 按状态筛选订单列表。
/// </summary>
Task<IReadOnlyList<Order>> SearchAsync(long tenantId, OrderStatus? status, PaymentStatus? paymentStatus, CancellationToken cancellationToken = default);
/// <summary>
/// 获取订单明细行。
/// </summary>
Task<IReadOnlyList<OrderItem>> GetItemsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取订单状态流转记录。
/// </summary>
Task<IReadOnlyList<OrderStatusHistory>> GetStatusHistoryAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取订单退款申请。
/// </summary>
Task<IReadOnlyList<RefundRequest>> GetRefundsAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 新增订单。
/// </summary>
Task AddOrderAsync(Order order, CancellationToken cancellationToken = default);
/// <summary>
/// 新增订单明细。
/// </summary>
Task AddItemsAsync(IEnumerable<OrderItem> items, CancellationToken cancellationToken = default);
/// <summary>
/// 新增订单状态记录。
/// </summary>
Task AddStatusHistoryAsync(OrderStatusHistory history, CancellationToken cancellationToken = default);
/// <summary>
/// 新增退款申请。
/// </summary>
Task AddRefundAsync(RefundRequest refund, CancellationToken cancellationToken = default);
/// <summary>
/// 持久化变更。
/// </summary>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -11,7 +11,7 @@ public sealed class PaymentRecord : MultiTenantEntityBase
/// <summary>
/// 关联订单。
/// </summary>
public Guid OrderId { get; set; }
public long OrderId { get; set; }
/// <summary>
/// 支付方式。

View File

@@ -11,12 +11,12 @@ public sealed class PaymentRefundRecord : MultiTenantEntityBase
/// <summary>
/// 原支付记录标识。
/// </summary>
public Guid PaymentRecordId { get; set; }
public long PaymentRecordId { get; set; }
/// <summary>
/// 关联订单标识。
/// </summary>
public Guid OrderId { get; set; }
public long OrderId { get; set; }
/// <summary>
/// 退款金额。

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TakeoutSaaS.Domain.Payments.Entities;
namespace TakeoutSaaS.Domain.Payments.Repositories;
/// <summary>
/// 支付记录仓储契约。
/// </summary>
public interface IPaymentRepository
{
/// <summary>
/// 依据标识获取支付记录。
/// </summary>
Task<PaymentRecord?> FindByIdAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 依据订单标识获取支付记录。
/// </summary>
Task<PaymentRecord?> FindByOrderIdAsync(long orderId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取支付对应的退款记录。
/// </summary>
Task<IReadOnlyList<PaymentRefundRecord>> GetRefundsAsync(long paymentId, long tenantId, CancellationToken cancellationToken = default);
/// <summary>
/// 新增支付记录。
/// </summary>
Task AddPaymentAsync(PaymentRecord payment, CancellationToken cancellationToken = default);
/// <summary>
/// 新增退款记录。
/// </summary>
Task AddRefundAsync(PaymentRefundRecord refund, CancellationToken cancellationToken = default);
/// <summary>
/// 持久化变更。
/// </summary>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -11,12 +11,12 @@ public sealed class Product : MultiTenantEntityBase
/// <summary>
/// 所属门店。
/// </summary>
public Guid StoreId { get; set; }
public long StoreId { get; set; }
/// <summary>
/// 所属分类。
/// </summary>
public Guid CategoryId { get; set; }
public long CategoryId { get; set; }
/// <summary>
/// 商品编码。

View File

@@ -11,7 +11,7 @@ public sealed class ProductAddonGroup : MultiTenantEntityBase
/// <summary>
/// 所属商品。
/// </summary>
public Guid ProductId { get; set; }
public long ProductId { get; set; }
/// <summary>
/// 分组名称。

View File

@@ -10,7 +10,7 @@ public sealed class ProductAddonOption : MultiTenantEntityBase
/// <summary>
/// 所属加料分组。
/// </summary>
public Guid AddonGroupId { get; set; }
public long AddonGroupId { get; set; }
/// <summary>
/// 选项名称。

Some files were not shown because too many files have changed in this diff Show More