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
fengruixiang 2026-07-21 15:50:55 +08:00
parent a5d4657a27
commit b2b41e59e4
7 changed files with 153 additions and 34 deletions

View File

@ -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": []
}
}
}

View File

@ -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"]

View File

@ -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": {

View File

@ -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"

View File

@ -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 OUhost 级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;