Compare commits

..

9 Commits

Author SHA1 Message Date
0f44197164 fix(store-fees): 保留包装费配置避免阶梯刷新丢失
All checks were successful
Build and Deploy AdminApi / build-and-deploy (push) Successful in 44s
2026-02-20 08:29:08 +08:00
2bf33e753f fix: 修复手动入驻未绑定管理员商户问题
All checks were successful
Build and Deploy AdminApi / build-and-deploy (push) Successful in 39s
2026-02-06 14:38:00 +08:00
msumshk
c02a15f1b9 chore: 部署环境改为 Development
All checks were successful
Build and Deploy AdminApi / build-and-deploy (push) Successful in 39s
2026-02-05 21:24:16 +08:00
msumshk
2eb309ccec feat: 使用 PostgreSQL xmin 系统列作为并发控制
All checks were successful
Build and Deploy AdminApi / build-and-deploy (push) Successful in 38s
2026-02-05 21:21:18 +08:00
msumshk
e7e420be32 feat: 添加 Identity 模型同步迁移(xmin并发控制)
All checks were successful
Build and Deploy AdminApi / build-and-deploy (push) Successful in 39s
2026-02-05 21:12:44 +08:00
msumshk
f40e74bb9c ci: 触发部署
All checks were successful
Build and Deploy AdminApi / build-and-deploy (push) Successful in 3m22s
2026-02-05 21:02:35 +08:00
msumshk
df75597985 ci: 重新触发部署
Some checks failed
Build and Deploy AdminApi / build-and-deploy (push) Failing after 1s
2026-02-05 21:00:43 +08:00
msumshk
9d0a525893 fix: 修复子模块克隆顺序问题
Some checks failed
Build and Deploy AdminApi / build-and-deploy (push) Has been cancelled
2026-02-05 20:59:15 +08:00
msumshk
1ea6af0ec2 ci: 触发部署
Some checks failed
Build and Deploy AdminApi / build-and-deploy (push) Has been cancelled
2026-02-05 20:45:53 +08:00
7 changed files with 1055 additions and 25 deletions

View File

@@ -20,7 +20,7 @@ jobs:
git reset --hard origin/dev
git submodule update --init --recursive
else
git clone --branch dev --recurse-submodules ssh://git@git.laosankeji.com:2222/msumshk/TakeoutSaaS.AdminApi.git .
git clone --branch dev ssh://git@git.laosankeji.com:2222/msumshk/TakeoutSaaS.AdminApi.git .
git submodule init
git config submodule.TakeoutSaaS.BuildingBlocks.url ssh://git@git.laosankeji.com:2222/msumshk/TakeoutSaaS.BuildingBlocks.git
git config submodule.TakeoutSaaS.Docs.url ssh://git@git.laosankeji.com:2222/msumshk/TakeoutSaaS.Docs.git
@@ -46,7 +46,7 @@ jobs:
--name adminapi \
--restart unless-stopped \
-p 7801:7801 \
-e ASPNETCORE_ENVIRONMENT=Production \
-e ASPNETCORE_ENVIRONMENT=Development \
takeoutsaas-adminapi:latest
- name: Clean up old images

View File

@@ -54,19 +54,10 @@ public sealed class UpdateStoreFeeCommandHandler(
fee.OrderPackagingFeeMode = request.PackagingFeeMode == PackagingFeeMode.Fixed
? request.OrderPackagingFeeMode
: OrderPackagingFeeMode.Fixed;
if (request.PackagingFeeMode == PackagingFeeMode.Fixed && request.OrderPackagingFeeMode == OrderPackagingFeeMode.Tiered)
{
fee.FixedPackagingFee = request.FixedPackagingFee ?? 0m;
// 非生效模式下也保留配置,避免模式切换后历史阶梯被清空。
var normalizedTiers = StoreFeeTierHelper.Normalize(request.PackagingFeeTiers);
fee.FixedPackagingFee = 0m;
fee.PackagingFeeTiersJson = StoreFeeTierHelper.Serialize(normalizedTiers);
}
else
{
fee.FixedPackagingFee = request.PackagingFeeMode == PackagingFeeMode.Fixed
? request.FixedPackagingFee ?? 0m
: 0m;
fee.PackagingFeeTiersJson = null;
}
fee.FreeDeliveryThreshold = request.FreeDeliveryThreshold;
// 4. (空行后) 保存并返回

View File

@@ -79,7 +79,7 @@ public sealed class IdentityUser : AuditableEntityBase
public string? Avatar { get; set; }
/// <summary>
/// 并发控制字段(映射到 PostgreSQL xmin
/// 并发控制字段(映射到 PostgreSQL xmin 系统列)。
/// </summary>
public uint RowVersion { get; set; }
}

View File

@@ -1,6 +1,8 @@
using MassTransit;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Domain.Common.Enums;
using TakeoutSaaS.Domain.Identity.Enums;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Domain.Merchants.Entities;
using TakeoutSaaS.Domain.Merchants.Enums;
using TakeoutSaaS.Domain.Merchants.Repositories;
@@ -18,6 +20,7 @@ namespace TakeoutSaaS.Infrastructure.App.Consumers;
/// </remarks>
public sealed class TenantCreatedEventConsumer(
IMerchantRepository merchantRepository,
IIdentityUserRepository identityUserRepository,
IIdGenerator idGenerator,
ILogger<TenantCreatedEventConsumer> logger) : IConsumer<TenantCreatedEvent>
{
@@ -32,10 +35,13 @@ public sealed class TenantCreatedEventConsumer(
if (existingMerchant is not null)
{
logger.LogInformation("租户 {TenantId} 的商户已存在(商户 ID{MerchantId}),跳过创建", tenantId, existingMerchant.Id);
// 2. 已存在商户时,仍然确保管理员账号已绑定商户
await BindAdminUserMerchantAsync(message.AdminUserId, tenantId, existingMerchant.Id, context.CancellationToken);
return;
}
// 2. 创建商户实体
// 3. 创建商户实体
var merchantId = idGenerator.NextId();
var merchantStatus = message.IsSkipApproval ? MerchantStatus.Approved : MerchantStatus.Pending;
var documentStatus = message.IsSkipApproval ? MerchantDocumentStatus.Approved : MerchantDocumentStatus.Pending;
@@ -65,7 +71,7 @@ public sealed class TenantCreatedEventConsumer(
await merchantRepository.AddMerchantAsync(merchant, context.CancellationToken);
logger.LogInformation("租户 {TenantId} 的商户 {MerchantId} 创建成功", tenantId, merchantId);
// 3. 创建证照记录(营业执照)
// 4. 创建证照记录(营业执照)
if (!string.IsNullOrWhiteSpace(message.BusinessLicenseUrl))
{
var businessLicenseDoc = new MerchantDocument
@@ -83,7 +89,7 @@ public sealed class TenantCreatedEventConsumer(
logger.LogInformation("商户 {MerchantId} 营业执照证照记录创建成功", merchantId);
}
// 4. 创建证照记录(法人身份证正面)
// 5. 创建证照记录(法人身份证正面)
if (!string.IsNullOrWhiteSpace(message.LegalPersonIdFrontUrl))
{
var idFrontDoc = new MerchantDocument
@@ -102,7 +108,7 @@ public sealed class TenantCreatedEventConsumer(
logger.LogInformation("商户 {MerchantId} 法人身份证正面证照记录创建成功", merchantId);
}
// 5. 创建证照记录(法人身份证背面)
// 6. 创建证照记录(法人身份证背面)
if (!string.IsNullOrWhiteSpace(message.LegalPersonIdBackUrl))
{
var idBackDoc = new MerchantDocument
@@ -119,8 +125,53 @@ public sealed class TenantCreatedEventConsumer(
logger.LogInformation("商户 {MerchantId} 法人身份证背面证照记录创建成功", merchantId);
}
// 6. 持久化所有变更
// 7. 持久化所有变更
await merchantRepository.SaveChangesAsync(context.CancellationToken);
logger.LogInformation("租户 {TenantId} 的商户 {MerchantId} 及证照记录全部创建完成", tenantId, merchantId);
// 8. 创建商户成功后,绑定管理员账号与商户关系
await BindAdminUserMerchantAsync(message.AdminUserId, tenantId, merchantId, context.CancellationToken);
}
private async Task BindAdminUserMerchantAsync(long adminUserId, long tenantId, long merchantId, CancellationToken cancellationToken)
{
// 1. 忽略无效管理员用户标识
if (adminUserId <= 0)
{
logger.LogWarning("租户 {TenantId} 的管理员用户 ID 无效,跳过商户绑定", tenantId);
return;
}
// 2. 查询管理员用户(可跟踪)
var adminUser = await identityUserRepository.GetForUpdateAsync(adminUserId, cancellationToken);
if (adminUser is null)
{
logger.LogWarning("租户 {TenantId} 的管理员用户 {AdminUserId} 不存在,跳过商户绑定", tenantId, adminUserId);
return;
}
// 3. 校验用户归属
if (adminUser.Portal != PortalType.Tenant || adminUser.TenantId != tenantId)
{
logger.LogWarning(
"管理员用户 {AdminUserId} 归属不匹配,预期租户 {TenantId},实际 Portal={Portal} TenantId={UserTenantId},跳过商户绑定",
adminUserId,
tenantId,
adminUser.Portal,
adminUser.TenantId);
return;
}
// 4. 已绑定相同商户则直接返回
if (adminUser.MerchantId == merchantId)
{
logger.LogInformation("管理员用户 {AdminUserId} 已绑定商户 {MerchantId},无需重复绑定", adminUserId, merchantId);
return;
}
// 5. 更新并保存商户绑定
adminUser.MerchantId = merchantId;
await identityUserRepository.SaveChangesAsync(cancellationToken);
logger.LogInformation("管理员用户 {AdminUserId} 已绑定商户 {MerchantId}", adminUserId, merchantId);
}
}

View File

@@ -0,0 +1,956 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using TakeoutSaaS.Infrastructure.Identity.Persistence;
#nullable disable
namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb
{
[DbContext(typeof(IdentityDbContext))]
[Migration("20260205131953_UseXminConcurrency")]
partial class UseXminConcurrency
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime?>("Consumed")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("ConsumerId")
.HasColumnType("uuid");
b.Property<DateTime?>("Delivered")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone");
b.Property<long?>("LastSequenceNumber")
.HasColumnType("bigint");
b.Property<Guid>("LockId")
.HasColumnType("uuid");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<int>("ReceiveCount")
.HasColumnType("integer");
b.Property<DateTime>("Received")
.HasColumnType("timestamp with time zone");
b.Property<byte[]>("RowVersion")
.HasColumnType("bytea");
b.HasKey("Id");
b.ToTable("InboxState");
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
{
b.Property<long>("SequenceNumber")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("SequenceNumber"));
b.Property<string>("Body")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<Guid?>("ConversationId")
.HasColumnType("uuid");
b.Property<Guid?>("CorrelationId")
.HasColumnType("uuid");
b.Property<string>("DestinationAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("EnqueueTime")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("ExpirationTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("FaultAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Headers")
.HasColumnType("text");
b.Property<Guid?>("InboxConsumerId")
.HasColumnType("uuid");
b.Property<Guid?>("InboxMessageId")
.HasColumnType("uuid");
b.Property<Guid?>("InitiatorId")
.HasColumnType("uuid");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<string>("MessageType")
.IsRequired()
.HasColumnType("text");
b.Property<Guid?>("OutboxId")
.HasColumnType("uuid");
b.Property<string>("Properties")
.HasColumnType("text");
b.Property<Guid?>("RequestId")
.HasColumnType("uuid");
b.Property<string>("ResponseAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime>("SentTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("SourceAddress")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("SequenceNumber");
b.HasIndex("EnqueueTime");
b.HasIndex("ExpirationTime");
b.HasIndex("OutboxId", "SequenceNumber")
.IsUnique();
b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber")
.IsUnique();
b.ToTable("OutboxMessage");
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b =>
{
b.Property<Guid>("OutboxId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("Created")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("Delivered")
.HasColumnType("timestamp with time zone");
b.Property<long?>("LastSequenceNumber")
.HasColumnType("bigint");
b.Property<Guid>("LockId")
.HasColumnType("uuid");
b.Property<byte[]>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea");
b.HasKey("OutboxId");
b.HasIndex("Created");
b.ToTable("OutboxState");
});
modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.IdentityUser", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasComment("实体唯一标识。");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Account")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("登录账号。");
b.Property<string>("Avatar")
.HasColumnType("text")
.HasComment("头像地址。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<long?>("DeletedBy")
.HasColumnType("bigint")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("展示名称。");
b.Property<string>("Email")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasComment("邮箱(租户内唯一)。");
b.Property<int>("FailedLoginCount")
.HasColumnType("integer")
.HasComment("登录失败次数。");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近登录时间UTC。");
b.Property<DateTime?>("LockedUntil")
.HasColumnType("timestamp with time zone")
.HasComment("锁定截止时间UTC。");
b.Property<long?>("MerchantId")
.HasColumnType("bigint")
.HasComment("所属商户(平台管理员为空)。");
b.Property<bool>("MustChangePassword")
.HasColumnType("boolean")
.HasComment("是否强制修改密码。");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("密码哈希。");
b.Property<string>("Phone")
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasComment("手机号(租户内唯一)。");
b.Property<int>("Portal")
.HasColumnType("integer")
.HasComment("账号所属 Portal。");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin")
.HasComment("并发控制字段(映射到 PostgreSQL xmin 系统列)。");
b.Property<int>("Status")
.HasColumnType("integer")
.HasComment("账号状态。");
b.Property<long?>("TenantId")
.HasColumnType("bigint")
.HasComment("所属租户 IDPortal=Tenant 时必填Portal=Admin 时必须为空)。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
b.HasIndex("Account")
.IsUnique()
.HasFilter("\"Portal\" = 0");
b.HasIndex("Email")
.IsUnique()
.HasFilter("\"Portal\" = 0 AND \"Email\" IS NOT NULL");
b.HasIndex("Phone")
.IsUnique()
.HasFilter("\"Portal\" = 0 AND \"Phone\" IS NOT NULL");
b.HasIndex("TenantId")
.HasFilter("\"Portal\" = 1");
b.HasIndex("TenantId", "Account")
.IsUnique()
.HasFilter("\"Portal\" = 1");
b.HasIndex("TenantId", "Email")
.IsUnique()
.HasFilter("\"Portal\" = 1 AND \"Email\" IS NOT NULL");
b.HasIndex("TenantId", "Phone")
.IsUnique()
.HasFilter("\"Portal\" = 1 AND \"Phone\" IS NOT NULL");
b.ToTable("identity_users", null, t =>
{
t.HasComment("后台账户实体(按 Portal 区分平台管理员与租户后台账号)。");
t.HasCheckConstraint("CK_identity_users_Portal_Tenant", "(\"Portal\" = 0 AND \"TenantId\" IS NULL) OR (\"Portal\" = 1 AND \"TenantId\" IS NOT NULL)");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MenuDefinition", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasComment("实体唯一标识。");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("AuthListJson")
.HasColumnType("text")
.HasComment("按钮权限列表 JSON存储 MenuAuthItemDto 数组)。");
b.Property<string>("Component")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("组件路径(不含 .vue。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<long?>("DeletedBy")
.HasColumnType("bigint")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("Icon")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("图标标识。");
b.Property<bool>("IsIframe")
.HasColumnType("boolean")
.HasComment("是否 iframe。");
b.Property<bool>("KeepAlive")
.HasColumnType("boolean")
.HasComment("是否缓存。");
b.Property<string>("Link")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasComment("外链或 iframe 地址。");
b.Property<string>("MetaPermissions")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasComment("Meta.permissions逗号分隔。");
b.Property<string>("MetaRoles")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasComment("Meta.roles逗号分隔。");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("菜单名称(前端路由 name。");
b.Property<long>("ParentId")
.HasColumnType("bigint")
.HasComment("父级菜单 ID根节点为 0。");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("路由路径。");
b.Property<int>("Portal")
.HasColumnType("integer")
.HasComment("菜单所属 Portal。");
b.Property<string>("RequiredPermissions")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("character varying(1024)")
.HasComment("访问该菜单所需的权限集合(逗号分隔)。");
b.Property<int>("SortOrder")
.HasColumnType("integer")
.HasComment("排序。");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasComment("标题。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
b.HasIndex("Portal", "ParentId", "SortOrder");
b.ToTable("menu_definitions", null, t =>
{
t.HasComment("后台菜单定义(按 Portal 区分 Admin/Tenant 两套菜单树)。");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MiniUser", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasComment("实体唯一标识。");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Avatar")
.HasColumnType("text")
.HasComment("头像地址。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<long?>("DeletedBy")
.HasColumnType("bigint")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("Nickname")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("昵称。");
b.Property<string>("OpenId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasComment("微信 OpenId。");
b.Property<long>("TenantId")
.HasColumnType("bigint")
.HasComment("所属租户 ID。");
b.Property<string>("UnionId")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasComment("微信 UnionId可能为空。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
b.HasIndex("TenantId");
b.HasIndex("TenantId", "OpenId")
.IsUnique();
b.ToTable("mini_users", null, t =>
{
t.HasComment("小程序用户实体。");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.Permission", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasComment("实体唯一标识。");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasComment("权限编码(全局唯一)。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<long?>("DeletedBy")
.HasColumnType("bigint")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("Description")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("描述。");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("权限名称。");
b.Property<long>("ParentId")
.HasColumnType("bigint")
.HasComment("父级权限 ID根节点为 0。");
b.Property<int>("Portal")
.HasColumnType("integer")
.HasComment("权限所属 Portal。");
b.Property<int>("SortOrder")
.HasColumnType("integer")
.HasComment("排序值,值越小越靠前。");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(16)
.HasColumnType("character varying(16)")
.HasComment("权限类型group/leaf。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.HasIndex("Portal", "ParentId", "SortOrder");
b.ToTable("permissions", null, t =>
{
t.HasComment("权限定义。");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.Role", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasComment("实体唯一标识。");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("角色编码(租户内唯一)。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<long?>("DeletedBy")
.HasColumnType("bigint")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("Description")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("描述。");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("角色名称。");
b.Property<int>("Portal")
.HasColumnType("integer")
.HasComment("角色所属 Portal。");
b.Property<long?>("TenantId")
.HasColumnType("bigint")
.HasComment("所属租户 IDPortal=Tenant 时必填Portal=Admin 时必须为空)。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique()
.HasFilter("\"Portal\" = 0");
b.HasIndex("TenantId")
.HasFilter("\"Portal\" = 1");
b.HasIndex("TenantId", "Code")
.IsUnique()
.HasFilter("\"Portal\" = 1");
b.ToTable("roles", null, t =>
{
t.HasComment("角色定义。");
t.HasCheckConstraint("CK_roles_Portal_Tenant", "(\"Portal\" = 0 AND \"TenantId\" IS NULL) OR (\"Portal\" = 1 AND \"TenantId\" IS NOT NULL)");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.RolePermission", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasComment("实体唯一标识。");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<long?>("DeletedBy")
.HasColumnType("bigint")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<long>("PermissionId")
.HasColumnType("bigint")
.HasComment("权限 ID。");
b.Property<int>("Portal")
.HasColumnType("integer")
.HasComment("关系所属 Portal。");
b.Property<long>("RoleId")
.HasColumnType("bigint")
.HasComment("角色 ID。");
b.Property<long?>("TenantId")
.HasColumnType("bigint")
.HasComment("所属租户 IDPortal=Tenant 时必填Portal=Admin 时必须为空)。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
b.HasIndex("TenantId")
.HasFilter("\"Portal\" = 1");
b.HasIndex("RoleId", "PermissionId")
.IsUnique()
.HasFilter("\"Portal\" = 0");
b.HasIndex("TenantId", "RoleId", "PermissionId")
.IsUnique()
.HasFilter("\"Portal\" = 1");
b.ToTable("role_permissions", null, t =>
{
t.HasComment("角色-权限关系。");
t.HasCheckConstraint("CK_role_permissions_Portal_Tenant", "(\"Portal\" = 0 AND \"TenantId\" IS NULL) OR (\"Portal\" = 1 AND \"TenantId\" IS NOT NULL)");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.RoleTemplate", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasComment("实体唯一标识。");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<long?>("DeletedBy")
.HasColumnType("bigint")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("Description")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("模板描述。");
b.Property<bool>("IsActive")
.HasColumnType("boolean")
.HasComment("是否启用。");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasComment("模板名称。");
b.Property<string>("TemplateCode")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("模板编码(唯一)。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
b.HasIndex("TemplateCode")
.IsUnique();
b.ToTable("role_templates", null, t =>
{
t.HasComment("角色模板定义(平台级)。");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.RoleTemplatePermission", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasComment("实体唯一标识。");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<long?>("DeletedBy")
.HasColumnType("bigint")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("PermissionCode")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasComment("权限编码。");
b.Property<long>("RoleTemplateId")
.HasColumnType("bigint")
.HasComment("模板 ID。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
b.HasIndex("RoleTemplateId", "PermissionCode")
.IsUnique();
b.ToTable("role_template_permissions", null, t =>
{
t.HasComment("角色模板-权限关系(平台级)。");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.UserRole", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasComment("实体唯一标识。");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<long?>("DeletedBy")
.HasColumnType("bigint")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<int>("Portal")
.HasColumnType("integer")
.HasComment("关系所属 Portal。");
b.Property<long>("RoleId")
.HasColumnType("bigint")
.HasComment("角色 ID。");
b.Property<long?>("TenantId")
.HasColumnType("bigint")
.HasComment("所属租户 IDPortal=Tenant 时必填Portal=Admin 时必须为空)。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.Property<long>("UserId")
.HasColumnType("bigint")
.HasComment("用户 ID。");
b.HasKey("Id");
b.HasIndex("TenantId")
.HasFilter("\"Portal\" = 1");
b.HasIndex("UserId", "RoleId")
.IsUnique()
.HasFilter("\"Portal\" = 0");
b.HasIndex("TenantId", "UserId", "RoleId")
.IsUnique()
.HasFilter("\"Portal\" = 1");
b.ToTable("user_roles", null, t =>
{
t.HasComment("用户-角色关系。");
t.HasCheckConstraint("CK_user_roles_Portal_Tenant", "(\"Portal\" = 0 AND \"TenantId\" IS NULL) OR (\"Portal\" = 1 AND \"TenantId\" IS NOT NULL)");
});
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
{
b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null)
.WithMany()
.HasForeignKey("OutboxId");
b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null)
.WithMany()
.HasForeignKey("InboxMessageId", "InboxConsumerId")
.HasPrincipalKey("MessageId", "ConsumerId");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb
{
/// <inheritdoc />
public partial class UseXminConcurrency : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// 删除旧的 RowVersion 列,改用 PostgreSQL 原生 xmin 系统列
migrationBuilder.DropColumn(
name: "RowVersion",
table: "identity_users");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// 恢复 RowVersion 列
migrationBuilder.AddColumn<byte[]>(
name: "RowVersion",
table: "identity_users",
type: "bytea",
rowVersion: true,
nullable: false,
defaultValue: new byte[0]);
}
}
}

View File

@@ -267,12 +267,12 @@ namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb
.HasColumnType("integer")
.HasComment("账号所属 Portal。");
b.Property<byte[]>("RowVersion")
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.IsRequired()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("bytea")
.HasComment("并发控制字段。");
.HasColumnType("xid")
.HasColumnName("xmin")
.HasComment("并发控制字段(映射到 PostgreSQL xmin 系统列)。");
b.Property<int>("Status")
.HasColumnType("integer")