feat(docs): add database migration guide and API integration report
parent
ad0ddcbe8b
commit
d13d7aa7c6
|
|
@ -1 +1 @@
|
||||||
docs/AML_Backend/docker-compose-local-dev/AML-local-dev.bak
|
docs/AML_Backend/docker-compose-local-dev/AML-local-dev.bak
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
# 数据库迁移指南
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
- vs开发环境
|
||||||
|
- 确保项目已经成功构建
|
||||||
|
- 确保已安装Entity Framework Core工具
|
||||||
|
|
||||||
|
## 迁移步骤
|
||||||
|
1. 在Visual Studio中将`EntityFrameworkCore.DbMigrations`设置为启动项目
|
||||||
|
2. 打开包管理器控制台(Package Manager Console)
|
||||||
|
3. 将默认项目设置为:`src\EntityFrameworkCore.DbMigrations`
|
||||||
|
4. 执行迁移命令:
|
||||||
|
```
|
||||||
|
Add-Migration <MigrationName>
|
||||||
|
```
|
||||||
|
5. 迁移将自动应用到数据库
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
- 每次修改实体模型后都需要创建新的迁移
|
||||||
|
- 可以使用`Remove-Migration`命令撤销上一次迁移
|
||||||
|
- 确保数据库连接字符串配置正确
|
||||||
|
|
@ -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>(201MB,SQL 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' && 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>② BFF(justsolutionsWebV2)</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) / agent;GoodsName 顯示 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>P1–P4 全部端到端打通,真實建單入庫,前端真實渲染。</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>
|
||||||
Loading…
Reference in New Issue