@ -1,304 +0,0 @@
<!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 >