Compare commits

...

3 Commits

7 changed files with 476 additions and 0 deletions

1
.gitignore vendored
View File

@ -0,0 +1 @@
docs/AML_Backend/docker-compose-local-dev/AML-local-dev.bak

1
AML_Backend 160000

@ -0,0 +1 @@
Subproject commit cd254759bddd1cb149071cda281f8701277865fd

1
AML_Frontend 160000

@ -0,0 +1 @@
Subproject commit 649a90af3b6600fe82834bb4d3530855abbd02fe

View File

@ -0,0 +1,167 @@
# 数据库迁移指南
本项目使用 Entity Framework Core 迁移。迁移文件生成在:
`AML_Backend/src/iCON.Abp.FX.EntityFrameworkCore.DbMigrations/Migrations/`
对应的 DbContext 为 `FXMigrationsDbContext`,设计时工厂为
`EntityFrameworkCore/FXMigrationsDbContextFactory.cs`
生成迁移有两条等价路径:**A. Visual Studio 包管理器控制台Windows** 与
**B. `dotnet ef` 命令行macOS / Linux / 无 VS 的开发环境)**。二者产物完全一致,
按你的操作系统任选其一即可。
> 说明:迁移**只在开发/构建环境生成**(生成 `*.cs` + 更新 `ModelSnapshot`
> 运行时由 `Database.Migrate()` 自动应用到数据库。**不要**用手写 `ALTER TABLE`
> 代替生成迁移——运行时会读取迁移里的 `TargetModel`(完整模型快照)做比对,
> 缺少对应迁移会导致模型不一致。手动改库仅可作为本地临时应急。
---
## 路径 A · Visual Studio 包管理器控制台Windows
### 前置条件
- Visual Studio 开发环境
- 确保解决方案已成功构建
- 已安装 EF Core 工具
### 步骤
1. 将 `EntityFrameworkCore.DbMigrations` 设为启动项目
2. 打开包管理器控制台Package Manager Console
3. 默认项目选择:`src\iCON.Abp.FX.EntityFrameworkCore.DbMigrations`
4. 执行:`Add-Migration <MigrationName>`
5. 迁移会在下次运行时自动应用
- 撤销上一个(尚未应用的)迁移:`Remove-Migration`
---
## 路径 B · `dotnet ef` 命令行macOS / Linux / 无 VS
`Add-Migration`PMC`dotnet ef migrations add`CLI是同一功能的两种入口。
在没有 Visual Studio 的开发机(如本机 macOS**完全可以就地生成迁移**,用 CLI 即可。
> 本机已按此流程实测生成过 `AddAssignedAgentUserIdToFeedback``Feedback` 表新增
> 可空列 `AssignedAgentUserId`),产物与既有 EF Core 5.0.17 迁移格式一致。
### B.0 前置条件(本机现状与坑)
| 前置项 | 现状 / 处理 |
|--------|-------------|
| **dotnet-ef 工具** | 项目未自带。用本地工具清单安装(见 B.1)。 |
| **.NET 运行时** | 项目 target `net6.0`;本机可能只装了更高版本(如 .NET 10。构建不受影响但运行设计时宿主需 **roll-forward**(见 B.2)。 |
| **设计时配置来源** | `FXMigrationsDbContextFactory``src/iCON.Abp.FX.DbMigrator/appsettings.json` + **Nacos** 读取 `DBConfig` / `ConnectionStrings`。开发机若连不上 Nacos默认 `192.168.1.120:8848`),需改用**本地优先配置**离线生成(见 B.3)。 |
### B.1 安装 dotnet-ef本地工具清单一次性
`AML_Backend/` 目录下:
```bash
cd AML_Backend
dotnet new tool-manifest # 若已有 .config/dotnet-tools.json 或 dotnet-tools.json 可跳过
dotnet tool install dotnet-ef # 或指定版本:--version "10.*"
dotnet ef --version # 验证
```
> `dotnet-ef` 工具版本可高于项目的 EF Core 版本(本机用 10.0.9 生成 EF Core 5 的迁移没问题)——
> 代码生成用的是**项目引用的 EF Core Design5.0.\***,所以产物仍是 5.0.17 格式,与既有迁移一致。
### B.2 运行时 roll-forward仅当本机缺少 net6.0 运行时)
`dotnet ef` 会构建启动项目(`net6.0`)并在该运行时里启动设计时宿主。本机若只有 .NET 10
需允许从 net6 向上滚动到已装的运行时,给命令加环境变量:
```bash
DOTNET_ROLL_FORWARD=LatestMajor dotnet ef migrations add ...
```
(若本机已装 .NET 6 运行时,则不需要这个变量。)
### B.3 离线配置:绕开不可达的 Nacos仅当连不上 Nacos
`FXMigrationsDbContextFactory` 通过 `AddNacosV2ConfigurationLocalFirst()` 读取配置:
**本地 `appsettings.json` 里若没有 `NacosConfig` 节点,就完全不连 Nacos、只用本地配置**。
利用这一点,把 `src/iCON.Abp.FX.DbMigrator/appsettings.json` 临时替换为本地优先版本
(去掉 `NacosConfig`,补上 `DBConfig` 与连接串)。
> 生成迁移(`migrations add`**不会真的连库**,只需连接串可解析(非空)即可;
> 指向本机 docker 的 SQL Server 最稳妥。
```bash
cd AML_Backend/src/iCON.Abp.FX.DbMigrator
cp appsettings.json appsettings.json.nacos-bak # 备份
cat > appsettings.json <<'JSON'
{
"DBConfig": { "DBType": "mssql", "ConnectionStringName": "Default" },
"ConnectionStrings": {
"Default": "Server=localhost,11433;Database=AbpAML;User Id=sa;Password=Aml@Local2026;TrustServerCertificate=True"
}
}
JSON
```
`localhost,11433` / `sa` / `Aml@Local2026``docs/AML_Backend/docker-compose-local-dev` 本地栈的库;
按你的本地环境替换。)
**生成完成后务必还原**,避免把本地连接串提交进仓库:
```bash
mv -f appsettings.json.nacos-bak appsettings.json
```
### B.4 生成迁移
`AML_Backend/` 目录下执行(项目与启动项目都指向 DbMigrations
设计时工厂就在其中,`../iCON.Abp.FX.DbMigrator/appsettings.json` 会被正确解析到):
```bash
cd AML_Backend
DOTNET_ROLL_FORWARD=LatestMajor dotnet ef migrations add <MigrationName> \
--project src/iCON.Abp.FX.EntityFrameworkCore.DbMigrations \
--startup-project src/iCON.Abp.FX.EntityFrameworkCore.DbMigrations \
--context FXMigrationsDbContext
```
看到 `Build succeeded.` + `Done.` 即成功。会生成两文件并更新快照:
- `Migrations/<时间戳>_<MigrationName>.cs``Up`/`Down`
- `Migrations/<时间戳>_<MigrationName>.Designer.cs`
- `Migrations/FXMigrationsDbContextModelSnapshot.cs`(追加本次模型变更)
**核对**`git diff --stat` 看快照,确认只包含你本次改的实体属性(若夹带了别的变更,
说明工作树里还有未生成迁移的模型改动,需甄别)。
撤销刚生成、尚未应用的迁移:
```bash
DOTNET_ROLL_FORWARD=LatestMajor dotnet ef migrations remove \
--project src/iCON.Abp.FX.EntityFrameworkCore.DbMigrations \
--startup-project src/iCON.Abp.FX.EntityFrameworkCore.DbMigrations \
--context FXMigrationsDbContext
```
### 已生成示例AssignedAgentUserId
`20260706234441_AddAssignedAgentUserIdToFeedback.cs`
```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "AssignedAgentUserId",
table: "AMLPortal_Feedbacks",
type: "uniqueidentifier",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AssignedAgentUserId",
table: "AMLPortal_Feedbacks");
}
```
---
## 注意事项
- 每次修改实体模型后都要生成一个新迁移,并**连同更新后的 `ModelSnapshot` 一起提交**。
- 迁移在下次运行 `DbMigrator` / Host`Database.Migrate()`)时自动应用到数据库。
- 生成迁移不需要真连库;**应用**迁移才需要连接串正确、且账号有 DDL 权限。
- 别把 B.3 里临时的本地连接串(`appsettings.json`)提交进仓库。

View File

@ -0,0 +1,304 @@
<!DOCTYPE html>
<html lang="zh-HK">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>justsolutionsWebV2 · plans-plus 真實 API 對接實現報告</title>
<style>
:root {
--bg: #f6f8fa;
--card: #ffffff;
--text: #24292f;
--muted: #57606a;
--border: #d0d7de;
--accent: #0969da;
--accent-soft: #ddf4ff;
--code-bg: #f0f3f6;
--th-bg: #f0f3f6;
--row-alt: #fafbfc;
--warn-bg: #fff8c5;
--warn-border: #d4a72c;
--note-bg: #ddf4ff;
--note-border: #54aeff;
--gap-bg: #ffebe9;
--gap-border: #ff8182;
--ok-bg: #dafbe1;
--ok-border: #4ac26b;
--get: #1a7f37;
--post: #9a6700;
--new: #8250df;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "PingFang HK", "Hiragino Sans GB", "Microsoft YaHei", Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.65;
font-size: 15px;
}
.wrap { max-width: 1100px; margin: 0 auto; padding: 32px 24px 80px; }
header.page {
background: var(--card); border: 1px solid var(--border);
border-radius: 12px; padding: 28px 32px; margin-bottom: 24px;
}
header.page h1 { margin: 0 0 10px; font-size: 26px; }
header.page p { margin: 4px 0; color: var(--muted); }
header.page .meta { font-size: 13px; }
.toc {
background: var(--card); border: 1px solid var(--border);
border-radius: 12px; padding: 18px 28px; margin-bottom: 28px;
}
.toc h2 { font-size: 13px; margin: 0 0 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
.toc ol { margin: 0; padding-left: 20px; columns: 2; column-gap: 36px; }
.toc li { margin: 4px 0; break-inside: avoid; }
.toc a { color: var(--accent); text-decoration: none; }
.toc a:hover { text-decoration: underline; }
section { margin-bottom: 36px; }
h2.sec {
font-size: 21px; border-bottom: 2px solid var(--border);
padding-bottom: 8px; margin: 0 0 18px; scroll-margin-top: 16px;
}
h3 { font-size: 17px; margin: 24px 0 10px; scroll-margin-top: 16px; }
h4 { font-size: 15px; margin: 16px 0 6px; color: var(--muted); }
p { margin: 10px 0; }
table {
border-collapse: collapse; width: 100%; margin: 14px 0;
font-size: 14px; background: var(--card);
border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
}
th, td { border: 1px solid var(--border); padding: 8px 11px; text-align: left; vertical-align: top; }
th { background: var(--th-bg); font-weight: 600; }
tr:nth-child(even) td { background: var(--row-alt); }
code {
background: var(--code-bg); padding: 1.5px 6px; border-radius: 5px;
font-family: "SF Mono", "JetBrains Mono", "Fira Code", Consolas, monospace; font-size: 12.5px;
}
pre {
background: #0d1117; color: #e6edf3; padding: 16px 18px;
border-radius: 8px; overflow-x: auto; font-size: 12.5px; line-height: 1.55;
}
pre code { background: none; padding: 0; color: inherit; font-size: 12.5px; }
.method { font-weight: 700; font-size: 11.5px; padding: 1px 7px; border-radius: 5px; color: #fff; display: inline-block; }
.method.get { background: var(--get); }
.method.post { background: var(--post); }
.method.new { background: var(--new); }
.tag { font-size: 11px; padding: 1px 7px; border-radius: 20px; border: 1px solid var(--border); color: var(--muted); white-space: nowrap; }
.callout { border-radius: 8px; padding: 12px 16px; margin: 14px 0; border: 1px solid; }
.callout p { margin: 4px 0; }
.callout.warn { background: var(--warn-bg); border-color: var(--warn-border); }
.callout.note { background: var(--note-bg); border-color: var(--note-border); }
.callout.gap { background: var(--gap-bg); border-color: var(--gap-border); }
.callout.ok { background: var(--ok-bg); border-color: var(--ok-border); }
.callout.newreq { background:#fbefff; border-color:#d8b9ff; }
.callout .lbl { font-weight: 700; }
.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.partial { background: var(--warn-bg); color:#7a5c00; border:1px solid var(--warn-border);}
.pill.todo { background: var(--gap-bg); color:#cf222e; border:1px solid var(--gap-border);}
.pill.newreq { background:#fbefff; color:#8250df; border:1px solid #d8b9ff;}
.endpoint-head {
display:flex; align-items:center; gap:10px; flex-wrap:wrap;
background: var(--code-bg); border:1px solid var(--border); border-radius:8px;
padding:10px 14px; margin: 6px 0 12px; font-family:"SF Mono",Consolas,monospace; font-size:13.5px;
}
.small { font-size: 13px; color: var(--muted); }
ul.tight { margin: 8px 0; padding-left: 22px; }
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;}
hr.soft { border:none; border-top:1px dashed var(--border); margin: 26px 0; }
.updated { font-size:12px; color:#8250df; font-weight:700; }
</style>
</head>
<body>
<div class="wrap">
<header class="page">
<h1>justsolutionsWebV2 · plans-plus 真實 API 對接<span style="color:#1a7f37">實現報告</span></h1>
<p><a href="plan-plus-api分析.html" style="color:var(--accent)"><code>plan-plus-api分析.html</code></a> 的方案,把 <code>public/plans-plus.html</code> 單頁訂閱流程從 mock 切換到 AML 後端(<code>iCON.Abp.AMLPortal</code>)真實 API並在本地 <code>docker-compose-local-dev</code> 環境跑通、逐流程實測(<b>支付除外</b>)。本文記錄實際落地內容與驗證結果。</p>
<p class="meta">範圍:<code>AML_Backend</code>(後端 3 項改動 + 配置)· <code>justsolutionsWebV2/server</code>BFF 真實路由)· <code>docker-compose-local-dev</code>(本地棧 + 數據恢復)</p>
<p class="meta updated">狀態2026-07-06 實測P1 只讀目錄 / P2 新購 / P3 續費·加購 / P4 聯絡我們 —— <b>全部端到端打通並真實入庫</b>;支付與單次查詢真實檢測編排本輪除外(保持 mock</p>
</header>
<div class="toc">
<h2>目錄</h2>
<ol>
<li><a href="#s1">1. 落地結論速覽</a></li>
<li><a href="#s2">2. 數據恢復(.bak → AbpAML</a></li>
<li><a href="#s3">3. 後端改動AML_Backend</a></li>
<li><a href="#s4">4. BFF 對接justsolutionsWebV2</a></li>
<li><a href="#s5">5. 本地運行與驗證方式</a></li>
<li><a href="#s6">6. 端到端測試結果</a></li>
<li><a href="#s7">7. 本輪除外與後續待辦</a></li>
<li><a href="#s8">8. 變更文件清單</a></li>
</ol>
</div>
<!-- 1 -->
<section id="s1">
<h2 class="sec">1. 落地結論速覽</h2>
<p>plans-plus 依賴的 <b>3 條只讀目錄</b>editions / plans/catalog / countries<b>2 條交互查詢</b>agents / tenants/lookup<b>下單提交</b>subscribe含 new/renew/topup<b>聯絡我們</b>subscribe-offline → CreateFeedback均已對接真實後端並實測通過。單次查詢與支付本輪除外仍走 mock 兜底。</p>
<table>
<thead><tr><th>前端端點</th><th>對應後端</th><th>狀態</th><th>實測結果</th></tr></thead>
<tbody>
<tr><td><code>GET /editions</code></td><td><code>Order/portal/GetEditionList</code></td><td><span class="pill done">完成</span></td><td>9 行業(過濾 Standard/DC、Others 置末);<code>jQSeparatedEditions</code> 對齊真實 CPA GUID</td></tr>
<tr><td><code>GET /plans/catalog</code></td><td><code>plan/portal/GetPlanList</code></td><td><span class="pill done">完成</span></td><td>HKG·HKD 目錄standard/cpa/addons 全帶 <code>planDetailId</code>KYC 月租 + jQuota 配套</td></tr>
<tr><td><code>GET /countries</code></td><td><code>Order/getCategoryByTypes</code></td><td><span class="pill done">完成</span></td><td>249 國;已改<b>免 token</b>(原用 OAuth本地無憑證會失敗</td></tr>
<tr><td><code>GET /agents/:code</code></td><td><code>SearchUserByCodeAndType</code> + <code>GetPlanList(agentUserId)</code></td><td><span class="pill done">完成</span></td><td>返回 <code>{code,name,tiers,email,phone,agentorId}</code>tiers 經二次 GetPlanList 推導,空=不過濾</td></tr>
<tr><td><code>GET /tenants/lookup</code></td><td><span class="pill newreq">新增</span> <code>Order/portal/queryRenewableTenantByEmail</code></td><td><span class="pill done">完成</span></td><td>後端新端點按郵箱聚合 <code>currentSubscription</code>+<code>referrer</code>cpa1 唯一命中</td></tr>
<tr><td><code>POST /subscribe</code> <span class="tag">new</span></td><td><code>Order/portal/CreateOrder</code></td><td><span class="pill done">完成</span></td><td>空 BR / 個人主體 / 加值項 / agent 均建單入庫BR/CI 已放開)</td></tr>
<tr><td><code>POST /subscribe</code> <span class="tag">renew·topup</span></td><td><code>Order/portal/TenantRenewal</code></td><td><span class="pill done">完成</span></td><td>cpa1 續費 / jQuota 加購均建單;修復內部 token URL 後通過</td></tr>
<tr><td><code>POST /subscribe-offline</code><br><span class="small">(聯絡我們/推薦人)</span></td><td><code>customer/CreateFeedback</code></td><td><span class="pill done">完成</span></td><td>訂單摘要序列化為 message有推薦人則寫 <code>AssignedAgentUserId</code> 路由收件人</td></tr>
<tr><td><code>/single-query</code> · <code>/payments/*</code></td><td>ConsumerPortal待後端· 支付方(未定)</td><td><span class="tag">本輪除外</span></td><td>保持 mock 兜底;文檔明確除外</td></tr>
</tbody>
</table>
</section>
<!-- 2 -->
<section id="s2">
<h2 class="sec">2. 數據恢復(.bak → AbpAML</h2>
<p><code>docs/AML_Backend/docker-compose-local-dev/AML-local-dev.bak</code>201MBSQL Server 備份,內部庫名 <code>AML-local-dev</code>)恢復真實開發數據,取代原「補種子 SQL」路線——恢復後目錄 / 租戶 / 代理計劃齊備,直接可對接。</p>
<div class="flow">docker stop aml-httpapi-host → ALTER DATABASE AbpAML SET SINGLE_USER WITH ROLLBACK IMMEDIATE → RESTORE DATABASE AbpAML FROM DISK WITH MOVE(AML-local-dev→AbpAML.mdf, AML-local-dev_log→AbpAML_log.ldf), REPLACE → docker start aml-httpapi-host</div>
<p class="small">恢復為 <b>AbpAML</b>host 連接的庫MOVE 邏輯文件到 <code>/var/opt/mssql/data/AbpAML.mdf</code>/<code>_log.ldf</code>;恢復前停 host 釋放連接、置 SINGLE_USER 取得獨佔。</p>
<table>
<thead><tr><th></th><th>恢復後</th><th>用途</th></tr></thead>
<tbody>
<tr><td><code>AMLPortal_Plans</code> / <code>PlanDetails</code></td><td>123 / 123</td><td>方案目錄(<b>按國家分兩套</b>HKG-HKD 與 JPN-USD<code>systemCode</code>/<code>countryCode</code> 區分)</td></tr>
<tr><td><code>SaasEditions</code></td><td>12含真實 CPA <code>3A1A296B-…</code></td><td>所屬行業</td></tr>
<tr><td><code>SaasTenants</code> / <code>AMLPortal_TenantPropertys</code></td><td>42 / 98</td><td>續費 / 加購測試數據</td></tr>
<tr><td><code>AMLPortal_AgentUserPlans</code></td><td>24</td><td>agent tiers 過濾</td></tr>
<tr><td><code>AMLPortal_Orders</code> / <code>AbpUsers</code></td><td>142 / 131</td><td>訂單反查 / 推薦人</td></tr>
</tbody>
</table>
<div class="callout note"><p><span class="lbl">catalog 國家過濾:</span><code>services/catalog.js</code> 默認過濾 <code>HKG</code>香港市場·HKD與舊 mock 幣種一致);<code>/plans-jp</code> 的 PAYG 定價另走 <code>JPN</code></p></div>
</section>
<!-- 3 -->
<section id="s3">
<h2 class="sec">3. 後端改動AML_Backend</h2>
<p>共 3 項代碼改動 + 2 項配置,全部重建 <code>httpapi-host</code> 鏡像後實測通過。</p>
<h3>3.1 BR/CI 非必填 <span class="pill done">完成</span></h3>
<p>注釋 <code>OrderService.cs</code> <code>CreateOrder</code> 的兩處 BR/CI 必填校驗(前置校驗處 + <code>Organization BR/CI required</code> 處),保留唯一性校驗 <code>ExistsByOrganizationBRCI</code>(對 BR、CI 皆空返回 <code>false</code>,空值安全)。個人主體 / 空 BR 可下單。</p>
<h3>3.2 新增 <code>queryRenewableTenantByEmail</code> 端點 <span class="pill newreq">新增</span></h3>
<div class="endpoint-head"><span class="method post">POST</span> <span>/api/amlPortal/Order/portal/queryRenewableTenantByEmail</span> <span class="tag">AbpAutoAuth("Portal")</span></div>
<p>解決舊 <code>queryRenewableTenant</code> 只能按<b>租戶名</b> DB 搜、無法按郵箱過濾的維度錯位。新端點<b>維度反轉</b></p>
<ul class="tight">
<li><code>UserName=='admin' &amp;&amp; Email==email</code> 定位租戶管理員(禁多租戶過濾,全庫掃描可接受);</li>
<li>取該租戶 <code>IsActive</code><code>TenantProperty</code>(含 <code>Order→OrderDetails→Plan</code>),按 <code>CreationTime</code> 取最新;</li>
<li>反查訂單明細聚合 <code>currentSubscription</code>:基礎方案(B)→planId/名稱/月數/價AdlU→<code>addons.users</code>(PCS 之和)jQ/j→<code>jquotaPackageId</code><code>EnableKYC</code>→kyc並解析 <code>EditionName</code>(由 EditionId<code>referrer</code>(由 AgentorId 解析用戶)。</li>
</ul>
<p class="small">新增 DTO<code>QueryRenewableTenantByEmailParam</code><code>RenewableTenantByEmailDto</code>(含 <code>CurrentSubscriptionDto</code> / <code>RenewableReferrerDto</code>)。</p>
<h3>3.3 <code>CreateFeedback</code><code>AssignedAgentUserId</code>(聯絡推薦人路由)<span class="pill done">完成</span></h3>
<p><code>Feedback</code> 實體 + <code>CreateFeedbackDto</code> 加可空 <code>AssignedAgentUserId</code>AutoMapper 同名自動映射);<code>CustomerService.CreateFeedback</code> 於有值時用 <code>GetUsersByIDs</code> 解析推薦人郵箱,作 <code>SendEmailOnPortalFeedback</code> 的收件人(無郵箱回退平台銷售),給客戶本人的確認郵件不變。</p>
<div class="callout warn"><p><span class="lbl">EF 遷移(部署待辦):</span>本地以 <code>ALTER TABLE AMLPortal_Feedbacks ADD AssignedAgentUserId uniqueidentifier NULL</code> 加列(實體屬性映射到該列即可運行)。<b>正式部署需在構建環境跑 <code>dotnet ef migrations add</code></b> 生成遷移(就一句 <code>AddColumn</code>)——原因:運行時 <code>Database.Migrate()</code> 會讀取遷移的 <code>TargetModel</code>(完整模型快照),手寫遷移不可行。</p></div>
<h3>3.4 對齊 jQSeparatedEditions CPA GUID <span class="pill done">完成</span></h3>
<p><code>appsettings.local.json</code><code>Portal.jQSeparatedEditions.EditionIds</code> 由庫中不存在的 <code>3A1A2969-…</code> 改為真實 CPA edition <code>3A1A296B-DAB4-4B94-B267-4424683B8916</code>,否則前端選 CPA 不觸發 <code>isCpa</code> 方案集切換。</p>
<h3>3.5 修正內部 token URL本地<span class="pill done">完成</span></h3>
<div class="callout gap"><p><span class="lbl">實測中發現的坑:</span><code>AppConfig:General:ApiLocalhostUrl</code> 出廠 <code>https://localhost:44331/</code>,但本地 host 只監聽 HTTP → <code>TenantRenewal</code> / 建租戶等內部 <code>AbpTokenService.connect/token</code> 調用 <b>SSL 握手失敗500</b><code>docker-compose.local.yml</code> host env 已加 <code>AppConfig__General__ApiLocalhostUrl: "http://localhost:44331/"</code>。(<code>CreateOrder</code> 走異步隊列建租戶未同步觸發,故只有 renew/topup 報錯——由此定位。)</p></div>
</section>
<!-- 4 -->
<section id="s4">
<h2 class="sec">4. BFF 對接justsolutionsWebV2</h2>
<p>採 BFF 範式:瀏覽器只調本站 <code>/api/*</code>,服務端匿名代理後端並裁剪響應。前端 <code>plans-plus.js</code> <b>零改動</b></p>
<div class="callout note"><p><span class="lbl">免 token</span>門戶端點均 <code>[AbpAutoAuth("Portal")]</code><code>SearchUserByCodeAndType</code><code>[AllowAnonymous]</code>)——後端 <code>AbpAutoAuthMiddleware</code> 服務端注入門戶訪客 token 並覆蓋 Authorization故 BFF 匿名 POST/GET 即可,<b>無需配 AUTH_*</b>。統一助手 <code>server/services/portal.js</code></p></div>
<table>
<thead><tr><th>文件</th><th>職責</th></tr></thead>
<tbody>
<tr><td><code>services/portal.js</code> <span class="pill newreq"></span></td><td>匿名 POST/GET 助手axios + 自簽證書放行 + <code>{code,msg,data}</code> 拆包)</td></tr>
<tr><td><code>services/catalog.js</code> <span class="pill newreq"></span></td><td>GetPlanList → 按國家(默認 HKG)過濾 → filterPlan 拆 standard/cpa/addons → 產出前端契約 + <code>planDetailId</code> 索引5 分鐘緩存</td></tr>
<tr><td><code>routes/editions.js</code> <span class="pill newreq"></span></td><td>GetEditionList → 多語言映射 + 過濾 Standard/DC + Others 置末 + <code>jQSeparatedEditions</code> 鍵名/值轉小寫</td></tr>
<tr><td><code>routes/plans.js</code> <span class="pill newreq"></span></td><td><code>GET /plans/catalog[?countryCode]</code> → catalog 服務</td></tr>
<tr><td><code>routes/agents.js</code> <span class="pill newreq"></span></td><td><code>GET /agents/:code</code> → SearchUser + GetPlanList(agentUserId) 推導 tiers</td></tr>
<tr><td><code>routes/tenants.js</code> <span class="pill newreq"></span></td><td><code>GET /tenants/lookup?email=</code> → queryRenewableTenantByEmail字段直通 + 命中態 + 日期格式化</td></tr>
<tr><td><code>routes/subscribe.js</code> <span class="pill newreq"></span></td><td><code>POST /subscribe</code>(new→CreateOrder / renew·topup→TenantRenewal組裝 PlanList 回填 planDetailId、KYC PCS=月數) + <code>POST /subscribe-offline</code>(序列化摘要 + agentCode 解析→AssignedAgentUserId → CreateFeedback)</td></tr>
<tr><td><code>routes/countries.js</code> <span class="pill partial"></span></td><td>由 OAuth 改<b>免 token</b>(本地無憑證),輸出不變</td></tr>
<tr><td><code>index.js</code> <span class="pill partial"></span></td><td>真實路由挂在 <b>mock 之前</b>真實路由處理訂閱流程mock 只兜底 single-query/options、PAYG-GetPlanList、payments、promos</td></tr>
<tr><td><code>package.json</code> <span class="pill partial"></span></td><td>補回 <code>start:local</code> 腳本</td></tr>
</tbody>
</table>
<h4>PlanList 組裝subscribe 核心)</h4>
<pre><code>PlanList = []
if type != 'topup': push({PlanId: plan.planId, PlanDetailId: plan.planDetailId, PCS:1})
if addons.users > 0: push({PlanId: refs.user.planId, PlanDetailId: refs.user.planDetailId, PCS: users})
if addons.kyc: push({PlanId: refs.kyc.planId, PlanDetailId: refs.kyc.planDetailId, PCS: kycRentalMonths}) // KYC 月租
if addons.jquotaPackageId: push({PlanId: jqId, PlanDetailId: planDetailByPlanId[jqId], PCS:1})</code></pre>
</section>
<!-- 5 -->
<section id="s5">
<h2 class="sec">5. 本地運行與驗證方式</h2>
<h4>① 後端棧docker-compose-local-dev</h4>
<pre><code>cd AML_Backend/docker-compose-local-dev
docker compose -f docker-compose.local.yml build httpapi-host # 編譯後端改動
docker compose -f docker-compose.local.yml up -d --no-deps httpapi-host # 只重建 host不重跑 migrator
# 後端 Swagger: http://localhost:44331/swagger DB: localhost,11433 (sa / Aml@Local2026)</code></pre>
<h4>② BFFjustsolutionsWebV2</h4>
<pre><code>cd justsolutionsWebV2
npm run start:local
# = APP_ENV=dev API_BASE_URL=http://localhost:44331 PLANS_PLUS_MOCK=true PORT=8090
# 頁面: http://localhost:8090/plans-plus (日本版 /plans-jp</code></pre>
<p class="small"><code>PLANS_PLUS_MOCK=true</code> 讓 single-query/payments 走 mock 兜底;真實訂閱路由挂在 mock 之前優先處理。</p>
</section>
<!-- 6 -->
<section id="s6">
<h2 class="sec">6. 端到端測試結果</h2>
<p>以下均為 <b>APP_ENV=dev、指向本地 docker 後端</b> 的真實調用(非 mock並核對數據庫寫入。</p>
<table>
<thead><tr><th>流程</th><th>驗證</th></tr></thead>
<tbody>
<tr><td>只讀目錄</td><td>editions=9 行業CPA 用對齊 GUID· catalog standard/cpa/addons 全帶 planDetailId · countries=249 · agents/C99099 返回 tiers/email/agentorId</td></tr>
<tr><td>租戶查詢</td><td><code>tenants/lookup?email=307736951@qq.com</code> → cpa1 唯一命中,聚合 editionId=CPA / referrer / currentSubscription(kyc=true, expiry 2027)</td></tr>
<tr><td>新購(多變體)</td><td>4 筆真實訂單入庫:基礎 / <b>個人主體+空 BR</b> / <b>全加值項</b>(Standard+jQuota+增加使用人+KYC) / agentGoodsName 顯示 PlanList 組裝正確</td></tr>
<tr><td>續費 / 加購</td><td>cpa1 續費(CPA 24 Months) + jQuota 200 加購 均建單入庫(修 token URL 後)</td></tr>
<tr><td>聯絡我們/推薦人</td><td><code>subscribe-offline</code> → Feedback 寫入,<code>AssignedAgentUserId=3A09C9F3…</code>(推薦人)、摘要序列化 245 字</td></tr>
<tr><td>前端渲染(無頭 Chrome</td><td>行業下拉填充真實 editions、jQuota 真實 HKD 價渲染、無 JS 錯誤</td></tr>
</tbody>
</table>
<div class="callout ok"><p><span class="lbl">結論:</span>P1P4 全部端到端打通,真實建單入庫,前端真實渲染。</p></div>
</section>
<!-- 7 -->
<section id="s7">
<h2 class="sec">7. 本輪除外與後續待辦</h2>
<ul class="tight">
<li><span class="pill todo">除外</span> <b>在線支付</b>:文檔明確本輪除外;前端 submit 已臨時去支付鏈、直接顯示「已提交成功」。</li>
<li><span class="pill todo">除外</span> <b>單次查詢真實檢測編排</b>:支付完成後 AML 後端內部 <code>CreateConsumerLink</code><code>CreateConsumerOrder</code>(待後端 + 需 2C 租戶/檢測引擎),本輪保持 mock。</li>
<li><span class="pill partial">部署待辦</span> <b><code>AssignedAgentUserId</code> 正式 EF 遷移</b>:本地已 ALTER 加列;部署環境需 <code>dotnet ef migrations add</code> 生成遷移trivial AddColumn</li>
<li><span class="pill partial">數據</span> <b>邊界情況</b>:測試數據中 <code>test@test.com</code> 對應 3 個租戶(郵箱非全局唯一,生產唯一);部分租戶 active TenantProperty 無關聯 Order → <code>currentSubscription.planId</code> 為空(前端 <code>renewalPlanObj</code> 兜底)。</li>
<li><span class="pill partial">展示</span> <b>後端無字段</b><code>bestValue</code> / <code>note*</code> / <code>nameJP</code> / edition 多語言由 BFF 靜態映射補足(日文暫回退英文)。</li>
</ul>
</section>
<!-- 8 -->
<section id="s8">
<h2 class="sec">8. 變更文件清單</h2>
<h4>AML_Backend</h4>
<ul class="tight">
<li><code>modules/iCON.Abp.AMLPortal/…/OrderService.cs</code> — BR/CI 放開 + <code>QueryRenewableTenantByEmail</code></li>
<li><code>…/Application.Contracts/OrderAppLayer/{IOrderService,QueryRenewableTenantByEmailParam,RenewableTenantByEmailDto}.cs</code></li>
<li><code>…/HttpApi/Controllers/OrderController.cs</code> — 新端點路由</li>
<li><code>…/Domain/DbEntity/Feedback.cs</code> · <code>…/Application.Contracts/CustomerAppLayer/CreateFeedbackDto.cs</code> · <code>…/Application/CustomerService.cs</code><code>AssignedAgentUserId</code></li>
<li><code>docker-compose-local-dev/appsettings.local.json</code> — jQSeparatedEditions GUID</li>
<li><code>docker-compose-local-dev/docker-compose.local.yml</code><code>ApiLocalhostUrl</code> http override</li>
</ul>
<h4>justsolutionsWebV2</h4>
<ul class="tight">
<li>新增 <code>server/services/{portal,catalog}.js</code><code>server/routes/{editions,plans,agents,tenants,subscribe}.js</code></li>
<li>修改 <code>server/routes/countries.js</code>(免 token<code>server/index.js</code>(挂載)、<code>package.json</code><code>start:local</code></li>
</ul>
<hr class="soft">
<p class="small">本報告基於 2026-07-06 本地 docker 環境實測。配套方案文檔見 <a href="plan-plus-api分析.html" style="color:var(--accent)">plan-plus-api分析.html</a></p>
</section>
</div>
</body>
</html>

1
justsolutionsWeb 160000

@ -0,0 +1 @@
Subproject commit 148e9f9292166c512282d57306944a6c1216860b

@ -0,0 +1 @@
Subproject commit f2f01bb4b62758b5ad901505c7a0e3540d93c956