Compare commits

..

No commits in common. "a0baa7a1819414481edc231fca63d8887bc0dc78" and "c6bff91c452bf8a345d44a09951561cf9b6a4586" have entirely different histories.

10 changed files with 224 additions and 2535 deletions

View File

@ -1,7 +1,5 @@
# CLAUDE.md # CLAUDE.md
人与助手之间的语言交互默认为简体中文。
python 要使用 uv run 运行。
不要随意创建说明文档(比如 .md),除非用户有要求。
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
需要遵守:@AGENTS.md 需要遵守:@AGENTS.md

View File

@ -6,17 +6,8 @@
# - Targets .NET 6 (the projects are net6.0; the CI Dockerfile still says 5.0). # - 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. # - Does not COPY the non-existent src/access-token.bin.
# - Produces two runtime targets: `host` (the API) and `migrator` (data init). # - Produces two runtime targets: `host` (the API) and `migrator` (data init).
# - Bakes docker-compose-local-dev/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 # Build context = repository root (AML_Backend). See Dockerfile.local.dockerignore.
# 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/.
############################ ############################
# Restore + build (whole solution available) # Restore + build (whole solution available)
@ -54,28 +45,12 @@ RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet publish "src/iCON.Abp.FX.DbMigrator/iCON.Abp.FX.DbMigrator.csproj" \ dotnet publish "src/iCON.Abp.FX.DbMigrator/iCON.Abp.FX.DbMigrator.csproj" \
-c "$BUILD_CONFIGURATION" -o /app/migrator /p:UseAppHost=false -m:1 -c "$BUILD_CONFIGURATION" -o /app/migrator /p:UseAppHost=false -m:1
############################
# Runtime base: refresh CA certificates so outbound HTTPS to services whose
# chain the stock image can't complete (e.g. the stag iCS at *.iconsz.com,
# ZeroSSL/Sectigo-issued -> "PartialChain") validates. The app's own cert
# bypass only covers legacy WebClient (ServicePointManager), not HttpClient,
# so we fix trust at the OS level to cover every code path.
############################
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime-base
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& update-ca-certificates \
&& rm -rf /var/lib/apt/lists/*
############################ ############################
# Runtime: API host # Runtime: API host
############################ ############################
FROM runtime-base AS host FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS host
WORKDIR /app WORKDIR /app
COPY --from=publish-host /app/host ./ 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
EXPOSE 44331 EXPOSE 44331
ENTRYPOINT ["dotnet", "iCON.Abp.FX.HttpApi.Host.dll"] ENTRYPOINT ["dotnet", "iCON.Abp.FX.HttpApi.Host.dll"]
@ -84,12 +59,10 @@ ENTRYPOINT ["dotnet", "iCON.Abp.FX.HttpApi.Host.dll"]
# Seed contributors read data files and the full AppConfig relative to the # Seed contributors read data files and the full AppConfig relative to the
# app base directory, so we reuse the host's appsettings.json / Data / wwwroot. # app base directory, so we reuse the host's appsettings.json / Data / wwwroot.
############################ ############################
FROM runtime-base AS migrator FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS migrator
WORKDIR /app WORKDIR /app
COPY --from=publish-migrator /app/migrator ./ COPY --from=publish-migrator /app/migrator ./
# Same Nacos-free local config as the host (the migrator's own appsettings.json COPY --from=publish-host /app/host/appsettings.json ./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=publish-host /app/host/Data ./Data COPY --from=publish-host /app/host/Data ./Data
COPY --from=publish-host /app/host/wwwroot ./wwwroot COPY --from=publish-host /app/host/wwwroot ./wwwroot
ENTRYPOINT ["dotnet", "iCON.Abp.FX.DbMigrator.dll"] ENTRYPOINT ["dotnet", "iCON.Abp.FX.DbMigrator.dll"]

View File

@ -33,33 +33,10 @@ services:
retries: 30 retries: 30
start_period: 60s start_period: 60s
rabbitmq:
image: rabbitmq:3.13-management # has native arm64, no platform override needed
container_name: aml-rabbitmq
environment:
# Match RabbitMQConfig in appsettings.local.json (user/pass/vhost).
RABBITMQ_DEFAULT_USER: "admin"
RABBITMQ_DEFAULT_PASS: "123456"
RABBITMQ_DEFAULT_VHOST: "bthost"
ports:
# Host ports are shifted to avoid clashing with any other local RabbitMQ
# (e.g. a general dev-infra broker already on 5672/15672). The API talks to
# this broker over the compose network via internal port 5672 regardless.
- "5673:5672" # AMQP (host 5673 -> container 5672)
- "15673:15672" # management UI (http://localhost:15673 admin/123456)
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 5s
retries: 20
start_period: 30s
db-migrator: db-migrator:
build: build:
# Context is the AML_Backend repo root (one level up); this compose file and context: .
# the Dockerfile live in docker-compose-local-dev/. dockerfile: Dockerfile.local
context: ..
dockerfile: docker-compose-local-dev/Dockerfile.local
target: migrator target: migrator
image: aml-dbmigrator:local image: aml-dbmigrator:local
container_name: aml-dbmigrator container_name: aml-dbmigrator
@ -73,16 +50,14 @@ services:
httpapi-host: httpapi-host:
build: build:
context: .. context: .
dockerfile: docker-compose-local-dev/Dockerfile.local dockerfile: Dockerfile.local
target: host target: host
image: aml-httpapi-host:local image: aml-httpapi-host:local
container_name: aml-httpapi-host container_name: aml-httpapi-host
depends_on: depends_on:
mssql: mssql:
condition: service_healthy condition: service_healthy
rabbitmq:
condition: service_healthy
db-migrator: db-migrator:
condition: service_completed_successfully condition: service_completed_successfully
ports: ports:
@ -100,15 +75,11 @@ services:
App__SelfUrl: "http://localhost:44331" App__SelfUrl: "http://localhost:44331"
AuthServer__Authority: "http://localhost:44331" AuthServer__Authority: "http://localhost:44331"
AuthServer__RequireHttpsMetadata: "false" AuthServer__RequireHttpsMetadata: "false"
# RabbitMQ: use the bundled broker service and run the background consumer. # Silence integrations that point at unreachable infra in local dev.
AppConfig__RabbitMQConfig__HostName: "rabbitmq" AppConfig__RabbitMQConfig__EnableConsumer: "false"
AppConfig__RabbitMQConfig__EnableConsumer: "true" AppConfig__RabbitMQConfig__HostName: "127.0.0.1"
# iCS is a separate iCON service (not in this repo). Point at the shared stag
# instance so document/survey/engine features work without running iCS locally.
# (Requires the iCS credentials in appsettings.local.json to be valid on stag.)
AppConfig__iCS__iCSRootUrl: "https://stag-abp-api.iconsz.com/"
# Still unavailable locally: Elasticsearch-backed sanction sync.
AppConfig__JobConfig__SanctionJob__Enabled: "false" AppConfig__JobConfig__SanctionJob__Enabled: "false"
AppConfig__JobConfig__EngineListSyncJob__Enabled: "false"
restart: unless-stopped restart: unless-stopped
volumes: volumes:

View File

@ -2,40 +2,28 @@
用 docker-compose 在本地一键拉起 `iCON.Abp.FX.HttpApi.Host`,包含 SQL Server 数据库容器,并自动完成数据库迁移与种子数据初始化,用于本地开发调试。 用 docker-compose 在本地一键拉起 `iCON.Abp.FX.HttpApi.Host`,包含 SQL Server 数据库容器,并自动完成数据库迁移与种子数据初始化,用于本地开发调试。
> 适用平台macOS含 Apple Silicon / arm64。相关文件均位于 `AML_Backend/docker-compose-local-dev/` 目录下构建上下文build context是上一级的仓库根目录 `AML_Backend/` > 适用平台macOS含 Apple Silicon / arm64。相关文件均位于 `AML_Backend/` 目录下。
--- ---
## 1. 架构概览 ## 1. 架构概览
`docker-compose.local.yml` 定义个服务: `docker-compose.local.yml` 定义个服务:
| 服务 | 作用 | 说明 | | 服务 | 作用 | 说明 |
|------|------|------| |------|------|------|
| `mssql` | SQL Server 2022 数据库 | arm64 上通过 `platform: linux/amd64`Rosetta 模拟)运行;数据持久化在命名卷 `mssql-data` | | `mssql` | SQL Server 2022 数据库 | arm64 上通过 `platform: linux/amd64`Rosetta 模拟)运行;数据持久化在命名卷 `mssql-data` |
| `rabbitmq` | 消息队列(异步任务中枢) | `rabbitmq:3.13-management`arm64 原生);`host`/`consumer` 靠它跑数据采集、报告生成、AI 检测、门户订单等异步链路。宿主端口错开为 `5673`/`15673`(见第 8 节) |
| `db-migrator` | 数据初始化 | 运行一次:执行 EF Core 迁移 + 种子数据完成后退出exit 0 | | `db-migrator` | 数据初始化 | 运行一次:执行 EF Core 迁移 + 种子数据完成后退出exit 0 |
| `httpapi-host` | 后端 API 主机 | 等 `db-migrator` 成功后才启动;对外暴露 `44331`;随宿主启动 `RabbitMQConsumerService` 消费上述队列 | | `httpapi-host` | 后端 API 主机 | 等 `db-migrator` 成功后才启动;对外暴露 `44331` |
启动顺序由健康检查与 `depends_on` 保证:`mssql` + `rabbitmq` 健康 → `db-migrator` 跑完 → `httpapi-host` 启动。 启动顺序由健康检查与 `depends_on` 保证:`mssql` 健康 → `db-migrator` 跑完 → `httpapi-host` 启动。
配套文件(均在 `docker-compose-local-dev/` 内) 配套文件:
- `Dockerfile.local`:本地多阶段构建镜像(**.NET 6**),产出 `host``migrator` 两个目标。 - `Dockerfile.local`:本地多阶段构建镜像(**.NET 6**),产出 `host``migrator` 两个目标。
- `Dockerfile.local.dockerignore`:本地构建专用忽略表(仅排除 `bin/obj/.git`。BuildKit 会优先采用「与 Dockerfile 同目录、同名 + `.dockerignore`」的这个文件,从而**覆盖**仓库根目录的 `.dockerignore`(根目录那个会把 `appsettings.json`、`docker-compose*` 都排除掉,不适用于本地构建)。 - `Dockerfile.local.dockerignore`:本地构建专用忽略表(保留 `appsettings.json`,仅排除 `bin/obj/.git` 等)。
- `appsettings.local.json`**本地专用、绕过 Nacos 的完整配置**(见第 1.1 节),构建时会被覆盖进镜像的 `appsettings.json`
> ⚠️ 仓库自带的 CI 用 `src/iCON.Abp.FX.HttpApi.Host/Dockerfile` 已过时(写的是 .NET 5、且 COPY 一个不存在的 `src/access-token.bin`**不要**用它做本地构建,请使用 `Dockerfile.local` > ⚠️ 仓库自带的 CI 用 `src/iCON.Abp.FX.HttpApi.Host/Dockerfile` 已过时(写的是 .NET 5、且 COPY 一个不存在的 `src/access-token.bin`**不要**用它做本地构建,请使用 `Dockerfile.local`
### 1.1 配置来源:为什么要绕过 Nacos
最新代码已把配置中心切到 **Nacos**`src/iCON.Abp.FX.HttpApi.Host/appsettings.json` 现在只剩一个 `NacosConfig`真正的配置连接串、AuthServer、AppConfig、RabbitMQ、任务开关等都从 Nacos 服务器 `http://192.168.1.120:8848`DataId `aml.dev.json`)拉取。
该 Nacos 地址是公司内网,本机通常不可达;且它指向的是共享 dev 环境(连接串指向远程 dev 库),与「本地自包含隔离栈」的目标冲突。
`Program.cs` 里 Nacos 是**条件加载**的——仅当配置里存在 `NacosConfig` 段才会 `AddNacosV2Configuration`。因此本地方案是:用 `appsettings.local.json`(一份**不含 `NacosConfig`、但包含完整配置**的文件)覆盖镜像里的 `appsettings.json`App 便不再尝试连接 Nacos直接用本地配置启动再由 compose 的环境变量把连接串等指向本地 `mssql` 容器。
> `appsettings.local.json` 取自 Nacos 改造前commit `297d3bb6` 的父提交)那版完整 `appsettings.json`,是目前唯一可自包含运行的完整配置快照。若今后 Nacos 里新增了启动必需的配置项,需要手动同步补进这个文件。
--- ---
## 2. 前置条件 ## 2. 前置条件
@ -89,12 +77,10 @@ volumes:
## 4. 一键启动 ## 4. 一键启动
```bash ```bash
cd AML_Backend/docker-compose-local-dev cd AML_Backend
docker compose -f docker-compose.local.yml up -d --build docker compose -f docker-compose.local.yml up -d --build
``` ```
> 也可在仓库根目录运行:`cd AML_Backend && docker compose -f docker-compose-local-dev/docker-compose.local.yml up -d --build`。
首次构建较慢(需还原大量 ABP 商业 NuGet 包并编译约 40 个项目,并拉取 SQL Server 镜像)。 首次构建较慢(需还原大量 ABP 商业 NuGet 包并编译约 40 个项目,并拉取 SQL Server 镜像)。
构建已做内存优化(`-m:1` 单项目编译 + NuGet 缓存挂载),避免 Roslyn 因内存不足被杀OOM / exit 137 构建已做内存优化(`-m:1` 单项目编译 + NuGet 缓存挂载),避免 Roslyn 因内存不足被杀OOM / exit 137
@ -114,7 +100,6 @@ docker compose -f docker-compose.local.yml ps
| Swagger | http://localhost:44331/swagger FX API + AMLPortal API | | Swagger | http://localhost:44331/swagger FX API + AMLPortal API |
| OpenID 配置 | http://localhost:44331/.well-known/openid-configuration | | OpenID 配置 | http://localhost:44331/.well-known/openid-configuration |
| 数据库 | `localhost,11433`,用户 `sa`,密码 `Aml@Local2026`,库名 `AbpAML` | | 数据库 | `localhost,11433`,用户 `sa`,密码 `Aml@Local2026`,库名 `AbpAML` |
| RabbitMQ 管理台 | http://localhost:15673 ,用户 `admin` / 密码 `123456`vhost `bthost` |
| 应用管理员 | `admin@abp.io` / `1q2w3E*` | | 应用管理员 | `admin@abp.io` / `1q2w3E*` |
种子数据包含管理员账号、249 个国家、155 种货币、转账类型、文本模板、检测阈值等(共约 124 张表)。 种子数据包含管理员账号、249 个国家、155 种货币、转账类型、文本模板、检测阈值等(共约 124 张表)。
@ -124,7 +109,7 @@ docker compose -f docker-compose.local.yml ps
## 6. 常用操作 ## 6. 常用操作
```bash ```bash
cd AML_Backend/docker-compose-local-dev cd AML_Backend
# 查看后端日志注意Release 构建日志写文件docker logs 可能为空,见下) # 查看后端日志注意Release 构建日志写文件docker logs 可能为空,见下)
docker compose -f docker-compose.local.yml logs -f httpapi-host docker compose -f docker-compose.local.yml logs -f httpapi-host
@ -158,12 +143,9 @@ docker exec aml-mssql /opt/mssql-tools18/bin/sqlcmd \
- `ASPNETCORE_URLS=http://+:44331``App__SelfUrl` / `AuthServer__Authority=http://localhost:44331`(保证 IdentityServer 颁发者与浏览器访问地址一致),`AuthServer__RequireHttpsMetadata=false`。 - `ASPNETCORE_URLS=http://+:44331``App__SelfUrl` / `AuthServer__Authority=http://localhost:44331`(保证 IdentityServer 颁发者与浏览器访问地址一致),`AuthServer__RequireHttpsMetadata=false`。
- `ConnectionStrings__Default` 指向 `mssql` 服务(覆盖 appsettings 里的 `localhost`)。 - `ConnectionStrings__Default` 指向 `mssql` 服务(覆盖 appsettings 里的 `localhost`)。
- **RabbitMQ**`AppConfig__RabbitMQConfig__HostName=rabbitmq`(指向 compose 内的 broker、`...__EnableConsumer=true`(开启后台消费)。账号/vhost`admin`/`123456`/`bthost`)沿用 appsettings.local.json`rabbitmq` 服务的 `RABBITMQ_DEFAULT_*` 一致。 - 屏蔽不可达的外部集成:`AppConfig__RabbitMQConfig__HostName=127.0.0.1`(快速失败)、`...__EnableConsumer=false`;关闭 `SanctionJob`、`EngineListSyncJob`。
- **iCS**`AppConfig__iCS__iCSRootUrl=https://stag-abp-api.iconsz.com/`iCS 服务端不在本仓库,指向共享 stag 实例,让文档/问卷/检测引擎等功能可用。iCS 用的是 ZeroSSL/Sectigo 证书、且服务器只下发部分证书链;`Dockerfile.local` 的 `runtime-base` 阶段刷新了容器 CA 库,否则 HttpClient 会因 `PartialChain` 握手失败(应用自带的证书旁路只作用于旧式 WebClient不管 HttpClient
> 依赖 stag 那边的 iCS 凭证有效;若 stag 不可达或换了证书,相关功能会失败但不影响 host 启动。
- 仍关闭 `SanctionJob`(依赖本地未部署的 Elasticsearch。`EngineListSyncJob` 因 iCS 已指向 stag 而恢复默认启用。
> 仍有部分检测功能依赖 Elasticsearch`192.168.1.120:9200` 等)等远程/局域网服务,本 compose 未包含。如需本地联调,需另行配置对应服务地址。 > 仍有部分检测功能依赖 Elasticsearch / iCS 等远程或局域网服务,本 compose 未包含。如需本地联调这些功能,需另行配置对应服务地址。
--- ---
@ -176,22 +158,15 @@ docker exec aml-mssql /opt/mssql-tools18/bin/sqlcmd \
| 构建时 `csc.dll exited with code 137` / `cannot allocate memory` | 编译 `DbMigrations` 大项目时 OOM | 已用 `-m:1` 缓解;确保 Docker 内存充足,必要时串行构建 | | 构建时 `csc.dll exited with code 137` / `cannot allocate memory` | 编译 `DbMigrations` 大项目时 OOM | 已用 `-m:1` 缓解;确保 Docker 内存充足,必要时串行构建 |
| `docker logs httpapi-host` 为空但服务异常 | Release 构建只写文件日志(`#if DEBUG` 才有控制台 sink日志在容器内 `/app/Logs/<级别>/<日期>/` | `docker cp aml-httpapi-host:/app/Logs ./Logs` 后查看,或临时用 `--build-arg BUILD_CONFIGURATION=Debug` 构建以获得控制台日志 | | `docker logs httpapi-host` 为空但服务异常 | Release 构建只写文件日志(`#if DEBUG` 才有控制台 sink日志在容器内 `/app/Logs/<级别>/<日期>/` | `docker cp aml-httpapi-host:/app/Logs ./Logs` 后查看,或临时用 `--build-arg BUILD_CONFIGURATION=Debug` 构建以获得控制台日志 |
| 访问 Swagger 401/跳转异常 | `AuthServer:Authority` 与实际访问地址不一致 | 确认仍是 `http://localhost:44331`,端口映射 `44331:44331` 未改 | | 访问 Swagger 401/跳转异常 | `AuthServer:Authority` 与实际访问地址不一致 | 确认仍是 `http://localhost:44331`,端口映射 `44331:44331` 未改 |
| 启动报 `Bind for 0.0.0.0:5672 failed: port is already allocated` | 宿主已有其它 RabbitMQ 占用 5672/15672 | 本 compose 已把宿主端口错开为 `5673`/`15673`;若仍冲突,改 `rabbitmq.ports` 即可(容器间通信走内部 5672不受影响 |
| 日志含 `SSL connection could not be established ... PartialChain` | 容器 CA 库无法补全 iCS(stag) 的 ZeroSSL 证书链,且 HttpClient 不认应用的证书旁路 | 由 `Dockerfile.local``runtime-base` 刷新 CA 库解决;若复现,确认镜像是最新构建(`--build`|
| iCS 功能报 401/token 失败(非 TLS | stag 那边的 iCS 凭证/租户失效 | 与后端确认 `AppConfig:iCS:Credential` 在 stag 是否有效;非致命,不影响 host |
--- ---
## 9. 相关文件清单 ## 9. 相关文件清单
``` ```
AML_Backend/ # 构建上下文build context根目录 AML_Backend/
├─ NuGet.Config # ABP 商业源Dockerfile 会 COPY
├─ src/iCON.Abp.FX.HttpApi.Host/ # API 主机工程
└─ docker-compose-local-dev/ # ← 本地开发相关文件都在这里
├─ docker-compose.local.yml # 本地三服务编排(本说明对应文件) ├─ docker-compose.local.yml # 本地三服务编排(本说明对应文件)
├─ Dockerfile.local # 本地 .NET 6 多阶段构建host / migrator 两个目标) ├─ Dockerfile.local # 本地 .NET 6 多阶段构建host / migrator 两个目标)
├─ Dockerfile.local.dockerignore # 本地构建忽略表(覆盖根目录 .dockerignore ├─ Dockerfile.local.dockerignore # 本地构建忽略表(保留 appsettings.json
├─ appsettings.local.json # 绕过 Nacos 的完整本地配置(覆盖进镜像) └─ src/iCON.Abp.FX.HttpApi.Host/ # API 主机工程
└─ docker-compose-local-dev.md # 本说明文档
``` ```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

View File

@ -90,7 +90,6 @@
.callout.note { background: var(--note-bg); border-color: var(--note-border); } .callout.note { background: var(--note-bg); border-color: var(--note-border); }
.callout.gap { background: var(--gap-bg); border-color: var(--gap-border); } .callout.gap { background: var(--gap-bg); border-color: var(--gap-border); }
.callout.ok { background: var(--ok-bg); border-color: var(--ok-border); } .callout.ok { background: var(--ok-bg); border-color: var(--ok-border); }
.callout.newreq { background:#fbefff; border-color:#d8b9ff; }
.callout .lbl { font-weight: 700; } .callout .lbl { font-weight: 700; }
.pill { display:inline-block; font-size:11px; font-weight:700; padding:1px 8px; border-radius:20px; } .pill { display:inline-block; font-size:11px; font-weight:700; padding:1px 8px; border-radius:20px; }
.pill.done { background: var(--ok-bg); color:#1a7f37; border:1px solid var(--ok-border);} .pill.done { background: var(--ok-bg); color:#1a7f37; border:1px solid var(--ok-border);}
@ -107,7 +106,6 @@
ul.tight li { margin: 4px 0; } ul.tight li { margin: 4px 0; }
.flow { font-family:"SF Mono",Consolas,monospace; font-size:13px; background:var(--code-bg); padding:10px 14px; border-radius:8px; border:1px solid var(--border); overflow-x:auto; white-space:nowrap;} .flow { font-family:"SF Mono",Consolas,monospace; font-size:13px; background:var(--code-bg); padding:10px 14px; border-radius:8px; border:1px solid var(--border); overflow-x:auto; white-space:nowrap;}
hr.soft { border:none; border-top:1px dashed var(--border); margin: 26px 0; } hr.soft { border:none; border-top:1px dashed var(--border); margin: 26px 0; }
.updated { font-size:12px; color:#8250df; font-weight:700; }
</style> </style>
</head> </head>
<body> <body>
@ -115,21 +113,15 @@
<header class="page"> <header class="page">
<h1>justsolutionsWebV2 · plans-plus 真實 API 對接分析</h1> <h1>justsolutionsWebV2 · plans-plus 真實 API 對接分析</h1>
<p><code>public/plans-plus.html</code> 的單頁訂閱流程,從本地 mock 切換到 AML 後端(<code>iCON.Abp.AMLPortal</code>)真實 API並配合本地 <code>docker-compose-local-dev</code> 環境落地(<b>支付除外</b>)。</p> <p><code>public/plans-plus.html</code> 的單頁訂閱流程,從本地 mock 切換到 AML 後端(<code>iCON.Abp.AMLPortal</code>)真實 API。</p>
<p class="meta">範圍:<code>justsolutionsWebV2/public/js/plans-plus.js</code>(前端契約) · <code>justsolutionsWebV2/server/</code>BFF 代理層 + mock · <code>AML_Backend/modules/iCON.Abp.AMLPortal</code>(後端) · <code>AML_Backend/docker-compose-local-dev</code>(本地棧)</p> <p class="meta">範圍:<code>justsolutionsWebV2/public/js/plans-plus.js</code>(前端契約) · <code>justsolutionsWebV2/server/</code>BFF 代理層) · <code>AML_Backend/modules/iCON.Abp.AMLPortal</code>(後端)</p>
<p class="meta">參照:旧站 <code>justsolutionsWeb</code><code>PlanService</code>(同一套後端端點) · mock 契約權威來源 <code>server/mock/plans-plus.js</code></p> <p class="meta">參照:旧站 <code>justsolutionsWeb</code><code>PlanService</code>(同一套後端端點) · 见 <code>docs/justsolutionsWeb/api-calls.md</code></p>
<p class="meta updated">本次更新2026-07修正 BR/CI 校驗現狀、補入 <b>單次查詢</b>/<b>線下對接</b>/<b>agent tiers</b>/<b>KYC 租賃</b>/<b>主體類型</b> 等前端新契約,並新增「本地開發環境對接」與「本地種子數據缺口」章節。</p>
<p class="meta updated">補充更新2026-07釐清「<b>聯絡我們/聯絡推薦人</b>」語義——<b>不創建任何訂單/租戶/線索</b>,而是把當前<b>訂單摘要</b><b>既有 contact us API<code>POST /api/contact</code> → 後端 <code>customer/CreateFeedback</code></b>發送給平台管理員若已填有效推薦人代碼則改送該推薦人其郵箱。原「線下對接下單」定位5.8)已據此改寫。</p>
<p class="meta updated">更正2026-07<b>BR/CI 非必填</b>(業務決策)——去除 <code>CreateOrder</code><code>OrderService.cs:154</code>/<code>:190</code> 兩處必填校驗(保留唯一性校驗)。文檔原「仍必填 / 阻塞 / 需業務決策」表述作廢;第 1/5.7/6.3/7-9 章已統一。<b>核對:當前 working copy 這兩處仍為 active需確認放開已在對接/部署環境應用。</b></p>
<p class="meta updated">再次更新2026-07<b>單次查詢(隨付即用 · Pay-As-You-Go後端已支持</b>——由 AML 主模塊 <code>ConsumerPortal</code>「即時檢測」承接。<b>調用方已確認為 AML 後端內部編排</b><b>訂單支付完成後</b>,由後端在 2C 租戶上下文內依次 <code>CreateConsumerLink</code>(建鏈接、拿 accessToken<code>CreateConsumerOrder</code>(建訂單、即時檢測、結果郵件);<b>justsolutionsWebV2 只負責下單 + 收款,不調這兩個接口</b>(既非 V2 BFF、也非瀏覽器發起。原 5.6「後端零支持」結論<b>作廢</b>,第 1/2/4/5.6/7-9 章已據此改寫。</p>
<p class="meta updated">租戶查詢落定2026-07<b><code>GET /tenants/lookup?email=</code> 需後端新增「按管理員郵箱查可續費租戶」端點</b>。現 <code>queryRenewableTenant</code> 在 DB 層<b>只能按租戶名 <code>Contains</code></b><code>TenantAdminEmail</code> 是查完後遍歷<b>全部租戶用戶</b>逐條回填(<code>OrderService.cs:2372-2379</code><b>無法按郵箱在 DB 過濾</b>。BFF「以空 keyword 拉全部可續費租戶、再本地按郵箱篩」的變通<b>不採用</b>(現端點按租戶名搜、返回整張租戶列表,查詢維度與聚合歸屬都更該在後端);<b>後端按郵箱全庫掃描本身可接受</b>(租戶數少、低頻交互,可復用現 <code>GetAllUsers()</code> 全庫掃描、改按郵箱過濾)。第 1 / 5.5 / 7 / 8 / 9 章已據此改寫,並補入新端點的具體設計(入參、反查邏輯、聚合返回)。</p>
<p class="meta updated">單次查詢定價修正2026-07<b>隨付即用PAYG的收款金額改由 AML <code>GetPlanList</code><code>countryCode</code> 返回的 PAYG(P2G) 方案價決定</b>(不再是 V2 <code>/single-query/options</code> 分項單價之和);<b>檢測內容為固定 4 項、純展示、用戶不可勾選/取消</b>——<code>證件核驗(V)</code><code>名單篩查(ES=E)</code><code>AI 增強型篩查(A)</code><code>信貸記錄篩查(失信人=D)</code>(叫法後續可能再調整)。頁面 <code>/plans-jp</code>(日本別名,<code>server/index.js</code> 直接以 <code>plans-plus.html</code> 承接、URL 不變;日本 IP 訪 <code>/plans</code> 自動 302 至此)對應 <code>countryCode:"JPN"</code><b>目前 <code>countryCode</code> 僅日本一國</b>PAYG 為<b>單一整包價</b>,非分項之和)。原 5.6「op→功能碼非 1:1 / 公司查冊歸屬 / V2 分項單價與後端 jQ 計費對齊」等缺口據此<b>收斂</b>;第 1 / 3.1 / 5.6 / 7 / 8 / 9 章已改寫。</p>
</header> </header>
<div class="toc"> <div class="toc">
<h2>目錄</h2> <h2>目錄</h2>
<ol> <ol>
<li><a href="#s1">1. 結論速覽(含本次更正)</a></li> <li><a href="#s1">1. 結論速覽</a></li>
<li><a href="#s2">2. 總體架構與調用鏈</a></li> <li><a href="#s2">2. 總體架構與調用鏈</a></li>
<li><a href="#s3">3. plans-plus 前端依賴的 API 契約</a></li> <li><a href="#s3">3. plans-plus 前端依賴的 API 契約</a></li>
<li><a href="#s4">4. 後端真實端點清單AMLPortal</a></li> <li><a href="#s4">4. 後端真實端點清單AMLPortal</a></li>
@ -137,46 +129,39 @@
<li><a href="#s5-1">5.1 GET /editions所屬行業</a></li> <li><a href="#s5-1">5.1 GET /editions所屬行業</a></li>
<li><a href="#s5-2">5.2 GET /plans/catalog方案目錄</a></li> <li><a href="#s5-2">5.2 GET /plans/catalog方案目錄</a></li>
<li><a href="#s5-3">5.3 GET /countries註冊地</a></li> <li><a href="#s5-3">5.3 GET /countries註冊地</a></li>
<li><a href="#s5-4">5.4 GET /agents/:code推薦人 + tiers</a></li> <li><a href="#s5-4">5.4 GET /agents/:code推薦人</a></li>
<li><a href="#s5-5">5.5 GET /tenants/lookup按郵箱查租戶</a></li> <li><a href="#s5-5">5.5 GET /tenants/lookup續費查租戶</a></li>
<li><a href="#s5-6">5.6 單次查詢 single-queryConsumerPortal 即時檢測)</a></li> <li><a href="#s5-6">5.6 POST /subscribe下單</a></li>
<li><a href="#s5-7">5.7 POST /subscribe下單</a></li> <li><a href="#s5-7">5.7 POST /payments/create支付</a></li>
<li><a href="#s5-8">5.8 聯絡我們/推薦人(發訂單摘要·不下單)</a></li> <li><a href="#s6">6. plans-plus 相對旧站的新增需求</a></li>
<li><a href="#s5-9">5.9 POST /payments/create支付·暫緩</a></li> <li><a href="#s7">7. 實施建議與分期</a></li>
<li><a href="#s6">6. 本地開發環境對接docker-compose-local-dev</a></li> <li><a href="#s8">8. 待確認問題清單</a></li>
<li><a href="#s7">7. plans-plus 相對旧站的新增需求</a></li>
<li><a href="#s8">8. 實施建議與分期</a></li>
<li><a href="#s9">9. 待確認問題清單</a></li>
</ol> </ol>
</div> </div>
<!-- ───────────── 1 ───────────── --> <!-- ───────────── 1 ───────────── -->
<section id="s1"> <section id="s1">
<h2 class="sec">1. 結論速覽(含本次更正)</h2> <h2 class="sec">1. 結論速覽</h2>
<p>plans-plus 前端目前依賴 <b>3 條「只讀目錄」端點</b>editions / plans/catalog / countries<b>2 條「交互查詢」端點</b>agents / tenants/lookup<b>1 組單次查詢端點</b>single-query<b>2 條「下單提交」端點</b>subscribe / payments、以及 <b>1 條「聯絡我們」通知端點</b>contact——<b>不創建訂單</b>,僅發送訂單摘要),共 4 種訂閱流程:<code>new</code>(新購)、<code>renew</code>(續費)、<code>topup</code>(加購)、<code>single</code>(按次查詢)。</p> <p>plans-plus 前端共依賴 <b>7 個業務端點</b>(外加支付狀態輪詢 / webhook 2 個)。其中大部分可直接復用旧站 <code>justsolutionsWeb</code> 已經對接好的同一批 AMLPortal 後端端點;落地工作主要是在 <code>justsolutionsWebV2/server/routes/</code> 新增 BFF 處理器,做「後端原始響應 → 前端精簡契約」的字段轉換(與既有 <code>countries.js</code> / <code>industries.js</code> 完全同一範式)。</p>
<table> <table>
<thead><tr><th>前端端點</th><th>對應後端</th><th>復用程度</th><th>主要缺口 / 本次更正</th></tr></thead> <thead><tr><th>前端端點</th><th>對應後端</th><th>復用程度</th><th>主要缺口</th></tr></thead>
<tbody> <tbody>
<tr><td><code>GET /editions</code></td><td><code>Order/portal/GetEditionList</code></td><td><span class="pill partial">改造</span></td><td>多語言 edition 名稱(後端僅 displayName本地庫僅有 1 個 <code>Standard</code></td></tr> <tr><td><code>GET /editions</code></td><td><code>Order/portal/GetEditionList</code></td><td><span class="pill partial">改造</span></td><td>多語言 edition 名稱(後端只有 displayName</td></tr>
<tr><td><code>GET /plans/catalog</code></td><td><code>plan/portal/GetPlanList</code></td><td><span class="pill partial">改造</span></td><td>復刻 filterPlan<b>KYC 改月租</b>、jQuota 改配套<code>bestValue</code>/<code>note</code>/<code>nameJP</code> 後端無<b>本地庫 0 條 Plan</b></td></tr> <tr><td><code>GET /plans/catalog</code></td><td><code>plan/portal/GetPlanList</code></td><td><span class="pill partial">改造</span></td><td>需復刻旧站 filterPlan 拆分<code>bestValue</code>/<code>note</code> 後端無</td></tr>
<tr><td><code>GET /countries</code></td><td><code>Order/getCategoryByTypes</code></td><td><span class="pill done">已實現</span></td><td>—(<code>routes/countries.js</code> 已可用)</td></tr> <tr><td><code>GET /countries</code></td><td><code>Order/getCategoryByTypes</code></td><td><span class="pill done">已實現</span></td><td>—(<code>routes/countries.js</code> 已可用)</td></tr>
<tr><td><code>GET /agents/:code</code></td><td><code>SearchUserByCodeAndType</code> + <code>GetPlanList(agentUserId)</code></td><td><span class="pill partial">改造</span></td><td><b>新增 <code>tiers</code></b>(可售方案級別)、<code>email</code>/<code>phone</code>、隱藏 <code>agentorId</code></td></tr> <tr><td><code>GET /agents/:code</code></td><td><code>identity/users/SearchUserByCodeAndType</code></td><td><span class="pill partial">改造</span></td><td>響應 → <code>{code,name}</code> 轉換</td></tr>
<tr><td><code>GET /tenants/lookup</code></td><td><code>queryRenewableTenant</code><b>新增</b> <code>queryRenewableTenantByEmail</code></td><td><span class="pill todo">需後端新增</span></td><td><b>已定:後端新增按郵箱端點</b>。現端點 DB 層只按<b>租戶名 <code>Contains</code></b> 搜、郵箱查後回填→不能按郵箱過濾;前端<b>按郵箱、唯一命中</b>(不再多租戶消歧);新端點直接聚合 <code>currentSubscription</code> + <code>referrer</code>(見 5.5</td></tr> <tr><td><code>GET /tenants/lookup</code></td><td><code>Order/portal/queryRenewableTenant</code></td><td><span class="pill todo">缺口大</span></td><td>後端按<b>租戶名</b>搜,前端按<b>郵箱</b>查;<code>currentSubscription</code> 需重建</td></tr>
<tr><td><code>GET /single-query/options</code><br><code>POST /single-query</code></td><td>V2 只到「下單+收款」;<br>支付後 <b>AML 後端內部</b> <code>CreateConsumerLink</code>+<code>CreateConsumerOrder</code></td><td><span class="pill partial">改造</span></td><td><b>更正:後端已支持</b>AML 即時檢測);兩步為<b>後端內部編排</b>V2/瀏覽器均不調;<b>PAYG 定價改由 <code>GetPlanList</code>(countryCode) 的 PAYG 方案價</b>、檢測內容固定 4 項(V/E/A/D)純展示不可選(見 5.6</td></tr> <tr><td><code>POST /subscribe</code></td><td><code>CreateOrder</code> / <code>TenantRenewal</code></td><td><span class="pill partial">改造</span></td><td>需補 <code>planDetailId</code><code>topup</code> 類型無對應端點</td></tr>
<tr><td><code>POST /subscribe</code></td><td><code>CreateOrder</code> / <code>TenantRenewal</code></td><td><span class="pill partial">改造</span></td><td><code>planDetailId</code><b>BR/CI 非必填(已定)</b><code>topup</code>/個人主體待定</td></tr> <tr><td><code>POST /payments/create</code></td><td>QFPay 收銀台(旧站前端拼 URL</td><td><span class="pill todo">缺口大</span></td><td>需服務端簽名 + <code>PaymentWebhook</code> 回調</td></tr>
<tr><td><code>POST /contact</code><br><span class="small">(聯絡我們/推薦人)</span></td><td><code>customer/CreateFeedback</code></td><td><span class="pill done">復用現有</span></td><td><b>不創建訂單</b>:訂單摘要經 contact us API 發管理員;填了推薦人則發推薦人(見 5.8</td></tr>
<tr><td><code>POST /payments/create</code></td><td></td><td><span class="tag">暫緩</span></td><td><b>本輪支付除外</b>:前端提交後直接顯示「已提交成功」(見 5.9</td></tr>
</tbody> </tbody>
</table> </table>
<div class="callout gap">
<p><span class="lbl">本次三個最重要的更正 / 硬缺口:</span></p>
<p><b>BR/CI 非必填(業務決策,已定)</b>plans-plus 新購(含個人主體 / 空 BR/CI需能下單 ⇒ <b>BR/CI 不作必填</b>。實現=注释 <code>CreateOrder</code> 兩處必填校驗(<code>OrderService.cs:154</code><code>:190</code>);唯一性校驗 <code>ExistsByOrganizationBRCI</code> 對「BR、CI 皆空」返回 <code>false</code><code>:1124</code><b>空值安全通過,無需其它改動</b><span class="pill todo">核對</span> <b>當前 working copy 這兩處仍為 active</b>——若對接/部署環境尚未放開,空 BR/CI 仍會被攔,需確認該改動已應用。注意此為全局改動(旧站共用 <code>CreateOrder</code>),「空 BR 不再攔截」影響請業務知悉(見第 9 章 Q1</p>
<p><b>單次查詢single-query後端已支持本輪更正原「零支持」結論作廢</b>:走 AML 主模塊的 <b>ConsumerPortal 即時檢測</b>,且<b>調用方為 AML 後端內部編排</b>——<b>訂單支付完成後</b>,後端在 2C 租戶上下文內 <code>CreateConsumerLink</code>(建鏈接、拿 accessToken<code>CreateConsumerOrder</code>(建訂單、即時檢測、結果郵件)。<b>V2 只下單 + 收款,不調這兩個接口</b><b>本輪再定PAYG 檢測內容固定為 <code>證件核驗 / 名單篩查(ES) / AI 增強型篩查 / 信貸記錄篩查(失信人)</code> 4 項純展示(用戶不可勾選/取消,確定性映射 <code>V/E/A/D</code>),收款金額取自 <code>GetPlanList</code><code>countryCode</code><code>/plans-jp→JPN</code>)返回的 PAYG(P2G) 方案價</b>——原「op→功能碼非 1:1 / 公司查冊無碼 / V2 分項單價與後端 jQ 對齊」缺口收斂。<code>CreateConsumerLink</code> 非匿名(<code>RealtimeScreeningManagement</code> + <code>AML.ConsumerPortal.Enable</code>)在後端 2C 租戶上下文中天然滿足,故 <b>V2 BFF 無需持該租戶憑證</b><span class="pill partial">改造</span> 詳見 5.6。</p>
<p><b>本地種子數據缺口</b>:本地 docker 庫 <code>AMLPortal_Plans</code> / <code>AMLPortal_PlanDetails</code> 均為 <b>0 條</b><code>SaasEditions</code> 僅 1 條 <code>Standard</code><code>AMLPortal_AgentUserPlans</code> 為 0。⇒ 直接對接會拿到空目錄,<b>必須先補種子數據</b>才能跑通(見第 6 章)。</p>
</div>
<div class="callout ok"> <div class="callout ok">
<p><span class="lbl">仍然成立的好消息:</span>後端 <code>CreateOrder</code> 已忽略前端傳入的 <code>AdminPassword</code>,改用 <code>appConfig.General.TenantAdminDefaultPassword</code><code>OrderService.cs:215,236</code>)。因此 plans-plus 去掉「管理員密碼」步驟<b>不構成阻塞</b>——後端會給租戶管理員發激活/重置密碼郵件。續費 <code>TenantRenewal</code> 亦繼承上期 <code>AdminPassword</code><code>:2262</code>)。</p> <p><span class="lbl">好消息:</span>後端 <code>CreateOrder</code> 已忽略前端傳入的 <code>AdminPassword</code>,改用 <code>appConfig.General.TenantAdminDefaultPassword</code><code>OrderService.cs:215,236</code>)。因此 plans-plus 去掉「管理員密碼」步驟<b>不構成阻塞</b>——後端會發重置密碼郵件給租戶管理員。</p>
</div>
<div class="callout gap">
<p><span class="lbl">兩個真正的硬缺口:</span></p>
<p><b>按郵箱查租戶</b><code>queryRenewableTenant</code> 只支持 <code>keyword</code>(按 <code>TenantName.Contains</code>。plans-plus 用郵箱查、且要支持「一郵箱多租戶」選擇——需新增後端端點,或在 BFF 全量拉取後按郵箱過濾(不可取)。</p>
<p><b>topup加購流程</b>plans-plus 新增的純加購(不含基礎方案、只買 增加用戶/KYC/jQuota旧站沒有後端亦無直接端點。需確認映射到 <code>TenantRenewal</code>(僅加值項 planList還是新增端點。</p>
</div> </div>
</section> </section>
@ -185,129 +170,87 @@
<h2 class="sec">2. 總體架構與調用鏈</h2> <h2 class="sec">2. 總體架構與調用鏈</h2>
<p>justsolutionsWebV2 採「<b>BFFBackend-for-Frontend</b>」式:瀏覽器只調用本站 <code>/api/*</code>,由 Node/Express 依 <code>APP_ENV</code> 決定走 mock 還是真實後端,<b>前端代碼零改動</b>即可切換環境。</p> <p>justsolutionsWebV2 採「<b>BFFBackend-for-Frontend</b>」式:瀏覽器只調用本站 <code>/api/*</code>,由 Node/Express 依 <code>APP_ENV</code> 決定走 mock 還是真實後端,<b>前端代碼零改動</b>即可切換環境。</p>
<div class="flow">瀏覽器 plans-plus.js → window.api.get/post('/api/*') → Express server → <div class="flow">瀏覽器 plans-plus.js → window.api.get/post('/api/*') → Express server →
&nbsp;&nbsp;├─ APP_ENV=test ………………… server/mock/routes.js + mock/plans-plus.js內存假數據 &nbsp;&nbsp;├─ APP_ENV=test ………… server/mock/routes.js內存假數據
&nbsp;&nbsp;├─ dev/stag/prod + PLANS_PLUS_MOCK=true … 先掛 mock/plans-plus.js其餘 /api/* 才透傳後端 &nbsp;&nbsp;└─ dev/stag/prod …… server/routes/*.jsBFF→ AuthService 取 token → AMLPortal 後端</div>
&nbsp;&nbsp;└─ dev/stag/prod後端就緒後…… server/routes/*.jsBFF→ AuthService 取 token → AMLPortal 後端</div>
<ul class="tight"> <ul class="tight">
<li><b>前端封裝</b> <code>public/js/api.js</code><code>api.get('/editions')</code> 實際請求 <code>/api/editions</code>。所有 plans-plus 端點均走此封裝。</li> <li><b>前端封裝</b> <code>public/js/api.js</code><code>api.get('/editions')</code> 實際請求 <code>/api/editions</code>。所有 plans-plus 端點均走此封裝。</li>
<li><b>環境切換</b> <code>server/config.js</code><code>mockEnabled = (APP_ENV==='test')</code>另有 <code>plansPlusMock</code><code>PLANS_PLUS_MOCK</code>)開關——在真實環境下<b>仍讓 plans-plus 這組端點走 mock</b>,其餘 <code>/api/*</code> 透傳後端。真實對接時把它置 <code>false</code>,並在 <code>server/routes/</code> 補齊業務路由</li> <li><b>環境切換</b> <code>server/config.js</code><code>mockEnabled = (APP_ENV==='test')</code>真實環境需配 <code>API_BASE_URL</code> 與 OAuth 憑證(<code>.env.*</code></li>
<li><b>掛載順序</b> <code>server/index.js</code>:真實環境依次掛 <code>contact / trial-application / industries / countries</code> 路由,再按 <code>plansPlusMock</code> 決定是否掛 mock最後掛 proxy 兜底<b>新增 plans-plus 真實路由就照此在代理之前追加 <code>app.use(apiPrefix, createXxxRouter(config))</code></b></li> <li><b>掛載順序</b> <code>server/index.js</code>:真實環境先掛 <code>routes/*.js</code> 業務路由,再掛 proxy 兜底mock 環境掛 <code>mock/routes.js</code><b>新增 plans-plus 真實路由就照此追加 <code>app.use(apiPrefix, createXxxRouter(config))</code></b></li>
<li><b>鑑權</b> <code>server/services/auth.js</code>OAuth2 password 流程取 token 並緩存(提前 5 分鐘刷新),對應旧站硬編碼的門戶訪客憑證,現改為 <code>.env</code> 配置(<code>AUTH_USERNAME/PASSWORD/CLIENT_ID/...</code>。BFF 每次調後端用 <code>createAuthConfig()</code><code>Authorization: Bearer</code></li> <li><b>鑑權</b> <code>server/services/auth.js</code>OAuth2 password 流程取 token 並緩存(提前 5 分鐘刷新),對應旧站硬編碼的 <code>customer1</code> 訪客憑證,現改為 <code>.env</code> 配置。BFF 每次調後端用 <code>createAuthConfig()</code><code>Authorization: Bearer</code></li>
</ul> </ul>
<div class="callout note"> <div class="callout note">
<p><span class="lbl">關鍵差異(與旧站):</span>旧站 Angular 直接從瀏覽器調 <code>/api/amlPortal/*</code>token 存 localStorage。V2 改為<b>瀏覽器不直接接觸後端</b>,由服務端 BFF 持有憑證、收口後端調用並裁剪響應。因此本文每個端點都拆成「前端契約」與「BFF→後端映射」兩層。</p> <p><span class="lbl">關鍵差異(與旧站):</span>旧站 Angular 直接從瀏覽器調 <code>https://api-aml.iconsz.com/api/amlPortal/*</code>token 存 localStorage。V2 改為<b>瀏覽器不直接接觸後端</b>,由服務端 BFF 持有憑證、收口後端調用並裁剪響應。因此本文每個端點都拆成「前端契約」與「BFF→後端映射」兩層。</p>
</div>
<div class="callout warn">
<p><span class="lbl">支付本輪除外:</span>當前 <code>plans-plus.js</code><code>submit()</code> 已臨時改為——<code>POST /subscribe</code> 成功後<b>直接顯示「已提交成功」</b><code>showSubmitted()</code><b>不再</b>調 <code>/payments/create</code>、不跳收銀台(源碼注釋標明「臨時改動…還原方法」)。⇒ 本輪落地只需打通到 <code>/subscribe</code> 為止;支付見 5.9 暫緩。</p>
</div>
<div class="callout note">
<p><span class="lbl">single-query 是 BFF 範式的例外:</span>其餘端點都是「瀏覽器 → V2 <code>/api/*</code> → BFF 代理後端」;但單次查詢的<b>即時檢測觸發CreateConsumerLink/CreateConsumerOrder不走 BFF 代理</b>——由 <b>AML 後端在支付完成後內部編排</b>2C 租戶上下文V2 只到「下單 + 收款」為止。詳見 5.6。</p>
</div> </div>
</section> </section>
<!-- ───────────── 3 ───────────── --> <!-- ───────────── 3 ───────────── -->
<section id="s3"> <section id="s3">
<h2 class="sec">3. plans-plus 前端依賴的 API 契約</h2> <h2 class="sec">3. plans-plus 前端依賴的 API 契約</h2>
<p>以下是 <code>plans-plus.js</code> 實際讀寫的字段(即 BFF <b>必須產出/接受</b>的契約,目前由 <code>mock/plans-plus.js</code> 滿足)。真實對接時 BFF 輸出必須與此<b>逐字段一致</b>,否則前端渲染/計價會出錯。</p> <p>以下是 <code>plans-plus.js</code> 實際讀寫的字段(即 BFF <b>必須產出/接受</b>的契約,目前由 <code>mock/routes.js</code> 滿足)。真實對接時 BFF 輸出必須與此<b>逐字段一致</b>,否則前端渲染/計價會出錯。</p>
<h3>3.1 載入期(頁面初始化並發拉取 <code>loadAll()</code></h3> <h3>3.1 載入期(頁面初始化並發拉取)</h3>
<p>並發請求 <code>/editions</code><code>/plans/catalog</code><code>/countries</code><code>/single-query/options</code>,四者均以 <code>{ success:true, data:… }</code> 為成功標誌。</p> <p><code>loadAll()</code> 並發請求 <code>/editions</code><code>/plans/catalog</code><code>/countries</code>,三者均以 <code>{ success:true, data:… }</code> 為成功標誌。</p>
<pre><code>GET /editions → { success, data:{ editionList:[{id, displayName, nameCN, nameJP}], <pre><code>GET /editions → { success, data:{ editionList:[{id, displayName, nameCN, nameJP}],
jQSeparatedEditions:{ editionIds:[…] } } } jQSeparatedEditions:{ editionIds:[…] } } }
GET /plans/catalog→ { success, data:{ standard:[Plan], cpa:[Plan], addons:{ GET /plans/catalog→ { success, data:{ standard:[Plan], cpa:[Plan], addons:{
user:{ unitPrice, name* }, user:{unitPrice,…}, kyc:{unitPrice,…},
kyc:{ monthlyPrice, name* }, // ← 改為「月租」 jquota:{ packages:[{id, jq, price, nameCN/EN/JP}] } } } }
jquota:{ packages:[{id, jq, price, name*}] } } } }
GET /countries → { success, data:[{code, name, nameTC, nameSC, nameJP, phoneCode}] } GET /countries → { success, data:[{code, name, nameTC, nameSC, nameJP, phoneCode}] }
GET /single-query/options → { success, data:{ operations:[
{id, nameCN/EN/JP, descCN/EN/JP}] } } // ← 固定 4 項純展示,不可勾選/取消price 不再逐項計
// 固定 4 項:證件核驗(V) / 名單篩查ES(E) / AI 增強型篩查(A) / 信貸記錄篩查失信人(D)
// PAYG 收款價 → 取 GetPlanList(countryCode) 的 PAYG(P2G) 方案 price單一整包價非分項之和
POST /amlPortal/plan/portal/GetPlanList body = { // ← PAYG 定價來源(沿用旧站請求體 + countryCode
pageIndex:0, pageSize:100, filter:"", getAllItems:false,
tag1List:[], tag2List:[], tag3List:[], countryCode:"JPN" } // 目前 countryCode 僅日本一國
Plan = { planId, tag2Code, nameCN, nameEN, nameJP, periodMonths, Plan = { planId, tag2Code, nameCN, nameEN, nameJP, periodMonths,
price, originalPrice, qCount(-1=無限), userCountLimit, price, originalPrice, qCount(-1=無限), userCountLimit,
bestValue, noteCN, noteEN, noteJP }</code></pre> bestValue, noteCN, noteEN, noteJP }</code></pre>
<div class="callout note">
<p><span class="lbl">KYC 改為「設備月租」:</span>catalog 的 <code>addons.kyc</code> 不再是 <code>unitPrice</code>,而是 <code>monthlyPrice</code>。前端 <code>kycUnit()</code> = <code>monthlyPrice × 所選方案 periodMonths</code>(隨方案期數自動變動,一次付清、無套餐優惠價,見 <code>plans-plus.js</code> <code>kycPriceForMonths()</code>。jQuota 亦由「按量」改為「選配套」(<code>jquotaPackageId</code>)。</p>
</div>
<div class="callout note">
<p><span class="lbl">單次查詢PAYG目錄改為「固定展示 + 外部定價」:</span><code>/single-query/options</code> 的 4 項<b>不再是可勾選的計費項</b>,而是<b>固定信息展示</b><code>證件核驗 / 名單篩查(ES) / AI 增強型篩查 / 信貸記錄篩查(失信人)</code>,用戶不可選/取消;名稱後續可能調整)——可由前端硬編碼、或後端返回固定 4 項,<b>不含 <code>price</code></b>。PAYG 的<b>收款金額</b>單獨取自 <code>GetPlanList</code><code>countryCode</code> 返回的 PAYG(P2G) 方案價:<code>/plans-jp</code>(日本別名頁)傳 <code>countryCode:"JPN"</code>——<b>目前 <code>countryCode</code> 僅日本一國</b>(後端 <code>GetPlanListParam.CountryCode</code> 支持「該國專屬 + 全球通用(CountryCode=null)」過濾,將來擴國時沿用同一機制)。該價為<b>單一整包價</b>。⇒ 前端需依 URL/地域推導 <code>countryCode</code> 並在載入期取價(見 5.6)。</p>
</div>
<h3>3.2 交互期(按需)</h3> <h3>3.2 交互期(按需)</h3>
<pre><code>GET /tenants/lookup?email= // ← 只按郵箱;郵箱全局唯一 → 至多命中 1 個租戶 <pre><code>GET /tenants/lookup?email=&name=
→ { success, match:'none'|'unique', tenant:Tenant|null } → { success, match:'none'|'unique'|'multiple',
// 註:前端已不再做「一郵箱多租戶」消歧;舊契約的 match:'multiple' / candidates[] 已廢棄 tenant:Tenant|null, candidates:[{tenantId, tenantName}] }
GET /agents/:code GET /agents/:code → { success, found:bool, data:{code, name}|null }
→ { success, found:bool, data:{ code, name, tiers:[tag2Code…], email, phone }|null }
// tiers該推薦人可售的方案級別P2G/Std/Pre/CPA前端據此再過濾行業方案集
Tenant = { tenantId, tenantName, editionId, editionName, jurisdiction, br, ci, Tenant = { tenantId, tenantName, editionId, editionName, jurisdiction, br, ci,
referrer:{code,name}|null, // ← 註冊時填寫的推薦人,續費頁展示
currentSubscription:{ planId, nameCN/EN/JP, periodMonths, price, currentSubscription:{ planId, nameCN/EN/JP, periodMonths, price,
qCount, userCountLimit, startDate, expiryDate, usedQuota, qCount, userCountLimit, startDate, expiryDate, usedQuota,
addons:{ users:int, kyc:bool, jquotaPackageId:string } } }</code></pre> addons:{ users:int, kyc:bool, jquotaPackageId:string } } }</code></pre>
<h3>3.3 提交期</h3> <h3>3.3 提交期(兩步串聯)</h3>
<p><code>submit()</code>(在線下單)與 <code>submitContact()</code>(聯絡我們/推薦人)共用 <code>collectPayload()</code> 收集當前表單。本輪支付除外——<code>submit()</code><code>POST /subscribe</code>,成功即顯示「已提交成功」;<code>submitContact()</code> <b>不創建任何訂單</b>,把訂單摘要 <code>POST</code> 到聯絡端點(見 5.8),成功後顯示「已收到資料,將盡快聯絡」。<span class="small">(現網代碼仍 <code>POST /subscribe-offline</code>;本次語義調整後應改走 contact us API見 5.8「與現網代碼的差異」。)</span></p> <p><code>submit()</code><code>POST /subscribe</code><code>orderId</code>,再 <code>POST /payments/create</code><code>redirectUrl</code> 跳轉支付。</p>
<pre><code>POST /subscribe body = { <pre><code>POST /subscribe body = {
type:'new'|'renew'|'topup', edition, isCpa, startDate, type:'new'|'renew'|'topup', edition, isCpa, startDate,
plan:Plan, planPrice, isRenewalRate, plan:Plan, planPrice, isRenewalRate,
addons:{ users, userUnitPrice, addons:{ users, userUnitPrice, kyc, kycUnitPrice,
kyc, kycUnitPrice, kycRentalMonths, // ← 含租賃月數
jquotaPackageId, jquotaPackageName, jquotaUnits, jquotaPrice }, jquotaPackageId, jquotaPackageName, jquotaUnits, jquotaPrice },
agentCode, subtotal, total, agentCode, subtotal, total,
// type=new 追加: subjectType('corp'|'individual'), company, jurisdiction, br, // type=new 追加: company, jurisdiction, br, contact, phoneCode, phone, email, address
// contact, phoneCode, phone, email, address
// 個人主體contact=companybr/address 恒為空)
// type=renew/topup 追加: tenantId, company, email, currentSubscription // type=renew/topup 追加: tenantId, company, email, currentSubscription
} → { success, data:{ orderId, status, createdAt } } } → { success, data:{ orderId, status, createdAt } }
POST /contact body = { // ← 聯絡我們/推薦人:不創建訂單,僅發送摘要 POST /payments/create body = { orderId, amount, currency:'HKD', company, email }
name, email, company, // 客戶聯絡人 / 郵箱 / 公司(租戶/主體)名 → { success, data:{ paymentId, status, gateway, redirectUrl } }</code></pre>
subject:'訂閱諮詢 · {type}', // 可帶方案名
message: <collectPayload() type//+////>,
agentUserId? // 有推薦人時附帶其後端 Guid → 後端解析郵箱、收件人改為該推薦人
} → { success, data:{ feedbackId } }
POST /single-query body = {
type:'single', subjectType:'individual'|'company', subject, email,
operations:[{id, name, price}], total
} → { success, data:{ orderId, status, createdAt } }</code></pre>
<div class="callout warn"> <div class="callout warn">
<p><span class="lbl">注意:</span><code>/subscribe</code> 的 body 不含 <code>planDetailId</code>,也不含各加值項(增加用戶/KYC/jQuota對應的 <code>planId</code>/<code>planDetailId</code>。而後端 <code>CreateOrder</code>/<code>TenantRenewal</code><code>PlanList</code> 每項都<b>必須</b>同時帶 <code>PlanId</code>+<code>PlanDetailId</code><code>OrderService.cs:176,2215</code> 聯合校驗)。⇒ BFF 需在服務端依 <code>/plans/catalog</code> 結果<b>反查補齊</b> planDetailId見 5.7)。</p> <p><span class="lbl">注意:</span><code>/subscribe</code> 的 body 不含 <code>planDetailId</code>,也不含各加值項(增加用戶/KYC/jQuota對應的 <code>planId</code>/<code>planDetailId</code>。而後端 <code>CreateOrder</code>/<code>TenantRenewal</code><code>PlanList</code> 每項都<b>必須</b>同時帶 <code>PlanId</code>+<code>PlanDetailId</code><code>OrderService.cs:176</code> 聯合校驗)。⇒ BFF 需在服務端依 <code>/plans/catalog</code> 結果<b>反查補齊</b> planDetailId見 5.6)。</p>
</div>
<div class="callout note">
<p><span class="lbl">提交按鈕門檻:</span>前端 <code>computeValidity()</code> 在 terms 勾選 + 各類型必填齊備前禁用「付款」與「聯絡我們」兩個按鈕。<code>new</code> 要求 company/jurisdiction/emailcorp 另需 contact、方案已選、P2G 方案必選 jQuota<code>renew</code> 要求已解析到租戶;<code>topup</code> 要求已解析租戶且至少選一個加值項且未過期;<code>single</code> 要求 subject/email/至少一項查詢。</p>
</div> </div>
</section> </section>
<!-- ───────────── 4 ───────────── --> <!-- ───────────── 4 ───────────── -->
<section id="s4"> <section id="s4">
<h2 class="sec">4. 後端真實端點清單AMLPortal</h2> <h2 class="sec">4. 後端真實端點清單AMLPortal</h2>
<p>111 均在 <code>AML_Backend/modules/iCON.Abp.AMLPortal</code>,並已被旧站 <code>justsolutionsWeb/PlanService</code> 使用驗證過。前綴 <code>/api/amlPortal/*</code>agent 校驗端點在 Identity 模塊 <code>/api/identity/*</code>)。<b>1213 為單次查詢用到的方法,位於 AML 主模塊 <code>iCON.Abp.AML</code><code>/api/aml/ConsumerPortal/*</code>);但<u>由 AML 後端在支付完成後內部編排調用</u>V2 BFF 不代理、瀏覽器不直調</b>(見 5.6)。</p> <p>以下端點均在 <code>AML_Backend/modules/iCON.Abp.AMLPortal</code>,並已被旧站 <code>justsolutionsWeb/PlanService</code> 使用驗證過。前綴 <code>/api/amlPortal/*</code>,鑑權走 <code>[AbpAutoAuth("Portal")]</code>(門戶訪客 token</p>
<table> <table>
<thead><tr><th>#</th><th>方法 / 路由</th><th>用途</th><th>位置</th></tr></thead> <thead><tr><th>#</th><th>方法 / 路由</th><th>用途</th><th>Controller</th></tr></thead>
<tbody> <tbody>
<tr><td>1</td><td><span class="method post">POST</span> <code>/api/amlPortal/plan/portal/GetPlanList</code></td><td>取方案列表(Tag1: B/jQ/j/AdlU/KYC</td><td>PlanController:37 · PlanService:54</td></tr> <tr><td>1</td><td><span class="method post">POST</span> <code>/api/amlPortal/plan/portal/GetPlanList</code></td><td>取方案列表B/jQ/j/AdlU/KYC</td><td>PlanController:37</td></tr>
<tr><td>2</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/GetEditionList</code></td><td>取 edition所屬行業+ jQSeparatedEditions</td><td>OrderController:136 · OrderService:1103</td></tr> <tr><td>2</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/GetEditionList</code></td><td>取 edition所屬行業+ jQSeparatedEditions</td><td>OrderController:136</td></tr>
<tr><td>3</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/getCategoryByTypes</code></td><td>取國家列表(<code>typeCodes:['COUNTRY']</code></td><td>OrderController:184</td></tr> <tr><td>3</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/getCategoryByTypes</code></td><td>取國家列表(<code>typeCodes:['COUNTRY']</code></td><td>OrderController:184</td></tr>
<tr><td>4</td><td><span class="method get">GET</span> <code>/api/identity/users/SearchUserByCodeAndType/{code}/true</code></td><td>按 UserCode 精確查用戶(校驗推薦人/代理)</td><td>CustomIdentityUserController:306</td></tr> <tr><td>4</td><td><span class="method get">GET</span> <code>/api/identity/users/SearchUserByCodeAndType/{code}/true</code></td><td>校驗推薦人/代理 code</td><td>Identity非 AMLPortal</td></tr>
<tr><td>5</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/queryRenewableTenant</code></td><td>查可續費租戶(按 <code>keyword</code>=租戶名 <code>Contains</code></td><td>OrderController:308 · OrderService:2353</td></tr> <tr><td>5</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/queryRenewableTenant</code></td><td>查可續費租戶(按 <code>keyword</code>=租戶名)</td><td>OrderController:308</td></tr>
<tr><td>6</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/ExistsByOrganizationBRCI</code></td><td>校驗 BR/CI 是否已存在</td><td>OrderController:150 · OrderService:1122</td></tr> <tr><td>6</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/ExistsByOrganizationBRCI</code></td><td>校驗 BR/CI 是否已存在</td><td>OrderController:150</td></tr>
<tr><td>7</td><td><span class="method post">POST</span> <code>/api/amlPortal/customer/portal/CheckEmailExists/{email}</code></td><td>校驗管理員郵箱是否已用</td><td>CustomerController</td></tr> <tr><td>7</td><td><span class="method post">POST</span> <code>/api/amlPortal/customer/portal/CheckEmailExists/{email}</code></td><td>校驗管理員郵箱是否已用</td><td>CustomerController:121</td></tr>
<tr><td>8</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/CreateOrder</code></td><td>新租戶下單</td><td>OrderController:53 · OrderService:146</td></tr> <tr><td>8</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/CreateOrder</code></td><td>新租戶下單</td><td>OrderController:53</td></tr>
<tr><td>9</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/TenantRenewal</code></td><td>租戶續費下單</td><td>OrderController:321 · OrderService:2197</td></tr> <tr><td>9</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/TenantRenewal</code></td><td>租戶續費下單</td><td>OrderController:321</td></tr>
<tr><td>10</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/PaymentWebhook</code></td><td>支付回調(支付方服務端調,非前端)</td><td>OrderController:163</td></tr> <tr><td>10</td><td><span class="method post">POST</span> <code>/api/amlPortal/Order/portal/PaymentWebhook</code></td><td>支付回調QFPay 服務端調,非前端)</td><td>OrderController:163</td></tr>
<tr><td>11</td><td><span class="method post">POST</span> <code>/api/amlPortal/customer/CreateFeedback</code></td><td>聯絡我們/推薦人:接收訂單摘要(<b>不下單</b><b></b><code>AssignedAgentUserId</code> 收件人路由(見 5.8</td><td>CustomerController · <code>routes/contact.js</code> 已封裝</td></tr>
<tr><td>12</td><td><span class="method post">POST</span> <code>/api/aml/ConsumerPortal/CreateConsumerLink</code><br><span class="small">(後端內部編排調用)</span></td><td>單次查詢第 1 步:建即時檢測鏈接(回 <code>accessToken</code><b>·非匿名</b>RealtimeScreeningManagement + <code>AML.ConsumerPortal.Enable</code>)——在後端 2C 租戶上下文內滿足</td><td>ConsumerPortalController:340 · ConsumerPortalService:964</td></tr>
<tr><td>13</td><td><span class="method post">POST</span> <code>/api/aml/ConsumerPortal/CreateConsumerOrder</code><br><span class="small">(後端內部編排調用)</span></td><td>單次查詢第 2 步:憑 <code>accessToken</code> 建訂單並即時檢測(異步跑模塊 + 結果郵件)<b>·匿名</b></td><td>ConsumerPortalController:78 · ConsumerPortalService:325</td></tr>
</tbody> </tbody>
</table> </table>
<p class="small">DTO 位置:<code>Application.Contracts/PlanAppLayer/*</code>GetPlanListParam、PlanDto、PlanDetailDto<code>Application.Contracts/OrderAppLayer/*</code>CreateOrderParam、SelectPlanItem、TenantRenewalParam、QueryRenewableTenantParam、TenantPropertyDtoagent 返回 DTO<code>iCON.Abp.FX.Users/AppUserDto</code>。單次查詢 DTO<code>iCON.Abp.AML/Application.Contracts/ConsumerPortalAppLayer/*</code>CreateConsumerLinkParam、CreateConsumerCustomerDto、CreateConsumerLinkDto、CreateConsumerOrderParam、ConsumerLinkDto+ <code>IndividualAppLayer/CreateIndividualDto</code><code>OrgLayer/CreateOrganizationDto</code></p> <p class="small">DTO 位置:<code>Application.Contracts/PlanAppLayer/*</code>GetPlanListParam、PlanDto、PlanDetailDto<code>Application.Contracts/OrderAppLayer/*</code>CreateOrderParam、SelectPlanItem、TenantRenewalParam、QueryRenewableTenantParam、TenantPropertyDto</p>
</section> </section>
<!-- ───────────── 5 ───────────── --> <!-- ───────────── 5 ───────────── -->
@ -318,24 +261,22 @@ POST /single-query body = {
<!-- 5.1 --> <!-- 5.1 -->
<h3 id="s5-1">5.1 GET /editions所屬行業 <span class="pill partial">改造</span></h3> <h3 id="s5-1">5.1 GET /editions所屬行業 <span class="pill partial">改造</span></h3>
<div class="endpoint-head"><span class="method get">GET</span> /api/editions &nbsp;&nbsp; <span class="method post">POST</span> /api/amlPortal/Order/portal/GetEditionList</div> <div class="endpoint-head"><span class="method get">GET</span> /api/editions &nbsp;&nbsp; <span class="method post">POST</span> /api/amlPortal/Order/portal/GetEditionList</div>
<p>後端返回 <code>{ code:0, data:{ editionList:[<b>完整 Edition 實體</b>], jQSeparatedEditions:{ EditionIds:[…], EditionNames:[…] } } }</code><code>OrderService.cs:1103</code> 直接把 <code>SaasEdition</code> 實體與 <code>_appConfig.Portal.jQSeparatedEditions</code> 原樣返回)。前端要 <code>editionList:[{id, displayName, nameCN, nameJP}]</code><code>jQSeparatedEditions.editionIds</code></p> <p>後端返回 <code>{ code:0, data:{ editionList:[{id, displayName, …}], jQSeparatedEditions:{editionIds:[…]} } }</code>。前端要 <code>editionList:[{id, displayName, nameCN, nameJP}]</code><code>jQSeparatedEditions.editionIds</code></p>
<table> <table>
<thead><tr><th>前端字段</th><th>後端來源</th><th>說明</th></tr></thead> <thead><tr><th>前端字段</th><th>後端來源</th><th>說明</th></tr></thead>
<tbody> <tbody>
<tr><td><code>id</code></td><td><code>editionList[].id</code>Guid</td><td>直接映射;後續作 <code>OrganizationReference</code></td></tr> <tr><td><code>id</code></td><td><code>editionList[].id</code>Guid</td><td>直接映射;後續作 <code>OrganizationReference</code></td></tr>
<tr><td><code>displayName</code></td><td><code>editionList[].displayName</code></td><td>英文名Saas Edition 的 DisplayName</td></tr> <tr><td><code>displayName</code></td><td><code>editionList[].displayName</code></td><td>英文名</td></tr>
<tr><td><code>nameCN</code> / <code>nameJP</code></td><td><b>後端無</b></td><td>缺口,見下</td></tr> <tr><td><code>nameCN</code> / <code>nameJP</code></td><td><b>後端無</b></td><td>缺口,見下</td></tr>
<tr><td><code>jQSeparatedEditions.editionIds</code></td><td><code>jQSeparatedEditions.EditionIds</code></td><td>驅動 CPA/標準方案集切換(<code>state.isCpa</code>;注意後端是 <b>PascalCase</b>BFF 需轉小寫鍵名並 <code>toLowerCase()</code></td></tr> <tr><td><code>jQSeparatedEditions.editionIds</code></td><td>同名字段</td><td>驅動 CPA/標準方案集切換(<code>state.isCpa</code></td></tr>
</tbody> </tbody>
</table> </table>
<div class="callout gap"> <div class="callout gap">
<p><span class="lbl">缺口:</span>edition 多語言名稱。後端 <code>GetEditionList</code> 只有 <code>displayName</code>。plans-plus 的 <code>editionName()</code> 在 tc/jp 下優先取 <code>nameCN</code>/<code>nameJP</code>,缺失時 fallback 回 displayName ⇒ <b>不阻塞,但中日文會顯示英文</b>。建議BFF 維護 <code>editionId/displayName → {nameCN,nameJP}</code> 映射表(與 <code>industries.js</code> 現有靜態行業翻譯同思路)。</p> <p><span class="lbl">缺口:</span>edition 多語言名稱。後端 <code>GetEditionList</code> 只有 <code>displayName</code>,旧站也僅用 displayName。plans-plus 的 <code>editionName()</code> 在 tc/jp 下會優先取 <code>nameCN</code>/<code>nameJP</code>,缺失時已能 fallback 回 displayName ⇒ <b>不阻塞,但中日文會顯示英文</b></p>
<p><span class="lbl">建議:</span>BFF 維護一份 <code>editionId/displayName → {nameCN,nameJP}</code> 映射表(與 <code>industries.js</code> 現有靜態行業翻譯同思路),或後端在 edition 上補多語言字段。</p>
</div> </div>
<div class="callout note"> <div class="callout note">
<p><span class="lbl">旧站行為(可選沿用):</span><code>getEditionList()</code><b>過濾掉 <code>Standard</code></b>、把 <code>Others</code> 排到末尾,並把 editionIds <code>toLowerCase()</code>。plans-plus 目前未做此整理;若要一致,這段邏輯應放 BFF。</p> <p><span class="lbl">旧站行為(可選沿用):</span><code>getEditionList()</code><b>過濾掉 <code>Standard</code></b>、並把 <code>Others</code> 排到列表末尾,同時把 editionIds <code>toLowerCase()</code>。plans-plus 目前未做此整理;若要與旧站一致,這段邏輯應放 BFF。</p>
</div>
<div class="callout warn">
<p><span class="lbl">本地現狀:</span>本地庫 <code>SaasEditions</code> <b>只有 1 條 <code>Standard</code></b>Id <code>3A220FC7-9577-660B-729A-4024B1E4BEAA</code>),而 <code>appsettings.local.json</code><code>Portal.jQSeparatedEditions.EditionIds</code>/<code>EditionOUMappings</code> 引用的是另一組 GUID<code>3A1A2969-…</code>/<code>3A0149C4-…</code>),與實庫不符。⇒ 若要在本地看到多行業 + CPA 切換,需<b>先補 Edition 種子並對齊配置</b>(見第 6 章)。</p>
</div> </div>
<!-- 5.2 --> <!-- 5.2 -->
@ -344,18 +285,16 @@ POST /single-query body = {
<p>BFF 以旧站同款請求體調 <code>GetPlanList</code>,再復刻旧站 <code>filterPlan()</code> 把扁平 <code>items</code><code>tag1Code</code> 拆成 5 組,組裝成 <code>{standard, cpa, addons}</code></p> <p>BFF 以旧站同款請求體調 <code>GetPlanList</code>,再復刻旧站 <code>filterPlan()</code> 把扁平 <code>items</code><code>tag1Code</code> 拆成 5 組,組裝成 <code>{standard, cpa, addons}</code></p>
<h4>請求體(沿用 PlanService.getAllPlanList</h4> <h4>請求體(沿用 PlanService.getAllPlanList</h4>
<pre><code>{ pageIndex:0, pageSize:9999, filter:'', getAllItems:false, <pre><code>{ pageIndex:0, pageSize:9999, filter:'', getAllItems:false,
tag1List:['B','jQ','j','AdlU','KYC'], tag2List:[], tag3List:[], agentUserId:null } tag1List:['B','jQ','j','AdlU','KYC'], tag2List, tag3List, agentUserId }</code></pre>
// 後端 GetPlanList 對 Tag2List/Tag3List命中或 Tag2Code 為空皆放行OrderService/PlanService:78-80</code></pre>
<p>後端返回 <code>{ code:0, data:{ totalCount, items:[PlanDto{…, planDetails:[PlanDetailDto]}] } }</code><code>PlanService.cs:107</code> <code>PagedResultDto&lt;PlanDto&gt;</code>)。<b>注意</b><code>getAllItems:false</code> 時後端已過濾 <code>Enabled</code> 且僅保留在有效期內的 planDetails<code>PlanService.cs:99-104</code>)。</p>
<h4>後端 items → 前端 Plan 字段映射</h4> <h4>後端 items → 前端 Plan 字段映射</h4>
<table> <table>
<thead><tr><th>前端 Plan</th><th>後端來源item / planDetails[0]</th><th>備註</th></tr></thead> <thead><tr><th>前端 Plan</th><th>後端來源item / planDetails[0]</th><th>備註</th></tr></thead>
<tbody> <tbody>
<tr><td><code>planId</code></td><td><code>item.id</code></td><td></td></tr> <tr><td><code>planId</code></td><td><code>item.id</code></td><td></td></tr>
<tr><td><code>planDetailId</code> ⚠️</td><td><code>item.planDetails[0].id</code></td><td><b>前端契約現缺此字段</b>,但下單必需 ⇒ BFF 須補進 Plan提交時回填見 5.7</td></tr> <tr><td><code>planDetailId</code> ⚠️</td><td><code>item.planDetails[0].id</code></td><td><b>前端契約現缺此字段</b>,但下單必需 ⇒ BFF 須補進 Plan提交時回填見 5.6</td></tr>
<tr><td><code>tag2Code</code></td><td><code>item.tag2Code</code></td><td>Std/P2G/CPA/Pre…CPA 判定 + agent tiers 過濾</td></tr> <tr><td><code>tag2Code</code></td><td><code>item.tag2Code</code></td><td>Std/P2G/CPA/Pre…CPA 判定用</td></tr>
<tr><td><code>nameCN</code>/<code>nameEN</code></td><td><code>item.nameCN</code>/<code>item.nameEN</code></td><td>旧站會截掉「(…」後綴,可沿用</td></tr> <tr><td><code>nameCN</code>/<code>nameEN</code></td><td><code>item.nameCN</code>/<code>nameEN</code></td><td>旧站會截掉「(…」後綴,可沿用</td></tr>
<tr><td><code>nameJP</code></td><td><b>後端無</b>PlanDto 只有 CN/EN</td><td>缺口fallback EN</td></tr> <tr><td><code>nameJP</code></td><td><b>後端無</b></td><td>缺口fallback EN</td></tr>
<tr><td><code>periodMonths</code></td><td><code>planDetails[0].periodMonths</code></td><td></td></tr> <tr><td><code>periodMonths</code></td><td><code>planDetails[0].periodMonths</code></td><td></td></tr>
<tr><td><code>price</code>/<code>originalPrice</code></td><td><code>planDetails[0].price</code>/<code>originalPrice</code></td><td>劃線價用 originalPrice</td></tr> <tr><td><code>price</code>/<code>originalPrice</code></td><td><code>planDetails[0].price</code>/<code>originalPrice</code></td><td>劃線價用 originalPrice</td></tr>
<tr><td><code>qCount</code></td><td><code>planDetails[0].qCount</code></td><td><code>-1</code> 表無限</td></tr> <tr><td><code>qCount</code></td><td><code>planDetails[0].qCount</code></td><td><code>-1</code> 表無限</td></tr>
@ -368,70 +307,49 @@ POST /single-query body = {
<ul class="tight"> <ul class="tight">
<li><code>standard</code><code>tag1Code==='B' && tag2Code!=='CPA'</code><code>cpa</code><code>tag1Code==='B' && tag2Code==='CPA'</code></li> <li><code>standard</code><code>tag1Code==='B' && tag2Code!=='CPA'</code><code>cpa</code><code>tag1Code==='B' && tag2Code==='CPA'</code></li>
<li><code>addons.user.unitPrice</code><code>tag1Code==='AdlU'</code><code>planDetails[0].price</code>(並記其 planId/planDetailId 備下單)</li> <li><code>addons.user.unitPrice</code><code>tag1Code==='AdlU'</code><code>planDetails[0].price</code>(並記其 planId/planDetailId 備下單)</li>
<li><code>addons.kyc.monthlyPrice</code><code>tag1Code==='KYC'</code> <code>planDetails[0].price</code><b>當作月租單價</b>,見下方 KYC 映射)</li> <li><code>addons.kyc.unitPrice</code><code>tag1Code==='KYC'</code> 同上</li>
<li><code>addons.jquota.packages[]</code><code>tag1Code==='jQ'||'j'</code>:每項 <code>{ id:planId, jq:qCount, price:planDetails[0].price, name* }</code>,並各自記 planDetailId</li> <li><code>addons.jquota.packages[]</code><code>tag1Code==='jQ'||'j'</code>:每項 <code>{ id:planId, jq:qCount, price:planDetails[0].price, name* }</code>,並各自記 planDetailId</li>
</ul> </ul>
<div class="callout warn">
<p><span class="lbl">KYC「月租」映射本次新增難點</span>前端把 KYC 當設備月租:<code>租金 = monthlyPrice × 方案 periodMonths</code>,一次付清。後端 KYC plan 只有<b>單一 Price</b>、旧站按 <code>PCS=1</code> 購買。兩種對法:<br>
<b>把後端 KYC <code>planDetails[0].price</code> 當「月租單價」,下單時 <code>PCS = kycRentalMonths</code></b>(則後端 PaidAmount = price × 月數,與前端計價自洽)——<b>推薦,且無需後端改動</b><br>
② 或後端為 KYC 按周期建多條 PlanDetail / 增月租字段。<br>
⇒ 需與後端確認 KYC 計費口徑BFF 反查 planDetailId 時把 <code>kycRentalMonths</code> 填入該行 <code>PCS</code></p>
</div>
<div class="callout gap"> <div class="callout gap">
<p><span class="lbl">缺口:</span><code>bestValue</code><code>note*</code><code>nameJP</code> 後端 <code>PlanDto</code>/<code>PlanDetailDto</code> 無對應字段。短期在 BFF 按 <code>systemCode</code> 寫死映射bestValue / 賣點文案 / 日文名),不阻塞;長期可後端補字段</p> <p><span class="lbl">缺口:</span><code>bestValue</code>(最超值標記)、<code>note*</code>(賣點文案)後端 <code>PlanDto</code>/<code>PlanDetailDto</code> 無對應字段。建議:① BFF 配置(按 systemCode 標 bestValue / 文案);或 ② 後端在 Plan 增 <code>bestValue</code> 與多語言 note。短期可在 BFF 寫死映射,不阻塞。</p>
</div> </div>
<div class="callout warn"> <div class="callout note">
<p><span class="lbl">本地現狀(阻塞):</span>本地庫 <code>AMLPortal_Plans</code>/<code>AMLPortal_PlanDetails</code><b>0 條</b><code>GetPlanList</code> 會返回空目錄。<b>必須先補 Plan 種子</b>(含 Tag1Code=B/jQ/j/AdlU/KYC、Tag2Code=Std/P2G/CPA/Pre 與對應 PlanDetail才能渲染方案卡見第 6 章)</p> <p><span class="lbl">提示:</span>plans-plus 的 jQuota 改為「<b>選配套</b>」(<code>jquotaPackageId</code>),不再是旧站的「按數量」。後端 jQ 計劃本就是離散 plan天然契合——每個 jQ plan 即一個 package。</p>
</div> </div>
<!-- 5.3 --> <!-- 5.3 -->
<h3 id="s5-3">5.3 GET /countries註冊地 <span class="pill done">已實現</span></h3> <h3 id="s5-3">5.3 GET /countries註冊地 <span class="pill done">已實現</span></h3>
<div class="endpoint-head"><span class="method get">GET</span> /api/countries &nbsp;&nbsp; <span class="method post">POST</span> /api/amlPortal/Order/getCategoryByTypes</div> <div class="endpoint-head"><span class="method get">GET</span> /api/countries &nbsp;&nbsp; <span class="method post">POST</span> /api/amlPortal/Order/getCategoryByTypes</div>
<p><code>server/routes/countries.js</code> <b>已實現並可直接用</b>:取 <code>typeCodes:['COUNTRY']</code>,從 <code>categoryTranslations</code> 提 zh-hk/zh-cn/ja/en-us輸出 <code>{code, name, nameTC, nameSC, nameJP, phoneCode}</code>,正好滿足 plans-plus 的 <code>countryDisplay()</code><code>phoneCode</code> 自動填充。<b>無需改動</b>(種子含 249 個國家)。</p> <p><code>server/routes/countries.js</code> <b>已實現並可直接用</b>:取 <code>typeCodes:['COUNTRY']</code>,從 <code>categoryTranslations</code> 提 zh-hk/zh-cn/ja/en-us輸出 <code>{code, name, nameTC, nameSC, nameJP, phoneCode}</code>,正好滿足 plans-plus 的 <code>countryDisplay()</code><code>phoneCode</code> 自動填充。<b>無需改動</b></p>
<!-- 5.4 --> <!-- 5.4 -->
<h3 id="s5-4">5.4 GET /agents/:code推薦人代理 + tiers <span class="pill partial">改造</span></h3> <h3 id="s5-4">5.4 GET /agents/:code推薦人代理 <span class="pill partial">改造</span></h3>
<div class="endpoint-head"><span class="method get">GET</span> /api/agents/:code &nbsp;&nbsp; <span class="method get">GET</span> /api/identity/users/SearchUserByCodeAndType/{code}/true &nbsp;(+&nbsp;GetPlanList)</div> <div class="endpoint-head"><span class="method get">GET</span> /api/agents/:code &nbsp;&nbsp; <span class="method get">GET</span> /api/identity/users/SearchUserByCodeAndType/{code}/true</div>
<p>前端要 <code>{ success, found, data:{ code, name, tiers:[tag2Code…], email, phone } }</code>。後端 <code>SearchUserByCodeAndType</code><code>UserCode</code> 精確匹配,返回 <code>List&lt;AppUserDto&gt;</code><code>{Id, UserName, Email, Name, Surname, PhoneNumber, ExtraProperties}</code>。BFF 取首條做轉換;無匹配 → <code>found:false</code></p> <p>前端要 <code>{ success, found, data:{code, name} }</code>。後端Identity返回匹配到的用戶含 name/userName/extraProperties.code 等。BFF 取首條 → <code>found:true, data:{code, name}</code>;無匹配 → <code>found:false, data:null</code></p>
<table> <table>
<thead><tr><th>前端</th><th>後端來源AppUserDto</th><th>備註</th></tr></thead> <thead><tr><th>前端</th><th>後端用戶字段</th></tr></thead>
<tbody> <tbody>
<tr><td><code>data.code</code></td><td><code>ExtraProperties.UserCode</code></td><td>agent codeEF 屬性 UserCode</td></tr> <tr><td><code>data.code</code></td><td>用戶 codeagent code</td></tr>
<tr><td><code>data.name</code></td><td><code>Name</code>(+<code>Surname</code>) / <code>UserName</code></td><td>拼接展示名</td></tr> <tr><td><code>data.name</code></td><td><code>name</code> / <code>surname</code> / <code>userName</code></td></tr>
<tr><td><code>data.email</code></td><td><code>Email</code></td><td>「聯絡推薦人」用</td></tr> <tr><td>(下單用)<code>agentorId</code></td><td>用戶 <code>id</code>Guid→ 提交時作 <code>CreateOrderParam.AgentorId</code></td></tr>
<tr><td><code>data.phone</code></td><td><code>PhoneNumber</code></td><td>「聯絡推薦人」用</td></tr>
<tr><td>(下單用)<code>agentorId</code></td><td><code>Id</code>Guid</td><td>提交時作 <code>CreateOrderParam.AgentorId</code>;建議 BFF 在 <code>data</code> 內附帶或服務端緩存 code→id</td></tr>
<tr><td><code>data.tiers</code> ⚠️</td><td><b>後端無直接字段</b></td><td>見下「tiers 推導」</td></tr>
</tbody> </tbody>
</table> </table>
<div class="callout gap"> <div class="callout warn">
<p><span class="lbl">tiers 推導(本次新增):</span>plans-plus 用 <code>state.agent.tiers</code>tag2Code 列表)在「所屬行業選定的方案集」上<b>再過濾</b>——只顯示該代理可售級別。後端 agent 的可售方案由 <code>AMLPortal_AgentUserPlans</code> 建模,並由 <code>GetPlanList</code><code>agentUserId</code> 過濾(<code>PlanService.cs:87-92</code>)。⇒ BFF 兩種實現:<br> <p><span class="lbl">關鍵:</span>前端只展示 <code>name</code>,但下單需要 agent 的 <b>Guid id</b><code>CreateOrderParam.AgentorId</code>。BFF 應在 <code>/agents/:code</code> 響應內<b>順帶緩存 code→id</b>(或前端 <code>data</code> 內含 id否則 <code>/subscribe</code> 階段需再查一次。建議 <code>data</code> 增隱藏字段 <code>agentorId</code></p>
<b>推導 tiers</b>:再調一次 <code>GetPlanList{ agentUserId: agent.Id, tag1List:['B'] }</code>,取 distinct <code>tag2Code</code><code>tiers</code>(保持前端過濾邏輯);<br> <p>備註AMLPortal 另有 <code>Agentor/portal/GetAgentorByCode</code>,但已標【廢棄】;旧站用的是 Identity 的 <code>SearchUserByCodeAndType</code>,沿用之。</p>
<b>或改為服務端過濾</b><code>/plans/catalog</code> 帶上 <code>agentUserId</code> 直接返回該代理可售方案(則前端 tiers 過濾成空操作)。<br>
本地缺 <code>AgentUserPlan</code> 數據,<b>短期可讓 BFF 在 <code>tiers</code> 缺失時返回空數組 = 不過濾</b>(顯示該行業全部方案),與前端 <code>computePlanSet()</code> 對「tiers 為空不過濾」的處理一致。</p>
</div>
<div class="callout note">
<p>備註AMLPortal 另有 <code>Agentor/portal/GetAgentorByCode</code>/<code>GetAgentorByName</code>,旧站與 plans-plus 均用 Identity 的 <code>SearchUserByCodeAndType</code>,沿用之。</p>
</div> </div>
<!-- 5.5 --> <!-- 5.5 -->
<h3 id="s5-5">5.5 GET /tenants/lookup按郵箱查租戶<span class="pill todo">需後端新增端點</span></h3> <h3 id="s5-5">5.5 GET /tenants/lookup續費查租戶 <span class="pill todo">缺口大</span></h3>
<div class="endpoint-head"><span class="method get">GET</span> /api/tenants/lookup?email= &nbsp;&nbsp; <span class="method new">NEW</span> /api/amlPortal/Order/portal/<b>queryRenewableTenantByEmail</b></div> <div class="endpoint-head"><span class="method get">GET</span> /api/tenants/lookup?email=&amp;name= &nbsp;&nbsp; <span class="method post">POST</span> /api/amlPortal/Order/portal/queryRenewableTenant</div>
<p>前端按<b>郵箱</b>查租戶,返回 <code>match:'none'|'unique'</code> 兩態(<b>不再</b>多租戶消歧——註釋明確「租戶名與郵箱均全局唯一 → 一郵箱至多 1 個租戶」)。命中後帶出 <code>currentSubscription</code> + <code>referrer</code> 預填續費/加購表單。</p> <p>前端按<b>郵箱</b>查租戶支持「none / unique / multiple」三態命中後帶出 <code>currentSubscription</code> 預填續費表單。但後端 <code>queryRenewableTenant</code> 只接受 <code>{keyword}</code> 且按 <b><code>TenantName.Contains(keyword)</code></b> 搜索(<code>OrderService.cs:2363</code>),返回 <code>List&lt;TenantPropertyDto&gt;</code>(內部已把 admin 郵箱填入 <code>TenantAdminEmail</code>)。</p>
<p>但現有後端 <code>queryRenewableTenant</code> 只接受 <code>{keyword}</code> 且按 <b><code>TenantName.Contains(keyword)</code></b> 在 DB 層搜索(<code>OrderService.cs:2362-2364</code>),返回 <code>List&lt;TenantPropertyDto&gt;</code>——<code>TenantAdminEmail</code> 是查完命中租戶後、再 <code>GetAllUsers()</code> 遍歷<b>全庫用戶</b><code>UserName=='admin' &amp;&amp; TenantId</code> 逐條回填(<code>:2369-2379</code><b>邏輯上「先有租戶、後有郵箱」,無法在 DB 層按郵箱過濾</b>。⇒ 前端「給郵箱、要唯一命中」的查詢維度與現端點<b>根本錯位</b></p> <h4>兩個不匹配點</h4>
<div class="callout gap"> <ol class="tight" style="padding-left:20px">
<p><span class="lbl">已定決策:後端新增「按管理員郵箱查可續費租戶」端點。</span>不採用 BFF 變通(以空 <code>keyword</code> 拉全部可續費租戶、再服務端按 <code>TenantAdminEmail===email</code> 過濾)——現端點按<b>租戶名</b>搜、返回<b>整張可續費租戶列表</b>,讓 BFF 承接全表列表並自拼 <code>currentSubscription</code>/<code>referrer</code> 的訂單反查,既非其職責、也易錯;<b>查詢維度與聚合歸屬都更該落在後端</b><b>(注:問題不在掃描成本——後端側按郵箱全庫掃描本身可接受,租戶數少、低頻交互;而在查詢維度錯位與聚合歸屬。)</b>新端點把查詢維度反過來(<b>先按郵箱定位 admin 用戶 → 再取其租戶</b>),順帶一次性聚合 plans-plus 續費/加購頁所需的 <code>currentSubscription</code> + <code>referrer</code>,避免 BFF 拼裝易錯的訂單反查。</p> <li><b>查詢維度不符</b>:前端給郵箱,後端按名字搜。<br>
</div> 短期 BFF 變通:以空/寬 keyword 拉取後在服務端按 <code>TenantAdminEmail===email</code> 過濾並按郵箱分組(⚠️ 全量拉取、性能與越權風險,僅臨時)。<br>
<h4>新增端點設計(供後端實作參照)</h4> 正解:<b>後端新增「按管理員郵箱查可續費租戶」端點</b>(返回同 <code>TenantPropertyDto</code> 結構)。</li>
<div class="endpoint-head"><span class="method new">NEW</span> <span class="method post">POST</span> /api/amlPortal/Order/portal/queryRenewableTenantByEmail &nbsp;<span class="tag">[AbpAutoAuth("Portal")]</span></div> <li><b>currentSubscription 需重建</b>:前端的 <code>currentSubscription</code>planId、name、periodMonths、price、qCount、userCountLimit、startDate、expiryDate、usedQuota、addons並非 <code>TenantPropertyDto</code> 直接字段,需由 TenantProperty + 其 <code>Order</code> + 正在生效的 plan 推導。可參考 <code>GetCurrServiceInfo</code><code>CurrServiceInfoDto.ActiveBPlans</code>、EffectiveEndTime、QCountLeft 等)。</li>
<ul class="tight"> </ol>
<li><b>入參</b> <code>QueryRenewableTenantByEmailParam { string Email }</code>(新 DTO置於 <code>Application.Contracts/OrderAppLayer/</code>,與 <code>QueryRenewableTenantParam</code> 並列)。鑑權沿用現 <code>queryRenewableTenant</code><code>[AbpAutoAuth("Portal")]</code>BFF 免自帶 token見第 6 章)。</li>
<li><b>反查邏輯(維度反轉,且省掉全庫掃描)</b><br>
① 禁用多租戶過濾 <code>_dataFilter.Disable&lt;IMultiTenant&gt;()</code>按郵箱定位租戶管理員用戶——沿用現端點「admin 郵箱 = <code>UserName=='admin'</code> 用戶的 Email」語義可直接<b>復用現 <code>GetAllUsers()</code> 全庫掃描</b>(與現 <code>queryRenewableTenant</code> 一致),改在內存按 <code>u.UserName=='admin' &amp;&amp; u.Email==Email</code> 過濾即可(郵箱全局唯一 → 至多 1 條)。<b>全庫掃描在此可接受</b>——租戶 / 管理員用戶數量少、續費查詢為低頻交互,<b>無需為此下推 DB 過濾或加索引</b>。無命中 → 回空 ⇒ 前端 <code>match:'none'</code><br>
② 取該用戶 <code>TenantId</code> 對應的 <b>active</b> <code>TenantProperty</code><code>IsActive &amp;&amp; TargetTenantID==tenantId</code>,按 <code>CreationTime</code> 取最新一條),並校驗其可續費(<code>TenantStatus</code> / <code>EffectiveEndTime</code>——過期仍返回,由前端做「過期/臨期」展示,見 5.7 topup 阻斷)。<br>
③ 組裝聚合返回:<code>referrer</code> 直取 <code>AgentorCode/AgentorName</code><code>currentSubscription</code> 明細planId / 名稱 / periodMonths / price / addons沿用 <code>GetCurrServiceInfo</code><code>OrderService.cs:398-418</code>)的 <b>Order → OrderDetails → Plan/PlanDetail</b> 反查思路,區別是它以 <code>CurrentTenant.Id</code> 為基、此處以顯式 <code>TargetTenantID</code> 為基Portal 上下文 + 禁多租戶過濾)。</li>
<li><b>返回</b>:建議新增聚合 DTO<code>RenewableTenantDto</code> = <code>TenantPropertyDto</code> 關鍵字段 + <code>referrer</code> + 明細化 <code>currentSubscription</code>),讓 BFF 幾乎<b>直通轉發</b>,不必自己再拼訂單明細。</li>
</ul>
<h4>TenantPropertyDto → 前端 Tenant 映射(可得部分)</h4> <h4>TenantPropertyDto → 前端 Tenant 映射(可得部分)</h4>
<table> <table>
<thead><tr><th>前端</th><th>TenantPropertyDto</th><th>備註</th></tr></thead> <thead><tr><th>前端</th><th>TenantPropertyDto</th><th>備註</th></tr></thead>
@ -439,151 +357,46 @@ POST /single-query body = {
<tr><td><code>tenantId</code></td><td><code>TargetTenantID</code></td><td></td></tr> <tr><td><code>tenantId</code></td><td><code>TargetTenantID</code></td><td></td></tr>
<tr><td><code>tenantName</code></td><td><code>TenantName</code></td><td></td></tr> <tr><td><code>tenantName</code></td><td><code>TenantName</code></td><td></td></tr>
<tr><td><code>editionId</code>/<code>editionName</code></td><td><code>EditionId</code>/<code>EditionName</code></td><td></td></tr> <tr><td><code>editionId</code>/<code>editionName</code></td><td><code>EditionId</code>/<code>EditionName</code></td><td></td></tr>
<tr><td><code>referrer</code></td><td><code>{AgentorCode, AgentorName}</code></td><td>新增映射</td></tr>
<tr><td><code>currentSubscription.expiryDate</code></td><td><code>EffectiveEndTime</code></td><td>過期判定(前端 daysUntil</td></tr> <tr><td><code>currentSubscription.expiryDate</code></td><td><code>EffectiveEndTime</code></td><td>過期判定(前端 daysUntil</td></tr>
<tr><td><code>currentSubscription.startDate</code></td><td><code>EffectiveStartTime</code></td><td>未過期續費 → 默認接續此日</td></tr> <tr><td><code>currentSubscription.startDate</code></td><td><code>EffectiveStartTime</code></td><td>未過期續費 → 默認接續此日</td></tr>
<tr><td><code>currentSubscription.qCount</code>/<code>usedQuota</code></td><td><code>QCountPurchase</code> / <code>QCountPurchaseQCountLeft</code></td><td>已用 = 購買 剩餘</td></tr> <tr><td><code>currentSubscription.qCount</code>/<code>usedQuota</code></td><td><code>QCountPurchase</code> / <code>QCountPurchaseQCountLeft</code></td><td>已用 = 購買 剩餘</td></tr>
<tr><td><code>currentSubscription.userCountLimit</code></td><td><code>UserCountLimit</code></td><td></td></tr> <tr><td><code>currentSubscription.userCountLimit</code></td><td><code>UserCountLimit</code></td><td></td></tr>
<tr><td><code>currentSubscription.addons.kyc</code></td><td><code>EnableKYC</code></td><td>topup 時鎖「已購買」</td></tr> <tr><td><code>currentSubscription.addons.kyc</code></td><td><code>EnableKYC</code></td><td>topup 時鎖「已購買」</td></tr>
<tr><td><code>currentSubscription.planId</code>/<code>name*</code>/<code>price</code>/<code>periodMonths</code></td><td><code>Order</code>/<code>OrderDetail</code> 反查</td><td><b>推導</b>,續費續價(沿用上期 price依賴此</td></tr> <tr><td><code>currentSubscription.planId</code>/<code>name*</code>/<code>price</code></td><td><code>Order</code>/<code>OrderDetail</code> 反查</td><td><b>推導</b>,續費續價(沿用上期 price依賴此</td></tr>
<tr><td><code>currentSubscription.addons.users</code>/<code>jquotaPackageId</code></td><td>經訂單明細反查</td><td><b>推導</b></td></tr> <tr><td><code>currentSubscription.addons.users</code>/<code>jquotaPackageId</code></td><td>經訂單明細反查</td><td><b>推導</b></td></tr>
</tbody> </tbody>
</table> </table>
<div class="callout note"> <div class="callout gap">
<p><span class="lbl">聚合由誰拼裝:</span>上表「可得部分」是 <code>TenantPropertyDto</code> 直出字段;而 <code>currentSubscription</code><b>planId / 名稱 / 上期 price / periodMonths</b><b>addons.users / jquotaPackageId</b> 均需經 <code>Order → OrderDetails</code>(按 <code>Tag1Code</code>B=基礎、AdlU=增購用戶、jQ/j=jQuota、KYC反查、<code>usedQuota = QCountPurchase QCountLeft</code>。按本次決策,<b>這段反查應落在新端點 <code>queryRenewableTenantByEmail</code> 內(服務端一次算好)</b>,而非 BFF——後端已有 <code>GetCurrServiceInfo</code><code>OrderService.cs:398</code>)反查 <code>ActiveBPlans</code> 的現成範式可借用只需擴出加值項明細與上期價。BFF <code>routes/tenants.js</code> 因此退化為<b>字段直通 + 命中態none/unique包裝</b></p> <p><span class="lbl">建議後端改動:</span>新增端點 <code>queryRenewableTenantByEmail(email[, tenantName])</code>,直接返回 plans-plus 所需的「租戶 + currentSubscription含 planId/上期價/已購加值項)」聚合結構,避免在 BFF 拼裝易錯的訂單反查</p>
</div> </div>
<!-- 5.6 --> <!-- 5.6 -->
<h3 id="s5-6">5.6 單次查詢 single-query隨付即用 · Pay-As-You-Go <span class="pill partial">後端已支持 · 後端內部編排</span></h3> <h3 id="s5-6">5.6 POST /subscribe下單 <span class="pill partial">改造</span></h3>
<div class="endpoint-head"><span class="method get">GET</span> /api/single-query/options固定展示 &nbsp;·&nbsp; <span class="method post">POST</span> /amlPortal/plan/portal/GetPlanList <span class="small">{countryCode}PAYG 定價)</span> &nbsp;·&nbsp; <span class="method post">POST</span> /api/single-query &nbsp;<span class="small">(收單 + 支付)</span>&nbsp;⟶ 支付完成 ⟶&nbsp; <span class="tag">AML 後端內部</span> CreateConsumerLink → CreateConsumerOrder</div>
<div class="callout newreq">
<p><span class="lbl">本輪定案PAYG 定價 + 固定檢測項):</span>隨付即用的<b>檢測內容固定為 4 項、純信息展示,用戶不可勾選/取消</b>——<code>證件核驗</code><code>名單篩查(即 ES 檢測)</code><code>AI 增強型篩查</code><code>信貸記錄篩查(即失信人檢測)</code>(叫法後續可能再調整),確定性映射後端功能碼 <code>V / E / A / D</code>(無 <code>S</code> 風險評估問卷、<code>R</code> CDD 報告、<code>O</code> OCR<b>收款金額不再由 V2 分項單價相加,而是取自 AML <code>GetPlanList</code><code>countryCode</code> 返回的 PAYG(P2G) 方案價</b><code>/plans-jp</code> 頁傳 <code>countryCode:"JPN"</code><b>目前 <code>countryCode</code> 僅日本一國</b>,前端依 URL/地域推導),該價為<b>單一整包價</b>(非分項之和)。⇒ 原「op→功能碼非 1:1、公司查冊無獨立碼、V2 分項單價與後端 jQ 對齊」三處缺口<b>收斂</b>:檢測項固定 4 = E/A/V/D定價口徑統一到 <code>GetPlanList</code></p>
</div>
<div class="callout ok">
<p><span class="lbl">本輪更正(原「後端零支持」結論作廢;並釐清調用方):</span>單次查詢由 <b>AML 主模塊的 ConsumerPortal「即時檢測」</b>承接(<code>iCON.Abp.AML</code>)。<b>調用方已確認:這兩步不是 V2 BFF、也不是瀏覽器發起而是 AML 後端在「訂單支付完成後」、於 2C 租戶上下文<u>內部編排</u>執行</b>——<code>CreateConsumerLink</code>(建鏈接拿 <code>accessToken</code>)→ <code>CreateConsumerOrder</code>(建訂單、即時檢測、結果郵件)。<b>justsolutionsWebV2 的職責止於「下單 + 收款」,完全不觸碰這兩個接口。</b></p>
</div>
<div class="callout note">
<p><span class="lbl">為何不能放在 V2 BFF</span><code>CreateConsumerLink</code> <b>非匿名</b><code>[Authorize(RealtimeScreeningManagement)]</code> + <code>[RequiresFeature(AML.ConsumerPortal.Enable)]</code><code>ConsumerPortalController.cs:337-340</code>),且在 <code>CurrentTenant</code> 下建鏈接。V2 BFF 只持「門戶訪客」憑證(<code>__tenant=Portal</code>),既無實時篩查權限、也非 2C 租戶上下文 ⇒ 調不動。放到後端內部後2C 租戶上下文與功能開關天然滿足,<b>鑑權不再是 BFF 的問題</b><code>CreateConsumerOrder</code> 雖是 <code>[AllowAnonymous]</code><code>:76-78</code>,憑 <code>accessToken</code>),但既然第 1 步已在後端,第 2 步同處後端內部串接最自然。</p>
</div>
<h4>職責邊界與調用鏈</h4>
<div class="flow">V2 · 瀏覽器 + BFF
&nbsp;&nbsp;GET /api/single-query/options ……… 查詢項目目錄V2 自定義,見 3.1
&nbsp;&nbsp;POST /api/single-query ………………… 建「待支付」單次查詢單(存 郵箱/主體/所選項目/金額)→ orderId
&nbsp;&nbsp;POST /api/payments/create …………… 收款(見 5.9;本輪支付除外)
──────────────── 支付完成webhook / 回調)────────────────
AML 後端 · 2C 租戶上下文內部編排](非 V2、非瀏覽器發起
&nbsp;&nbsp;固定 4 項檢測 → functionCodes = E/A/V/D證件核驗V·名單篩查ES=E·AI增強A·失信D無 S/R/O
&nbsp;&nbsp;① CreateConsumerLink { 郵箱, functionCodes:"EAVD", validHours } → accessToken
&nbsp;&nbsp;② CreateConsumerOrder { accessToken, objectType, individual|organization, functionCodes:["E","A","V","D"] }
&nbsp;&nbsp;→ 建訂單(直接 Paid) + 每碼一條 DetectionTask → MQ 異步跑檢測 → 完成發結果郵件</div>
<div class="callout warn">
<p><span class="lbl">待後端明確的銜接點(由後端定義,非 V2 落地):</span>「支付完成」如何把<b>單次查詢單的資料(郵箱/主體/所選項目)+ 支付結果</b>交給後端這段內部編排,常見兩種接法:<br>
<b>後端持單</b>V2 在 <code>POST /single-query</code> 時即把單推給後端一個「2C 收單/意向」端點落庫;支付 webhook 直接打到<b>後端</b>,後端據單觸發 ①②。<br>
<b>V2 持單、支付後通知後端</b>V2 收到支付成功後,調後端一個<b>專門的 2C 觸發端點</b>(其鑑權/形態由後端定義),該端點內部再跑 ①②。<br>
無論哪種,<b>①② 本身都是後端內部行為</b>V2 始終不直接調 <code>CreateConsumerLink</code>/<code>CreateConsumerOrder</code></p>
</div>
<h4>後端內部兩步payload 供參考)</h4>
<p><b>第 1 步 · 建即時檢測鏈接</b> <code>CreateConsumerLink</code><code>ConsumerPortalService.cs:964</code>以「結果接收郵箱」upsert <code>ConsumerCustomer</code>,在當前 2C 租戶下建鏈接 → 回 <code>ConsumerLinkDto</code>(含 <code>accessToken</code>)。</p>
<pre><code>{ "createConsumerCustomerDto": { "email":"result@company.com", "name":null, "phone":null, "remark":null },
"createConsumerLinkDto": { "functionCodes":"EAVD", "validHours":72,
"validEndTime":"2026-07-09T06:00:47.000Z" } }
// 回OperationDto.Success(ConsumerLinkDto{ accessToken, functionCodes, expireTime, tenantId, … })</code></pre>
<p><b>第 2 步 · 建訂單並即時檢測</b> <code>CreateConsumerOrder</code><code>:325</code>):據 token 定位鏈接與租戶 → 計價(jQ) → 建 Individual/Organization 實體 → 建 <code>ConsumerPortalOrder</code><b>直接置 PaymentStatus=Paid、OrderStatus=Pendding</b><code>:388</code>)→ 每個 functionCode 建一條 <code>DetectionTask</code> → 發 RabbitMQ 消息,由 <code>ProcessConsumerPortalOrder</code> <b>異步</b>跑檢測模塊,完成後 <code>SendConsumerLinkResultMail</code> 發結果郵件。</p>
<pre><code>{ "accessToken":"xxx", "objectType":"Individual",
"individual": { "fullNameEN":"张三", "fullNameZH":"", "dateOfBirth":{"year":0,"month":0,"day":0},
"address":"", "addressOfLiving":"", "nationalityCode":null,
"documentRequiredFileIds":[], "identityDocumentTypeCode":"", "identityDocumentNumber":"",
"gender":"", "phone":"", "email":"", "occupation":"", "entityIdentityDocuments":[] },
"organization": null,
"functionCodes": ["E","A","V","D"] } // ← 固定 4 項PAYG 檢測內容不可選)
// 回OperationDto.Success({ entity, order }) // order.Id / order.OrderCode 可作結果查詢鍵</code></pre>
<h4>V2 收集字段 ↔ 後端入參映射</h4>
<p class="small">V2 通過 <code>POST /single-query</code>(或銜接端點)把下列字段交給後端;後端在內部編排時據此組裝上面兩個 payload。</p>
<table>
<thead><tr><th>V2 收集字段</th><th>後端入參</th><th>說明</th></tr></thead>
<tbody>
<tr><td><code>email</code>(結果接收郵箱)</td><td><code>createConsumerCustomerDto.email</code></td><td>必填;後端按 email upsert <code>ConsumerCustomer</code>,並作結果郵件收件人</td></tr>
<tr><td><code>subjectType</code> <code>'individual'|'company'</code></td><td><code>objectType</code> <code>'Individual'|'Organization'</code></td><td>枚舉 <code>ObjectTypeEnum</code>Individual=0 / Organization=1company→Organization</td></tr>
<tr><td><code>subject</code>(單一名稱字段)</td><td><code>individual.fullNameEN</code><code>organization.fullNameEN</code></td><td>⚠️ V2 只收一個名稱;後端 <code>CreateIndividualDto/CreateOrganizationDto</code> 字段眾多但均可空 → <b>最小可用只填全名</b>,其餘留空/默認</td></tr>
<tr><td>固定 4 項檢測V2 不收選擇)</td><td>兩處 <code>functionCodes</code>(鏈接=字符串 <code>"EAVD"</code>、訂單=數組 <code>["E","A","V","D"]</code></td><td><b>固定映射</b>證件核驗→V、名單篩查ES→E、AI 增強→A、失信→D兩步一致</td></tr>
<tr><td>(鏈接有效期)</td><td><code>createConsumerLinkDto.validHours</code></td><td>後端據此算 <code>ExpireTime</code><b>樣例中的 <code>validEndTime</code> 後端 DTO 無此字段、被忽略</b></td></tr>
</tbody>
</table>
<div class="callout ok">
<p><span class="lbl">功能碼映射(本輪固定,非 1:1 問題消除):</span>PAYG 檢測內容<b>固定 4 項</b>、用戶不可選,直接確定性對應後端功能碼(後端只認 <b>E/A/V/D/S/R</b>O=OCR 排除PAYG 僅用前 4 個):</p>
<ul class="tight">
<li><code>證件核驗</code><code>V</code>(證件核驗)</li>
<li><code>名單篩查(即 ES 檢測)</code><code>E</code>ES 實體篩查)</li>
<li><code>AI 增強型篩查</code><code>A</code>AI 檢測) <span class="small">E、A 在後端合併為一次 <code>EntitiesInvestigation</code> 計費,<code>ConsumerPortalService.cs:277</code></span></li>
<li><code>信貸記錄篩查(即失信人檢測)</code><code>D</code>(失信查詢)</li>
</ul>
<p>⇒ 原「多個 op 坍縮為同一 E/A 計費」「公司查冊無獨立碼」的映射難點<b>不復存在</b>新方案無「公司查冊」項S/R 不在 PAYG<b>定價口徑亦統一</b>V2 收款金額 = <code>GetPlanList</code><code>countryCode</code> 的 PAYG(P2G) 方案價,不再與後端逐碼 jQ 扣費逐項對賬——後端 <code>CreateConsumerOrder</code> 仍按 2C 租戶 jQ 計費/扣減(僅 CHN/HKG 計 KYC jQ<code>:297</code>),但那是後端內部帳,與 V2 對外報價解耦。</p>
</div>
<h4>鑑權(均在後端內部滿足)</h4>
<table>
<thead><tr><th>方法</th><th>鑑權</th><th>後端內部編排下的落地</th></tr></thead>
<tbody>
<tr><td><code>CreateConsumerLink</code></td><td><b>非匿名</b><code>[Authorize(RealtimeScreeningManagement)]</code> + <code>[RequiresFeature(AML.ConsumerPortal.Enable)]</code><code>ConsumerPortalController.cs:337-340</code></td><td>在後端 2C 租戶上下文內執行 ⇒ <b>租戶上下文與功能開關天然滿足,無需 V2 憑證</b>。待確認的只是「用哪個 2C 租戶」及其 ConsumerPortal 功能/權限已開</td></tr>
<tr><td><code>CreateConsumerOrder</code></td><td><b>匿名</b> <code>[AllowAnonymous]</code><code>:76-78</code></td><td><code>accessToken</code> 定位租戶;後端內部緊接第 1 步串行調用</td></tr>
</tbody>
</table>
<h4>結果回取(可選)</h4>
<ul class="tight">
<li>檢測<b>異步</b>完成後後端自動發結果郵件(滿足 V2「結果將發送至此郵箱」文案V2 可不輪詢。</li>
<li>若 V2 要頁內展示:<code>POST /api/aml/ConsumerPortal/Get2CRecordDetail/{accessToken}</code><code>/Get2CRecordDetailByOrderId/{orderId}</code>(均匿名)取檢測記錄;<code>/CheckAndGenerateReport</code> 生成 CDD 報告 PDF。<span class="small">(此類<b>只讀</b>回取因匿名,才可能由 V2/瀏覽器直取。)</span></li>
</ul>
<div class="callout gap">
<p><span class="lbl">仍存在的缺口 / 落地要點:</span></p>
<p><b>銜接點</b>V2「支付完成」與後端內部編排的對接上方 ①/② 兩種接法)由後端定義端點/webhook 承接。</p>
<p><b>op→功能碼映射</b><span class="pill done">已收斂</span>:檢測內容固定 4 項、確定性映射 <code>E/A/V/D</code>(無「公司查冊」、無 S/RV2 只需固定傳 <code>functionCodes="EAVD"</code>(或後端直接寫死該 4 項)。</p>
<p><b>2C 租戶</b>:確認/建立隨付即用專用租戶、開 <code>AML.ConsumerPortal.Enable</code> 功能、配實時篩查權限——供後端內部編排落上下文(<b>不再需要在 V2 BFF 配該租戶憑證</b>)。</p>
<p><b>對外定價</b><span class="pill done">已確認</span>V2 收款金額 = <code>GetPlanList</code><code>countryCode</code><code>/plans-jp→JPN</code>)返回的 PAYG(P2G) 方案價,<b>為單一整包價</b>(非分項之和),前端載入期取價、純展示 4 項檢測。後端 <code>CreateConsumerOrder</code> 仍把訂單直接置 <code>Paid</code> 並按 2C 租戶 jQ 扣費(<code>GetPriceTwoC</code>/線上支付分支已注釋 <code>:196</code>)——屬後端內部帳,與 V2 對外報價解耦。<b>現階段 <code>countryCode</code> 僅日本JPN</b>:即只有 <code>/plans-jp</code> 有 PAYG 定價,其餘國家待種 P2G 方案 + PlanDetail 後再開(機制沿用,不需改前端契約)。</p>
<p><b>本地環境</b>:需該租戶 + 檢測引擎(iCS)/RabbitMQ 就緒,<code>ProcessConsumerPortalOrder</code> 才能真正跑出結果;否則本地 single-query 可繼續走 mock僅在對接環境驗證真調用。</p>
</div>
<div class="callout note">
<p><span class="lbl">V2 側建議形態:</span><code>/single-query/options</code> 收斂為<b>固定 4 項純展示</b>(可前端硬編碼或後端返回固定項,<b>不含 price</b>PAYG 收款價由前端載入期<b>依 URL/地域推導 <code>countryCode</code> 調 <code>GetPlanList</code></b>取 PAYG(P2G) 方案價(或經 BFF <code>/plans/catalog</code> 帶 countryCode 一併取回)。<code>/single-query</code>(收單,返回 orderId契約不變<b>不新增對 ConsumerPortal 的代理路由</b>。V2 只需把「支付完成」與後端銜接(見上 ①/②),<b>兩步真調用全部在後端內部</b>——前端與 BFF 無需感知 CreateConsumerLink/CreateConsumerOrder<b>不傳檢測項選擇</b>(固定 4 項在後端寫死或由 V2 固定傳 <code>EAVD</code>)。</p>
</div>
<!-- 5.7 -->
<h3 id="s5-7">5.7 POST /subscribe下單 <span class="pill partial">改造</span></h3>
<div class="endpoint-head"><span class="method post">POST</span> /api/subscribe &nbsp;&nbsp; 按 type 分流</div> <div class="endpoint-head"><span class="method post">POST</span> /api/subscribe &nbsp;&nbsp; 按 type 分流</div>
<p>BFF 依 <code>body.type</code> 分流;核心是<b>組裝 <code>PlanList</code></b>(基礎方案 + 各加值項,每項補 <code>planDetailId</code>)。</p> <p>BFF 依 <code>body.type</code> 分流到不同後端端點並把前端的「人類友好」payload 翻成後端 DTO核心是<b>組裝 <code>PlanList</code></b>(基礎方案 + 各加值項,每項補 <code>planDetailId</code>)。</p>
<h4>① type='new' → CreateOrder</h4> <h4>① type='new' → CreateOrder</h4>
<table> <table>
<thead><tr><th>CreateOrderParam</th><th>前端 payload</th><th>備註</th></tr></thead> <thead><tr><th>CreateOrderParam</th><th>前端 payload</th><th>備註</th></tr></thead>
<tbody> <tbody>
<tr><td><code>PlanList[]</code></td><td>plan + addons</td><td>見下「PlanList 組裝」</td></tr> <tr><td><code>PlanList[]</code></td><td>plan + addons</td><td>見下「PlanList 組裝」</td></tr>
<tr><td><code>TenantName</code></td><td><code>company</code></td><td>必填;後端校驗重名<code>:183</code></td></tr> <tr><td><code>TenantName</code></td><td><code>company</code></td><td>必填;後端校驗重名</td></tr>
<tr><td><code>TenantAdminEmail</code></td><td><code>email</code></td><td>必填;後端 <code>CheckEmailExists</code> 校驗未註冊(<code>:159</code></td></tr> <tr><td><code>TenantAdminEmail</code></td><td><code>email</code></td><td>必填;後端再校驗未註冊</td></tr>
<tr><td><code>Jurisdiction</code></td><td><code>jurisdiction</code></td><td>必填<code>:153</code></td></tr> <tr><td><code>Jurisdiction</code></td><td><code>jurisdiction</code></td><td>必填</td></tr>
<tr><td><code>OrganizationReference</code></td><td><code>edition</code>Guid</td><td>必填;校驗 edition 存在<code>:200</code></td></tr> <tr><td><code>OrganizationReference</code></td><td><code>edition</code>Guid</td><td>必填;校驗 edition 存在</td></tr>
<tr><td><code>OrganizationBR</code> / <code>OrganizationCI</code></td><td><code>br</code> / —(前端不收 CI</td><td><span class="pill done">非必填</span> <b>BR/CI 已定非必填</b>,空值可下單(見下)</td></tr> <tr><td><code>OrganizationBR</code> / <code>OrganizationCI</code></td><td><code>br</code> / —</td><td><span class="pill done">已改</span> 後端已去除「BR 或 CI 至少一個」必填校驗(<code>OrderService.cs:154,190</code> 注釋),可不填;唯一性校驗保留,僅在實際填寫時生效</td></tr>
<tr><td><code>EffectiveStartTime</code></td><td><code>startDate</code></td><td></td></tr> <tr><td><code>EffectiveStartTime</code></td><td><code>startDate</code></td><td></td></tr>
<tr><td><code>ContactPerson</code></td><td><code>contact</code></td><td>個人主體 = <code>company</code></td></tr> <tr><td><code>ContactPerson</code></td><td><code>contact</code></td><td></td></tr>
<tr><td><code>CompanyAddress</code></td><td><code>address</code></td><td>可選(個人主體為空)</td></tr> <tr><td><code>CompanyAddress</code></td><td><code>address</code></td><td>可選</td></tr>
<tr><td><code>Phone</code></td><td><code>phoneCode + phone</code></td><td>可選BFF 拼接</td></tr> <tr><td><code>Phone</code></td><td><code>phoneCode + phone</code></td><td>可選BFF 拼接</td></tr>
<tr><td><code>AgentorId</code></td><td><code>agentCode</code> 解析的 Guid</td><td>見 5.4;空則後端指派頂級 salesAdmin<code>:283-287</code></td></tr> <tr><td><code>AgentorId</code></td><td><code>agentCode</code> 解析的 Guid</td><td>見 5.4</td></tr>
<tr><td><code>AdminPassword</code></td><td></td><td><b>忽略</b>(後端用默認密碼,發激活郵件)</td></tr> <tr><td><code>AdminPassword</code></td><td></td><td><b>忽略</b>(後端用默認密碼,發重置郵件)</td></tr>
<tr><td><code>IsOfflinePayment</code>/<code>IsAgentBehalf</code></td><td></td><td>在線下單固定 <code>false</code></td></tr> <tr><td><code>IsOfflinePayment</code>/<code>IsAgentBehalf</code></td><td></td><td>固定 <code>false</code></td></tr>
</tbody> </tbody>
</table> </table>
<div class="callout ok"> <div class="callout ok">
<p><span class="lbl">BR/CI 非必填(已定決策):</span>plans-plus 新購允許空 BR/CI——含 <b>corp 主體</b>BR 維持「可選」、可空)與 <b>individual 主體</b><code>br</code> 恒空、無 CI。實現注释 <code>CreateOrder</code> <code>OrderService.cs:154</code><code>:190</code> 兩處必填校驗(<code>if (BR 空 &amp;&amp; CI 空) return "…required"</code>);緊隨的唯一性校驗 <code>ExistsByOrganizationBRCI</code> 對「BR、CI 皆空」返回 <code>false</code><code>:1124</code><b>空值安全通過,無需其它改動</b></p> <p><span class="lbl">BR/CI 已放開2026-06</span>後端 <code>CreateOrder</code> 原有兩處「BR 或 CI 至少一個」必填校驗(<code>OrderService.cs:154</code><code>:190</code>已注釋去除plans-plus 可不填 BR/CI 直接下單。<b>保留</b>緊隨其後的唯一性校驗 <code>ExistsByOrganizationBRCI</code><code>OrderService.cs:191</code>)——兩者皆空時返回 <code>false</code> 放行填寫時仍攔重複。DB 列可空(<code>nvarchar(max)</code>),無需遷移。</p>
<p>⚠️ <b>核對現狀</b>:當前 working copy 的 <code>:154</code>/<code>:190</code> 仍為 <b>active</b>;若對接/部署環境尚未放開,空 BR/CI 會被攔,需先應用該改動。</p> <p><span class="lbl">影響:</span>① 改動為<b>全局</b>,旧站共用 <code>CreateOrder</code> 同樣放開(旧站前端仍自填 BR行為不變② 空 BR 不再參與去重,原「同公司只能註冊一次」對空 BR 失效;③ 下游 <code>TenantInfo</code><code>OrderService.cs:791</code>)、偵測報告(<code>ReportService.cs:276</code>)僅透傳展示、可容忍空值,但合規/STR 報告該欄會留白,需業務確認可接受。<b>續費/加購走 <code>TenantRenewal</code>BR 繼承自上期訂單,不受影響。</b></p>
</div> </div>
<h4>② type='renew' → TenantRenewal</h4> <h4>② type='renew' → TenantRenewal</h4>
@ -592,15 +405,18 @@ POST /single-query body = {
<tbody> <tbody>
<tr><td><code>TargetTenantID</code></td><td><code>tenantId</code></td></tr> <tr><td><code>TargetTenantID</code></td><td><code>tenantId</code></td></tr>
<tr><td><code>PlanList[]</code></td><td>plan + addons同下組裝</td></tr> <tr><td><code>PlanList[]</code></td><td>plan + addons同下組裝</td></tr>
<tr><td><code>TenantAdminEmail</code></td><td><code>email</code>(後端校驗與租戶 admin 郵箱一致 <code>:2239</code></td></tr> <tr><td><code>TenantAdminEmail</code></td><td><code>email</code></td></tr>
<tr><td><code>AgentorId</code></td><td>空 → 延續原代理<code>:2321-2323</code></td></tr> <tr><td><code>AgentorId</code></td><td>空 → 延續原代理</td></tr>
<tr><td><code>IsOfflinePayment</code>/<code>IsAgentBehalf</code></td><td>固定 <code>false</code></td></tr> <tr><td><code>IsOfflinePayment</code>/<code>IsAgentBehalf</code></td><td>固定 <code>false</code></td></tr>
</tbody> </tbody>
</table> </table>
<p class="small">續費續價:後端按 planDetail 的 <code>Price</code> 計;前端的「續費續價(沿用上期 price」屬展示層優惠<b>後端不認前端傳入的 price</b>。若要真正續價,需後端支持自定義價 / 專屬續費 PlanDetail——本輪按目錄價下單前端續費折扣僅為展示待業務確認</p>
<h4>③ type='topup' → TenantRenewal僅加值項 <span class="pill newreq">需後端確認語義</span></h4> <h4>③ type='topup' → <span class="pill newreq">新增需求 · 無對應端點</span></h4>
<p>純加購(無基礎方案,只買 增加用戶/KYC/jQuota。後端無專屬端點。<b>候選 A推薦</b>:復用 <code>TenantRenewal</code><code>PlanList</code> 只含加值項(不含 B 類)。源碼上 <code>TenantRenewal</code> 只對 <code>Tag1Code==='B'</code> 的行設服務起止期(<code>:2300-2314</code>),非 B 行只疊加配額/用戶 ⇒ 理論上「只疊配額不延期」語義成立,但需確認 <code>ProcessTenantEventQueue</code> 下游對「無 B 行的續費單」處理正確。<b>候選 B</b>:後端新增 <code>TenantTopup</code>(可借鑒 admin 的 <code>UpdateTenantProperty</code> <code>:2389</code>,它就是疊 PlanList 重算配額)。</p> <p>純加購(無基礎方案,只買 增加用戶/KYC/jQuota。後端無直接端點。候選方案</p>
<ul class="tight">
<li><b>A推薦</b>:復用 <code>TenantRenewal</code><code>PlanList</code> 只含加值項(不含 B 類基礎方案),由後端確認是否允許「無基礎方案」的續費單並只疊加配額/開關。需後端確認語義。</li>
<li><b>B</b>:後端新增 <code>TenantTopup</code> 端點,明確「在現有有效訂閱上疊加配額/用戶/KYC不延長有效期」。<code>UpdateTenantProperty</code>admin邏輯可借鑒它就是疊加 PlanList 並重算配額),但屬 admin 線下流程。</li>
</ul>
<h4>PlanList 組裝(三種 type 共用,核心難點)</h4> <h4>PlanList 組裝(三種 type 共用,核心難點)</h4>
<pre><code>PlanList = [] <pre><code>PlanList = []
@ -610,9 +426,9 @@ if (type!=='topup') PlanList.push({ PlanId: plan.planId,
// 增加用戶PCS = addons.users // 增加用戶PCS = addons.users
if (addons.users>0) PlanList.push({ PlanId: AdlU.planId, if (addons.users>0) PlanList.push({ PlanId: AdlU.planId,
PlanDetailId: AdlU.planDetailId, PCS: addons.users }) PlanDetailId: AdlU.planDetailId, PCS: addons.users })
// KYC設備月租PCS = kycRentalMonths把 KYC planDetail.price 當月租單價;見 5.2 // KYCPCS = 1
if (addons.kyc) PlanList.push({ PlanId: KYC.planId, if (addons.kyc) PlanList.push({ PlanId: KYC.planId,
PlanDetailId: KYC.planDetailId, PCS: addons.kycRentalMonths }) PlanDetailId: KYC.planDetailId, PCS: 1 })
// jQuota選中的配套 ×1 // jQuota選中的配套 ×1
if (addons.jquotaPackageId) if (addons.jquotaPackageId)
PlanList.push({ PlanId: jqPack.planId, PlanList.push({ PlanId: jqPack.planId,
@ -621,193 +437,112 @@ if (addons.jquotaPackageId)
<p><span class="lbl">planDetailId 從哪來:</span>前端 payload 沒有 planDetailId 與各加值項 planId。BFF 須持有 <code>/plans/catalog</code> 的服務端版本(含 planDetailId<code>plan.planId</code> 與加值項類型<b>反查補齊</b>。建議把 catalog 結果在 BFF 內存緩存(隨 token 一併刷新),<code>/subscribe</code> 時直接查表。</p> <p><span class="lbl">planDetailId 從哪來:</span>前端 payload 沒有 planDetailId 與各加值項 planId。BFF 須持有 <code>/plans/catalog</code> 的服務端版本(含 planDetailId<code>plan.planId</code> 與加值項類型<b>反查補齊</b>。建議把 catalog 結果在 BFF 內存緩存(隨 token 一併刷新),<code>/subscribe</code> 時直接查表。</p>
</div> </div>
<div class="callout note"> <div class="callout note">
<p><span class="lbl">後端返回OrderDto</span>下單成功返回 <code>OperationDto.Success(OrderDto)</code>,含 <code>orderCode</code><code>goodsName</code><code>orderStatus</code><code>planPrice</code> 等。BFF 應把 <code>orderId/orderCode</code> 回給前端的 <code>data.orderId</code><b>免費/0 元</b>訂單後端直接進 <code>PendingActive</code> 並發激活郵件(<code>:296-303</code><b>在線非 0 元</b><code>MockupPayment</code>(本地佔位,見 5.9</p> <p><span class="lbl">後端返回OperationDto</span>下單成功返回含 <code>orderCode</code><code>planName</code><code>paymentStatus</code><code>txamt</code> 等(旧站 <code>plan4.component</code> 用其拼 QFPay URL。BFF 應把其中 <code>orderId/orderCode</code> 回給前端的 <code>data.orderId</code>,並<b>暫存 orderCode/amount</b> 供下一步支付用</p>
</div> </div>
<!-- 5.8 --> <!-- 5.7 -->
<h3 id="s5-8">5.8 聯絡我們/推薦人(發送訂單摘要 · 不創建訂單) <span class="pill done">復用現有 contact API</span></h3> <h3 id="s5-7">5.7 POST /payments/create支付 <span class="pill todo">缺口大</span></h3>
<div class="endpoint-head"><span class="method post">POST</span> /api/contact &nbsp;&nbsp; <span class="method post">POST</span> /api/amlPortal/customer/CreateFeedback</div> <div class="endpoint-head"><span class="method post">POST</span> /api/payments/create &nbsp;&nbsp; QFPay 收銀台(服務端拼簽名 URL</div>
<p>訂單摘要下方的「<b>聯絡我們 / 聯絡推薦人</b>」按鈕與付款按鈕<b>同樣的有效性門檻</b>(由 <code>updateSubmitEnabled()</code> 控制),但<b>不創建任何訂單/租戶/線索</b>——只是把用戶當前<b>已填寫的訂單摘要</b><code>collectPayload()</code> 產出的 type/行業/方案/加值項/期間/總價與聯絡資料)整理成一段留言,經<b>既有 contact us API</b> 發出,讓管理員(或推薦人)主動跟進。成功後前端顯示「已收到資料,將盡快聯絡」(<code>showOfflineSubmitted()</code>)。</p> <p>旧站<b>沒有</b>獨立的 create-payment 後端端點:<code>plan4.component.payFn()</code> 在前端直接用 <code>orderCode</code>/<code>planName</code>/<code>txamt</code> 拼 QFPay checkstand URL 並 <code>sha256</code> 簽名後 <code>location.href</code> 跳轉。plans-plus 改成調 <code>/payments/create</code><code>redirectUrl</code><b>這段簽名邏輯應移到 BFF 服務端</b>API key 不可暴露前端)。</p>
<div class="callout ok"> <h4>BFF 實現要點(對標旧站 QFPay 參數)</h4>
<p><span class="lbl">好消息——復用已實現端點:</span>站內已有 <code>server/routes/contact.js</code><code>POST /api/contact</code> → 後端 <code>customer/CreateFeedback</code><b>免 token</b>,見 <code>contact.js:40,143</code>plans-plus 的「聯絡我們」<b>直接復用它即可</b><b>無需新增後端 lead 表/端點</b>。BFF 只需接受一段摘要 <code>message</code>+ 收件人路由參數)並轉為 <code>CreateFeedback</code></p>
</div>
<h4>收件人路由:管理員 vs 推薦人</h4>
<ul class="tight">
<li><b>未填推薦人</b>(或 <code>type≠'new'</code>):發給<b>平台管理員</b>——即現有 <code>CreateFeedback</code> 的既定收件邏輯(後端反饋 + <code>ADMIN_EMAIL</code> 通知,<code>contact.js:17</code>)。</li>
<li><b>已填有效推薦人代碼</b><code>state.agent</code> 已解析;前端 <code>hasReferrer()</code> 僅在 <code>new</code> 流程為真):改發給<b>該推薦人</b>。推薦人 <code>email</code>/<code>phone</code> 已由 <code>/agents/:code</code> 帶出(見 5.4),前端可隨摘要一併提交。</li>
</ul>
<table> <table>
<thead><tr><th>contact 端點入參</th><th>plans-plus 來源</th><th>對應 CreateFeedback</th></tr></thead> <thead><tr><th>QFPay 參數</th><th>來源</th></tr></thead>
<tbody> <tbody>
<tr><td><code>company</code></td><td><code>payload.company</code>(新購=公司/個人名;續費/加購=租戶名;單次=查詢主體)</td><td><code>companyName</code></td></tr> <tr><td><code>appcode</code></td><td>環境配置(<code>.env</code></td></tr>
<tr><td><code>name</code></td><td><code>payload.contact</code> / 公司名 / 主體名</td><td><code>customerName</code></td></tr> <tr><td><code>goods_name</code></td><td>下單返回 <code>planName</code> / 前端 <code>planName</code></td></tr>
<tr><td><code>email</code></td><td><code>payload.email</code>(客戶郵箱)</td><td><code>customerEmail</code>(供回覆)</td></tr> <tr><td><code>out_trade_no</code></td><td>下單返回 <code>orderCode</code></td></tr>
<tr><td><code>subject</code></td><td>固定文案「訂閱諮詢 · {type}」(可帶方案名)</td><td><code>typeOfQuery</code></td></tr> <tr><td><code>txamt</code></td><td><code>apis.test ? 10 : amount*100</code>(單位:分)</td></tr>
<tr><td><code>message</code> <span class="pill todo">未組裝</span></td><td><b>訂單摘要文本</b>type/行業/方案+價/加值項用戶·KYC月租·jQuota/期間/小計/總價/推薦人碼 —— <b>現網尚未產出此文本</b>(見下方缺口)</td><td><code>Message</code>(需把 <code>collectPayload()</code> 序列化為可讀文字;後端限 ≤1000 字)</td></tr> <tr><td><code>txcurrcd</code></td><td><code>HKD</code></td></tr>
<tr><td><code>agentUserId</code> <span class="pill newreq">擬新增</span></td><td><code>state.agent</code> 的後端 Guid<code>/agents/:code</code> 帶出,見 5.4</td><td>→ 擬新增的 <code>CreateFeedback.AssignedAgentUserId</code><b>收件人路由</b>,後端據此解析推薦人郵箱(見下)</td></tr> <tr><td><code>return_url</code>/<code>failed_url</code></td><td>V2 的 <code>pay-return</code> / 失敗頁</td></tr>
<tr><td><code>notify_url</code></td><td><code>{API_BASE_URL}/api/amlPortal/Order/portal/PaymentWebhook</code>(端點 10QFPay 服務端回調後端)</td></tr>
<tr><td><code>sign</code></td><td><code>sha256(排序參數串 + api_key)</code><b>服務端計算</b></td></tr>
</tbody> </tbody>
</table> </table>
<div class="callout gap"> <p>BFF 返回 <code>{ success, data:{ redirectUrl:&lt;QFPay checkstand URL&gt; } }</code>,前端 <code>location.href</code> 跳轉。支付結果由 QFPay 異步回調後端 <code>PaymentWebhook</code> 落單(權威來源);前端返回頁可輪詢訂單狀態。</p>
<p><span class="lbl">關鍵缺口——摘要 <code>message</code> 目前「未組裝」:</span>現網 <code>submitContact()</code><code>plans-plus.js:1243</code>)送的是 <code>collectPayload()</code><b>結構化對象</b>type/edition/plan/addons/total…+ <code>channel</code> + <code>referrer</code><b>並無任何 <code>message</code> 文本</b><code>renderSummary()</code> 只把摘要畫進右側 DOM 面板(<code>#ppSum*</code> 的 textContent不產出可提交字符串。⇒ 要把「訂單摘要」當作 contact us 的 <code>message</code><b>必須新增一步序列化</b><br> <div class="callout note">
· <b>推薦BFF 端拼裝</b>——前端照舊送結構化字段,<code>/api/contact</code> 路由把 type/行業/方案+價/加值項/期間/小計/總價/推薦人碼格式化為可讀多行文本填入 <code>message</code>(順帶多語言與脫敏);<br> <p><span class="lbl">mock 對照:</span>當前 mock 用 <code>/payments/create</code> + <code>/payments/:pid</code>(輪詢)+ <code>/payments/:pid/webhook</code> 模擬「下單→收銀台→webhook→返回頁輪詢」全鏈路。真實環境收銀台與 webhook 由 QFPay 提供BFF 只需:① 拼簽名 URL② 提供查單(透傳後端訂單狀態)給返回頁輪詢。</p>
· 或前端拼裝(複用 <code>renderSummary()</code> 的同源計算:<code>currentPlan()</code>/<code>computeSubtotal()</code>/各加值項)。<br>
<b>長度上限</b>:後端 <code>CreateFeedback</code><code>Message ≤ 1000</code> 字(<code>CustomerService.cs:507</code>BFF <code>contact.js</code> 亦要求 <code>message</code> 非空 ⇒ 序列化摘要需精簡到 1000 字內。</p>
</div> </div>
<div class="callout warn"> <div class="callout warn">
<p><span class="lbl">與現網代碼的差異(需前端小改):</span>當前 <code>plans-plus.js</code><code>submitContact()</code><code>POST /subscribe-offline</code>(帶 <code>channel:'offline'</code> + <code>referrer</code>、且<b><code>message</code></b>),語義是「線下對接線索」。按本次決策應改為:<b>組裝訂單摘要文本 + <code>POST /api/contact</code></b>(帶 <code>message</code> 摘要 + <code>agentUserId</code> 路由),<b>不再</b>創建 lead/訂單。BFF 側可保留 <code>/subscribe-offline</code> 作向後兼容別名(轉調同一 contact 處理),或直接改前端調用點與 mock</p> <p><span class="lbl">免支付分支:</span>旧站當訂單 <code>paymentStatus===200</code><code>txamt===0</code>(如純免費/0 元)時直接跳成功頁,不去 QFPay。plans-plus 的 P2G / 0 元情形需在 BFF 比照處理(<code>redirectUrl</code> 直接給成功頁)</p>
</div> </div>
<div class="callout note">
<p><span class="lbl">「送推薦人」路由 —— 已定方案②(後端加收件人字段),<span class="pill todo">待實現</span></span>方案細節(供實作參照,尚未落碼):<br>
<code>CreateFeedbackDto</code><code>Feedback</code> 實體各加可空 <code>AssignedAgentUserId</code>Guid?<code>CreateMap&lt;CreateFeedbackDto,Feedback&gt;</code> 同名自動映射,無需改 profile<br>
<code>CustomerService.CreateFeedback</code><code>CustomerService.cs:496</code>)於其有值時,用 <code>ICustomIdentityUserRepository.GetUsersByIDs</code> 解析該推薦人郵箱,作為 <code>SendEmailOnPortalFeedback</code> 第 6 參數 <code>salesEmail</code> 的收件人(推薦人無郵箱時回退平台銷售 <code>_appConfig.Portal.SalesEmailAddress</code>);「給客戶本人」的確認郵件不變;<br>
③ EF 遷移為 <code>AMLPortal_Feedbacks</code> 增一列 <code>uniqueidentifier NULL</code>SQL Server 遷移工程;<b>MySQL 遷移工程停在 2021 <code>Initial</code>、未並行維護</b>,如啟用該 provider 需補同名列)。<br>
<b>BFF 對接</b><code>/api/contact</code><code>agentUserId</code><code>/agents/:code</code> 帶出的後端 Guid見 5.4)透傳為 <code>AssignedAgentUserId</code>,推薦人郵箱由後端解析、前端/BFF 不必自行取。</p>
<p><span class="lbl">為何不走「BFF 抄送、零改後端」(原備選①已否決):</span>曾設想 BFF 拿 <code>agentEmail</code> 自行把摘要<b>抄送</b>推薦人、不動後端。<b>核對後不成立</b>:① contact API 的 <code>CreateFeedbackDto</code> <b>無 cc/bcc 字段</b>;② 底層 <code>CreateEmailQueue(subject, to, cc, bcc, …)</code> 雖有 cc/bcc<code>EmailMessageService.cs:44</code>),但 <code>SendEmailOnPortalFeedback</code> 調用時均傳 <code>null</code>,且該方法<b>對外調不到</b>,唯一可發任意郵件的 <code>EmailQueueController.CreateEmailQueue</code> 端點<b>已被注釋</b><code>EmailQueueController.cs:51</code>);③ V2 BFF 自身<b>無發信能力</b>(無 SMTP/nodemailer只轉調後端。⇒「零改後端」需給 BFF 加 SMTP 或解開後端發信端點(皆非零改動),故以方案②(加 <code>AssignedAgentUserId</code>)為準。</p>
</div>
<!-- 5.9 -->
<h3 id="s5-9">5.9 POST /payments/create支付 <span class="tag">本輪除外</span></h3>
<div class="endpoint-head"><span class="method post">POST</span> /api/payments/create &nbsp;&nbsp; <span class="tag">暫緩</span></div>
<p>當前 <code>plans-plus.js</code><code>submit()</code> 已臨時<b>移除支付鏈</b><code>/subscribe</code> 成功後直接 <code>showSubmitted()</code> 顯示「已提交成功」,不調 <code>/payments/create</code>、不跳收銀台。⇒ <b>本輪不實現支付端點。</b></p>
<p class="small">後端側:在線非 0 元訂單 <code>CreateOrder</code>/<code>TenantRenewal</code> 目前走 <code>MockupPayment(newOrder)</code>(本地佔位、非真實支付方),配合 <code>PaymentWebhook</code>(端點 10。真實支付QFPay 收銀台簽名 URL + webhook留待後續階段屆時 QFPay 簽名邏輯應在 BFF 服務端完成API key 不可暴露前端mock 的 <code>pay-gateway</code>/<code>pay-return</code>/<code>/payments/:id</code> 輪詢鏈可作參照。</p>
</section> </section>
<!-- ───────────── 6 ───────────── --> <!-- ───────────── 6 ───────────── -->
<section id="s6"> <section id="s6">
<h2 class="sec">6. 本地開發環境對接docker-compose-local-dev</h2> <h2 class="sec">6. plans-plus 相對旧站的新增需求</h2>
<p>本地棧(<code>AML_Backend/docker-compose-local-dev/</code>)已把後端 <code>iCON.Abp.FX.HttpApi.Host</code> 起在 <code>http://localhost:44331</code>Swagger 同址SQL Server 在 <code>localhost,11433</code>。要把 V2 的 plans-plus BFF 指到它,需三件事:<b>配 .env、對齊 Portal 訪客憑證、補種子數據</b></p> <p>以下是 plans-plus 單頁流程相對旧站 4 步嚮導<b>新增或改變</b>的點,逐一標注對接影響。</p>
<h3>6.1 V2 側配置(把 BFF 指向本地後端)</h3>
<pre><code># 便捷腳本(已加入 package.jsonAPP_ENV=dev + 本地後端 + plans-plus mock 兜底 + PORT 8090
npm run start:local
# 等價於:
cross-env APP_ENV=dev API_BASE_URL=http://localhost:44331 PLANS_PLUS_MOCK=true PORT=8090 node server/index.js</code></pre>
<div class="callout ok">
<p><span class="lbl">實測結論(免 token</span>plans-plus 依賴的門戶端點 <code>GetPlanList / GetEditionList / getCategoryByTypes / CreateOrder / TenantRenewal / queryRenewableTenant</code> 均為 <code>[AbpAutoAuth("Portal")]</code>——後端 <code>AbpAutoAuthMiddleware</code><b>服務端</b>注入門戶訪客 token 並覆蓋調用方 <code>Authorization</code><code>SearchUserByCodeAndType</code><code>[AllowAnonymous]</code>。⇒ <b>BFF 無需自帶 token</b>,直接匿名 POST 即可。因此本地不必配 <code>AUTH_*</code>(且本地 <code>Portal</code> 租戶未種子化,<code>connect/token?__tenant=Portal</code> 反而會報 <code>Tenant not found</code>)。新增的 5 個真實路由與已改的 <code>countries.js</code> 均按此免 token 實現。</p>
</div>
<h3>6.2 種子數據缺口(對接前必補)</h3>
<p>本地庫當前(實測):</p>
<table> <table>
<thead><tr><th></th><th>本地現狀</th><th>plans-plus 需要</th></tr></thead> <thead><tr><th>#</th><th>新增/變更</th><th>對接影響</th><th>狀態</th></tr></thead>
<tbody> <tbody>
<tr><td><code>AMLPortal_Plans</code></td><td><b>0 條</b></td><td>B(Std/P2G/Pre/CPA) + jQ/j + AdlU + KYC 各若干</td></tr> <tr><td>1</td><td><b>topup 加購類型</b>(無基礎方案,只買加值項)</td><td>後端無端點,需確認映射 TenantRenewal 或新增端點</td><td><span class="pill todo">需後端</span></td></tr>
<tr><td><code>AMLPortal_PlanDetails</code></td><td><b>0 條</b></td><td>每 Plan 至少 1 條(含 price/periodMonths/qCount/userCountLimit</td></tr> <tr><td>2</td><td><b>按郵箱查租戶</b> + 一郵箱多租戶選擇</td><td>後端 queryRenewableTenant 按名字搜,需新增按郵箱端點</td><td><span class="pill todo">需後端</span></td></tr>
<tr><td><code>SaasEditions</code></td><td>1 條 <code>Standard</code><code>3A220FC7-…</code></td><td>多行業VASP/TCSP/MSO/CPA…至少 1 個 CPA edition 對齊 jQSeparatedEditions</td></tr> <tr><td>3</td><td><b>jQuota 改為選配套</b>package非按量</td><td>BFF 把 jQ plan 映成 packages 即可</td><td><span class="pill partial">BFF</span></td></tr>
<tr><td><code>AMLPortal_AgentUserPlans</code></td><td>0 條</td><td>(可選)測 agent tiers 過濾時需要</td></tr> <tr><td>4</td><td><b>KYC「已購買」鎖定</b>topup 已有則禁買)</td><td>依賴租戶 <code>EnableKYC</code>BFF 帶出即可</td><td><span class="pill partial">BFF</span></td></tr>
<tr><td>配置 <code>Portal.jQSeparatedEditions.EditionIds</code></td><td><code>3A1A2969-…</code>(庫中不存在)</td><td>對齊到實際 CPA edition 的 GUID</td></tr> <tr><td>5</td><td><b>續費續價</b>(沿用上期 price顯示續費折扣</td><td>依賴 currentSubscription.price需訂單反查</td><td><span class="pill todo">需後端</span></td></tr>
<tr><td>6</td><td><b>去掉管理員密碼步驟</b></td><td>後端已忽略 AdminPassword發重置郵件</td><td><span class="pill done">無影響</span></td></tr>
<tr><td>7</td><td><b>bestValue / 賣點 note</b>(方案卡片標記與文案)</td><td>後端 Plan 無此字段BFF 配置或後端補</td><td><span class="pill partial">BFF/後端</span></td></tr>
<tr><td>8</td><td><b>edition 多語言名稱</b>(中/日)</td><td>後端只有 displayNameBFF 補映射</td><td><span class="pill partial">BFF</span></td></tr>
<tr><td>9</td><td><b>BR 標為可選</b>UI</td><td>後端已去除 BR/CI 必填校驗(<code>OrderService.cs:154,190</code>),可不填即下單;唯一性校驗保留、僅在填寫時生效</td><td><span class="pill done">已改</span></td></tr>
<tr><td>10</td><td><b>支付 create 端點化</b>(前端拿 redirectUrl</td><td>QFPay 簽名邏輯移到 BFF 服務端</td><td><span class="pill todo">BFF</span></td></tr>
</tbody> </tbody>
</table> </table>
<div class="callout gap">
<p><span class="lbl">補種子的兩條路:</span><b>直接寫 SQL</b><code>SaasEditions</code>/<code>AMLPortal_Plans</code>/<code>AMLPortal_PlanDetails</code> 插測試數據(快,適合本地聯調;注意 <code>AMLPortal_Plans</code> 需正確的 <code>Tag1Code</code>/<code>Tag2Code</code>/<code>Enabled=1</code>/<code>IsActive=1</code>,並讓 <code>PlanDetail.EffectTime/ExpireTime</code> 覆蓋當前);② <b>加後端 DataSeedContributor</b><code>AMLPortalDataSeedContributor</code> 目前只 seed TenantProperty補 Plan/Edition 種子,跑 <code>db-migrator</code> 幂等注入(更可復現,改動後端代碼)。<b>本地聯調建議 ①</b>,並把 CPA edition 的 GUID 同步進 <code>appsettings.local.json</code> 後重啟 host。</p>
</div>
<h3>6.3 本輪落地狀態(已實現 · 2026-07 實測通過)</h3>
<table>
<thead><tr><th></th><th>狀態</th><th>說明</th></tr></thead>
<tbody>
<tr><td>Edition + Plan/PlanDetail 種子</td><td><span class="pill done">已補</span></td><td><code>docker-compose-local-dev/seed-plans-plus.sql</code>(幂等,鏡像 mock 目錄CPA edition GUID 對齊 jQSeparatedEditions<code>down -v</code> 後重跑)</td></tr>
<tr><td>後端 BR/CI 必填放開</td><td><span class="pill partial">待應用/核對</span></td><td>決策=非必填:注释 <code>OrderService.cs:154,190</code> 兩處必填校驗(保留 :191 唯一性,空值安全)。<b>當前 working copy 這兩處仍 active</b>——需確認已在對接/部署環境放開</td></tr>
<tr><td>BFF 真實路由</td><td><span class="pill done">已寫</span></td><td>新增 <code>routes/editions.js · plans.js · agents.js · tenants.js · subscribe.js</code> + <code>services/catalog.js</code><code>countries.js</code> 改為免 token<code>index.js</code> 掛載於 mock/代理之前</td></tr>
<tr><td>訂閱三態</td><td><span class="pill done">new 已驗</span> <span class="pill partial">renew/topup 待數據</span></td><td>newcorp/個人/CPA/加值項)實測返回 orderId、PlanPrice 正確(含 KYC 月租 PCS=月數renew/topup 路由已寫,但本地無可續費租戶數據,待後端「按郵箱查」或先建租戶</td></tr>
<tr><td>single-queryPAYG 隨付即用)</td><td><span class="pill done">前端+mock 已實現</span> <span class="pill partial">後端對接待接</span></td><td>本輪落地<b>新模型</b><b>檢測內容固定 4 項純展示、用戶不可選</b><code>/single-query/options</code> 去單價、加 <code>code</code>=V/E/A/D<b>收款價取 <code>GetPlanList</code>(countryCode) 的 PAYG(P2G) 單一整包價</b>——新增原始端點 <code>POST /amlPortal/plan/portal/GetPlanList</code> mock返回結構<b>與真後端逐字段一致</b><code>{code,msg,data:{totalCount,items:[…planDetails[].price]},version}</code>),切真後端免改前端。<code>/plans-jp→JPN</code>目前唯一有價國家mock ¥3,000 佔位);提交帶 <code>functionCodes="EAVD"</code>、幣種隨方案JPY。改動<code>public/js/plans-plus.js · plans-plus.html · translations.js</code> + <code>server/mock/plans-plus.js</code>。支付本輪除外</td></tr>
<tr><td>聯絡我們(不下單)</td><td><span class="pill partial">改接 contact</span></td><td>復用 <code>routes/contact.js</code><code>CreateFeedback</code>;前端調用點由 <code>/subscribe-offline</code><code>/contact</code>,推薦人路由由 BFF 補</td></tr>
</tbody>
</table>
<p class="small">端到端驗證(<code>npm run start:local</code> + 本地 docker 後端):<code>/editions</code>10 行業 + CPA 切換)· <code>/plans/catalog</code>standard/cpa/addons 全對)· <code>/countries</code>249· <code>/agents/:code</code>(無數據 found:false· <code>/subscribe</code>new 各變體均返回 orderId、訂單入庫。頁面 <code>GET /plans-plus</code> HTTP 200。</p>
<p class="small"><b>PAYGsingle-query本輪驗證<code>APP_ENV=test</code> mock 模式):</b><code>/single-query/options</code> 返固定 4 項、無單價;<code>POST /amlPortal/plan/portal/GetPlanList{countryCode:"JPN"}</code>→P2G ¥3,000JPY<code>{HKG}</code>→空目錄(價格不可用);抽取 <code>plans-plus.js</code> 實際 <code>extractPaygPlan/paygPlanPrice/fmtMoneyCur</code> 跑真實響應:<code>P2G / 3000 / JPY / ¥3,000</code> 通過、HKG→<code>null/0</code>(禁用提交)。<code>GET /plans-jp</code> HTTP 200、含 <code>ppSqPriceValue</code> 橫幅。<span style="color:var(--muted)">(真後端 GetPlanList 對接與 ConsumerPortal 支付後編排見 5.6,仍待後端。)</span></p>
</section> </section>
<!-- ───────────── 7 ───────────── --> <!-- ───────────── 7 ───────────── -->
<section id="s7"> <section id="s7">
<h2 class="sec">7. plans-plus 相對旧站的新增需求</h2> <h2 class="sec">7. 實施建議與分期</h2>
<h3>7.1 新增 / 改動文件清單justsolutionsWebV2/server</h3>
<table> <table>
<thead><tr><th>#</th><th>新增/變更</th><th>對接影響</th><th>狀態</th></tr></thead> <thead><tr><th>文件</th><th>職責</th><th>後端依賴</th></tr></thead>
<tbody> <tbody>
<tr><td>1</td><td><b>單次查詢 single-query</b>(隨付即用,第 4 種流程)</td><td><b>後端已支持</b>ConsumerPortal 即時檢測);<b>兩步由 AML 後端在支付完成後內部編排</b>CreateConsumerLink→CreateConsumerOrderV2 只下單+收款。<b>檢測內容固定 4 項純展示不可選</b>V/E/A/D<b>定價取 <code>GetPlanList</code>(countryCode) 的 PAYG 方案價</b><code>/plans-jp→JPN</code>),映射與 2C 租戶均在後端(見 5.6</td><td><span class="pill partial">改造</span></td></tr> <tr><td><code>routes/editions.js</code>(新)</td><td><code>GET /editions</code> + 多語言/過濾整理</td><td>GetEditionList</td></tr>
<tr><td>2</td><td><b>聯絡我們/推薦人</b>(發訂單摘要 · <b>不創建訂單</b></td><td>復用既有 <code>contact</code>/<code>CreateFeedback</code>;「送推薦人」需後端加 <code>AssignedAgentUserId</code> 收件人字段(小改)+ BFF 拼裝 <code>message</code></td><td><span class="pill partial">復用+後端小改</span></td></tr> <tr><td><code>routes/plans.js</code>(新)</td><td><code>GET /plans/catalog</code> + filterPlan 拆分 + 緩存 planDetailId</td><td>GetPlanList</td></tr>
<tr><td>3</td><td><b>agent tiers</b>(推薦人可售級別過濾方案)</td><td>SearchUser 不含 tiers需經 GetPlanList(agentUserId) 推導</td><td><span class="pill partial">BFF</span></td></tr> <tr><td><code>routes/countries.js</code></td><td>已存在,直接掛載</td><td>getCategoryByTypes</td></tr>
<tr><td>4</td><td><b>agent email/phone</b>(聯絡推薦人)</td><td>AppUserDto 已含 Email/PhoneNumberBFF 帶出</td><td><span class="pill partial">BFF</span></td></tr> <tr><td><code>routes/agents.js</code>(新)</td><td><code>GET /agents/:code</code> → {code,name,agentorId}</td><td>SearchUserByCodeAndType</td></tr>
<tr><td>5</td><td><b>主體類型 corp/individual</b>(新租戶)</td><td>個人主體無 BR/CIBR/CI 已定非必填 ⇒ 不再阻塞(待 :154/:190 放開已應用)</td><td><span class="pill done">非阻塞</span></td></tr> <tr><td><code>routes/tenants.js</code>(新)</td><td><code>GET /tenants/lookup</code>(依賴後端新端點)</td><td>queryRenewableTenant(ByEmail)</td></tr>
<tr><td>6</td><td><b>BR/CI 非必填</b>(已定)</td><td>注释 <code>OrderService.cs:154,190</code>(保留唯一性);當前 working copy 仍 active待確認已放開</td><td><span class="pill partial">待應用/核對</span></td></tr> <tr><td><code>routes/subscribe.js</code>(新)</td><td><code>POST /subscribe</code> 分流 + PlanList 組裝</td><td>CreateOrder / TenantRenewal / (Topup)</td></tr>
<tr><td>7</td><td><b>KYC 改設備月租</b>monthlyPrice × 月數)</td><td>後端 KYC 單價 ⇒ 下單 PCS=月數(見 5.2</td><td><span class="pill partial">BFF/後端確認</span></td></tr> <tr><td><code>routes/payments.js</code>(新)</td><td><code>POST /payments/create</code> 拼 QFPay 簽名 URL + 查單</td><td>PaymentWebhook回調</td></tr>
<tr><td>8</td><td><b>jQuota 改選配套</b>package非按量</td><td>後端 jQ plan 天然離散BFF 映 packages</td><td><span class="pill partial">BFF</span></td></tr> <tr><td><code>index.js</code></td><td>在真實環境段 <code>app.use(apiPrefix, createXxxRouter(config))</code> 追加各路由</td><td></td></tr>
<tr><td>9</td><td><b>按郵箱查租戶 + 郵箱唯一單命中</b></td><td><b>已定:後端新增 <code>queryRenewableTenantByEmail</code></b>(現端點 DB 層只按租戶名搜、郵箱查後回填→不能按郵箱過濾);不再多租戶消歧;新端點聚合 currentSubscription+referrer見 5.5</td><td><span class="pill todo">需後端新增</span></td></tr> <tr><td><code>.env.*</code> / <code>config.js</code></td><td>補 QFPay <code>appcode/api_key</code>、edition/plan 文案映射等</td><td></td></tr>
<tr><td>10</td><td><b>續費頁展示 referrer</b></td><td>TenantPropertyDto.AgentorCode/Name 映射</td><td><span class="pill partial">BFF</span></td></tr> </tbody>
<tr><td>11</td><td><b>續費續價 / 顯示舊方案</b>Req 9</td><td>續價後端不認前端 price舊方案卡屬前端渲染</td><td><span class="pill todo">後端確認</span></td></tr> </table>
<tr><td>12</td><td><b>過期加購阻斷</b>(須先續費)</td><td>純前端門檻,無對接影響</td><td><span class="pill done">無影響</span></td></tr>
<tr><td>13</td><td><b>去掉管理員密碼步驟</b></td><td>後端忽略 AdminPassword發激活郵件</td><td><span class="pill done">無影響</span></td></tr> <h3>7.2 可能的後端改動(建議與後端團隊確認)</h3>
<tr><td>14</td><td><b>bestValue / note / edition&plan 多語言</b></td><td>後端無字段BFF 配置或後端補</td><td><span class="pill partial">BFF/後端</span></td></tr> <ul class="tight">
<tr><td>15</td><td><b>支付端點化</b>(本輪除外)</td><td>submit 暫不跳支付,直接顯示已提交</td><td><span class="tag">暫緩</span></td></tr> <li><b>按郵箱查可續費租戶</b>端點,返回聚合 currentSubscription含 planId / 上期 price / 已購加值項)。</li>
<li><b>topup 語義</b>明確「無基礎方案的加購」如何下單TenantRenewal 是否允許 / 新增 Topup 端點)。</li>
<li>可選Plan 增 <code>bestValue</code> 與多語言 <code>note</code>edition 增多語言名稱。</li>
</ul>
<h3>7.3 建議分期</h3>
<table>
<thead><tr><th>階段</th><th>內容</th><th>可獨立交付</th></tr></thead>
<tbody>
<tr><td>P1 · 只讀目錄</td><td>editions + plans/catalog + countries已就緒+ agents</td><td>頁面可正常渲染方案/行業/國家,校驗推薦人</td></tr>
<tr><td>P2 · 新購下單</td><td>subscribe(new) + payments/createQFPay</td><td>新租戶完整下單支付閉環</td></tr>
<tr><td>P3 · 續費</td><td>tenants/lookup後端新端點+ subscribe(renew)</td><td>續費閉環,含續價</td></tr>
<tr><td>P4 · 加購</td><td>topup後端確認後</td><td>加購閉環</td></tr>
</tbody> </tbody>
</table> </table>
</section> </section>
<!-- ───────────── 8 ───────────── --> <!-- ───────────── 8 ───────────── -->
<section id="s8"> <section id="s8">
<h2 class="sec">8. 實施建議與分期</h2> <h2 class="sec">8. 待確認問題清單</h2>
<h3>8.1 新增 / 改動文件清單justsolutionsWebV2/server</h3>
<table>
<thead><tr><th>文件</th><th>職責</th><th>後端依賴</th></tr></thead>
<tbody>
<tr><td><code>routes/editions.js</code>(新)</td><td><code>GET /editions</code> + 多語言/過濾整理 + jQSeparated 鍵名轉換</td><td>GetEditionList</td></tr>
<tr><td><code>routes/plans.js</code>(新)</td><td><code>GET /plans/catalog</code> + filterPlan 拆分 + KYC 月租/jQ 配套 + 緩存 planDetailId</td><td>GetPlanList</td></tr>
<tr><td><code>routes/countries.js</code></td><td>已存在,直接掛載</td><td>getCategoryByTypes</td></tr>
<tr><td><code>routes/agents.js</code>(新)</td><td><code>GET /agents/:code</code> → {code,name,tiers,email,phone,agentorId}</td><td>SearchUserByCodeAndType (+GetPlanList)</td></tr>
<tr><td><code>routes/tenants.js</code>(新)</td><td><code>GET /tenants/lookup?email=</code> → 字段直通 + 命中態(none/unique) 包裝currentSubscription/referrer 聚合由後端算好)</td><td><b>新增</b> queryRenewableTenant<b>ByEmail</b></td></tr>
<tr><td><code>routes/subscribe.js</code>(新)</td><td><code>POST /subscribe</code> 分流 + PlanList 組裝(含 KYC PCS=月數)</td><td>CreateOrder / TenantRenewal / (Topup)</td></tr>
<tr><td><code>routes/contact.js</code></td><td>已存在;「聯絡我們」復用之——把結構化 payload 序列化為 <code>message</code> 摘要 + 透傳 <code>agentUserId</code>,轉 <code>CreateFeedback</code>(收件人路由由後端 <code>AssignedAgentUserId</code> 承接)</td><td>CreateFeedback</td></tr>
<tr><td><code>mock/plans-plus.js</code></td><td>single-query / payments <b>暫留 mock</b><code>subscribe-offline</code> 改由 contact 承接(可保留為兼容別名)</td><td></td></tr>
<tr><td><code>index.js</code></td><td>代理之前追加各真實路由 <code>app.use(apiPrefix, createXxxRouter(config))</code></td><td></td></tr>
<tr><td><code>services/auth.js</code> / <code>.env.*</code></td><td>門戶訪客憑證 + <code>__tenant=Portal</code>edition/plan 文案映射</td><td></td></tr>
</tbody>
</table>
<h3>8.2 可能的後端改動(與後端團隊確認)</h3>
<ul class="tight">
<li><b>BR/CI 非必填</b>:已定去除 <code>CreateOrder</code> 的 BR/CI 必填校驗(注释 :154/:190保留唯一性請確認已在對接/部署環境應用(當前 working copy 仍 active</li>
<li><b>按郵箱查可續費租戶</b><span class="pill todo">已定 · 待實現</span> 新增 <code>POST Order/portal/queryRenewableTenantByEmail</code>(入參 <code>{Email}</code><code>[AbpAutoAuth("Portal")]</code>)——先按 <code>UserName=='admin' &amp;&amp; Email==</code> 定位租戶(可復用現 <code>GetAllUsers()</code> 全庫掃描、改按郵箱過濾,掃描成本可接受),返回聚合 currentSubscriptionplanId / 上期 price / 已購加值項,反查思路借用 <code>GetCurrServiceInfo</code>+ referrerAgentorCode/Name。詳見 5.5。</li>
<li><b>topup 語義</b>:確認 <code>TenantRenewal</code>PlanList 僅加值項)是否「只疊配額不延期」,或新增 <code>TenantTopup</code></li>
<li><b>KYC 計費口徑</b>:確認 KYC plan 是否可按 <code>PCS=月數</code> 計月租。</li>
<li><b>single-query</b>:已用 ConsumerPortal 即時檢測,<b>兩步由後端內部編排</b>(見 5.6)。<b>檢測內容固定 4 項V/E/A/D、定價取 <code>GetPlanList</code>(countryCode) 的 PAYG 方案價</b>。待後端確認:① V2「支付完成」與後端編排的銜接端點/webhook② 隨付即用專用「2C 租戶」+ <code>AML.ConsumerPortal.Enable</code> 功能 + 實時篩查權限(供後端落上下文,<b>V2 無需持該租戶憑證</b>);③ <s>op→功能碼映射</s><b>已定固定 EAVD</b>——僅需確認後端是「V2 固定傳 EAVD」還是「後端寫死」<s>收款金額對齊</s><b>已確認</b>PAYG = <b>單一整包價</b><b>目前 <code>countryCode</code> 僅日本JPN</b>,僅 <code>/plans-jp</code> 有 P2G 方案,餘國待種 PlanDetail 後再開。</li>
<li><b>聯絡我們送推薦人</b><span class="pill todo">待實現</span> 已定方案——後端 <code>CreateFeedback</code><code>AssignedAgentUserId</code> 收件人字段DTO + 實體 + 服務解析郵箱 + EF 遷移):有值發推薦人、否則發平台銷售。</li>
<li>可選Plan 增 <code>bestValue</code>/多語言 <code>note</code>/<code>nameJP</code>edition 增多語言名稱。</li>
</ul>
<h3>8.3 建議分期</h3>
<table>
<thead><tr><th>階段</th><th>內容</th><th>可獨立交付</th></tr></thead>
<tbody>
<tr><td>P0 · 本地種子</td><td>補 Edition + Plan/PlanDetail 種子,對齊配置(第 6 章)</td><td>後端返回非空目錄,可對接</td></tr>
<tr><td>P1 · 只讀目錄</td><td>editions + plans/catalog + countries已就緒+ agents</td><td>頁面渲染方案/行業/國家,校驗推薦人</td></tr>
<tr><td>P2 · 新購下單</td><td>subscribe(new)BR/CI 已定非必填:注释 :154/:190</td><td>新租戶下單至「已提交成功」(支付除外)</td></tr>
<tr><td>P3 · 續費/加購</td><td>tenants/lookup依賴後端新端點 <code>queryRenewableTenantByEmail</code>+ subscribe(renew/topup)</td><td>續費/加購閉環(支付除外);<b>阻塞於後端新端點</b></td></tr>
<tr><td>P4 · 單次查詢/聯絡/支付</td><td>single-query支付後接 ConsumerPortal 兩步CreateConsumerLink→CreateConsumerOrder+ 聯絡我們(接 contact API + 推薦人路由)+ payments後端就緒後</td><td>完整流程 + 在線支付</td></tr>
</tbody>
</table>
</section>
<!-- ───────────── 9 ───────────── -->
<section id="s9">
<h2 class="sec">9. 待確認問題清單</h2>
<ol class="tight" style="padding-left:20px"> <ol class="tight" style="padding-left:20px">
<li><b>BR/CI</b><span class="pill done">已定非必填</span> plans-plus 新購(含個人主體 / 空 BR/CI可下單。實現注释 <code>CreateOrder</code><code>OrderService.cs:154,190</code>(保留 :191 唯一性,空值安全)。<b>待辦</b>:確認該放開已在對接/部署環境生效(<b>當前 working copy 兩處仍 active</b>);並知悉全局影響(旧站共用 <code>CreateOrder</code>,空 BR 不再攔截)。</li> <li><b>租戶查詢:</b>後端能否新增「按管理員郵箱查可續費租戶」?返回是否能直接帶 currentSubscriptionplanId / 上期價 / 已購加值項)?</li>
<li><b>單次查詢:</b><span class="pill partial">已定方向</span> 後端<b>已支持</b>——ConsumerPortal 即時檢測;<b>調用方AML 後端內部編排</b>(支付完成後 <code>CreateConsumerLink</code><code>CreateConsumerOrder</code>V2 只下單+收款,見 5.6)。<b>本輪再定</b>:檢測內容<b>固定 4 項純展示不可選</b>證件核驗V / 名單篩查ES=E / AI 增強A / 失信D<b>收款金額取 <code>GetPlanList</code><code>countryCode</code> 的 PAYG(P2G) 方案價</b><code>/plans-jp→JPN</code>)。<b>待後端確認</b>:① V2「支付完成」與後端編排的銜接方式後端持單 + 收 webhook或 V2 支付後調後端專門 2C 觸發端點);② 隨付即用專用「2C 租戶」+ <code>AML.ConsumerPortal.Enable</code> + 實時篩查權限(供後端落上下文,<b>V2 無需該租戶憑證</b>);③ <code>functionCodes="EAVD"</code> 由 V2 固定傳、還是後端寫死;④ <span class="pill done">已確認</span> PAYG = <b>單一整包價</b>(非分項之和);<b>目前 <code>countryCode</code> 僅日本JPN</b>,僅 <code>/plans-jp</code> 有 P2G 方案 + PlanDetail餘國後續再種。</li> <li><b>topup</b>純加購走 TenantRenewalPlanList 僅加值項)後端是否接受、語義是否「只疊配額不延期」?還是需新端點?</li>
<li><b>聯絡我們(不下單):</b><span class="pill done">已定方案</span> 復用 <code>contact</code>/<code>CreateFeedback</code> 發送訂單摘要(不創建訂單/線索);「送推薦人」擬由後端 <code>CreateFeedback.AssignedAgentUserId</code> 承接(<span class="pill todo">待實現</span>,見 5.8)。<b>待辦</b>:後端加字段 + 服務路由 + EF 遷移;前端調用點由 <code>/subscribe-offline</code><code>POST /api/contact</code> 並透傳 <code>agentUserId</code>BFF 映為 <code>AssignedAgentUserId</code></li> <li><b>BR/CI</b><span class="pill done">已解決</span> 後端已去除「BR 或 CI 至少一個」必填校驗(<code>OrderService.cs:154,190</code> 注釋2026-06plans-plus 可不填 BR/CI 直接下單;唯一性(去重)校驗保留,僅在填寫時生效。<b>待業務確認</b>:① 改動為全局,旧站共用 <code>CreateOrder</code> 是否接受同樣放開;② 空 BR 不再去重、合規/STR 報告該欄留白是否可接受。</li>
<li><b>租戶查詢:</b><span class="pill done">已定新增後端端點</span> <code>queryRenewableTenantByEmail(email)</code> 直接返回「租戶 + currentSubscriptionplanId/上期價/已購加值項)+ referrer」聚合見 5.5)。<b>待後端確認</b>:① 「admin 郵箱」是否恆等於 <code>UserName=='admin'</code> 用戶的 Email<code>queryRenewableTenant</code> 即此語義)——若存在非 <code>admin</code> 用戶名的租戶管理員,需改按角色定位;② 過期租戶是否照樣返回(供前端做臨期/過期展示 + topup 阻斷);③ 聚合 DTO 形態(新 <code>RenewableTenantDto</code> vs 擴 <code>TenantPropertyDto</code>)。</li> <li><b>bestValue / note / edition 多語言:</b>由 BFF 配置維護,還是後端在數據上補字段?</li>
<li><b>topup</b>純加購走 <code>TenantRenewal</code>PlanList 僅加值項)後端是否接受、語義是否「只疊配額不延期」?</li> <li><b>QFPay</b>V2 是否沿用旧站同一 <code>appcode</code>/<code>api_key</code>/收銀台域名簽名算法是否一致sha256 排序串 + key</li>
<li><b>KYC 月租:</b>KYC plan 是否可按 <code>PCS=租賃月數</code> 計費planDetail.price 當月租單價)?</li> <li><b>0 元 / P2G</b>免支付分支的判定(<code>paymentStatus===200</code><code>txamt===0</code>)是否照搬?</li>
<li><b>續費續價:</b>後端 <code>TenantRenewal</code> 是否支持沿用上期價/自定義價?否則前端續費折扣僅展示、實際按目錄價。</li> <li><b>agentorId</b><code>/agents/:code</code> 響應是否可附帶 agent 的 Guid id避免下單時二次查詢</li>
<li><b>agent tiers</b>BFF 經 <code>GetPlanList(agentUserId)</code> 推導 tiers還是改為服務端直接按 agent 過濾 catalog</li> <li><b>鑑權憑證:</b>V2 BFF 用的門戶訪客 OAuth 帳號(<code>.env</code>)權限是否覆蓋 CreateOrder / queryRenewableTenant<code>[AbpAutoAuth("Portal")]</code>,應可)?</li>
<li><b>agentorId</b><code>/agents/:code</code> 響應附帶 agent Guid id避免下單時二次查詢</li>
<li><b>鑑權憑證:</b>V2 BFF 門戶訪客帳號(<code>Portal@iconsz.com</code><code>__tenant=Portal</code>)權限是否覆蓋 CreateOrder / queryRenewableTenant / SearchUserByCodeAndType</li>
<li><b>本地種子:</b>Plan/Edition 種子走臨時 SQL 還是入 <code>AMLPortalDataSeedContributor</code>CPA edition GUID 是否同步進 <code>appsettings.local.json</code></li>
<li><b>bestValue / note / 多語言:</b>由 BFF 配置維護,還是後端在數據上補字段?</li>
</ol> </ol>
</section> </section>
<p class="small" style="text-align:center;margin-top:48px;color:var(--muted)"> <p class="small" style="text-align:center;margin-top:48px;color:var(--muted)">
本分析基於源碼靜態閱讀 + 本地 docker 庫實測2026-07<code>plans-plus.js</code> / <code>server/*</code>(含 mock 契約)/ <code>AMLPortal</code> Controllers 與 Service / <code>iCON.Abp.AML</code><code>ConsumerPortalController</code><code>ConsumerPortalService</code>(單次查詢即時檢測)/ <code>CustomIdentityUserController</code> / 旧站 <code>PlanService</code> / <code>appsettings.local.json</code> 與本地數據庫。涉及後端行為(BR/CI 校驗、續費續價、topup / KYC 計費語義、單次查詢 2C 計費與鑑權)以實際接口 + 後端確認為準。 本分析基於源碼靜態閱讀:<code>plans-plus.js</code> / <code>server/*</code> / <code>AMLPortal</code> Controllers 與 DTO / 旧站 <code>PlanService</code>。涉及後端行為續費續價、topup 語義)以實際接口為準。
</p> </p>
</div> </div>