328 lines
19 KiB
HTML
328 lines
19 KiB
HTML
<!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 KYC 訂閱欄位與租用邏輯分析</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; }
|
||
.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 .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.no { background: var(--gap-bg); color:#cf222e; border:1px solid var(--gap-border);}
|
||
.small { font-size: 13px; color: var(--muted); }
|
||
ul.tight { margin: 8px 0; padding-left: 22px; }
|
||
ul.tight li { margin: 5px 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; }
|
||
.path { font-family:"SF Mono",Consolas,monospace; font-size:12.5px; color:var(--muted); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="wrap">
|
||
|
||
<header class="page">
|
||
<h1>plans-plus · <span style="color:#1a7f37">KYC 訂閱欄位與租用邏輯分析</span></h1>
|
||
<p>釐清 <code>plans-plus.html</code> 續費查詢頁「上次訂閱資訊」中的 <b>KYC</b> 欄位到底代表什麼、KYC 功能是否受「有沒有租設備」限制、以及是否每個租戶預設開啟;並記錄本次為此做的文案修正。</p>
|
||
<p class="meta">範圍:<code>AML_Backend</code>(<code>iCON.Abp.AMLPortal</code> / <code>iCON.Abp.AML</code>)· <code>AML_Frontend</code>(KYC section)· <code>justsolutionsWebV2</code>(plans-plus 前端文案)</p>
|
||
<p class="meta">結論一句話:<b>「上次訂閱資訊」的 KYC = 上次訂閱是否含「KYC 讀卡設備租用」加購項</b>;租不租設備<b>只影響前端的「機器掃描」入口</b>,不影響上傳/手動錄入 KYC,且<b>非每個租戶預設開啟</b>。</p>
|
||
</header>
|
||
|
||
<nav class="toc">
|
||
<h2>目錄</h2>
|
||
<ol>
|
||
<li><a href="#tldr">一、結論速覽</a></li>
|
||
<li><a href="#chain">二、欄位資料鏈路(前端 → 後端)</a></li>
|
||
<li><a href="#assign">三、EnableKYC 何時被賦值為 true</a></li>
|
||
<li><a href="#gate">四、租設備是否影響「使用 KYC 功能」</a></li>
|
||
<li><a href="#default">五、是否每個租戶預設開啟</a></li>
|
||
<li><a href="#fix">六、本次文案修正</a></li>
|
||
<li><a href="#refs">七、關鍵代碼位置索引</a></li>
|
||
</ol>
|
||
</nav>
|
||
|
||
<!-- ───────────────────────────────────────────── -->
|
||
<section id="tldr">
|
||
<h2 class="sec">一、結論速覽</h2>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr><th style="width:38%">問題</th><th style="width:12%">答案</th><th>說明</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>「上次訂閱資訊」的 KYC 應該是「是否租用 KYC 設備」嗎?</td>
|
||
<td><span class="pill done">基本是</span></td>
|
||
<td>該值 = <code>TenantProperty.EnableKYC</code>,而它由「下單方案是否含 <code>Tag1Code = KYC</code> 的方案行」決定;此方案行在 plans-plus 對應加購項「<b>KYC 讀卡設備租用</b>」。故它實質表示「上次訂閱有無租 KYC 讀卡設備」。</td>
|
||
</tr>
|
||
<tr>
|
||
<td>有沒有租 KYC 設備,會影響用戶在系統中使用 KYC 功能嗎?</td>
|
||
<td><span class="pill partial">只影響掃描入口</span></td>
|
||
<td><code>enableKYC</code> 在前端<b>只控制「ID Scan/機器掃描」按鈕</b>的顯隱。「上傳」與「手動錄入」兩條路徑始終可用;後端 <code>KYCService/KYCController</code> <b>完全不校驗</b> EnableKYC。</td>
|
||
</tr>
|
||
<tr>
|
||
<td>「能否使用 KYC 功能」是每個租戶預設開啟的嗎?</td>
|
||
<td><span class="pill no">不是</span></td>
|
||
<td><code>EnableKYC</code> 對一般租戶<b>預設 false</b>,僅在下單方案含 KYC 加購時為 true(續費沿用上次值)。唯一預設 <code>true</code> 的是 <b>Host 宿主租戶</b>。但因前端僅擋掃描按鈕,所有租戶預設仍可用「上傳/錄入」做 KYC。</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<!-- ───────────────────────────────────────────── -->
|
||
<section id="chain">
|
||
<h2 class="sec">二、欄位資料鏈路(前端 → 後端)</h2>
|
||
|
||
<p>plans-plus 續費頁「上次訂閱資訊」卡片的「已購增值 / Add-ons」列,經由這條鏈路取得 KYC 標記:</p>
|
||
|
||
<div class="flow">plans-plus.js <code>previousAddonText()</code> → <code>currentSubscription.addons.kyc</code> → BFF 直通 → <code>CurrentSubscriptionAddonsDto.Kyc</code> ← <code>tp.EnableKYC</code></div>
|
||
|
||
<table>
|
||
<thead><tr><th style="width:34%">層</th><th>內容</th></tr></thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>前端顯示<br><span class="path">plans-plus.js:570-581 / plans-plus.html:347</span></td>
|
||
<td><code>showTenantInfo()</code> 把 <code>previousAddonText(s)</code> 寫入 <code>#ppTiAddons</code>;當 <code>addons.kyc === true</code> 時,列表加入一個 KYC 標籤。</td>
|
||
</tr>
|
||
<tr>
|
||
<td>續費查詢 DTO<br><span class="path">RenewableTenantByEmailDto.cs:46-54</span></td>
|
||
<td><code>CurrentSubscriptionAddonsDto.Kyc</code>,欄位註解明寫「<b>是否已購 KYC(TenantProperty.EnableKYC)</b>」。</td>
|
||
</tr>
|
||
<tr>
|
||
<td>後端聚合<br><span class="path">OrderService.cs:2460</span></td>
|
||
<td><code>Addons = new CurrentSubscriptionAddonsDto { Kyc = tp.EnableKYC }</code>——直接取當前生效 <code>TenantProperty.EnableKYC</code>。</td>
|
||
</tr>
|
||
<tr>
|
||
<td>底層欄位<br><span class="path">TenantProperty.cs:69-71</span></td>
|
||
<td><code>public bool EnableKYC</code>,註解「是否啟用了 KYC 功能」。<b>命名雖是「功能開關」,但賦值來源只綁定「有沒有買 KYC 設備加購」(見下節)。</b></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="callout note">
|
||
<p><span class="lbl">加購項對應:</span>plans-plus 第 4 步的 KYC 加購項在 <span class="path">plans-plus.html:468</span> 標題為「<b>KYC ID Reader Rental / KYC 身份讀取設備租用</b>」——一台<b>實體讀卡機</b>,按訂閱期租用。這正是產生 <code>Tag1Code = KYC</code> 方案行、進而把 <code>EnableKYC</code> 置 true 的東西。</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ───────────────────────────────────────────── -->
|
||
<section id="assign">
|
||
<h2 class="sec">三、EnableKYC 何時被賦值為 true</h2>
|
||
|
||
<p><code>EnableKYC</code> 全庫僅有 3 種寫入來源(其餘皆為 EF 遷移快照),<b>沒有任何「按功能開關」的入口</b>:</p>
|
||
|
||
<table>
|
||
<thead><tr><th style="width:26%">場景</th><th style="width:30%">代碼</th><th>取值</th></tr></thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>新租戶下單</td>
|
||
<td><span class="path">OrderService.cs:763</span></td>
|
||
<td><code>EnableKYC = checkHasEnableKYCInPlan(purchasePlanDetailList)</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>續費(新開訂單路徑)</td>
|
||
<td><span class="path">OrderService.cs:2633</span></td>
|
||
<td><code>EnableKYC = checkHasEnableKYCInPlan(purchasePlanDetailList)</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>續費(事件隊列生效路徑)</td>
|
||
<td><span class="path">OrderService.cs:872</span></td>
|
||
<td><code>EnableKYC = lastTenantProperty.EnableKYC</code>(<b>沿用上一份</b>)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Host 宿主初始化</td>
|
||
<td><span class="path">TenantPropertyDataSeeder.cs:63-79</span></td>
|
||
<td><code>EnableKYC = true</code>(<b>僅當 <code>currentTenant.Id == null</code>,即 Host</b>)</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<p>判定函式只看方案的 Tag1:</p>
|
||
<pre><code>// OrderService.cs:2694-2697
|
||
private bool checkHasEnableKYCInPlan(List<PlanDetail> purchasePlanDetailList)
|
||
{
|
||
return purchasePlanDetailList
|
||
.Where(a => a.Plan.Tag1Code == PlanTag1Enums.KYC.ToString()).Any();
|
||
}</code></pre>
|
||
|
||
<div class="callout ok">
|
||
<p><span class="lbl">推論:</span><code>EnableKYC</code> 語義上等同「<b>該訂閱是否含 KYC 讀卡設備租用</b>」,而不是一個獨立的「KYC 功能總開關」。所以把「上次訂閱資訊」裡的它讀作「是否租用 KYC 設備」是準確的。</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ───────────────────────────────────────────── -->
|
||
<section id="gate">
|
||
<h2 class="sec">四、租設備是否影響「使用 KYC 功能」</h2>
|
||
|
||
<h3>4.1 前端:只擋「機器掃描」按鈕</h3>
|
||
<p>AML_Frontend 的 KYC section 讀取當前租戶的 <code>enableKYC</code>:</p>
|
||
<pre><code>// kyc-section.component.ts:147-148
|
||
this.amlTenant.getCurrentTenantProperty$().subscribe(res => {
|
||
this.enableKYC = res?.enableKYC ?? false
|
||
})</code></pre>
|
||
|
||
<p>而它<b>只作用在「ID Scan(機器讀卡掃描)」這一個按鈕</b>上(<code>switchType === 1</code>):</p>
|
||
<table>
|
||
<thead><tr><th style="width:20%">按鈕</th><th style="width:16%">switchType</th><th style="width:22%">受 enableKYC 控制?</th><th>說明</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>ID Scan(機器掃描)</td><td>1</td><td><span class="pill partial">是(*ngIf="enableKYC")</span></td><td>插實體讀卡機掃描證件;未租設備則隱藏。<span class="path">kyc-section.component.html:10</span></td></tr>
|
||
<tr><td>Upload2(上傳)</td><td>2</td><td><span class="pill done">否 · 始終顯示</span></td><td>上傳證件圖片走 OCR/校驗。<span class="path">kyc-section.component.html:18-24</span></td></tr>
|
||
<tr><td>Input(手動錄入)</td><td>3</td><td><span class="pill done">否 · 始終顯示</span></td><td>手動輸入證件資訊。<span class="path">kyc-section.component.html:26-32</span></td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h3>4.2 後端:無任何門檻</h3>
|
||
<p>後端 <code>KYCService</code> / <code>KYCController</code> <b>從不讀取 <code>EnableKYC</code></b>。全庫對該欄位的非遷移讀取僅有兩處:續費沿用(<span class="path">OrderService.cs:872</span>)與續費查詢上報(<span class="path">OrderService.cs:2460</span>)。因此它<b>純粹是前端那顆掃描按鈕的顯隱旗標</b>,不構成服務端鑑權。</p>
|
||
|
||
<div class="callout warn">
|
||
<p><span class="lbl">結論:</span>沒租設備的租戶<b>照樣能用完整 KYC</b>(上傳 / 手動錄入 → OCR、校驗、檢索),只是看不到「插讀卡機掃證件」這個入口。<br>「租 KYC 設備」= 解鎖前端硬體掃描按鈕,而非解鎖 KYC 功能本身。</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ───────────────────────────────────────────── -->
|
||
<section id="default">
|
||
<h2 class="sec">五、是否每個租戶預設開啟</h2>
|
||
<ul class="tight">
|
||
<li><b>一般業務租戶:</b><code>EnableKYC</code> 預設 <b>false</b>,僅當下單方案含 KYC 設備加購才為 true;續費時沿用上一份的值(<span class="path">OrderService.cs:872</span>)。</li>
|
||
<li><b>Host 宿主租戶:</b>是唯一被 seeder 預設 <code>true</code> 的(<span class="path">TenantPropertyDataSeeder.cs:63-79</span>,<code>currentTenant.Id == null</code>),並非每個業務租戶。</li>
|
||
<li><b>但「使用 KYC 功能」廣義上仍預設可用:</b>因前端只擋掃描按鈕,所有租戶預設可走「上傳 / 錄入」完成 KYC。</li>
|
||
</ul>
|
||
<div class="callout note">
|
||
<p><span class="lbl">一句話:</span>廣義 KYC(上傳/錄入)對所有租戶預設可用;<code>EnableKYC</code>(=是否租了 KYC 讀卡設備)預設關閉,它只額外解鎖前端「機器掃描證件」按鈕。</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ───────────────────────────────────────────── -->
|
||
<section id="fix">
|
||
<h2 class="sec">六、本次文案修正</h2>
|
||
<p>原本 plans-plus「上次訂閱資訊」把 KYC 加購項顯示為含糊的「<code>KYC/KYB</code>」(易被誤解成一種核查功能)。已改為與加購項一致的「<b>KYC 設備租用</b>」表述,以準確反映「是否租用 KYC 設備」。<b>後端無改動。</b></p>
|
||
|
||
<table>
|
||
<thead><tr><th style="width:34%">位置</th><th style="width:33%">修改前</th><th style="width:33%">修改後</th></tr></thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><span class="path">plans-plus.js:575</span><br>(<code>previousAddonText</code>,「已購增值」列)</td>
|
||
<td><code>parts.push('KYC/KYB')</code></td>
|
||
<td><code>parts.push(tr('ppx_addon_kyc_label'))</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="path">plans-plus.html:579</span><br>(訂單摘要 <code>#ppSumKyc</code> 靜態回退文字)</td>
|
||
<td><code>KYC/KYB Verification</code></td>
|
||
<td><code>KYC ID Reader Rental</code></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<p class="small">改用的 i18n 鍵 <code>ppx_addon_kyc_label</code> 三語皆已是租用表述:繁中「KYC 身份讀取設備租用」/ EN「KYC ID Reader Rental」/ 日「KYC IDリーダー機器レンタル」,故翻譯本體無需改動。訂單摘要 <code>ppx_sum_kyc</code> 的翻譯值原本就已是租用表述,本次僅同步修正其過期的 HTML 靜態回退文字。</p>
|
||
|
||
<div class="callout ok">
|
||
<p><span class="lbl">驗證:</span>本地以 <code>npm run start:local</code> 啟動 <code>justsolutionsWebV2</code> 即可看到「上次訂閱資訊 → 已購增值」與訂單摘要處的 KYC 顯示為「KYC 設備租用」表述。</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ───────────────────────────────────────────── -->
|
||
<section id="refs">
|
||
<h2 class="sec">七、關鍵代碼位置索引</h2>
|
||
<table>
|
||
<thead><tr><th style="width:30%">角色</th><th>檔案 : 行</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>底層欄位定義</td><td class="path">AML_Backend/modules/iCON.Abp.AMLPortal/…/Domain/DbEntity/TenantProperty.cs:69-71</td></tr>
|
||
<tr><td>賦值:新租戶 / 續費 / 判定函式</td><td class="path">…/AMLPortal.Application/OrderService.cs:763、2633、872、2694-2697</td></tr>
|
||
<tr><td>續費查詢上報 KYC</td><td class="path">…/AMLPortal.Application/OrderService.cs:2460</td></tr>
|
||
<tr><td>續費查詢 DTO</td><td class="path">…/AMLPortal.Application.Contracts/OrderAppLayer/RenewableTenantByEmailDto.cs:46-54</td></tr>
|
||
<tr><td>Host 預設 true</td><td class="path">…/AMLPortal.Domain/Seeders/TenantPropertyDataSeeder.cs:63-79</td></tr>
|
||
<tr><td>前端讀 enableKYC / 掃描按鈕門檻</td><td class="path">AML_Frontend/…/kyc-section/kyc-section.component.ts:147-148、.html:10</td></tr>
|
||
<tr><td>AML DTO 映射</td><td class="path">…/iCON.Abp.AML.Application/AMLAppAutoMapperProfile.cs:78 · …/TenantConfigLayer/TenantPropertyDto.cs:118</td></tr>
|
||
<tr><td>plans-plus 顯示 / 加購項</td><td class="path">justsolutionsWebV2/public/js/plans-plus.js:570-581 · public/plans-plus.html:468、579</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<hr class="soft">
|
||
<p class="small">本文為程式碼閱讀分析結論,隨代碼演進可能變動;引用行號以撰寫時的倉庫狀態為準。</p>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|