-- ============================================================================ -- 本地开发种子:root OU + 通知收件用户 -- -- 用途 -- appsettings.local.json 的 AppConfig:Portal:EditionOUMappings 里,EditionId="root" -- 映射到 OU `3A0971B1-75B7-C43A-F39D-3CED32E0DF30`。该 ID 来自 dev 环境(这份配置是 -- Nacos 改造前的 dev 快照),本地种子库里并没有这个 OU。 -- -- 不跑这个脚本会怎样 -- 门户订单支付后自动建租户会失败,且报错极具误导性: -- CustomTenantController.CreateAsync 最后一步「发邮件给 root OU」 -- → _organizationUnitRepository.FindAsync(ouid) 返回 null -- → GetMembersAsync(null) 在 EF 表达式求值时抛 NullReferenceException -- → 接口 500 → OrderService.CreateTenant 捕获 → 整个事务回滚 -- 现象是:订单已支付(PaymentStatus=200),但 Orders.TargetTenantID 为 NULL、 -- SaasTenants 里查无此租户,AMLPortal_TenantEventQueues 留下一条 Status=300(重试中)。 -- 日志里只有 "An exception was thrown while attempting to evaluate a LINQ query -- parameter expression",很难联想到是缺一条 OU 数据。 -- (GetParentAgentList 里也有同一段 root OU 逻辑,同样依赖它。) -- -- 何时执行 -- 任何一次重建本地库之后,都要跑一遍: -- · `docker compose -f docker-compose.local.yml down -v`(EF 迁移 + 代码种子重建) -- · 或 RESTORE `AML-local-dev.bak`(目前本地库的实际数据来源) -- 两条路都不会带出这条 OU:它既不在代码种子里(属环境数据),那份 .bak 里也没有。 -- -- 执行方式 -- docker exec -i aml-mssql /opt/mssql-tools18/bin/sqlcmd \ -- -S localhost -U sa -P "Aml@Local2026" -C -d AbpAML \ -- -i /dev/stdin < seed-root-ou.sql -- -- 或先 docker cp 进容器再 -i 指定路径。 -- -- 幂等:可重复执行,已存在则跳过。 -- ============================================================================ SET NOCOUNT ON; -- 收件邮箱:租户创建成功的通知会真实发送到这里(经 iCS(stag) → smtp.yandex.com, -- 不走 appsettings 里的 Abp.Mailing.Smtp,那个 127.0.0.1:25 本地并不存在)。 -- 想换收件人改这里即可。 DECLARE @NotifyEmail nvarchar(256) = N'f-r-x@163.com'; DECLARE @UserName nvarchar(256) = N'local-sales-admin'; -- 必须与 appsettings.local.json 中 EditionOUMappings[EditionId="root"].OUIDs[0] 一致 DECLARE @OuId uniqueidentifier = '3A0971B1-75B7-C43A-F39D-3CED32E0DF30'; DECLARE @UserId uniqueidentifier; BEGIN TRAN; -- 1) root OU(host 级:TenantId / ParentId 均为 NULL) -- Code 只需在 host 范围内不与现有 OU 冲突;种子数据已占用 00001~00003。 IF NOT EXISTS (SELECT 1 FROM AbpOrganizationUnits WHERE Id = @OuId) BEGIN INSERT INTO AbpOrganizationUnits (Id, TenantId, ParentId, Code, DisplayName, ExtraProperties, ConcurrencyStamp, CreationTime, IsDeleted) VALUES (@OuId, NULL, NULL, N'00099', N'Local Root OU (sales admin)', N'{}', CONVERT(nvarchar(40), NEWID()), GETDATE(), 0); PRINT '[seed] AbpOrganizationUnits: root OU created'; END ELSE PRINT '[seed] AbpOrganizationUnits: root OU already exists, skipped'; -- 2) 通知收件用户(host 级,不设密码 —— 仅用于被 GetMembersAsync 查到并取 Email/UserName) SELECT @UserId = Id FROM AbpUsers WHERE TenantId IS NULL AND NormalizedEmail = UPPER(@NotifyEmail); IF @UserId IS NULL BEGIN SET @UserId = NEWID(); INSERT INTO AbpUsers (Id, TenantId, UserName, NormalizedUserName, Name, Email, NormalizedEmail, EmailConfirmed, PasswordHash, SecurityStamp, IsExternal, PhoneNumberConfirmed, TwoFactorEnabled, LockoutEnabled, AccessFailedCount, ExtraProperties, ConcurrencyStamp, CreationTime, IsDeleted) VALUES (@UserId, NULL, @UserName, UPPER(@UserName), N'Local Sales Admin', @NotifyEmail, UPPER(@NotifyEmail), 1, NULL, CONVERT(nvarchar(40), NEWID()), 0, 0, 0, 0, 0, N'{}', CONVERT(nvarchar(40), NEWID()), GETDATE(), 0); PRINT '[seed] AbpUsers: notify user created'; END ELSE PRINT '[seed] AbpUsers: notify user already exists, skipped'; -- 3) 用户挂到 OU 下(FindAllParentAgentsFromOU 靠这层关联找人) IF NOT EXISTS (SELECT 1 FROM AbpUserOrganizationUnits WHERE UserId = @UserId AND OrganizationUnitId = @OuId) BEGIN INSERT INTO AbpUserOrganizationUnits (UserId, OrganizationUnitId, TenantId, CreationTime) VALUES (@UserId, @OuId, NULL, GETDATE()); PRINT '[seed] AbpUserOrganizationUnits: membership created'; END ELSE PRINT '[seed] AbpUserOrganizationUnits: membership already exists, skipped'; COMMIT; -- 验证:应返回恰好一行 SELECT u.UserName, u.Email, ou.DisplayName, ou.Code FROM AbpUsers u JOIN AbpUserOrganizationUnits uo ON uo.UserId = u.Id JOIN AbpOrganizationUnits ou ON ou.Id = uo.OrganizationUnitId WHERE ou.Id = @OuId;