chore(docker): 本地开发栈迁至 monorepo 根目录并适配跨仓库构建上下文
docker-compose-local-dev/ 从 AML_Backend 子仓库移到父仓库根目录,与
docs/AML_Backend/ 下的旧副本合并为唯一一份,避免两处各自漂移。
因为这套栈现在位于 AML_Backend 仓库之外,构建上下文必须跨仓库指向:
- context 改为 ../AML_Backend;dockerfile 用 ../docker-compose-local-dev/
Dockerfile.local,从上下文外引用(BuildKit 支持)
- appsettings.local.json 已不在构建上下文内,原来的
COPY docker-compose-local-dev/appsettings.local.json 会失效,改用命名附加
上下文传入:compose 侧 additional_contexts: {localcfg: .},Dockerfile 侧
COPY --from=localcfg(host 与 migrator 两个 stage 各一处)
同时补入旧副本没有的 seed-root-ou.sql。AML-local-dev.bak 由 *.bak 规则忽略。
镜像已重建并实测:恢复 .bak + 跑种子后,门户端点 GetEditionList /
GetPlanList 均返回 200。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
COPY docker-compose-local-dev/appsettings.local.json 会失效,改用命名附加
上下文传入:compose 侧 additional_contexts: {localcfg: .},Dockerfile 侧
COPY --from=localcfg(host 与 migrator 两个 stage 各一处)
同时补入旧副本没有的 seed-root-ou.sql。AML-local-dev.bak 由 *.bak 规则忽略。
镜像已重建并实测:恢复 .bak + 跑种子后,门户端点 GetEditionList /
GetPlanList 均返回 200。
main
parent
a5d4657a27
commit
b2b41e59e4
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"mssql": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-mssql"],
|
||||
"env": {
|
||||
"MSSQL_CONNECTION_STRING": "Server=localhost;Database=YourDatabase;User Id=YourUser;Password=YourPassword;TrustServerCertificate=True"
|
||||
},
|
||||
"disabled": false,
|
||||
"autoApprove": []
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@executeautomation/playwright-mcp-server"],
|
||||
"disabled": false,
|
||||
"autoApprove": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,17 +6,19 @@
|
|||
# - Targets .NET 6 (the projects are net6.0; the CI Dockerfile still says 5.0).
|
||||
# - Does not COPY the non-existent src/access-token.bin.
|
||||
# - Produces two runtime targets: `host` (the API) and `migrator` (data init).
|
||||
# - Bakes docker-compose-local-dev/appsettings.local.json over appsettings.json.
|
||||
# - Bakes appsettings.local.json over appsettings.json.
|
||||
# The repo's appsettings.json now only carries a NacosConfig section and pulls
|
||||
# the real settings from a Nacos server (192.168.1.120:8848) that is not
|
||||
# reachable in local dev. Program.cs only loads Nacos when a NacosConfig
|
||||
# section exists, so replacing appsettings.json with a Nacos-free, fully
|
||||
# self-contained config makes the app start without Nacos.
|
||||
#
|
||||
# Build context = repository root (AML_Backend); this Dockerfile lives one level
|
||||
# down in docker-compose-local-dev/. BuildKit uses the sibling
|
||||
# docker-compose-local-dev/Dockerfile.local.dockerignore (not the repo-root
|
||||
# .dockerignore), which keeps appsettings.json and docker-compose-local-dev/.
|
||||
# Build context = the AML_Backend repo root. This Dockerfile lives OUTSIDE that
|
||||
# repo, in the monorepo-root docker-compose-local-dev/, so appsettings.local.json
|
||||
# is not reachable from the main context; compose passes that directory in as the
|
||||
# named additional context `localcfg` (see docker-compose.local.yml). BuildKit
|
||||
# uses the sibling Dockerfile.local.dockerignore (not AML_Backend/.dockerignore),
|
||||
# which keeps appsettings.json.
|
||||
|
||||
############################
|
||||
# Restore + build (whole solution available)
|
||||
|
|
@ -75,7 +77,7 @@ WORKDIR /app
|
|||
COPY --from=publish-host /app/host ./
|
||||
# Local dev: replace the Nacos-only appsettings.json with a full self-contained
|
||||
# config (no NacosConfig section -> the app does not try to reach Nacos).
|
||||
COPY docker-compose-local-dev/appsettings.local.json ./appsettings.json
|
||||
COPY --from=localcfg appsettings.local.json ./appsettings.json
|
||||
EXPOSE 44331
|
||||
ENTRYPOINT ["dotnet", "iCON.Abp.FX.HttpApi.Host.dll"]
|
||||
|
||||
|
|
@ -89,7 +91,7 @@ WORKDIR /app
|
|||
COPY --from=publish-migrator /app/migrator ./
|
||||
# Same Nacos-free local config as the host (the migrator's own appsettings.json
|
||||
# is Nacos-only too); reuse the host's seed Data / wwwroot content.
|
||||
COPY docker-compose-local-dev/appsettings.local.json ./appsettings.json
|
||||
COPY --from=localcfg appsettings.local.json ./appsettings.json
|
||||
COPY --from=publish-host /app/host/Data ./Data
|
||||
COPY --from=publish-host /app/host/wwwroot ./wwwroot
|
||||
ENTRYPOINT ["dotnet", "iCON.Abp.FX.DbMigrator.dll"]
|
||||
|
|
@ -304,6 +304,13 @@
|
|||
"RetryTimes": 0,
|
||||
"RetryMinutes": 0
|
||||
},
|
||||
"ClearExpiredPaygOrderJob": {
|
||||
"Enabled": true,
|
||||
"RunOnStart": true,
|
||||
"IntervalSeconds": 300,
|
||||
"RetryTimes": 0,
|
||||
"RetryMinutes": 0
|
||||
},
|
||||
"TenantTobeExpiredRemindJob": {
|
||||
"Enabled": true,
|
||||
"RunOnStart": true,
|
||||
|
|
@ -386,7 +393,7 @@
|
|||
"RequestTokenPath": "connect/token?__tenant=Portal",
|
||||
"RequestTokenParams": "grant_type=password,response_type=token,username=Portal@iconsz.com,password=1qaz@WSX,scope=FX,client_id=PortalClient,client_secret=1qaz@WSX",
|
||||
"AbpTokenExpiredSeconds": "3600",
|
||||
"PortalTenantId": "3A07C0C9-50E2-638F-C2BB-61A4DA42B8D4",
|
||||
"PortalTenantId": "3A087539-4DCE-150D-A3EE-9AAD1F533D02",
|
||||
"ExpireOrderMins": 5,
|
||||
"IsMockupPayment": true,
|
||||
"SalesEmailAddress": "307736951@qq.com",
|
||||
|
|
@ -432,8 +439,22 @@
|
|||
}
|
||||
],
|
||||
"jQSeparatedEditions": {
|
||||
"EditionIds": [ "3A1A2969-6200-7B6C-1FE2-F4001F2F17E1" ],
|
||||
"EditionIds": [ "3A1A296B-DAB4-4B94-B267-4424683B8916" ],
|
||||
"EditionNames": [ "CPA" ]
|
||||
},
|
||||
"OfflineSubscribeOrderMail": {
|
||||
"Cc": [],
|
||||
"Bcc": []
|
||||
}
|
||||
},
|
||||
"TwoC": {
|
||||
"TenantId": null,
|
||||
"LinkValidHours": 72,
|
||||
"ExpireOrderMins": 60,
|
||||
"IsMockupPayment": true,
|
||||
"OfflineOrderMail": {
|
||||
"Cc": [],
|
||||
"Bcc": []
|
||||
}
|
||||
},
|
||||
"BusinessDefaultData": {
|
||||
|
|
@ -56,10 +56,14 @@ services:
|
|||
|
||||
db-migrator:
|
||||
build:
|
||||
# Context is the AML_Backend repo root (one level up); this compose file and
|
||||
# the Dockerfile live in docker-compose-local-dev/.
|
||||
context: ..
|
||||
dockerfile: docker-compose-local-dev/Dockerfile.local
|
||||
# This stack lives at the monorepo root, OUTSIDE the AML_Backend repo, so the
|
||||
# build context points into that subproject and the Dockerfile is referenced
|
||||
# from here. appsettings.local.json is no longer inside the context, so it is
|
||||
# passed in as a named additional context (`localcfg`) instead.
|
||||
context: ../AML_Backend
|
||||
dockerfile: ../docker-compose-local-dev/Dockerfile.local
|
||||
additional_contexts:
|
||||
localcfg: .
|
||||
target: migrator
|
||||
image: aml-dbmigrator:local
|
||||
container_name: aml-dbmigrator
|
||||
|
|
@ -73,8 +77,10 @@ services:
|
|||
|
||||
httpapi-host:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker-compose-local-dev/Dockerfile.local
|
||||
context: ../AML_Backend
|
||||
dockerfile: ../docker-compose-local-dev/Dockerfile.local
|
||||
additional_contexts:
|
||||
localcfg: .
|
||||
target: host
|
||||
image: aml-httpapi-host:local
|
||||
container_name: aml-httpapi-host
|
||||
|
|
@ -100,6 +106,14 @@ services:
|
|||
App__SelfUrl: "http://localhost:44331"
|
||||
AuthServer__Authority: "http://localhost:44331"
|
||||
AuthServer__RequireHttpsMetadata: "false"
|
||||
# 内部 token 请求(AbpTokenService → connect/token):本地 host 仅监听 HTTP,
|
||||
# 覆盖 appsettings 的 https 默认值,否则 TenantRenewal / 建租户等内部调用会 SSL 握手失败。
|
||||
AppConfig__General__ApiLocalhostUrl: "http://localhost:44331/"
|
||||
# 在线支付:appsettings.local.json 里 Portal.IsMockupPayment=true,会让 CreateOrder 建单后
|
||||
# 立即自造一次 QFPay 回调把订单置为已支付 —— 这样前端永远走不到收银台。置 false 才能测
|
||||
# 真实支付链(下单 → 收银台 → PaymentWebhook → 订单转已支付)。
|
||||
# 若只想跳过支付、快速拿到已开通的租户,改回 "true" 即可。
|
||||
AppConfig__Portal__IsMockupPayment: "false"
|
||||
# RabbitMQ: use the bundled broker service and run the background consumer.
|
||||
AppConfig__RabbitMQConfig__HostName: "rabbitmq"
|
||||
AppConfig__RabbitMQConfig__EnableConsumer: "true"
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
-- ============================================================================
|
||||
-- 本地开发种子: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;
|
||||
Loading…
Reference in New Issue