Files
group_xinghuo_jinrong/docs/course/jinrong-module-risk/index.html
T
zhanghongyu_0626 793c0307f8 feat(risk): Enhance risk management functionality and access control
- Updated `RiskListAccess` and `ThresholdWriteAccess` to enforce access control in the risk repository and threshold repository, ensuring only authorized roles can perform sensitive operations.
- Introduced new methods in `RiskRepository` for counting pending alerts and listing alerts with access checks, improving data security and compliance.
- Enhanced the `chat.py` and `deps.py` files to integrate compliance roles into the risk management matrix, allowing for more granular access control.
- Updated documentation to reflect the new testing baseline of 825 passed tests, indicating improved stability and functionality across the application.

This update significantly strengthens the risk management capabilities, ensuring robust access control and compliance with organizational policies.
2026-09-11 17:07:22 +08:00

885 lines
51 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>风控监测 Agent 深潜 · 交互导览</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: #D94F30;
--color-accent-hover: #B83E24;
--color-accent-light: #FCEEEA;
--color-accent-muted: #E87A62;
}
</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">风控监测 Agent 深潜</span>
<div class="nav-dots" id="nav-dots" role="tablist">
<button class="nav-dot" data-target="module-1" data-tooltip="四条能力线" role="tab" aria-label="模块 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>
<button class="nav-dot" data-target="module-4" data-tooltip="演示与缺口" role="tab" aria-label="模块 4: 演示与缺口"></button>
<button class="nav-dot" data-target="module-5" data-tooltip="FR-8/9/10" role="tab" aria-label="模块 5: FR-8/9/10"></button>
<button class="nav-dot" data-target="module-6" data-tooltip="L3 与角色" role="tab" aria-label="模块 6: L3 与角色"></button>
<button class="nav-dot" data-target="module-7" data-tooltip="写侧并发" role="tab" aria-label="模块 7: 写侧并发"></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">预警、适当性、AML<br>与模拟交易</h1>
<p class="module-lead animate-in">
风控专员 Demo <code>STAFF-30001</code> 带 <code>risk_officer</code> + <code>risk_demo</code> 角色:
能看预警台账、处置、跑 AML 扫描,还能在模拟交易页触发规则引擎。
本地登录页一键切换,改 JWT 角色后需<strong>重新登录</strong>。
仓库基线 <code>merger</code> · <code>python -m pytest</code> → <strong>825 passed</strong>。
</p>
<div class="screen animate-in">
<h2>四条能力线(REST + 对话 Tool 共用底座)</h2>
<div class="pattern-cards">
<div class="pattern-card">
<h3>预警台账 FR-4</h3>
<p><code>GET /api/risk/alerts</code> 分页查询;<code>POST .../handle</code> 人工处置(仅 risk_officer)。响应固定带 disclaimer。</p>
</div>
<div class="pattern-card">
<h3>适当性校验 R-02 / FR-2</h3>
<p><code>POST /api/risk/suitability/check</code> 与网关共用 <span class="term" data-definition="Core 只读查询层(core_ro.py):从 jinrong_core 模拟库读 L0/持仓/流水,并做 C×R 适当性矩阵判定。">Core 只读层</span> 的 <code>check_suitability</code>(旧 SUIT-001~008 已退役)。客户本人 / 理财师名下 / 风控全量。</p>
</div>
<div class="pattern-card">
<h3>AML 扫描 FR-5</h3>
<p><code>POST /api/risk/aml/scan</code>:仅 risk_officer 触发全量扫描,生成 aml 类预警。</p>
</div>
<div class="pattern-card">
<h3>模拟交易 FR-1</h3>
<p><code>POST /api/simulate/trade</code>:risk_demo 或客户本人可提交;走 模拟写 Core 网关(trade_gateway.py) → 适当性 → 规则引擎(risk_engine.py)。</p>
</div>
</div>
<div class="callout callout-accent">
<strong>鉴权与问数不同:</strong> 风控 REST 走 <code>get_auth_context</code>,前端 <code>risk.ts</code> 必须带 <code>X-Agent-Type: risk</code> + Bearer。缺头会 401 <code>AUTH_401_MISSING_AGENT_TYPE</code>。
</div>
</div>
<div class="screen animate-in">
<h2>群聊:风控专员打开预警页</h2>
<p>浏览器通过 riskHeaders 声明自己在敲风控窗:</p>
<div class="chat-window" id="chat-risk-m1">
<div class="chat-messages">
<div class="chat-message" data-msg="0" data-sender="browser" style="display:none">
<div class="chat-avatar" style="background:#D94F30">前</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D94F30">RiskAlertsPage</span>
<p>GET /api/risk/alerts · Bearer + X-Agent-Type: risk</p>
</div>
</div>
<div class="chat-message" data-msg="1" data-sender="api" style="display:none">
<div class="chat-avatar" style="background:#E87A62">API</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#E87A62">deps.get_auth_context</span>
<p>验 JWT + 矩阵:STAFF-30001 risk_officer 放行</p>
</div>
</div>
<div class="chat-message" data-msg="2" data-sender="repo" style="display:none">
<div class="chat-avatar" style="background:#B83E24">库</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#B83E24">RiskRepository</span>
<p>list_alerts(status=pending_review) → items + total + disclaimer</p>
</div>
</div>
<div class="chat-message" data-msg="3" data-sender="ui" style="display:none">
<div class="chat-avatar" style="background:#D4A843">页</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D4A843">Table + Modal</span>
<p>展示预警列表;点处置调 handleAlert → POST .../handle</p>
</div>
</div>
</div>
<div class="chat-typing" id="chat-risk-m1-typing" style="display:none">
<div class="chat-avatar" id="chat-risk-m1-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">CODE · 模拟交易入口(simulate.py)鉴权</span>
<pre><code><span class="code-line"><span class="code-keyword">if not</span> (auth.has_role(<span class="code-string">"risk_demo"</span>)</span>
<span class="code-line"> <span class="code-keyword">or</span> (auth.is_customer()</span>
<span class="code-line"> <span class="code-keyword">and</span> auth.customer_id == req.customer_id)):</span>
<span class="code-line"> deny(auth, <span class="code-string">"AUTH_403_ROLE"</span>, ...)</span>
<span class="code-line"><span class="code-keyword">return</span> submit_trade(req.model_dump(), actor_id=auth.actor_id)</span></code></pre>
</div>
<div class="translation-english">
<span class="translation-label">白话</span>
<div class="translation-lines">
<p class="tl">模拟交易不是谁都能点:要么 risk_demo 演示号,要么客户给自己下单。</p>
<p class="tl">STAFF-30001 有 risk_demo 权限,可在 RiskSimulatePage 代客触发规则。</p>
<p class="tl">越权会 deny 并写审计,不会悄悄放行。</p>
</div>
</div>
</div>
<div class="quiz-container" id="quiz-risk-m1">
<div class="quiz-question-block"
data-correct="option-b"
data-explanation-right="risk_demo 是 STAFF-30001 的演示权限,允许在模拟交易页提交 trade 请求。"
data-explanation-wrong="普通 staff 没有 risk_demo;模拟交易专门给演示账号或客户本人。">
<h3 class="quiz-question">STAFF-30001 能在模拟交易页提交申购吗?</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>能,因为有 risk_demo 角色</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>能,任何 staff 都行</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-risk-m1')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-risk-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">一笔演示交易<br>怎么长出预警单?</h1>
<p class="module-lead animate-in">
<code>RiskSimulatePage</code> 提交表单 → 模拟交易入口(<code>simulate.py</code>)验权 → 模拟写 Core 网关(<code>trade_gateway.py</code>)写库 →
规则引擎(<code>risk_engine.py</code>)扫规则 → 聚合进 <code>risk_alert</code>。
像演示用的「假收银台」:钱是假的,但风控流程是真的。
</p>
<div class="screen animate-in">
<h2>四段链路(动画)</h2>
<div class="flow-animation" data-steps='[
{"highlight":"flow-actor-1","label":"RiskSimulatePage:submitSimulateTrade(token, \"risk\", body)"},
{"highlight":"flow-actor-2","label":"模拟交易入口(simulate.py):POST /api/simulate/trade → 鉴权 risk_demo 或客户本人","packet":true,"from":"1","to":"2"},
{"highlight":"flow-actor-3","label":"模拟写 Core 网关(trade_gateway.py):R-02 适当性 → INSERT core_trade(唯一写 Core 路径)","packet":true,"from":"2","to":"3"},
{"highlight":"flow-actor-4","label":"规则引擎(risk_engine.py)扫描 → 写 risk_alert(大额/集中度等)","packet":true,"from":"3","to":"4"},
{"highlight":"flow-actor-1","label":"台账 GET /api/risk/alerts 可见 + 固定 disclaimer","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>模拟交易 UI</span></div>
<div class="flow-actor" id="flow-actor-2"><span class="flow-actor-icon">🚪</span><span>模拟交易入口</span></div>
<div class="flow-actor" id="flow-actor-3"><span class="flow-actor-icon">⚙</span><span>模拟写 Core 网关</span></div>
<div class="flow-actor" id="flow-actor-4"><span class="flow-actor-icon">🛡</span><span>规则引擎</span></div>
</div>
<p class="flow-step-label">Preset A-3:CUST-3001 · 50 万 → 大额预警 · Preset A-1:适当性阻断</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">
<h2>R-02 阻断 vs R-01 大额待审(同网关、不同结果)</h2>
<p>模拟写 Core 网关(<code>trade_gateway.py</code>)里<strong>先</strong>跑适当性:不过 → 交易写不进去(R-02);过了 → 写入 <code>core_trade</code> → 规则引擎再扫 R-01 大额等 → 出 <code>pending_review</code> 预警。</p>
<div class="pattern-cards">
<div class="pattern-card">
<h3>A-1 · R-02 阻断</h3>
<p>CUST-1001 + PROD-161725 · 风评与产品 R 档不匹配 → 网关拒绝 · 可有 suitability 类预警 · <strong>无成交</strong></p>
</div>
<div class="pattern-card">
<h3>A-3 · R-01 大额待审</h3>
<p>CUST-3001 · 50 万申购 → 成交写入 Core → 引擎出大额类 <code>pending_review</code> · 台账可见</p>
</div>
</div>
<div class="callout callout-warning">
<strong>指挥 AI 时:</strong> 别把「阻断交易」和「出预警待审」混成一个状态——前者在网关,后者在引擎聚合。
</div>
</div>
<div class="screen animate-in">
<h2>代码地图</h2>
<div class="file-tree animate-in">
<div class="tree-item tree-folder">模拟交易入口(app/api/simulate.py)</div>
<div class="tree-item tree-indent">薄路由:鉴权 + 调 submit_trade</div>
<div class="tree-item tree-folder">模拟写 Core 网关(app/gateway/trade_gateway.py)</div>
<div class="tree-item tree-indent">适当性 → 写 core_trade → 调 risk 引擎</div>
<div class="tree-item tree-folder">app/service/risk/</div>
<div class="tree-item tree-indent">规则引擎(risk_engine.py) · alert_service.py · 规则聚合</div>
<div class="tree-item tree-folder">web/src/pages/risk/RiskSimulatePage.tsx</div>
<div class="tree-item tree-indent">预设 A-1 / A-3 一键填表</div>
</div>
</div>
<div class="screen animate-in">
<div class="translation-block">
<div class="translation-code">
<span class="translation-label">CODE · 模拟交易入口(simulate.py)</span>
<pre><code><span class="code-line"><span class="code-keyword">if not</span> (auth.has_role(<span class="code-string">"risk_demo"</span>)</span>
<span class="code-line"> <span class="code-keyword">or</span> (auth.is_customer() <span class="code-keyword">and</span></span>
<span class="code-line"> auth.customer_id == req.customer_id)):</span>
<span class="code-line"> deny(auth, <span class="code-string">"AUTH_403_ROLE"</span>, ...)</span>
<span class="code-line"><span class="code-keyword">return</span> submit_trade(req.model_dump(), ...)</span></code></pre>
</div>
<div class="translation-english">
<span class="translation-label">白话</span>
<div class="translation-lines">
<p class="tl">能点「提交模拟交易」的只有两类人:带 risk_demo 的风控演示号,或客户本人买自己的单。</p>
<p class="tl">STAFF-30001 在 jwt_service 里配了 risk_demo,所以能造 A-3 大额演示数据。</p>
</div>
</div>
</div>
<div class="quiz-container" id="quiz-risk-m2">
<div class="quiz-question-block"
data-correct="option-c"
data-explanation-right="模拟写 Core 网关(trade_gateway.py)是唯一允许向 jinrong_core 写 INSERT 的路径(模拟成交);Agent 画像不能直接改 Core 正式字段。"
data-explanation-wrong="问数和其他 Agent 对 Core 是只读;只有网关模拟交易可写 core_trade。">
<h3 class="quiz-question">Demo 里谁可以把新成交写进 core_trade?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>NL→SQL 编排(analyst_agent.py)问数 SQL</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>风控对话 Tool</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>模拟写 Core 网关(trade_gateway.py,经 simulate/trade)</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-risk-m2')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-risk-m2')">重做</button>
</div>
</div>
</div>
</section>
<section class="module" id="module-3">
<div class="module-inner">
<p class="eyebrow animate-in">模块 3 · REST 与对话 Tool</p>
<h1 class="module-title animate-in">台账 REST 要带 risk 头<br>对话靠六只 Tool</h1>
<p class="module-lead animate-in">
风控<span class="term" data-definition="REST:用 URL + HTTP 方法(GET/POST)访问的后端接口,浏览器和脚本都能调。">REST</span>
走模块鉴权工厂(<code>deps.py</code> 的 <code>get_auth_context</code>),前端必须带 <code>X-Agent-Type: risk</code>。
风控对话走顾问通用编排(<code>agent_service.py</code>)的 risk 分支 + SSE;Tool 由风控对话 Tool 注册表(<code>chat_tools.py</code>)注册 <strong>六只</strong>只读 Tool,经对话 Tool 编排(<code>tool_service.py</code>)分发。
</p>
<div class="screen animate-in">
<h2>预警 REST 两端点</h2>
<div class="pattern-cards">
<div class="pattern-card">
<h3>GET /api/risk/alerts</h3>
<p>risk_officer / risk_manager 全量;compliance 强制只看 aml;响应含 <code>disclaimer</code></p>
</div>
<div class="pattern-card">
<h3>POST /api/risk/alerts/{id}/handle</h3>
<p>仅 risk_officer;handler_result 三枚举 + 审计;409 状态冲突</p>
</div>
</div>
<div class="callout callout-warning">
<strong>曾踩过的坑:</strong> <code>web/src/api/risk.ts</code> 若漏 <code>X-Agent-Type: risk</code>,JWT 正确也 401。
问数 API 则相反——不要带这个头。
</div>
</div>
<div class="screen animate-in">
<h2>风控对话 Tool(chat_tools.py)注册表(C1 + 扩展)</h2>
<div class="file-tree animate-in">
<div class="tree-item tree-folder">风控对话 Tool(app/service/risk/chat_tools.py)</div>
<div class="tree-item tree-indent">alert_query — 客户或全量待审预警</div>
<div class="tree-item tree-indent">customer_context — L0 + L3 + 待审预警</div>
<div class="tree-item tree-indent">suitability_check — 只读校验 + 审计落库</div>
<div class="tree-item tree-indent">aml_lookup — 名单匹配</div>
<div class="tree-item tree-indent">query_overdue_alerts — 超期未处置(FR-9)</div>
<div class="tree-item tree-indent">query_agent_behavior — 代理人行为链(FR-10)</div>
</div>
<p>红线:Tool 只读,不生成预警单、不处置、不改正式风险等级。</p>
</div>
<div class="screen animate-in">
<h2>群聊:风控问「今天多少待审预警」</h2>
<div class="chat-window" id="chat-risk-m3">
<div class="chat-messages">
<div class="chat-message" data-msg="0" data-sender="browser" style="display:none">
<div class="chat-avatar" style="background:#D94F30">前</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D94F30">RiskChatPage</span>
<p>POST /api/chat/stream · X-Agent-Type: risk</p>
</div>
</div>
<div class="chat-message" data-msg="1" data-sender="chat" style="display:none">
<div class="chat-avatar" style="background:#E87A62">API</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#E87A62">tool_service</span>
<p>意图命中「待审预警」→ alert_query(customer_id 空缺=全量)</p>
</div>
</div>
<div class="chat-message" data-msg="2" data-sender="tool" style="display:none">
<div class="chat-avatar" style="background:#B83D24">Tool</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#B83D24">alert_query</span>
<p>RiskRepository.list_alerts → 今日新增口径 + alert_id 列表</p>
</div>
</div>
<div class="chat-message" data-msg="3" data-sender="llm" style="display:none">
<div class="chat-avatar" style="background:#D4A843">AI</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D4A843">DeepSeek</span>
<p>组织自然语言;可追问 query_agent_behavior 查 STAFF 行为链</p>
</div>
</div>
</div>
<div class="chat-typing" id="chat-risk-m3-typing" style="display:none">
<div class="chat-avatar" id="chat-risk-m3-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">前端 · risk.ts + simulate</span>
<pre><code><span class="code-line">apiFetch(<span class="code-string">'/api/risk/alerts'</span>, {</span>
<span class="code-line"> token, agentType: <span class="code-string">'risk'</span>, <span class="code-comment">// 必须</span></span>
<span class="code-line">})</span>
<span class="code-line"><span class="code-comment">// RiskSimulatePage → /api/simulate/trade 同样带 risk</span></span></code></pre>
</div>
<div class="translation-english">
<span class="translation-label">白话</span>
<div class="translation-lines">
<p class="tl">台账页、模拟交易页、风控对话——凡走 get_auth_context 的都要声明 risk 线。</p>
<p class="tl">处置预警用 handle REST;日常问数用对话 Tool,两者不要混接口。</p>
</div>
</div>
</div>
<div class="quiz-container" id="quiz-risk-m3">
<div class="quiz-question-block"
data-correct="option-a"
data-explanation-right="query_agent_behavior 查 audit/行为链,支持 agent_id 过滤;是 FR-10 专用 Tool。"
data-explanation-wrong="customer_context 是客户档案+L3;行为链是独立 Tool。">
<h3 class="quiz-question">风控对话里问「代理人 STAFF-Q 有没有越权记录」,应触发哪个 Tool?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>query_agent_behavior</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>customer_context</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>aml_lookup</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-risk-m3')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-risk-m3')">重做</button>
</div>
<p style="margin-top:2rem; font-size:0.9rem; color: var(--color-text-muted);">
仓库现状(2026-09-10):STAFF-30001 含 risk_demo;<code>python -m pytest</code> → <strong>825 passed</strong>。
</p>
</div>
</div>
</section>
<section class="module" id="module-4">
<div class="module-inner">
<p class="eyebrow animate-in">模块 4 · 演示与缺口</p>
<h1 class="module-title animate-in">后端有、前端没有<br>cron 和空台账怎么理解</h1>
<p class="module-lead animate-in">
风控引擎和 REST 大多已实现,但<strong>演示运维、cron 升级、部分前端按钮</strong>仍开放。
reset 后台账为空是正常现象——不是「风控坏了」。
</p>
<div class="screen animate-in">
<h2>后端有 · 前端未全接(2026-09)</h2>
<div class="pattern-cards">
<div class="pattern-card">
<h3>AML scan</h3>
<p><code>POST /api/risk/aml/scan</code> — API 有,<code>web/</code> 无专用操作页。</p>
</div>
<div class="pattern-card">
<h3>适当性双 URL</h3>
<p>平台 canonical <code>/api/compliance/suitability-check</code> vs 风控 <code>/api/risk/suitability/check</code> — 别再加第三条。</p>
</div>
<div class="pattern-card">
<h3>台账筛选</h3>
<p>类型/客户/日期高级筛选 — 后端能力部分有,UI 简版。</p>
</div>
</div>
</div>
<div class="screen animate-in">
<h2>FR-9/10:引擎在,cron 无 UI</h2>
<div class="flow-animation" data-steps='[
{"highlight":"flow-r-1","label":"对话 Tool query_overdue_alerts → 查库内已有超期单"},
{"highlight":"flow-r-2","label":"FR-9 超期升级 cron(escalation_scan.py)→ 定时扫描 pending_review 超阈值","packet":true,"from":"1","to":"2"},
{"highlight":"flow-r-3","label":"FR-10 行为扫描 cron(agent_behavior_scan.py)→ 行为模式扫描","packet":true,"from":"2","to":"3"},
{"highlight":"flow-r-1","label":"须运维手动跑脚本;Chat 里问不到「帮我跑 cron」","packet":true,"from":"3","to":"1"}
]'>
<div class="flow-actors">
<div class="flow-actor" id="flow-r-1"><span class="flow-actor-icon">💬</span><span>风控 Chat</span></div>
<div class="flow-actor" id="flow-r-2"><span class="flow-actor-icon">⏰</span><span>cron 脚本</span></div>
<div class="flow-actor" id="flow-r-3"><span class="flow-actor-icon">📊</span><span>MySQL 台账</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 class="callout callout-accent">
<strong>reset 后台账空:</strong> <code>reset.ps1</code> 只重建 <code>jinrong_core</code>;<code>risk_alert</code> 要跑 <code>prepare_risk_demo.sql</code> + AML 种子才有演示数据。完整步骤见 <code>docs/项目框架设计/演示SOP-风控模块.md</code> §2。
</div>
</div>
<div class="screen animate-in">
<h2>合规硬边界(再强调)</h2>
<p>预警默认 <code>pending_review</code> · 仅风控专员 <code>handle</code> 改状态 · <strong>不自动冻户</strong> · 不自动上报监管 · 不改正式 C1~C5。</p>
<div class="quiz-container" id="quiz-risk-m4">
<div class="quiz-question-block"
data-correct="option-a"
data-explanation-right="对。FR-9 升级靠 escalation_scan 脚本扫库,不是对话里触发定时任务。"
data-explanation-wrong="对话 Tool 只能查询已有数据,不能替代 cron 运维。">
<h3 class="quiz-question">风控专员说「在 Chat 里说一句就自动跑超期升级」,能实现吗?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>不能,要部署/手动跑 FR-9 超期升级 cron(escalation_scan.py)</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>能,加 LangGraph 节点调 cron</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>能,让客服 Agent 代跑</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-risk-m4')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-risk-m4')">重做</button>
</div>
</div>
</div>
</section>
<section class="module" id="module-5">
<div class="module-inner">
<p class="eyebrow animate-in">模块 5 · FR-8/9/10</p>
<h1 class="module-title animate-in">集中度、超期升级<br>与代理人行为链</h1>
<p class="module-lead animate-in">
PRD v1.1 追加的三条能力(FR-8~10)已在 <code>merger</code> 落地。
指挥 AI 改代码时要分清:<strong>规则引擎 / cron 脚本负责「出单、升级」</strong>;<strong>对话 Tool 只读查库</strong>,不能替运维跑定时任务。
</p>
<div class="screen animate-in">
<h2>三条规则 · 各管什么</h2>
<div class="pattern-cards">
<div class="pattern-card">
<h3>FR-8 · RISK-006 集中度</h3>
<p>客户持仓 R4+R5 占比 ≥ 阈值 → 并入<strong>事件类</strong>预警聚合(同客户同日一张事件单)。演示 A-10。</p>
</div>
<div class="pattern-card">
<h3>FR-9 · RISK-007 超期升级</h3>
<p><code>pending_review</code> 超阈值未处置 → 超期升级 cron(<code>escalation_scan.py</code>)写 payload 分级 · <strong>status 不变</strong>。</p>
</div>
<div class="pattern-card">
<h3>FR-10 · RISK-008 行为链</h3>
<p>代理人 A/B/C 条件命中 → 按 <code>actor_id</code> 维度出单;明细仅 officer/manager 可见。演示 A-12。</p>
</div>
</div>
</div>
<div class="screen animate-in">
<h2>数据流:引擎出单 vs Chat 只读查</h2>
<div class="flow-animation" data-steps='[
{"highlight":"flow-fr-1","label":"事件线:simulate/trade 或 cron → risk_engine / escalation / behavior_scan → 写 risk_alert"},
{"highlight":"flow-fr-2","label":"对话线:X-Agent-Type: risk → tool_service → query_overdue_alerts / query_agent_behavior","packet":true,"from":"1","to":"2"},
{"highlight":"flow-fr-3","label":"FR-8 集中度:customer_context Tool 读 core_ro.concentration_profile(与 RISK-006 同源)","packet":true,"from":"2","to":"3"},
{"highlight":"flow-fr-1","label":"Chat 不能调用 escalation_scan.py;只能查库里已有超期单","packet":true,"from":"3","to":"1"}
]'>
<div class="flow-actors">
<div class="flow-actor" id="flow-fr-1"><span class="flow-actor-icon">⚙</span><span>引擎 / cron</span></div>
<div class="flow-actor" id="flow-fr-2"><span class="flow-actor-icon">💬</span><span>风控 Chat Tool</span></div>
<div class="flow-actor" id="flow-fr-3"><span class="flow-actor-icon">🗄</span><span>risk_alert 表</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">对话 Tool · 超期查询(chat_tools.py)</span>
<pre><code><span class="code-line"><span class="code-comment"># query_overdue_alerts — 只读 FR-9 入口</span></span>
<span class="code-line"><span class="code-comment"># query_agent_behavior — 只读 FR-10 入口</span></span>
<span class="code-line"><span class="code-comment"># 无 Tool 封装 escalation_scan / agent_behavior_scan</span></span></code></pre>
</div>
<div class="translation-english">
<span class="translation-label">白话</span>
<div class="translation-lines">
<p class="tl">想让超期单「升级」→ 运维跑 <code>scripts/cron/escalation_scan.py</code>,不是改 prompt。</p>
<p class="tl">想让行为链出单 → 先积累 audit 痕迹,再跑 <code>agent_behavior_scan.py</code>。</p>
<p class="tl">Chat 里问「有哪些超期单」→ <code>query_overdue_alerts</code> 查现有数据即可。</p>
</div>
</div>
</div>
<div class="quiz-container" id="quiz-risk-m5">
<div class="quiz-question-block"
data-correct="option-b"
data-explanation-right="对。FR-9 升级改 payload 等级,status 仍为 pending_review;处置仍走 handle REST。"
data-explanation-wrong="超期升级不是自动 closed,也不改 handler_result。">
<h3 class="quiz-question">FR-9 超期升级后,预警单的 status 会怎样?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>自动变为 closed</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>仍为 pending_review,payload 承载升级级别</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>Chat Tool 直接改 status</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-risk-m5')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-risk-m5')">重做</button>
</div>
<p class="module-footer-note">相关文件:规则引擎(<code>risk_engine.py</code>)· 超期升级服务(<code>escalation_service.py</code>)· 代理人行为链服务(<code>agent_behavior_service.py</code>)· 风控对话 Tool(<code>chat_tools.py</code>)</p>
</div>
</div>
</section>
<section class="module" id="module-6">
<div class="module-inner">
<p class="eyebrow animate-in">模块 6 · L3 与角色</p>
<h1 class="module-title animate-in">监测档怎么写<br>谁能看谁能处置</h1>
<p class="module-lead animate-in">
L3 监测画像写入服务(<code>profile_l3.py</code>)把 AML 命中、规则结果写入 <code>customer_profile_l3</code>(<strong>防降级合并</strong>)。
模块鉴权工厂(<code>deps.py</code>)按 JWT 角色决定台账 REST 与对话 Tool 能看到什么——接前端或新 Tool 前先对这张矩阵。
</p>
<div class="screen animate-in">
<h2>L3 监测档 · 谁写、谁读</h2>
<div class="step-cards">
<div class="step-card">
<span class="step-num">①</span>
<p><strong>写入</strong> · AML 扫描服务(<code>aml_service.py</code>)命中 → 写 L3 <code>monitor_tier=high</code> + 预警出单(R-03 通知不冻户)</p>
</div>
<div class="step-card">
<span class="step-num">②</span>
<p><strong>合并</strong> · 只升不降(normal &lt; watch &lt; high);禁止 Agent 把 high 改回 normal</p>
</div>
<div class="step-card">
<span class="step-num">③</span>
<p><strong>读取</strong> · <code>customer_context</code> Tool:Core L0 + L3 + 待审预警摘要(只读,供风控 Chat 解读)</p>
</div>
</div>
<div class="callout callout-accent">
<strong>铁律:</strong> L3 是 enrich 监测标签,<strong>不覆盖</strong> Core 正式 C1~C5 风评;审计表只 INSERT。
</div>
</div>
<div class="screen animate-in">
<h2>角色矩阵(REST + 对话数据面)</h2>
<div class="pattern-cards">
<div class="pattern-card">
<h3>risk_officer</h3>
<p>台账全量 · <code>POST .../handle</code> 处置 · AML scan · 模拟 risk_demo · 行为链明细可见</p>
</div>
<div class="pattern-card">
<h3>risk_manager</h3>
<p>台账全量 · 一般不 handle(按矩阵)· 行为链明细可见</p>
</div>
<div class="pattern-card">
<h3>compliance</h3>
<p>台账强制 aml 域 · 无 simulate · 对话 Tool 仍只读</p>
</div>
</div>
</div>
<div class="screen animate-in">
<h2>群聊:AML 命中后 L3 与台账</h2>
<div class="chat-window" id="chat-risk-m6">
<div class="chat-messages">
<div class="chat-message" data-msg="0" data-sender="scan" style="display:none">
<div class="chat-avatar" style="background:#D94F30">扫</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D94F30">aml_service.scan</span>
<p>CUST-1002 展示名命中名单 → 独立 aml 预警单</p>
</div>
</div>
<div class="chat-message" data-msg="1" data-sender="l3" style="display:none">
<div class="chat-avatar" style="background:#E87A62">L3</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#E87A62">profile_l3.upsert</span>
<p>monitor_tier → high(若原档更低则抬升,不降级)</p>
</div>
</div>
<div class="chat-message" data-msg="2" data-sender="tool" style="display:none">
<div class="chat-avatar" style="background:#B83E24">Tool</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#B83E24">customer_context</span>
<p>风控 Chat 问客户档案 → 返回 L0 + L3 high + 待审预警数</p>
</div>
</div>
<div class="chat-message" data-msg="3" data-sender="officer" style="display:none">
<div class="chat-avatar" style="background:#D4A843">员</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D4A843">risk_officer</span>
<p>台账 handle 处置 · <strong>不</strong>自动冻户 · 不改 Core 正式风评</p>
</div>
</div>
</div>
<div class="chat-typing" id="chat-risk-m6-typing" style="display:none">
<div class="chat-avatar" id="chat-risk-m6-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">L3 防降级 · profile_l3.py 口径</span>
<pre><code><span class="code-line"><span class="code-comment"># monitor_tier: normal &lt; watch &lt; high</span></span>
<span class="code-line"><span class="code-comment"># 合并时取 max(tier),禁止写低档覆盖高档</span></span>
<span class="code-line"><span class="code-comment"># risk_score 一期 NULL,归评分模型首写</span></span></code></pre>
</div>
<div class="translation-english">
<span class="translation-label">白话</span>
<div class="translation-lines">
<p class="tl">指挥 AI 加「自动降档」= 违规;L3 只能抬监测强度或保持。</p>
<p class="tl">客户 Agent 不可读 L3 全文念给客户;只有风控/顾问 Tool 在 RBAC 内读。</p>
<p class="tl">处置预警用 REST handle,不是让 LLM 在对话里改库。</p>
</div>
</div>
</div>
<div class="quiz-container" id="quiz-risk-m6">
<div class="quiz-question-block"
data-correct="option-c"
data-explanation-right="对。AML 命中写 L3 high + 出 aml 预警;不自动冻户、不改 Core C 等级。"
data-explanation-wrong="R-03 是通知与监测,不是自动冻户或改正式风评。">
<h3 class="quiz-question">AML 扫描命中 CUST-1002 后,系统会做什么?</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>把 Core 风评改成 C5</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>写 L3 high + aml 预警,待人审</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-risk-m6')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-risk-m6')">重做</button>
</div>
<p class="module-footer-note">相关文件:L3 写入(<code>app/service/risk/profile_l3.py</code>)· AML(<code>aml_service.py</code>)· 模块鉴权(<code>app/api/deps.py</code>)· PRD <code>docs/PRD/PRD-风控监测Agent.md</code></p>
</div>
</div>
</section>
<section class="module" id="module-7">
<div class="module-inner">
<p class="eyebrow animate-in">模块 7 · 写侧并发</p>
<h1 class="module-title animate-in">同一天两笔交易<br>为什么还是一条预警?</h1>
<p class="module-lead animate-in">
进程内聚合锁原语(<code>locks.py</code>)把「同一客户、同一聚合 key」串行化;L3 监测画像写入服务(<code>profile_l3.py</code>)用
<code>computed_at</code> 乐观锁防丢更新。指挥 AI 改写路径时,先分清<strong>悲观锁(串行)</strong>和<strong>乐观锁(版本戳比对)</strong>各管哪一段。
</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 style="text-align:left;padding:0.5rem;">职责</th><th style="text-align:left;padding:0.5rem;">代码入口</th><th style="text-align:left;padding:0.5rem;">机制</th></tr>
</thead>
<tbody>
<tr>
<td style="padding:0.5rem;vertical-align:top;">预警同日聚合串行</td>
<td style="padding:0.5rem;vertical-align:top;">预警处置与聚合服务(<code>alert_service.py</code>)+ 锁原语(<code>locks.py</code> · <code>run_locked</code>)</td>
<td style="padding:0.5rem;vertical-align:top;">单进程 <code>threading.Lock</code>;拿锁超时降级执行,冲突由锁内重查兜底</td>
</tr>
<tr>
<td style="padding:0.5rem;vertical-align:top;">L3 写侧防覆盖</td>
<td style="padding:0.5rem;vertical-align:top;">L3 写入(<code>profile_l3.py</code>)+ 仓储更新(<code>risk_repository.update_l3</code>)</td>
<td style="padding:0.5rem;vertical-align:top;">乐观锁:<code>WHERE computed_at = :expected</code>,失败重读重试(最多 3 次)</td>
</tr>
<tr>
<td style="padding:0.5rem;vertical-align:top;">处置 + 审计同事务</td>
<td style="padding:0.5rem;vertical-align:top;">人工处置(<code>handle_alert</code>)+ 风控仓储(<code>risk_repository</code>)</td>
<td style="padding:0.5rem;vertical-align:top;">状态变更与 audit 同连接提交(B7 挂账⑦ 已收口)</td>
</tr>
</tbody>
</table>
<div class="callout callout-accent">
<strong>不做:</strong> 风控线不接 L1/L2 画像 Redis 热缓存(归客服/顾问);L3 读侧已有 <code>profile:l3:{customer_id}</code> cache-aside + 写后 DEL。
</div>
</div>
<div class="screen animate-in">
<div class="translation-block">
<div class="translation-code">
<span class="translation-label">锁原语 · 预警聚合 key 示例</span>
<pre><code><span class="code-line"><span class="code-comment"># alert_service:同日同客户事件类预警</span></span>
<span class="code-line">run_locked(</span>
<span class="code-line"> <span class="code-string">f"agg:event:{customer_id}:{today}"</span>,</span>
<span class="code-line"> <span class="code-keyword">lambda</span> locked: _merge_or_insert(...),</span>
<span class="code-line">)</span>
<span class="code-line"></span>
<span class="code-line"><span class="code-comment"># profile_l3:同一客户 L3 upsert</span></span>
<span class="code-line">run_locked(<span class="code-string">f"l3:{customer_id}"</span>, _write_with_optimistic_retry)</span></code></pre>
</div>
<div class="translation-english">
<span class="translation-label">白话</span>
<p>想象收银台<strong>叫号器</strong>:同一客户、同一天的「首单聚合」必须排队进窗口,窗口里再查「今天是否已有单」——两笔并发进来也不会开出两张重复首单。</p>
<p>L3 则是<strong>贴便签前先对表上的时间戳</strong>:若别人刚改过,你的 UPDATE 打不中,就重读再合并(只升不降监测档)。</p>
</div>
</div>
</div>
<div class="screen animate-in">
<h2>群聊:并发两笔 · 聚合与 L3</h2>
<div class="chat-window" id="chat-risk-m7">
<div class="chat-messages">
<div class="chat-message" data-msg="0" data-sender="t1" style="display:none">
<div class="chat-avatar" style="background:#D94F30">T1</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D94F30">trade #1</span>
<p>引擎出单 → 请求 <code>run_locked(agg:event:…)</code></p>
</div>
</div>
<div class="chat-message" data-msg="1" data-sender="t2" style="display:none">
<div class="chat-avatar" style="background:#E87A62">T2</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#E87A62">trade #2</span>
<p>同时到达 → 在锁外等待或超时降级;锁内重查并入同一 <code>risk_alert</code></p>
</div>
</div>
<div class="chat-message" data-msg="2" data-sender="l3" style="display:none">
<div class="chat-avatar" style="background:#B83E24">L3</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#B83E24">profile_l3</span>
<p>upsert 前读 <code>computed_at</code> → UPDATE 带 expected → 失败则重试</p>
</div>
</div>
<div class="chat-message" data-msg="3" data-sender="audit" style="display:none">
<div class="chat-avatar" style="background:#D4A843">审</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D4A843">audit</span>
<p>处置 handle:改 status + 写 audit_log <strong>同一事务</strong>(避免「状态变了没审计」)</p>
</div>
</div>
</div>
<div class="chat-typing" id="chat-risk-m7-typing" style="display:none">
<div class="chat-avatar" id="chat-risk-m7-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">
<h2>数据流:写侧三条线</h2>
<div class="flow-animation" data-steps='[
{"highlight":"flow-w1","label":"交易事件进引擎 → alert_service 拿 agg 锁"},
{"highlight":"flow-w2","label":"锁内查锚点 → 首单 INSERT / 后续 append 事件","packet":true,"from":"1","to":"2"},
{"highlight":"flow-w3","label":"profile_l3 upsert:run_locked + 乐观锁 retry","packet":true,"from":"2","to":"3"},
{"highlight":"flow-w1","label":"officer handle:status + audit 同事务","packet":true,"from":"3","to":"1"}
]'>
<div class="flow-actors">
<div class="flow-actor" id="flow-w1"><span class="flow-actor-icon">🔒</span><span>聚合锁</span></div>
<div class="flow-actor" id="flow-w2"><span class="flow-actor-icon">📋</span><span>risk_alert</span></div>
<div class="flow-actor" id="flow-w3"><span class="flow-actor-icon">📊</span><span>L3 + audit</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="quiz-container" id="quiz-risk-m7">
<div class="quiz-question-block"
data-correct="option-b"
data-explanation-right="对。L1/L2 Redis 热缓存是客服/顾问画像线;风控写侧是 L3 + 聚合锁 + 审计同事务。"
data-explanation-wrong="风控不做 L1/L2 Redis;L3 已有 cache-aside,写侧靠锁与乐观锁。">
<h3 class="quiz-question">指挥 AI「给风控加 L1 Redis 热读」合理吗?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>合理,四个 Agent 画像都应同一套 Redis</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>不合理;风控只需 L3 cache-aside,L1/L2 归客服/顾问</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>合理,用 Redis 替代所有 MySQL 写</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-risk-m7')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-risk-m7')">重做</button>
</div>
</div>
</div>
</section>
</main>
</body>
</html>