Files
group_xinghuo_jinrong/docs/course/jinrong-module-platform/index.html
T
zhanghongyu_0626 6f222f1c56 feat(course): Add course assembly script and module structure for advisor training
- Introduced `build_all.py` script to automate the assembly of course modules into a single `index.html` file.
- Created `index.html` for the main course overview, featuring a structured layout and navigation for various modules.
- Developed `_base.html` and `_footer.html` templates for the advisor module, ensuring consistent styling and structure.
- Added `build.sh` script for individual module assembly, enhancing modularity and ease of updates.
- Implemented multiple module HTML files detailing specific training scenarios and functionalities for advisors, including interactive elements and quizzes.

This update significantly enhances the course delivery framework, providing a comprehensive and interactive learning experience for advisors.
2026-09-09 23:22:59 +08:00

375 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>代销平台 API 深潜 · 交互导览</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700;12..96,800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400;1,9..40,500&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<style>
:root {
--color-accent: #E06B56;
--color-accent-hover: #C85A48;
--color-accent-light: #FCEEEA;
--color-accent-muted: #E89A8A;
}
</style>
<script src="main.js" defer></script>
</head>
<body>
<nav class="nav" id="nav">
<div class="progress-bar" id="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
<div class="nav-inner">
<span class="nav-title">代销平台 API 深潜</span>
<div class="nav-dots" id="nav-dots" role="tablist">
<button class="nav-dot" data-target="module-1" data-tooltip="v0.1 路由" role="tab" aria-label="模块 1: v0.1 路由"></button>
<button class="nav-dot" data-target="module-2" data-tooltip="平台鉴权" role="tab" aria-label="模块 2: 平台鉴权"></button>
<button class="nav-dot" data-target="module-3" data-tooltip="脱敏开关" role="tab" aria-label="模块 3: 脱敏开关"></button>
</div>
</div>
</nav>
<main id="main">
<section class="module" id="module-1">
<div class="module-inner">
<p class="eyebrow animate-in">模块 1 · 路由地图</p>
<h1 class="module-title animate-in">v0.1 路由:<br>customers / products / advisors / compliance</h1>
<p class="module-lead animate-in">
代销平台 API 是「读 Core 模拟库」的
<span class="term" data-definition="REST = 用固定 URL + HTTP 方法(GET/POST)读写数据的约定,浏览器和 App 都按这个格式发请求。">REST</span>
门面。路由按<strong>业务域</strong>切分,不用 <code>/api/platform/*</code> 前缀。
和 Agent 对话线(<code>/api/chat</code>)是两条平行轨道——指挥 AI 加「查持仓」时,先确认走哪条。
</p>
<div class="screen animate-in">
<h2>四块 canonical 域(以本文为准)</h2>
<div class="pattern-cards">
<div class="pattern-card">
<h3>/api/customers/*</h3>
<p>客户 L0 档案、持仓、流水。<code>customers.py</code> → <code>platform/customer_service</code> → <code>core_ro</code>。</p>
</div>
<div class="pattern-card">
<h3>/api/products/*</h3>
<p>产品列表、净值曲线。理财师/客户看产品详情都走这里,不另开 Agent Tool HTTP。</p>
</div>
<div class="pattern-card">
<h3>/api/advisors/*</h3>
<p>理财师名下客户归属。advisor 只能查本人 roster,风控/分析员可全量只读。</p>
</div>
<div class="pattern-card">
<h3>/api/compliance/*</h3>
<p>适当性判定等合规读接口。<strong>canonical</strong>:Agent 合并期改调同一 Service,不保留第二套路径。</p>
</div>
</div>
<div class="callout callout-accent">
<strong>重复能力以谁为准?</strong> 以本平台 API 为准。Agent 侧已有同能力 Tool 时,合并期改调 <code>app/service/platform/</code>,旧路径仅过渡。
</div>
</div>
<div class="screen animate-in">
<h2>三层分工(别让 AI 在路由里写 SQL)</h2>
<div class="flow-animation" data-steps='[
{"highlight":"flow-actor-1","label":"HTTP 请求进入 customers.py"},
{"highlight":"flow-actor-2","label":"get_platform_auth_context:验 JWT + 归属","packet":true,"from":"1","to":"2"},
{"highlight":"flow-actor-3","label":"platform_service:组装、分页、脱敏开关","packet":true,"from":"2","to":"3"},
{"highlight":"flow-actor-4","label":"core_ro:SELECT 只读,不感知 HTTP","packet":true,"from":"3","to":"4"},
{"highlight":"flow-actor-1","label":"ok() 统一外壳返回 JSON","packet":true,"from":"4","to":"1"}
]'>
<div class="flow-actors">
<div class="flow-actor" id="flow-actor-1"><span class="flow-actor-icon">🌐</span><span>api/*.py</span></div>
<div class="flow-actor" id="flow-actor-2"><span class="flow-actor-icon">🔑</span><span>deps 鉴权</span></div>
<div class="flow-actor" id="flow-actor-3"><span class="flow-actor-icon">📦</span><span>platform Service</span></div>
<div class="flow-actor" id="flow-actor-4"><span class="flow-actor-icon">🗄</span><span>core_ro</span></div>
</div>
<p class="flow-step-label">点击「下一步」看一层层往下走</p>
<div class="flow-controls">
<button class="btn flow-next-btn">下一步</button>
<button class="btn flow-reset-btn">重来</button>
</div>
</div>
</div>
<div class="screen animate-in">
<div class="translation-block">
<div class="translation-code">
<span class="translation-label">薄路由 · customers.py</span>
<pre><code><span class="code-line"><span class="code-comment">"""代销平台 · 客户 L0 与资产读 API(canonical · 不要求 X-Agent-Type)。"""</span></span>
<span class="code-line">@router.get(<span class="code-string">"/{customer_id}/holdings"</span>)</span>
<span class="code-line"><span class="code-keyword">def</span> <span class="code-function">list_holdings</span>(</span>
<span class="code-line"> customer_id: str,</span>
<span class="code-line"> auth: AuthContext = Depends(get_platform_auth_context),</span>
<span class="code-line">):</span>
<span class="code-line"> assert_platform_customer_access(auth, customer_id, ...)</span>
<span class="code-line"> <span class="code-keyword">return</span> ok(platform_service.list_holdings(customer_id))</span></code></pre>
</div>
<div class="translation-english">
<span class="translation-label">白话</span>
<div class="translation-lines">
<p class="tl">文件头就写明:这是平台 canonical 读接口,不要 X-Agent-Type。</p>
<p class="tl">URL 用复数 customers + 嵌套 holdings,参数名和域 ID 一致。</p>
<p class="tl">鉴权用平台专用函数,不是 chat 那条 get_auth_context。</p>
<p class="tl">先断言「你有没有权看这个 customer_id」,再调 Service,路由里不出现 SQL。</p>
</div>
</div>
</div>
<div class="quiz-container" id="quiz-platform-m1">
<div class="quiz-question-block"
data-correct="option-b"
data-explanation-right="对。v0.1 拍板:重复能力以本平台 API + platform Service 为准,Agent Tool 合并时改调同一层。"
data-explanation-wrong="不要在 Agent 里再抄一套查持仓 HTTP;会两套口径、联调必炸。">
<h3 class="quiz-question">理财师 Agent 也要查客户持仓,应该新建 /api/agent/holdings 吗?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>要,Agent 专用路径更清晰</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>不要,改调 platform Service 或现有 /api/customers 路径</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>直接在 Agent 里写 SQL 更快</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-platform-m1')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-platform-m1')">重做</button>
</div>
</div>
</div>
</section>
<section class="module" id="module-2">
<div class="module-inner">
<p class="eyebrow animate-in">模块 2 · 平台鉴权</p>
<h1 class="module-title animate-in">get_platform_auth_context:<br>只要 JWT,不要 X-Agent-Type</h1>
<p class="module-lead animate-in">
对话线(<code>/api/chat</code>)必须带
<span class="term" data-definition="HTTP 请求头里的一行,声明这次走 customer/advisor/risk/analyst 哪条 Agent 业务线。">X-Agent-Type</span>;
平台读 API 故意<strong>不要</strong>——App 和仪表盘只证明「你是谁」,不声明「你在敲哪扇 Agent 窗」。
</p>
<div class="screen animate-in">
<h2>和 chat 鉴权的对照表</h2>
<table style="width:100%; border-collapse:collapse; margin: 1.5rem 0; font-size: 0.95rem;">
<thead><tr><th>场景</th><th>函数</th><th>X-Agent-Type</th><th>归属断言</th></tr></thead>
<tbody>
<tr><td>平台 REST</td><td><code>get_platform_auth_context</code></td><td><strong>不要</strong></td><td><code>assert_platform_customer_access</code></td></tr>
<tr><td>对话 / 风控 REST</td><td><code>get_auth_context</code></td><td><strong>必须</strong></td><td><code>assert_customer_access</code> + 准入矩阵</td></tr>
</tbody>
</table>
<div class="callout callout-warning">
<strong>指挥 AI 接前端时:</strong> <code>apiFetch</code> 读持仓只带 Bearer;若误加 <code>X-Agent-Type: customer</code>,平台路由会忽略,但混进 chat 路由就会多一层校验甚至 401。
</div>
</div>
<div class="screen animate-in">
<h2>群聊:前端问「张三持仓多少」</h2>
<p>理财师工作台发 GET 持仓——只走平台三层,不进 LangGraph。</p>
<div class="chat-window" id="chat-platform-m2">
<div class="chat-messages">
<div class="chat-message" data-msg="0" data-sender="frontend" style="display:none">
<div class="chat-avatar" style="background: var(--color-accent)">🖥</div>
<div class="chat-bubble">
<span class="chat-sender" style="color: var(--color-accent)">前端</span>
<p>GET /api/customers/CUST-1001/holdings<br>Authorization: Bearer eyJ…<br><em>(无 X-Agent-Type)</em></p>
</div>
</div>
<div class="chat-message" data-msg="1" data-sender="api" style="display:none">
<div class="chat-avatar" style="background: #5A9DB8">🚪</div>
<div class="chat-bubble">
<span class="chat-sender" style="color: #5A9DB8">customers.py</span>
<p>Depends(get_platform_auth_context) → JWT 验签 OK<br>assert_platform_customer_access:STAFF-10086 名下有 CUST-1001 ✓</p>
</div>
</div>
<div class="chat-message" data-msg="2" data-sender="service" style="display:none">
<div class="chat-avatar" style="background: #E06B56">📦</div>
<div class="chat-bubble">
<span class="chat-sender" style="color: #E06B56">platform_service</span>
<p>list_holdings(customer_id) → prepare_row 脱敏开关检查 → 分页组装 items</p>
</div>
</div>
<div class="chat-message" data-msg="3" data-sender="core" style="display:none">
<div class="chat-avatar" style="background: #2D8B55">🗄</div>
<div class="chat-bubble">
<span class="chat-sender" style="color: #2D8B55">core_ro</span>
<p>SELECT … FROM holdings WHERE customer_id = ?<br>只读,不写 Core 正式账</p>
</div>
</div>
<div class="chat-message" data-msg="4" data-sender="frontend" style="display:none">
<div class="chat-avatar" style="background: var(--color-accent)">🖥</div>
<div class="chat-bubble">
<span class="chat-sender" style="color: var(--color-accent)">前端</span>
<p>收到 200 + data.items,渲染持仓表。全程没经过 chat.py。</p>
</div>
</div>
</div>
<div class="chat-typing" style="display:none">
<div class="chat-avatar" id="chat-platform-m2-typing-avatar">?</div>
<div class="chat-typing-dots">
<span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span>
</div>
</div>
<div class="chat-controls">
<button class="btn chat-next-btn">下一条</button>
<button class="btn chat-all-btn">自动播放</button>
<button class="btn chat-reset-btn">重来</button>
<span class="chat-progress"></span>
</div>
</div>
</div>
<div class="screen animate-in">
<div class="translation-block">
<div class="translation-code">
<span class="translation-label">deps.py</span>
<pre><code><span class="code-line"><span class="code-keyword">def</span> <span class="code-function">get_platform_auth_context</span>(request: Request) -> AuthContext:</span>
<span class="code-line"> <span class="code-string">"""平台 API 鉴权:JWT 通道 **不要求** X-Agent-Type。"""</span></span>
<span class="code-line"> auth_header = request.headers.get(<span class="code-string">"Authorization"</span>, <span class="code-string">""</span>)</span>
<span class="code-line"> <span class="code-keyword">if</span> auth_header[:<span class="code-number">7</span>].lower() == <span class="code-string">"bearer "</span>:</span>
<span class="code-line"> claims = verify_token(token)</span>
<span class="code-line"> <span class="code-keyword">return</span> _bind_state(request, _claims_to_auth(claims))</span>
<span class="code-line"> <span class="code-comment"># dev:X-Debug-Role / X-Debug-Actor 兜底</span></span></code></pre>
</div>
<div class="translation-english">
<span class="translation-label">白话</span>
<div class="translation-lines">
<p class="tl">函数名就写死:平台鉴权,不读 X-Agent-Type。</p>
<p class="tl">有 Bearer 就验 JWT,解析出 actor_id 和 roles。</p>
<p class="tl">本地开发没 JWT 时,可用 debug 头冒充身份(仅 development)。</p>
<p class="tl">和 get_auth_context 不同:后者在 JWT 通道会强制校验 X-Agent-Type + 准入矩阵。</p>
</div>
</div>
</div>
<div class="quiz-container" id="quiz-platform-m2">
<div class="quiz-question-block"
data-correct="option-a"
data-explanation-right="问数 POST /api/analyst/chat 走 get_platform_auth_context,前端 analyst.ts 只带 Bearer。"
data-explanation-wrong="analyst 问数是平台读能力,不是 /api/chat 对话分流,不要 X-Agent-Type。">
<h3 class="quiz-question">分析员「问数工作台」调 /api/analyst/chat,要带 X-Agent-Type: analyst 吗?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>不要,走平台鉴权</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>要,和 ChatPanel 一样</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-platform-m2')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-platform-m2')">重做</button>
</div>
</div>
</div>
</section>
<section class="module" id="module-3">
<div class="module-inner">
<p class="eyebrow animate-in">模块 3 · 出参脱敏</p>
<h1 class="module-title animate-in">PLATFORM_RESPONSE_DESENSITIZE:<br>模拟库原样,真库再遮</h1>
<p class="module-lead animate-in">
环境变量
<code>PLATFORM_RESPONSE_DESENSITIZE</code>
控制平台 API 出参是否打码姓名/手机/证件。
v0.1 默认<strong>关</strong>——本地 Core 模拟库联调看原值;接真 Core 再开,在
<span class="term" data-definition="Service 层 = 业务组装层,REST 和日后 Agent Tool 共用,避免两处各写一套脱敏逻辑。">Platform Service</span>
统一出口处理。
</p>
<div class="screen animate-in">
<h2>开关行为一览</h2>
<table style="width:100%; border-collapse:collapse; margin: 1.5rem 0; font-size: 0.95rem;">
<thead><tr><th>开关</th><th>模拟库联调</th><th>生产 / 真 Core</th></tr></thead>
<tbody>
<tr><td><code>false</code>(默认)</td><td>L0 字段原样返回</td><td>—</td></tr>
<tr><td><code>true</code></td><td>—</td><td>姓名/手机/证件/银行卡打码;<code>customer_id</code> 不脱敏</td></tr>
</tbody>
</table>
<div class="callout callout-accent">
<strong>实现位置:</strong> <code>app/service/platform/common.py</code> 的 <code>maybe_desensitize_row</code> → <code>prepare_row</code>。REST 与 Agent 适配层都应走这里,不要在路由里手写 mask。
</div>
</div>
<div class="screen animate-in">
<div class="translation-block">
<div class="translation-code">
<span class="translation-label">platform/common.py</span>
<pre><code><span class="code-line"><span class="code-keyword">def</span> <span class="code-function">maybe_desensitize_row</span>(row: dict | None) -> dict | None:</span>
<span class="code-line"> <span class="code-keyword">if</span> row <span class="code-keyword">is</span> None <span class="code-keyword">or not</span> settings.platform_response_desensitize:</span>
<span class="code-line"> <span class="code-keyword">return</span> row</span>
<span class="code-line"> <span class="code-keyword">for</span> key, masker <span class="code-keyword">in</span> (</span>
<span class="code-line"> (<span class="code-string">"display_name"</span>, d.mask_name),</span>
<span class="code-line"> (<span class="code-string">"mobile_phone"</span>, d.mask_phone),</span>
<span class="code-line"> (<span class="code-string">"id_card_no"</span>, d.mask_id_card),</span>
<span class="code-line"> ...</span>
<span class="code-line"> ): ...</span>
<span class="code-line"><span class="code-keyword">def</span> <span class="code-function">prepare_row</span>(row):</span>
<span class="code-line"> <span class="code-keyword">return</span> to_jsonable(maybe_desensitize_row(row) or row)</span></code></pre>
</div>
<div class="translation-english">
<span class="translation-label">白话</span>
<div class="translation-lines">
<p class="tl">开关关着 → 数据库行原样返回,方便本地对账。</p>
<p class="tl">开关开着 → 按字段名找 mask 函数,逐个打码。</p>
<p class="tl">prepare_row 是统一出口:先脱敏,再把 Decimal/日期转成 JSON 能序列化的类型。</p>
<p class="tl">指挥 AI 加新字段时,若含 PII,记得在这里登记 masker。</p>
</div>
</div>
</div>
</div>
<div class="screen animate-in">
<h2>自检:你能否指挥 AI 正确切换?</h2>
<div class="quiz-container" id="quiz-platform-m3">
<div class="quiz-question-block"
data-correct="option-c"
data-explanation-right="对。默认 false 是为了模拟库联调;上真库时在 .env 设 PLATFORM_RESPONSE_DESENSITIZE=true,脱敏在 Service 层自动生效。"
data-explanation-wrong="不要在每个路由里 copy 打码逻辑;开关在 settings,行为在 platform/common.py。">
<h3 class="quiz-question">准备接真 Core,要让出参打码手机号,最省事的做法是?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>在每个 customers.py 返回前手写 replace</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>让前端自己 mask 手机号</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>.env 设 PLATFORM_RESPONSE_DESENSITIZE=true</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<div class="quiz-question-block"
data-correct="option-b"
data-explanation-right="customer_id 是业务主键,联调和归属校验都要用,契约明确不脱敏。"
data-explanation-wrong="customer_id 不在 mask 列表里,脱敏只覆盖姓名/手机/证件/银行卡等 PII 字段。">
<h3 class="quiz-question">脱敏开启后,customer_id 会被打码吗?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>会,全部敏感字段都遮</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>不会,customer_id 保留原值</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-platform-m3')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-platform-m3')">重做</button>
</div>
</div>
</div>
</section>
</main>
</body>
</html>