diff --git a/.kiro/settings/mcp.json b/.kiro/settings/mcp.json deleted file mode 100644 index f927488..0000000 --- a/.kiro/settings/mcp.json +++ /dev/null @@ -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": [] - } - } -} diff --git a/docs/AML_Backend/docker-compose-local-dev/Dockerfile.local b/docker-compose-local-dev/Dockerfile.local similarity index 86% rename from docs/AML_Backend/docker-compose-local-dev/Dockerfile.local rename to docker-compose-local-dev/Dockerfile.local index d74c438..664de66 100644 --- a/docs/AML_Backend/docker-compose-local-dev/Dockerfile.local +++ b/docker-compose-local-dev/Dockerfile.local @@ -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"] diff --git a/docs/AML_Backend/docker-compose-local-dev/Dockerfile.local.dockerignore b/docker-compose-local-dev/Dockerfile.local.dockerignore similarity index 100% rename from docs/AML_Backend/docker-compose-local-dev/Dockerfile.local.dockerignore rename to docker-compose-local-dev/Dockerfile.local.dockerignore diff --git a/docs/AML_Backend/docker-compose-local-dev/appsettings.local.json b/docker-compose-local-dev/appsettings.local.json similarity index 99% rename from docs/AML_Backend/docker-compose-local-dev/appsettings.local.json rename to docker-compose-local-dev/appsettings.local.json index ddee77b..d0d25e2 100644 --- a/docs/AML_Backend/docker-compose-local-dev/appsettings.local.json +++ b/docker-compose-local-dev/appsettings.local.json @@ -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": { diff --git a/docs/AML_Backend/docker-compose-local-dev/docker-compose-local-dev.md b/docker-compose-local-dev/docker-compose-local-dev.md similarity index 100% rename from docs/AML_Backend/docker-compose-local-dev/docker-compose-local-dev.md rename to docker-compose-local-dev/docker-compose-local-dev.md diff --git a/docs/AML_Backend/docker-compose-local-dev/docker-compose.local.yml b/docker-compose-local-dev/docker-compose.local.yml similarity index 75% rename from docs/AML_Backend/docker-compose-local-dev/docker-compose.local.yml rename to docker-compose-local-dev/docker-compose.local.yml index 7319094..79c7302 100644 --- a/docs/AML_Backend/docker-compose-local-dev/docker-compose.local.yml +++ b/docker-compose-local-dev/docker-compose.local.yml @@ -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" diff --git a/docker-compose-local-dev/seed-root-ou.sql b/docker-compose-local-dev/seed-root-ou.sql new file mode 100644 index 0000000..4efdda4 --- /dev/null +++ b/docker-compose-local-dev/seed-root-ou.sql @@ -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;