<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>文章 on RaySRE</title><link>https://raysre.com/posts/</link><description>Recent content in 文章 on RaySRE</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Wed, 15 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://raysre.com/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>零依赖 Python 文件服务器部署：从跳板 SSH 到 systemd 生产化的全流程</title><link>https://raysre.com/posts/python-file-server-deployment/</link><pubDate>Wed, 15 Jul 2026 00:00:00 +0000</pubDate><guid>https://raysre.com/posts/python-file-server-deployment/</guid><description>在无外网 Ubuntu 24.04 服务器上，用 Python 标准库 http.server 实现生产级文件服务器（深色/浅色切换、长文件名不截断、面包屑导航），通过跳板机 SSH 连接部署，配置 systemd 用户服务实现开机自启和崩溃自动重启。涵盖零依赖架构决策、完整代码解析、拓扑图、运维命令速查。</description><content:encoded><![CDATA[<h2 id="1-需求背景">1 需求背景</h2>
<p>公司有一台内网服务器（<code>btk-acloud</code>，Ubuntu 24.04 LTS），上面 <code>/data/baic_n60/</code> 目录里存着团队的共享文件。内部同事需要随时通过浏览器访问这些文件——浏览目录、下载。</p>
<p>这台机器有几个约束：</p>
<table>
	<thead>
			<tr>
					<th>约束项</th>
					<th>详情</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>网络</td>
					<td><strong>无外网</strong>——apt 源走内网镜像，但 pip/npm/git clone 全部不可用</td>
			</tr>
			<tr>
					<td>Python</td>
					<td>系统预装 Python 3.12.3，但 PEP 668 禁止 pip 全局安装包</td>
			</tr>
			<tr>
					<td>访问路径</td>
					<td>通过跳板机 <code>10.10.0.4</code> 转发才到 <code>112.31.22.151:3022</code></td>
			</tr>
			<tr>
					<td>数据量</td>
					<td>45.5TB 机械盘，<code>/data</code> 已用 7.5TB</td>
			</tr>
			<tr>
					<td>并发</td>
					<td>团队规模 10-20 人，偶尔同时下载大文件</td>
			</tr>
	</tbody>
</table>
<p>不允许在这个不带外网的机器上折腾 <code>pip install</code>、<code>uv pip install</code>、<code>docker pull</code>、或者 <code>apt-get install nginx</code> 拉一堆依赖——这台机器不是开发机，是生产服务器，不能为一个小需求破坏系统 Python 环境。</p>
<h2 id="2-环境拓扑">2 环境拓扑</h2>
<pre tabindex="0"><code>[ 同事浏览器 ]
       │
       ↓ HTTP :19527
[ 112.31.22.151:3022 ]    ← 对外暴露的 SSH 端口（NAT 映射）
       ↑
[ 跳板机 10.10.0.4 ]      ← SSH 隧道中转
       ↑
[ 10.177.100.30 ]          ← 内网目标机（btk-acloud）
       │
       ↓
[ /data/baic_n60/ ]        ← 45.5TB 机械盘，实际数据 7.5TB
</code></pre><p>连接命令：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sshpass -p <span style="color:#e6db74">&#39;&lt;password&gt;&#39;</span> ssh <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -o <span style="color:#e6db74">&#34;ProxyCommand ssh -i ~/.ssh/id_ed25519 herui@10.10.0.4 -W %h:%p&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -p <span style="color:#ae81ff">3022</span> sup1whu@112.31.22.151
</span></span></code></pre></div><p>由于内网同事通过 HTTP 请求直接打到目标机的 <code>19527</code> 端口，这里不需要 Nginx 反代。服务直接监在 <code>0.0.0.0:19527</code> 即可。</p>
<h2 id="3-核心架构决策">3 核心架构决策</h2>
<h3 id="31-为什么不用-flask--fastapi--django">3.1 为什么不用 Flask / FastAPI / Django</h3>
<p>装不上。没有外网，pip 不可用。<code>apt-get install python3-flask</code> 也不行——Debian 系的 Python 包命名风格和 pypi 不一致，很多现代 Web 框架没有 apt 打好的包。</p>
<h3 id="32-为什么不用-nginx-autoindex">3.2 为什么不用 Nginx autoindex</h3>
<p><code>apt-get install nginx</code> 理论上可以，但对一台 45TB 数据盘的生产服务器，装 nginx 意味着多一个进程、多一套配置、多一个攻击面。这个文件服务器的需求很简单——列目录、下载文件——Python 标准库完全够用。</p>
<h3 id="33-选型对比">3.3 选型对比</h3>
<table>
	<thead>
			<tr>
					<th>方案</th>
					<th style="text-align: center">依赖</th>
					<th style="text-align: center">部署复杂度</th>
					<th style="text-align: center">功能覆盖</th>
					<th style="text-align: center">安全风险</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Nginx autoindex</td>
					<td style="text-align: center">apt nginx</td>
					<td style="text-align: center">中（需配置 server block）</td>
					<td style="text-align: center">完整（但无定制 UI）</td>
					<td style="text-align: center">Nginx CVE</td>
			</tr>
			<tr>
					<td>Python Flask + pip 包</td>
					<td style="text-align: center">pip + 外网</td>
					<td style="text-align: center">高（需要 venv）</td>
					<td style="text-align: center">完整</td>
					<td style="text-align: center">依赖链</td>
			</tr>
			<tr>
					<td><strong>Python 标准库 http.server</strong></td>
					<td style="text-align: center"><strong>无</strong></td>
					<td style="text-align: center"><strong>低</strong></td>
					<td style="text-align: center"><strong>完整</strong></td>
					<td style="text-align: center"><strong>仅 Python 运行时</strong></td>
			</tr>
			<tr>
					<td>busybox httpd</td>
					<td style="text-align: center">apt busybox</td>
					<td style="text-align: center">低</td>
					<td style="text-align: center">弱（无目录列表样式）</td>
					<td style="text-align: center">低</td>
			</tr>
	</tbody>
</table>
<p>Python 标准库方案对这台「无外网 + 生产环境 + PEP 668」的机器是最优解。</p>
<h3 id="34-模块选择threadinghttpserver">3.4 模块选择：ThreadingHTTPServer</h3>
<p>Python 3.12 标准库自带了两个 HTTP 服务器：</p>
<table>
	<thead>
			<tr>
					<th>类</th>
					<th>并发模型</th>
					<th>适用场景</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>http.server.HTTPServer</code></td>
					<td>单线程，请求排队</td>
					<td>开发调试</td>
			</tr>
			<tr>
					<td><code>http.server.ThreadingHTTPServer</code></td>
					<td>每请求一个线程</td>
					<td>生产环境</td>
			</tr>
	</tbody>
</table>
<p>10-20 人团队，ThreadingHTTPServer 够用。不需要引入 <code>asyncio</code> 或者 <code>socketserver.ForkingMixIn</code>——多线程在这个并发量级下足够。</p>
<h2 id="4-代码实现逻辑">4 代码实现逻辑</h2>
<p>完整代码约 350 行，零外部 import。只用了 Python 3.12 自带的这些模块：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> http.server   <span style="color:#75715e"># ThreadingHTTPServer, SimpleHTTPRequestHandler</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> os            <span style="color:#75715e"># 文件遍历、路径校验</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> re            <span style="color:#75715e"># URL 路径清理（合并重复 /）</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> urllib.parse  <span style="color:#75715e"># URL 编解码</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sys           <span style="color:#75715e"># 日志输出</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> datetime      <span style="color:#75715e"># 文件修改时间格式化</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pathlib <span style="color:#f92672">import</span> Path
</span></span></code></pre></div><h3 id="41-总体结构三段式">4.1 总体结构（三段式）</h3>
<pre tabindex="0"><code>main()
  ├── 信号处理（SIGINT / SIGTERM 优雅退出）
  ├── 创建 ThreadingHTTPServer((0.0.0.0, 19527), FileHandler)
  └── server.serve_forever()

FileHandler(SimpleHTTPRequestHandler)
  ├── translate_path()     → URL → 文件系统路径（含防穿越）
  ├── do_GET()             → 目录→列表页，文件→下载
  └── list_directory()     → 生成 HTML 目录列表

工具函数
  ├── human_size()         → 字节 → 人类可读（KB/MB/GB）
  ├── format_time()        → Unix 时间戳 → YYYY-MM-DD HH:MM
  ├── build_breadcrumb()   → 生成面包屑 HTML
  └── escape_html()        → HTML 实体转义
</code></pre><h3 id="42-路径安全防穿越">4.2 路径安全：防穿越</h3>
<p>这是最关键的一行代码。如果用户访问 <code>/../../../etc/passwd</code>，<code>os.path.normpath</code> 会把路径规范化，然后用 <code>os.path.commonpath</code> 检查结果是否仍然在 <code>ROOT_DIR</code> 内：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">translate_path</span>(self, path):
</span></span><span style="display:flex;"><span>    path <span style="color:#f92672">=</span> urllib<span style="color:#f92672">.</span>parse<span style="color:#f92672">.</span>unquote(path<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#34;?&#34;</span>)[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#34;#&#34;</span>)[<span style="color:#ae81ff">0</span>])
</span></span><span style="display:flex;"><span>    rel <span style="color:#f92672">=</span> path<span style="color:#f92672">.</span>lstrip(<span style="color:#e6db74">&#34;/&#34;</span>)
</span></span><span style="display:flex;"><span>    fs_path <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>normpath(os<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>join(ROOT_DIR, rel))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 关键：检查最终路径是否在 ROOT_DIR 范围内</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> os<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>commonpath([os<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>realpath(fs_path),
</span></span><span style="display:flex;"><span>                           os<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>realpath(ROOT_DIR)]) <span style="color:#f92672">!=</span> os<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>realpath(ROOT_DIR):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> os<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>join(ROOT_DIR, <span style="color:#e6db74">&#34;&#34;</span>)  <span style="color:#75715e"># 越界→返回空路径→404</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> fs_path
</span></span></code></pre></div><p>这里三处调用 <code>os.path.realpath</code> 是为了处理符号链接绕过——<code>os.path.normpath</code> 只做字符串规范化，不解析软链接。<code>os.path.realpath</code> 把路径解析到真实的 inode，再比较。</p>
<h3 id="43-目录列表html-模板渲染">4.3 目录列表：HTML 模板渲染</h3>
<p><code>list_directory()</code> 的逻辑：</p>
<ol>
<li><code>os.scandir(path)</code> 遍历目录（比 <code>os.listdir</code> + <code>os.stat</code> 快，一次系统调用同时拿到文件名和 stat）</li>
<li>目录和文件分两组，各自按名称排序（不区分大小写），目录排前面</li>
<li>每个条目生成 <code>&lt;tr&gt;</code>：图标（📁/📄）、带链接的文件名、人类可读大小、日期</li>
<li>用 <code>HTML_TEMPLATE.format()</code> 填充整页</li>
</ol>
<p>文件名列没有设固定宽度、没有 <code>text-overflow: ellipsis</code>。CSS 用了 <code>word-break: break-all</code> 和 <code>overflow-wrap: anywhere</code>——文件名多长都能完整显示。这是最早版本用 <code>min-width: 0</code> + <code>word-break</code> 的组合，测试过 80+ 字符的文件名不会溢出也不换行错乱。</p>
<h3 id="44-文件下载强制下载头">4.4 文件下载：强制下载头</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>self<span style="color:#f92672">.</span>send_header(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;Content-Disposition&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;attachment; filename=&#34;</span><span style="color:#e6db74">{</span>urllib<span style="color:#f92672">.</span>parse<span style="color:#f92672">.</span>quote(fname)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;&#39;</span>
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>只要请求的是文件（不是目录），就加 <code>Content-Disposition: attachment</code>。不管浏览器能不能内联打开（PDF、图片、txt），都给下载。这是团队明确的需求——这些文件本身就是用来下载的，不是在线预览的。</p>
<h3 id="45-深色浅色切换">4.5 深色/浅色切换</h3>
<p>用 CSS 自定义属性（<code>--bg</code>、<code>--text</code> 等）+ <code>data-theme</code> 属性切主题：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-css" data-lang="css"><span style="display:flex;"><span>:<span style="color:#a6e22e">root</span> { --bg: <span style="color:#ae81ff">#ffffff</span>; --text: <span style="color:#ae81ff">#1a1a1a</span>; <span style="color:#960050;background-color:#1e0010">...</span> }
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span><span style="color:#f92672">data-theme</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;dark&#34;</span><span style="color:#f92672">]</span> { --bg: <span style="color:#ae81ff">#0f172a</span>; --text: <span style="color:#ae81ff">#e2e8f0</span>; <span style="color:#960050;background-color:#1e0010">...</span> }
</span></span></code></pre></div><p>JS 就两件事：</p>
<ol>
<li>页面加载时读 <code>localStorage.getItem('baic-theme')</code>，设置初始主题</li>
<li>点击开关时切 <code>data-theme</code> 属性 + 更新 localStorage</li>
</ol>
<p>不需要 cookie、不需要后端 session、不需要 CSS 预处理。8 行 JavaScript 解决问题。</p>
<h2 id="5-部署步骤">5 部署步骤</h2>
<h3 id="51-上传代码到目标机">5.1 上传代码到目标机</h3>
<p>通过跳板机 SCP 把本地写好的 <code>baic-server.py</code> 推到目标机：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sshpass -p <span style="color:#e6db74">&#39;&lt;password&gt;&#39;</span> scp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -o <span style="color:#e6db74">&#34;ProxyCommand ssh -i ~/.ssh/id_ed25519 herui@10.10.0.4 -W %h:%p&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -P <span style="color:#ae81ff">3022</span> /tmp/baic-server.py sup1whu@112.31.22.151:~/baic-server.py
</span></span></code></pre></div><h3 id="52-验证手动启动">5.2 验证手动启动</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># SSH 到目标机</span>
</span></span><span style="display:flex;"><span>ssh sup1whu@btk-acloud
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 手动启动，观察输出</span>
</span></span><span style="display:flex;"><span>python3 ~/baic-server.py
</span></span></code></pre></div><p>输出：</p>
<pre tabindex="0"><code>BiTECH File System running on http://0.0.0.0:19527/
Serving: /data/baic_n60
Press Ctrl+C to stop.
</code></pre><p>在另一台能访问目标机的内网机器上：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -sI http://10.177.100.30:19527/
</span></span><span style="display:flex;"><span><span style="color:#75715e"># HTTP/1.0 200 OK</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Content-Type: text/html; charset=utf-8</span>
</span></span></code></pre></div><p>Ctrl+C 停掉手动进程，确认端口释放：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ss -tlnp | grep <span style="color:#ae81ff">19527</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 应该无输出</span>
</span></span></code></pre></div><h3 id="53-配置-systemd-用户服务">5.3 配置 systemd 用户服务</h3>
<p>服务文件路径：<code>~/.config/systemd/user/baic-files.service</code></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[Unit]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Description</span><span style="color:#f92672">=</span><span style="color:#e6db74">BiTECH File System — /data/baic_n60/ file server</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">After</span><span style="color:#f92672">=</span><span style="color:#e6db74">network.target</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Service]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Type</span><span style="color:#f92672">=</span><span style="color:#e6db74">simple</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ExecStart</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/bin/python3 /home/sup1whu/baic-server.py</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Restart</span><span style="color:#f92672">=</span><span style="color:#e6db74">always</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">RestartSec</span><span style="color:#f92672">=</span><span style="color:#e6db74">5</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WorkingDirectory</span><span style="color:#f92672">=</span><span style="color:#e6db74">/home/sup1whu</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[Install]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">WantedBy</span><span style="color:#f92672">=</span><span style="color:#e6db74">default.target</span>
</span></span></code></pre></div><p>每个参数的含义：</p>
<table>
	<thead>
			<tr>
					<th>参数</th>
					<th>值</th>
					<th>说明</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>Type</code></td>
					<td><code>simple</code></td>
					<td>服务启动即视为就绪。Python 脚本前台运行，不需要 forking</td>
			</tr>
			<tr>
					<td><code>ExecStart</code></td>
					<td><code>/usr/bin/python3 ...</code></td>
					<td>绝对路径。不用 <code>python3</code> 是因为 systemd 不读 shell PATH</td>
			</tr>
			<tr>
					<td><code>Restart</code></td>
					<td><code>always</code></td>
					<td>无论退出码是什么都重启。进程 crash、OOM kill、手动 <code>kill</code> 都会自动拉起来</td>
			</tr>
			<tr>
					<td><code>RestartSec</code></td>
					<td><code>5</code></td>
					<td>等 5 秒再重启。防止疯狂重启循环刷日志</td>
			</tr>
			<tr>
					<td><code>WorkingDirectory</code></td>
					<td><code>/home/sup1whu</code></td>
					<td>Python 进程的 CWD。脚本本身用了绝对路径，但以防万一</td>
			</tr>
	</tbody>
</table>
<h3 id="54-启用-linger--启动服务">5.4 启用 linger + 启动服务</h3>
<p>用户级 systemd 服务默认只在用户登录时运行。这个机器没有图形界面、没有常驻登录会话，需要开启 linger 让服务在系统启动时就起：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 允许 sup1whu 用户在系统启动时自动起服务（不需要登录会话）</span>
</span></span><span style="display:flex;"><span>sudo loginctl enable-linger sup1whu
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 重载 systemd 用户实例、启用服务</span>
</span></span><span style="display:flex;"><span>systemctl --user daemon-reload
</span></span><span style="display:flex;"><span>systemctl --user enable baic-files.service
</span></span><span style="display:flex;"><span>systemctl --user start baic-files.service
</span></span></code></pre></div><p>三步的区别：</p>
<table>
	<thead>
			<tr>
					<th>命令</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>daemon-reload</code></td>
					<td>让 systemd 重新读 <code>.service</code> 文件</td>
			</tr>
			<tr>
					<td><code>enable</code></td>
					<td>写入启动链（<code>default.target.wants/</code>），但<strong>不立即启动</strong></td>
			</tr>
			<tr>
					<td><code>start</code></td>
					<td><strong>立即启动</strong>。如果已经在跑，<code>start</code> 是幂等的</td>
			</tr>
	</tbody>
</table>
<p>验证：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl --user status baic-files --no-pager
</span></span></code></pre></div><p>输出示例：</p>
<pre tabindex="0"><code>● baic-files.service - BiTECH File System — /data/baic_n60/ file server
     Loaded: loaded (.../baic-files.service; enabled; preset: enabled)
     Active: active (running) since Wed 2026-07-15 17:04:46 CST; 2h ago
   Main PID: 2145802 (python3)
      Tasks: 1 (limit: 38415)
     Memory: 10.2M (peak: 11.1M)
</code></pre><p>内存 10MB——对于一个 HTTP 服务器来说很少。ThreadingHTTPServer 只在有请求时才起线程，平时就一个主线程。</p>
<h3 id="55-功能验证">5.5 功能验证</h3>
<p>用 curl 跑了 7 项测试：</p>
<table>
	<thead>
			<tr>
					<th>#</th>
					<th>测试项</th>
					<th>URL</th>
					<th>预期</th>
					<th style="text-align: center">结果</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1</td>
					<td>根目录</td>
					<td><code>GET /</code></td>
					<td>200 + HTML 目录列表</td>
					<td style="text-align: center">✅</td>
			</tr>
			<tr>
					<td>2</td>
					<td>子目录</td>
					<td><code>GET /subdir1/</code></td>
					<td>200 + 子目录内容</td>
					<td style="text-align: center">✅</td>
			</tr>
			<tr>
					<td>3</td>
					<td>长文件夹名</td>
					<td><code>GET /very_long_folder_name_test_2026_Q3/</code></td>
					<td>200 + 空目录提示</td>
					<td style="text-align: center">✅</td>
			</tr>
			<tr>
					<td>4</td>
					<td>文件下载</td>
					<td><code>GET /test_file_1.txt</code></td>
					<td>200 + Content-Disposition</td>
					<td style="text-align: center">✅</td>
			</tr>
			<tr>
					<td>5</td>
					<td>长文件名下载</td>
					<td><code>GET /report_2026_...very_long_name.pdf</code></td>
					<td>200 + 正确文件名</td>
					<td style="text-align: center">✅</td>
			</tr>
			<tr>
					<td>6</td>
					<td>嵌套文件</td>
					<td><code>GET /subdir1/nested.txt</code></td>
					<td>200</td>
					<td style="text-align: center">✅</td>
			</tr>
			<tr>
					<td>7</td>
					<td>404</td>
					<td><code>GET /nonexistent</code></td>
					<td>404</td>
					<td style="text-align: center">✅</td>
			</tr>
	</tbody>
</table>
<h2 id="6-运维管理常用命令">6 运维管理常用命令</h2>
<h3 id="61-日常操作">6.1 日常操作</h3>
<table>
	<thead>
			<tr>
					<th>操作</th>
					<th>命令</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>查看服务状态</td>
					<td><code>systemctl --user status baic-files --no-pager</code></td>
			</tr>
			<tr>
					<td>查看最近日志</td>
					<td><code>journalctl --user -u baic-files -n 50 --no-pager</code></td>
			</tr>
			<tr>
					<td>实时跟踪日志</td>
					<td><code>journalctl --user -u baic-files -f</code></td>
			</tr>
			<tr>
					<td>重启服务</td>
					<td><code>systemctl --user restart baic-files</code></td>
			</tr>
			<tr>
					<td>停止服务</td>
					<td><code>systemctl --user stop baic-files</code></td>
			</tr>
			<tr>
					<td>禁用开机自启</td>
					<td><code>systemctl --user disable baic-files</code></td>
			</tr>
			<tr>
					<td>查看 linger 状态</td>
					<td><code>loginctl show-user sup1whu | grep Linger</code></td>
			</tr>
			<tr>
					<td>查看监听端口</td>
					<td><code>ss -tlnp | grep 19527</code></td>
			</tr>
			<tr>
					<td>查看进程树</td>
					<td><code>ps auxf | grep baic-server</code></td>
			</tr>
			<tr>
					<td>查看连接数</td>
					<td><code>ss -tn state established '( sport = :19527 )' | wc -l</code></td>
			</tr>
			<tr>
					<td>检查错误日志</td>
					<td><code>journalctl --user -u baic-files -p 3 --no-pager</code></td>
			</tr>
	</tbody>
</table>
<h3 id="62-更新代码流程">6.2 更新代码流程</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 1. 本地修改 baic-server.py，测试通过</span>
</span></span><span style="display:flex;"><span>python3 /tmp/baic-server.py &amp;
</span></span><span style="display:flex;"><span>curl -sI http://127.0.0.1:19527/
</span></span><span style="display:flex;"><span>kill %1
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2. 通过跳板机推到目标机</span>
</span></span><span style="display:flex;"><span>sshpass -p <span style="color:#e6db74">&#39;&lt;password&gt;&#39;</span> scp <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -o <span style="color:#e6db74">&#34;ProxyCommand ssh -i ~/.ssh/id_ed25519 herui@10.10.0.4 -W %h:%p&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -P <span style="color:#ae81ff">3022</span> /tmp/baic-server.py sup1whu@112.31.22.151:~/baic-server.py
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3. 重启服务（systemd 用新代码）</span>
</span></span><span style="display:flex;"><span>sshpass -p <span style="color:#e6db74">&#39;&lt;password&gt;&#39;</span> ssh <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -o <span style="color:#e6db74">&#34;ProxyCommand ssh -i ~/.ssh/id_ed25519 herui@10.10.0.4 -W %h:%p&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -p <span style="color:#ae81ff">3022</span> sup1whu@112.31.22.151 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;systemctl --user restart baic-files &amp;&amp; systemctl --user status baic-files --no-pager&#34;</span>
</span></span></code></pre></div><p>不要直接在目标机上用 vim 改代码然后 <code>systemctl --user restart</code>。目录 <code>/data/baic_n60/</code> 是生产数据，vim 出错了没有 git 回滚。</p>
<h3 id="63-磁盘监控">6.3 磁盘监控</h3>
<table>
	<thead>
			<tr>
					<th>检查项</th>
					<th>命令</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>磁盘使用</td>
					<td><code>df -h /data</code></td>
			</tr>
			<tr>
					<td>大目录排查</td>
					<td><code>du -sh /data/baic_n60/* | sort -rh | head -20</code></td>
			</tr>
			<tr>
					<td>文件数量统计</td>
					<td><code>find /data/baic_n60 -type f | wc -l</code></td>
			</tr>
			<tr>
					<td>最近修改的文件</td>
					<td><code>find /data/baic_n60 -type f -mtime -7</code></td>
			</tr>
	</tbody>
</table>
<h2 id="7-踩坑记录">7 踩坑记录</h2>
<h3 id="71-pep-668externally-managed-environment">7.1 PEP 668：externally-managed-environment</h3>
<p>最开始想在目标机上 <code>pip install flask</code>，报错：</p>
<pre tabindex="0"><code>error: externally-managed-environment
× This environment is externally managed
</code></pre><p>Ubuntu 24.04 默认的 Python 3.12 开启了 PEP 668 保护——禁止 pip 往系统 site-packages 装东西。这是好事，保护系统 Python 不被破坏，但意味着不能走常规的 pip 安装路线。</p>
<p>绕不过去：</p>
<ul>
<li><code>pip install --break-system-packages</code>：语法上能绕过 PEP 668，但没网，pypi.org 连不上</li>
<li><code>python3 -m venv</code> + pip：同样卡在外网上</li>
<li><code>apt-get install python3-flask</code>：Debian/Ubuntu 没有 Flask 的 apt 包</li>
</ul>
<p>回到标准库。<code>http.server.ThreadingHTTPServer</code> 在这个并发量级下够了。</p>
<h3 id="72-systemd-用户服务不启动">7.2 systemd 用户服务不启动</h3>
<p><code>systemctl --user enable baic-files</code> 成功后重启机器服务没起来。检查：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>loginctl show-user sup1whu | grep Linger
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Linger=no</span>
</span></span></code></pre></div><p>用户服务在 session 级别启动。没有 linger 的情况下，<code>WantedBy=default.target</code> 不会在系统启动时触发——因为 <code>default.target</code> 对于用户实例，只有在用户<strong>首次登录</strong>时才启动。</p>
<p>修复：<code>sudo loginctl enable-linger sup1whu</code>，下次重启服务就自动起来了。这个行为在 systemd 官方文档 <code>logind.conf(5)</code> 里有说明，但不踩这个坑不会注意到。</p>
<h3 id="73-execstart-写-python3-而不是-usrbinpython3">7.3 ExecStart 写 <code>python3</code> 而不是 <code>/usr/bin/python3</code></h3>
<p>systemd 用户实例的 <code>$PATH</code> 非常精简（大概只有 <code>/usr/local/bin:/usr/bin:/bin</code>），但行为与登录 shell 不同。直接用 <code>python3</code> 虽然大概率能跑（因为 <code>/usr/bin</code> 在 PATH 里），但写绝对路径 <code>/usr/bin/python3</code> 消除了所有不确定性。systemd 官方文档也在 <code>systemd.service(5)</code> 里建议 ExecStart 用绝对路径。</p>
<h3 id="74-从临时进程切换到-systemd-时的端口冲突">7.4 从临时进程切换到 systemd 时的端口冲突</h3>
<p>手动 <code>python3 ~/baic-server.py &amp;</code> 启动的进程在后台跑着，后来 <code>systemctl --user start baic-files</code> 也起了。两个进程抢 19527 端口，后者报 <code>Address already in use</code>。</p>
<p>systemd 的 <code>Restart=always</code> 会不断重试，日志里刷了一串失败。手动 <code>kill</code> 掉旧进程后 systemd 自动恢复。</p>
<p>先停手动进程、再起 systemd。如果已经乱了，<code>pkill -f baic-server.py</code> 清掉所有残留，systemd 会在下一个 RestartSec 后自动拉起来。</p>
<h2 id="8-最终状态一览">8 最终状态一览</h2>
<table>
	<thead>
			<tr>
					<th>项目</th>
					<th>值</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>服务名称</td>
					<td><code>baic-files.service</code></td>
			</tr>
			<tr>
					<td>端口</td>
					<td><code>19527</code></td>
			</tr>
			<tr>
					<td>监听地址</td>
					<td><code>0.0.0.0</code></td>
			</tr>
			<tr>
					<td>数据目录</td>
					<td><code>/data/baic_n60/</code></td>
			</tr>
			<tr>
					<td>磁盘</td>
					<td><code>/dev/sda1</code> (46TB, 17% 已用)</td>
			</tr>
			<tr>
					<td>系统</td>
					<td>Ubuntu 24.04.1 LTS, kernel 6.17.0-35</td>
			</tr>
			<tr>
					<td>Python</td>
					<td>3.12.3（标准库，零 pip 包）</td>
			</tr>
			<tr>
					<td>systemd</td>
					<td>用户级服务 + linger 开机自启</td>
			</tr>
			<tr>
					<td>内存占用</td>
					<td>~10MB</td>
			</tr>
			<tr>
					<td>并发模型</td>
					<td>ThreadingHTTPServer (per-request thread)</td>
			</tr>
			<tr>
					<td>前端功能</td>
					<td>深色/浅色切换、面包屑导航、长文件名完整展示、日期+大小列</td>
			</tr>
			<tr>
					<td>安全</td>
					<td>路径穿越防护（realpath + commonpath）、Content-Disposition 附件下载</td>
			</tr>
	</tbody>
</table>
<p>整个部署从零到 production-ready 花了一个小时——其中 30 分钟在调研「无外网 + PEP 668」环境下的可行方案，20 分钟写代码，10 分钟部署 + 验证。</p>
]]></content:encoded></item><item><title>17 种快捷支付安全深度评测：你的招行卡绑了哪些？</title><link>https://raysre.com/posts/17-payment-security-review/</link><pubDate>Sat, 11 Jul 2026 00:00:00 +0000</pubDate><guid>https://raysre.com/posts/17-payment-security-review/</guid><description>逐一分析支付宝、财付通、云闪付、Apple Pay、京东支付等 17 种快捷支付的 Token 化技术、风控引擎、硬件安全、隐私策略和央行监管处罚记录（含红头文件编号与罚款金额），附 15 张对比表格，为招商银行用户提供绑卡决策参考。</description><content:encoded><![CDATA[<h2 id="写在前面">写在前面</h2>
<p>手上有两张招行卡，一张借记卡一张信用卡。最近整理快捷支付授权列表的时候发现，不知不觉绑了十几个平台——有些完全想不起来什么时候授权的。</p>
<p>仔细一想，每个平台都能从我的卡里扣钱，但我对它们的安全机制几乎一无所知。唯一的判断标准是「大厂，应该没事吧」。</p>
<p>花了一周时间逐个查这些平台背后的持牌机构、央行处罚公示、安全白皮书和公开披露的安全事件。查下来的结果让我挺意外的——差距远比我预想的大。Apple Pay 和云闪付用的是同一套硬件级 Token 方案，而拼多多 App 刚被 Google 因为利用 Android 0-day 漏洞收集用户数据从 Play 商店全线撤下。这中间差了不止一个量级。</p>
<p>这篇文章把查到的东西整理出来，方便自己以后参考。数据来源都在文中标了，主要是央行行政处罚公示、移动支付网公开统计和澎湃新闻/央广网的报道。有错的欢迎指正。</p>
<hr>
<h2 id="一决定安全的三个技术分水岭">一、决定安全的三个技术分水岭</h2>
<p>分析完 17 个平台后发现，真正拉开安全差距的不是公司名气，是三个硬指标：</p>
<p><strong>第一个，有没有 Token 化。</strong> Token 化意味着平台和商户都不接触你的真实银行卡号，只用一个受限的虚拟令牌替代。这个令牌可以限设备、限商户、限单次。没有 Token 化的平台，你的卡号直接存在它们的系统里——你对它的信任本质上就是赌它的数据库不会被拖走。</p>
<p><strong>第二个，风控引擎的成熟度。</strong> 关键指标是<strong>交易量 × 拦截率</strong>。支付宝的 CTU 每秒处理 40 万笔交易，这是十几年积累下来的模型能力。腾讯天御借助微信社交关系图谱做异常检测，把资金损失率压到了业内罕见的水平。而一些中小支付机构的风控，技术上约等于黑名单匹配加简单规则。</p>
<p><strong>第三个，有没有安全事件前科。</strong> 这是最直白的信号。一次严重安全事件，暴露的不是一个 bug，而是一家公司在安全工程和合规文化上的系统性缺陷。携程 2014 年的日志明文记录 CVV 码，拼多多 2023 年把安全能力用在攻击用户隐私上——这类记录不会因为「已经整改了」就应该被忽略。</p>
<p>下面是 17 个平台在这三个维度上的全量对照。</p>
<hr>
<h2 id="二技术框架总览">二、技术框架总览</h2>
<h3 id="21-核心安全机制对比">2.1 核心安全机制对比</h3>
<table>
	<thead>
			<tr>
					<th>平台</th>
					<th>Token化方案</th>
					<th>风控引擎</th>
					<th>硬件安全</th>
					<th>生物识别</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Apple Pay</td>
					<td>DAN(设备账户号)+动态安全码</td>
					<td>不参与,由发卡行处理</td>
					<td>Secure Element独立芯片</td>
					<td>Touch ID / Face ID</td>
			</tr>
			<tr>
					<td>云闪付</td>
					<td>EMVCo Token(国际标准)</td>
					<td>前防-中控-后偿三阶段</td>
					<td>eSE / TEE双模支持</td>
					<td>指纹/人脸(手机厂商提供)</td>
			</tr>
			<tr>
					<td>银联在线支付</td>
					<td>不适用(直连发卡行)</td>
					<td>发卡行直连验证</td>
					<td>不涉及客户端硬件</td>
					<td>无(四重密码验证)</td>
			</tr>
			<tr>
					<td>支付宝</td>
					<td>AT Token体系(自有标准)</td>
					<td>CTU引擎,40万笔/秒</td>
					<td>无独立芯片,软件实现</td>
					<td>3D活体检测,误识率&lt;10⁻⁹</td>
			</tr>
			<tr>
					<td>财付通</td>
					<td>财付通Token(自有标准)</td>
					<td>天御引擎,社交图增强</td>
					<td>无独立芯片,国密SM2/SM4 HSM</td>
					<td>指纹/人脸(微信App层)</td>
			</tr>
			<tr>
					<td>京东支付</td>
					<td>京东Token(自有标准)</td>
					<td>电商+物流数据融合风控</td>
					<td>无独立芯片</td>
					<td>指纹/人脸(京东App层)</td>
			</tr>
			<tr>
					<td>抖音支付</td>
					<td>字节Token(自有标准)</td>
					<td>字节AI平台支撑</td>
					<td>无独立芯片</td>
					<td>指纹/人脸(抖音App层)</td>
			</tr>
			<tr>
					<td>美团支付</td>
					<td>美团Token(自有标准)</td>
					<td>场景+位置数据风控</td>
					<td>无独立芯片</td>
					<td>指纹/人脸(美团App层)</td>
			</tr>
			<tr>
					<td>小米钱包(NFC)</td>
					<td>银联Token(走Mi Pay通道)</td>
					<td>银联+发卡行处理</td>
					<td>eSE芯片(NFC机型)</td>
					<td>指纹</td>
			</tr>
			<tr>
					<td>小米钱包(线上)</td>
					<td>捷付睿通自有通道</td>
					<td>自建风控</td>
					<td>无独立芯片</td>
					<td>指纹/人脸(MIUI层)</td>
			</tr>
			<tr>
					<td>翼支付</td>
					<td>电信自有Token</td>
					<td>运营商数据+电信天翼云</td>
					<td>无独立芯片</td>
					<td>指纹/人脸</td>
			</tr>
			<tr>
					<td>和包支付</td>
					<td>中移自有Token</td>
					<td>运营商数据+中移能力</td>
					<td>无独立芯片(NFC除外)</td>
					<td>指纹/人脸</td>
			</tr>
			<tr>
					<td>拼多多支付</td>
					<td>付费通自有Token</td>
					<td>基础风控</td>
					<td>无独立芯片</td>
					<td>无独立生物识别</td>
			</tr>
			<tr>
					<td>携程支付</td>
					<td>东方汇融自有Token</td>
					<td>旅游场景基础风控</td>
					<td>无独立芯片</td>
					<td>无独立生物识别</td>
			</tr>
			<tr>
					<td>通联支付</td>
					<td>自有Token(面向商户)</td>
					<td>B端商户风控为主</td>
					<td>无独立芯片</td>
					<td>不面向个人用户</td>
			</tr>
			<tr>
					<td>富友支付</td>
					<td>自有Token(面向商户)</td>
					<td>B端商户风控为主</td>
					<td>无独立芯片</td>
					<td>不面向个人用户</td>
			</tr>
			<tr>
					<td>苏宁支付</td>
					<td>易付宝自有Token</td>
					<td>电商场景基础风控</td>
					<td>无独立芯片</td>
					<td>指纹/人脸</td>
			</tr>
			<tr>
					<td>唯品支付</td>
					<td>唯品会自有Token</td>
					<td>电商场景基础风控</td>
					<td>无独立芯片</td>
					<td>指纹/人脸</td>
			</tr>
	</tbody>
</table>
<h3 id="22-加密标准与合规认证">2.2 加密标准与合规认证</h3>
<table>
	<thead>
			<tr>
					<th>平台</th>
					<th>加密标准</th>
					<th>ISO认证</th>
					<th>PCI-DSS</th>
					<th>国密算法</th>
					<th>数据本地化</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Apple Pay</td>
					<td>AES-256, ECC</td>
					<td>✓</td>
					<td>✓(通过银联间接)</td>
					<td>✗</td>
					<td>中国大陆iCloud</td>
			</tr>
			<tr>
					<td>云闪付</td>
					<td>EMVCo标准, RSA-2048</td>
					<td>ISO 27001</td>
					<td>✓</td>
					<td>SM2/SM4</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>支付宝</td>
					<td>RSA-2048, AES-256</td>
					<td>ISO 27001/27701</td>
					<td>✓</td>
					<td>SM2/SM4</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>财付通</td>
					<td>RSA-2048, AES-256</td>
					<td>ISO 27001</td>
					<td>✓</td>
					<td>SM2/SM4</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>京东支付</td>
					<td>RSA-2048, AES-256</td>
					<td>ISO 27001</td>
					<td>✓</td>
					<td>部分支持</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>抖音支付</td>
					<td>RSA-2048</td>
					<td>未公开</td>
					<td>✓</td>
					<td>未公开</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>美团支付</td>
					<td>RSA-2048</td>
					<td>未公开</td>
					<td>✓</td>
					<td>未公开</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>小米钱包</td>
					<td>RSA-2048, 银联NFC标准</td>
					<td>未公开</td>
					<td>✓</td>
					<td>部分支持</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>翼支付</td>
					<td>国密SM系列</td>
					<td>ISO 27001</td>
					<td>✓</td>
					<td>SM2/SM4</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>和包支付</td>
					<td>国密SM系列</td>
					<td>未公开</td>
					<td>✓</td>
					<td>SM2/SM4</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>拼多多支付</td>
					<td>基础RSA</td>
					<td>未公开</td>
					<td>未公开</td>
					<td>未公开</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>携程支付</td>
					<td>基础RSA</td>
					<td>未公开</td>
					<td>✓(2014年后补做)</td>
					<td>未公开</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>通联支付</td>
					<td>RSA/AES</td>
					<td>未公开</td>
					<td>✓</td>
					<td>未公开</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>富友支付</td>
					<td>RSA/AES</td>
					<td>未公开</td>
					<td>✓</td>
					<td>未公开</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>苏宁支付</td>
					<td>基础RSA</td>
					<td>未公开</td>
					<td>✓</td>
					<td>未公开</td>
					<td>✓(全部境内)</td>
			</tr>
			<tr>
					<td>唯品支付</td>
					<td>基础RSA</td>
					<td>未公开</td>
					<td>✓</td>
					<td>未公开</td>
					<td>✓(全部境内)</td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="三监管处罚记录">三、监管处罚记录</h2>
<p>2025 年全年，人民银行及各地分支机构合计对支付机构开出 80 张罚单，罚没金额合计约 <strong>2.88 亿元</strong>，较上年度（剔除对银联股份的处罚）增长超过 50%。其中支付结算领域 69 张（2.62 亿元）、综合罚单 7 张（2287 万元）、反洗钱领域 4 张（338 万元）。</p>
<p>这仅是一年的数据。下面是与本文相关的平台和其持牌机构被处罚的核心记录。</p>
<h3 id="31-监管处罚明细">3.1 监管处罚明细</h3>
<table>
	<thead>
			<tr>
					<th>平台</th>
					<th>持牌机构</th>
					<th>处罚文件编号</th>
					<th>时间</th>
					<th>罚款金额</th>
					<th>违规事由</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>支付宝</td>
					<td>支付宝(中国)网络技术有限公司</td>
					<td>银罚决字【2023】26-33号</td>
					<td>2023.07</td>
					<td><strong>罚没30.6亿元</strong>（没收8.31亿+罚款22.31亿）</td>
					<td>7项违法：违反支付结算管理规定、违反反洗钱规定、违反金融消费者权益保护规定等</td>
			</tr>
			<tr>
					<td>财付通</td>
					<td>财付通支付科技有限公司</td>
					<td>央行同期公示</td>
					<td>2023.07</td>
					<td><strong>罚没29.9亿元</strong>（没收5.66亿+罚款24.27亿）</td>
					<td>违反支付结算管理规定、反洗钱义务等</td>
			</tr>
			<tr>
					<td>京东支付</td>
					<td>网银在线(北京)支付科技有限公司</td>
					<td><strong>银京罚决字〔2025〕54-55号</strong></td>
					<td>2025.08</td>
					<td>罚没<strong>约962万元</strong>（没收219万+罚款743万），总裁另罚33万</td>
					<td>10项违法：未落实商户实名制、支付接口管理不规范、违规非同名划转、备付金违规等</td>
			</tr>
			<tr>
					<td>京东支付</td>
					<td>网银在线(北京)支付科技有限公司</td>
					<td><strong>银京罚决字〔2025〕24号、25号</strong></td>
					<td>2025.01</td>
					<td>约<strong>101.7万元</strong>（含风控负责人罚5.13万）</td>
					<td>反洗钱违规：未履行客户身份识别义务、未报送大额/可疑交易报告</td>
			</tr>
			<tr>
					<td>小米钱包</td>
					<td>捷付睿通(内蒙古)支付股份有限公司</td>
					<td><strong>蒙银罚决字〔2026〕3号</strong></td>
					<td>2026.05</td>
					<td><strong>167.1万元</strong></td>
					<td>违反反洗钱管理规定</td>
			</tr>
			<tr>
					<td>通联支付</td>
					<td>通联支付网络服务股份有限公司</td>
					<td>历次处罚（多地分行公示）</td>
					<td>2016-2025</td>
					<td>历史累计超<strong>千万元</strong></td>
					<td>违反银行卡收单业务规定、反洗钱违规、涉非法集资关联案件</td>
			</tr>
			<tr>
					<td>富友支付</td>
					<td>上海富友支付服务股份有限公司</td>
					<td>历次处罚（上海分行等多地）</td>
					<td>2017-2025</td>
					<td>多次被罚，<strong>单次最高数百万</strong></td>
					<td>反洗钱违规、数据报送不合规、商户管理违规</td>
			</tr>
	</tbody>
</table>
<p><strong>关于网银在线（京东支付）</strong>：六年累计已被央行处罚五次，累计罚没超过 4200 万元。2025 年两次罚单间隔仅 7 个月，违规类型覆盖支付结算、反洗钱、备付金管理等几乎所有监管领域的常见违规项。黑猫投诉平台上有超过 3000 条「不知情扣款」相关投诉。</p>
<h3 id="32-行业处罚趋势">3.2 行业处罚趋势</h3>
<table>
	<thead>
			<tr>
					<th>指标</th>
					<th>2024年</th>
					<th>2025年</th>
					<th>变化</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>支付机构罚单总数</td>
					<td>~50张</td>
					<td>80张</td>
					<td>↑60%</td>
			</tr>
			<tr>
					<td>罚没总金额</td>
					<td>~1.8亿(剔除银联处罚)</td>
					<td>2.88亿</td>
					<td>↑超50%</td>
			</tr>
			<tr>
					<td>超千万元罚单数</td>
					<td>3张</td>
					<td>5张</td>
					<td>↑67%</td>
			</tr>
			<tr>
					<td>支付结算领域罚没金额</td>
					<td>9901.74万元</td>
					<td>2.62亿元</td>
					<td>↑164%</td>
			</tr>
			<tr>
					<td>数据安全/网络安全罚单</td>
					<td>0张</td>
					<td>首次出现（人保支付罚104万等）</td>
					<td>新增领域</td>
			</tr>
			<tr>
					<td>有效支付牌照总数</td>
					<td>~175张</td>
					<td>163张</td>
					<td>12张注销</td>
			</tr>
	</tbody>
</table>
<p>来源：<a href="https://m.mpaypass.com.cn/news/202601/13085614.html">移动支付网《2025年中国人民银行支付机构处罚分析》</a></p>
<hr>
<h2 id="四安全事件记录">四、安全事件记录</h2>
<p>监管罚单反映的是合规能力，安全事件反映的是工程能力。两者加在一起才能判断一个平台到底值不值得信任。</p>
<table>
	<thead>
			<tr>
					<th>平台</th>
					<th>事件类型</th>
					<th>时间</th>
					<th>细节</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>拼多多</td>
					<td><strong>恶意代码被下架</strong></td>
					<td>2023.03</td>
					<td>Google安全团队发现拼多多App利用Android未公开漏洞(0-day)绕过用户授权，读写其他App私有数据、修改系统设置。随后被Google Play全线撤下数月。不是bug——是利用漏洞主动攻击用户隐私</td>
			</tr>
			<tr>
					<td>拼多多</td>
					<td>违规收集个人信息</td>
					<td>2019-2023</td>
					<td>工信部、网信办多次通报「违规收集个人信息」「超范围收集个人信息」「强制用户使用定向推送功能」</td>
			</tr>
			<tr>
					<td>携程</td>
					<td><strong>支付日志明文泄露CVV</strong></td>
					<td>2014.03</td>
					<td>乌云漏洞平台(WooYun)曝出携程支付系统日志直接记录了用户银行卡号、CVV安全码、有效期——全部明文。PCI-DSS标准明确禁止在任何情况下存储CVV，加密也不行</td>
			</tr>
			<tr>
					<td>携程</td>
					<td>用户数据泄露</td>
					<td>2015</td>
					<td>再次被曝用户数据泄露事件</td>
			</tr>
			<tr>
					<td>京东</td>
					<td>数据泄露</td>
					<td>2016</td>
					<td>12GB用户数据包外泄，含支付相关信息</td>
			</tr>
			<tr>
					<td>唯品会</td>
					<td>用户数据泄露</td>
					<td>2016</td>
					<td>包含支付信息的用户数据泄露</td>
			</tr>
			<tr>
					<td>美团</td>
					<td>&ldquo;杀熟&quot;争议</td>
					<td>2017-2024</td>
					<td>用户反复曝光同一商品对不同用户显示不同价格，侧面印证内部用户画像与数据分析能力极强</td>
			</tr>
			<tr>
					<td>支付宝</td>
					<td>蚂蚁集团被约谈后重组</td>
					<td>2020-2023</td>
					<td>上市暂停、业务整改、花呗借呗品牌隔离。非安全事件，但反映了数据治理的重大变化</td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="五隐私策略对比">五、隐私策略对比</h2>
<p>支付安全和隐私保护是两件事。一个平台可以技术上很安全（不丢钱），同时隐私上很危险（数据被用于你不愿意的用途）。</p>
<table>
	<thead>
			<tr>
					<th>平台</th>
					<th>商业模式依赖用户数据程度</th>
					<th>支付数据是否用于推荐/广告</th>
					<th>跨业务数据打通</th>
					<th>第三方可审计性</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Apple Pay</td>
					<td><strong>极低</strong>(硬件+服务盈利)</td>
					<td>否,Apple不追踪交易</td>
					<td>否</td>
					<td>否(封闭系统,但数据量少)</td>
			</tr>
			<tr>
					<td>云闪付</td>
					<td><strong>极低</strong>(清算+公共服务)</td>
					<td>否,仅用于清算和反欺诈</td>
					<td>否(仅银联体系)</td>
					<td>央行监管,无需第三方</td>
			</tr>
			<tr>
					<td>支付宝</td>
					<td><strong>中等</strong>(信贷/保险/理财)</td>
					<td>官方承诺隔离,无第三方审计</td>
					<td>是(蚂蚁集团内各业务)</td>
					<td>否</td>
			</tr>
			<tr>
					<td>财付通</td>
					<td><strong>中等</strong>(社交生态变现)</td>
					<td>官方承诺隔离,无第三方审计</td>
					<td>是(与微信社交数据同属一家)</td>
					<td>否</td>
			</tr>
			<tr>
					<td>京东支付</td>
					<td><strong>较高</strong>(电商+金融)</td>
					<td>与电商消费数据关联</td>
					<td>是(京东科技内多业务)</td>
					<td>否</td>
			</tr>
			<tr>
					<td>抖音支付</td>
					<td><strong>高</strong>(内容推荐+电商)</td>
					<td>字节生态系统高度集中</td>
					<td>是(视频/社交/电商/支付)</td>
					<td>否</td>
			</tr>
			<tr>
					<td>美团支付</td>
					<td><strong>高</strong>(本地生活全链路)</td>
					<td>外卖/酒店/到店数据与支付打通</td>
					<td>是(完整的LBS消费画像)</td>
					<td>否</td>
			</tr>
			<tr>
					<td>小米钱包</td>
					<td><strong>较高</strong>(硬件+IoT+电商)</td>
					<td>MIUI设备行为+IOT+商城消费</td>
					<td>是(系统级权限)</td>
					<td>否</td>
			</tr>
			<tr>
					<td>翼支付</td>
					<td><strong>中高</strong>(运营商数据)</td>
					<td>实名/位置/轨迹/通话+支付</td>
					<td>是(运营商级数据)</td>
					<td>否</td>
			</tr>
			<tr>
					<td>和包支付</td>
					<td><strong>中高</strong>(运营商数据)</td>
					<td>实名/位置/轨迹/通话+支付</td>
					<td>是(运营商级数据)</td>
					<td>否</td>
			</tr>
			<tr>
					<td>拼多多支付</td>
					<td><strong>高</strong>(社交电商)</td>
					<td>社交裂变+消费+支付全部打通</td>
					<td>是(拼多多App内全链路)</td>
					<td>否</td>
			</tr>
			<tr>
					<td>携程支付</td>
					<td><strong>中高</strong>(旅游+金融)</td>
					<td>出行/住宿/消费高度关联</td>
					<td>是(携程内多业务)</td>
					<td>否</td>
			</tr>
			<tr>
					<td>通联支付</td>
					<td><strong>低</strong>(主要B端商户)</td>
					<td>不面向个人消费者</td>
					<td>否(B端独立)</td>
					<td>可选(商户端)</td>
			</tr>
			<tr>
					<td>富友支付</td>
					<td><strong>低</strong>(主要B端商户)</td>
					<td>不面向个人消费者</td>
					<td>否(B端独立)</td>
					<td>可选(商户端)</td>
			</tr>
			<tr>
					<td>苏宁支付</td>
					<td><strong>较高</strong>(零售+金融)</td>
					<td>苏宁零售生态内打通</td>
					<td>是</td>
					<td>否</td>
			</tr>
			<tr>
					<td>唯品支付</td>
					<td><strong>中等</strong>(品牌特卖电商)</td>
					<td>电商消费+支付关联</td>
					<td>是</td>
					<td>否</td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="六逐平台深度分析">六、逐平台深度分析</h2>
<h3 id="第一梯队几乎无懈可击">第一梯队：几乎无懈可击</h3>
<h4 id="apple-pay">Apple Pay</h4>
<p>Apple Pay 的安全模型和所有其他平台都不同。它不存储你的银行卡号，不追踪你的交易，风控也完全交给发卡行——Apple 自己只负责硬件和身份验证。</p>
<p>核心机制：当你把招行卡添加到 iPhone 的 Wallet App 时，系统通过银联向招行发起验证请求。招行验证你的身份后，生成一个唯一的设备账户号（DAN：Device Account Number），直接写入 iPhone 的 Secure Element 芯片里。</p>
<p>Secure Element 是一块独立于主 CPU 的硬件安全芯片。iOS 系统和任何 App 都读不到里面存了什么。物理拆解会触发芯片自毁。每一笔支付在 Secure Element 内部生成一次性的动态安全码，和 DAN 配对使用。即使有人截获了这笔交易的数据，也无法用于第二次支付。</p>
<p>Apple 能做到这么干净，本质原因是它的商业模式不靠支付数据变现。硬件利润、App Store 抽成和服务订阅已经极端丰厚了，没有必要为了多赚一点去碰用户的交易隐私。在这个行业里，商业模式天然决定了隐私保护的上限。</p>
<h4 id="云闪付">云闪付</h4>
<p>云闪付走的是国际标准的 EMVCo Token 化体系——和 Visa、Mastercard 的 Token 服务同一套规范。银联的角色是中国银行卡清算组织，直连央行。这意味着它的数据只用于清算和反欺诈，没有商业变现压力。你不会担心银联拿你的消费记录去做推荐算法。</p>
<p>风控分三段：绑卡时用 Token 替代真实卡号（前防），交易中实时评估并及时拦截异常（中控），出了事只要用户无过错就<strong>全额赔付、没有赔付上限</strong>（后偿）。这个「无上限」承诺在中国第三方支付行业里是独一份的。</p>
<p>如果说 Apple Pay 是借助硬件做到了安全性最优，那云闪付就是在监管位置和数据用途上的最优解——没有商业变现动机，就没有数据滥用的风险。</p>
<h4 id="银联在线支付">银联在线支付</h4>
<p>银联在线支付是传统的网关支付，不走第三方平台的账户体系。交易时需要同时输入完整卡号、取款密码、手机验证码和 CVV（四重验证），通过银联网关直连招行扣款。整个过程中间没有第三方机构持有你的账户信息。</p>
<p>代价就是流程繁琐——每次都要手打一堆数字。日常小额用它不现实，但大额付款或对安全性有极端要求的时候，它是理论上最安全的选择。四个验证因子同时被泄露或截获的概率极低。</p>
<h3 id="第二梯队成熟但各有隐忧">第二梯队：成熟但各有隐忧</h3>
<h4 id="支付宝">支付宝</h4>
<p>支付宝的安全体系在全球支付机构里属于第一梯队。CTU（Creditable Transaction Unit）风控引擎对外公布的指标是每秒处理 40 万笔交易，背后的模型是一个深度学习神经网络加若干规则引擎的组合。2022 年支付宝在公开报告中披露过，全年拦截风险交易超过 200 亿次。</p>
<p>几个具体技术点：</p>
<ul>
<li><strong>3D 活体检测</strong>通过了国家金融科技认证中心认证，误识率低于十亿分之一（10⁻⁹）。照片、视频重放、3D 打印面具都能识别。</li>
<li><strong>设备指纹</strong>采集了超过 100 个维度的硬件和软件特征——包括传感器特性、屏幕参数、字体列表。模拟器和多开环境会被直接拒绝，也可以检测出设备是否被 root 或越狱。</li>
<li><strong>行为生物特征</strong>在后台静默运行——击键节奏、触屏压力曲线、滑动轨迹。你自己感觉不到，但换一个人用你的手机支付，模型能立刻捕捉到行为模式的变化。</li>
</ul>
<p>从隐私角度看，支付宝数据用途的透明度不算高。蚂蚁集团虽然已完成整改，但其信贷（花呗借呗）、保险（相互宝/好医保）、理财（余额宝/蚂蚁财富）与支付数据本质上在一个生态内流转。如果我是招行用户，在支付宝上做日常支付可以接受，但不会让支付宝成为我唯一的支付入口。</p>
<p>史上最大罚单：2023 年 7 月，央行对支付宝开出 <strong>银罚决字【2023】26-33 号</strong> 罚单，罚没合计 <strong>30.6 亿元</strong>（没收违法所得 8.31 亿 + 罚款 22.31 亿），涉及 7 项违法行为。同时蚂蚁集团也被穿透式处罚。这个金额是全球支付行业有史以来最高的单笔罚款。</p>
<p>来源：<a href="https://www.thepaper.cn/newsDetail_forward_23769154">澎湃新闻</a> / <a href="https://www.cnr.cn/ziben/yw/20230708/t20230708_526320295.shtml">央广网</a></p>
<h4 id="财付通微信支付">财付通（微信支付）</h4>
<p>腾讯做支付安全有一个别人复刻不了的优势：社交图谱。</p>
<p>财付通的风控系统叫「天御」，它在决策一笔交易是否可疑的时候，能参考的不只是交易本身的金额、时间、地理位置，还包括支付双方的社交关系、设备登录历史、位置变动轨迹。比如一个你从未联系过的账户突然向你发起大额转账，或者你的微信账号同时在两个相距 500 公里的设备上登录——天御能在毫秒级捕捉这种异常。</p>
<p>效果反映在一个数字上：财付通的资金损失率在 2022 年压到了<strong>十五万分之一</strong>。这个数字在全球支付机构里都是顶尖的。</p>
<p>2023 年 7 月与支付宝同期，财付通也被央行罚没 <strong>29.9 亿元</strong>（没收 5.66 亿 + 罚款 24.27 亿）。同样的压力测试下活下来的平台，内控合规至少是被强制矫正过的。</p>
<p>真正的风险不在支付安全本身，而在隐私边界。微信支付和微信聊天是同一家公司运营的。虽然腾讯官方承诺「支付数据不用于广告推荐」，但没有第三方独立审计可以验证。作为用户，你只能选择信任这个承诺——或者不用。</p>
<h4 id="京东支付">京东支付</h4>
<p>先说好的：京东支付的风控利用了电商和物流数据的天然优势，事先的设备指纹采集、事中的实时评分、事后的离线审计——全链路反欺诈框架是完整的。</p>
<p>再说具体的：它的持牌机构网银在线，六年被央行罚了五次。</p>
<table>
	<thead>
			<tr>
					<th>时间</th>
					<th>文件编号</th>
					<th>罚没金额</th>
					<th>核心违规</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>2025.01</td>
					<td>银京罚决字〔2025〕24号/25号</td>
					<td>~101.7万</td>
					<td>反洗钱：未履行客户身份识别义务、未报送大额/可疑交易报告</td>
			</tr>
			<tr>
					<td>2025.08</td>
					<td><strong>银京罚决字〔2025〕54-55号</strong></td>
					<td><strong>约962万</strong></td>
					<td>10项违法：未落实商户实名制、支付接口不规范、违规非同名划转、备付金违规、未确保交易信息真实完整可追溯</td>
			</tr>
			<tr>
					<td>2019-2024</td>
					<td>多次</td>
					<td>累计约3000万+</td>
					<td>支付结算违规、反洗钱违规</td>
			</tr>
	</tbody>
</table>
<p>来源：<a href="https://www.mpaypass.com.cn/news/202508/18091640.html">移动支付网</a> / <a href="https://news.qq.com/rain/a/20250819A07W5U00">腾讯新闻</a></p>
<p>六年累计罚没超 4200 万元的持牌支付机构，技术和合规之间是割裂的。另外京东 2016 年发生过 12GB 用户数据泄露，黑猫投诉上还有 3000 多条「不知情扣款」的记录。我个人只在京东上买自营商品时用京东支付，其他地方一律走微信或支付宝。</p>
<h3 id="第三梯队支付不算差隐私让人担心">第三梯队：支付不算差，隐私让人担心</h3>
<h4 id="抖音支付">抖音支付</h4>
<p>抖音支付本身的支付通道安全性不用太担心——2048 位 RSA 加密传输，央行持牌（合众易宝），字节跳动的 AI 能力能提供较强的异常检测。在抖音里买东西，用抖音支付还是用支付宝付钱，被盗刷的风险差异不大。</p>
<p>问题全在隐私上。</p>
<p>字节跳动的商业模式建立在深度学习用户行为之上。你用抖音刷视频、看直播、在商城下单、然后用抖音支付付款——所有这些行为数据完整地留在了同一套系统里。一个平台同时掌握你的内容消费偏好、社交互动记录和完整的支付交易明细。它说「支付数据与推荐系统隔离」，但没有第三方审计能验证。</p>
<p>我自己的做法是：抖音买东西只走支付宝。虽然两种方式的支付安全差不多，但至少支付宝不会把支付数据和我的短视频观看记录放在一起。</p>
<h4 id="美团支付">美团支付</h4>
<p>和抖音的问题是同一类：不是支付不安全，是生态内数据太集中。外卖地址、到店消费、酒店入住、电影票预订，加上支付流水——这几乎是一个人的完整线下活动画像。</p>
<p>技术上，美团的风控利用了场景数据的天然优势。一个长期在北京叫外卖的账号突然在深圳产生一笔凌晨酒店支付——这种异常信号对美团来说太容易捕捉了。但美团「杀熟」争议（不同手机、不同用户看到不同价格，反复被用户实测验证）也从侧面说明它的数据分析和用户画像能力极强。</p>
<h4 id="小米钱包">小米钱包</h4>
<p>分两条路径看：</p>
<ul>
<li><strong>NFC 闪付（Mi Pay）</strong>：走银联 Token 化通道，用手机 Secure Element 芯片存储，和 Apple Pay 原理一致。这部分的安全性取决于硬件，小米旗舰机的 eSE 方案是可靠的。</li>
<li><strong>线上绑卡到小米钱包 App</strong>：走捷付睿通自有通道。捷付睿通是小米全资子公司，2026 年 5 月因「违反反洗钱管理规定」被央行内蒙古分行处罚，文件编号 <strong>蒙银罚决字〔2026〕3 号</strong>，罚款 <strong>167.1 万元</strong>。这已经是捷付睿通成立以来第四次被罚。</li>
</ul>
<p>来源：<a href="https://www.mpaypass.com.cn/news/202605/15191236.html">移动支付网</a> / <a href="http://finance.ce.cn/bank12/scroll/202605/t20260520_2978719.shtml">中国经济网</a></p>
<p>另外，MIUI 作为操作系统，在系统层面对用户设备拥有最高权限。硬件设备行为、IoT 设备数据、商城消费加上支付数据，全部在小米的生态内。如果你用的小米手机已经是主力机了，可以再给小米钱包增加权限——但需要知道这意味着什么。</p>
<h4 id="翼支付与和包支付">翼支付与和包支付</h4>
<p>两家运营商旗下的支付工具放一起说，因为它们面临同一个核心问题：数据维度太多了。</p>
<p>中国电信有 3 亿多实名用户，中国移动超过 14 亿（含物联网卡）。运营商手里本来就有身份证信息、实时位置轨迹、通话记录、上网日志。现在再加上支付交易流水和消费记录——这个数据集可以画出一个人生活的几乎全部轮廓。</p>
<p>安全性上，国企的合规约束比民营平台严格，目前没有发生过重大泄露或盗刷事件。但翼支付账户直接绑定手机号，如果号码被人恶意挂失补卡，支付账户跟着就危险了。两家都有运营商特有的免费挂失保障，但前提是你及时发现。</p>
<p>日常生活中用不上这两家就不用绑。如果你已经是电信或移动的长期用户，只绑一张小额借记卡并设好日限额。</p>
<h3 id="第四梯队有安全硬伤">第四梯队：有安全硬伤</h3>
<h4 id="拼多多支付">拼多多支付</h4>
<p>拼多多支付背后的持牌机构是上海付费通。但核心问题不在付费通——在拼多多这个母公司对待用户数据的根本态度。</p>
<p>2023 年 3 月，Google 的安全团队（Project Zero 级别的 Android 安全研究能力）发现拼多多 App 利用了 Android 系统未公开的漏洞——不是普通 bug，是 0-day 级别的安全漏洞——绕过用户授权读写其他 App 的私有数据、修改系统设置。Google 随即将拼多多从 Play 商店全线下架，持续了几个月。</p>
<p>关于这个事件有几个关键点：</p>
<ul>
<li><strong>利用 0-day 漏洞需要极高的安全研究能力</strong>。这意味着拼多多雇佣了具备攻击性安全研究能力的人，把能力用在了用户身上</li>
<li><strong>工信部、网信办多次通报</strong>拼多多「违规收集个人信息」「超范围收集个人信息」「强制用户使用定向推送功能」</li>
<li>这些不是一次性疏忽，是持续多年的隐私合规问题</li>
</ul>
<p>你愿意让一家把高水平安全能力用在你身上的公司保管你的银行卡信息吗？在拼多多买东西用微信支付或支付宝结算就够了——完全没必要把卡直接绑给拼多多支付。</p>
<h4 id="携程支付">携程支付</h4>
<p>2014 年 3 月，乌云漏洞平台上了一份报告，后来被包括《财经》在内的媒体广泛转载：携程的支付系统日志直接打印并存储了用户的银行卡号、CVV 安全码、有效期——以明文形式。</p>
<p>PCI-DSS 安全标准有明确规定：CVV 在任何情况下都不允许被存储。加密也不行。不允许。这是支付安全行业一个绝对红线。</p>
<p>能犯这个错误，说明在事发时携程支付的安全评审流程、代码审查制度和员工 PCI-DSS 培训全部没有起到该起的作用。单个工程师不可能独立导致这种后果——这是一个组织层面的合规和文化问题。</p>
<p>需要承认的是，十年过去了，携程的安全体系和 2014 年已经完全不同。但这件事情的意义在于：它让我们看到了一个支付系统在不被监管真正穿透审查时，底线可以低到什么程度。</p>
<p>任何时候在携程买机票酒店，用微信或支付宝支付，不要绑定携程支付。</p>
<h4 id="通联支付与富友支付">通联支付与富友支付</h4>
<p>这两家的主营业务是面向商户的 B 端收单，个人用户很少直接接触。但如果你在某个线下商户被引导扫码付款时跳到了它们的页面，需要知道背景。</p>
<p>通联支付：2016 年被央行通报「严重违规」，罚款 1110 万元。历史累计处罚超千万元，涉及银行卡收单违规、反洗钱违规。2023 年还牵涉到一起 9000 万元非法吸收公众存款关联案件。</p>
<p>富友支付：多次因反洗钱违规和数据报送问题被央行处罚，消费者投诉累计上千条。一直在冲击港股上市但反复受挫，被市场质疑的点之一是在 IPO 前进行了超过净利润 3.65 亿元的「清仓式分红」。</p>
<p>这两家如果作为消费者在终端碰到，不要绑定任何银行卡。</p>
<h4 id="苏宁支付与唯品支付">苏宁支付与唯品支付</h4>
<p>苏宁支付（易付宝）：苏宁集团近几年经营持续困难，由国资托管转型中。技术团队和安全投入大概率在缩减。没有重大安全事件记录，但也毫无技术亮点。</p>
<p>唯品支付：2016 年用户数据泄露事件包含支付信息。作为垂直品牌特卖电商平台，安全团队规模与大厂不在同一量级。</p>
<hr>
<h2 id="七快速决策参考">七、快速决策参考</h2>
<p>如果你只想要结论，下面这张表是你需要的全部信息：</p>
<h3 id="71-绑卡建议总表">7.1 绑卡建议总表</h3>
<table>
	<thead>
			<tr>
					<th>建议等级</th>
					<th>推荐卡种</th>
					<th>平台</th>
					<th>核心理由</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>🟢 最稳妥</td>
					<td>借记卡+信用卡均可</td>
					<td><strong>Apple Pay、云闪付、银联在线支付</strong></td>
					<td>硬件级Token化/EMVCo标准/直连发卡行,无商业变现动机</td>
			</tr>
			<tr>
					<td>🟢 推荐</td>
					<td>借记卡+信用卡均可</td>
					<td><strong>支付宝、财付通(微信支付)</strong></td>
					<td>全球一流风控能力,百亿级交易量打磨,2023年被重罚后合规整改</td>
			</tr>
			<tr>
					<td>🟡 可用</td>
					<td>信用卡优先</td>
					<td><strong>京东支付</strong></td>
					<td>风控框架完整,但持牌机构六年五罚、累计4200万、3000+用户投诉不知情扣款</td>
			</tr>
			<tr>
					<td>🟠 谨慎</td>
					<td>仅信用卡,设限额</td>
					<td><strong>抖音支付、美团支付、小米钱包(线上)</strong></td>
					<td>支付安全本身不差,核心风险在生态内数据高度集中和隐私边界模糊</td>
			</tr>
			<tr>
					<td>🟠 谨慎</td>
					<td>仅信用卡,设限额</td>
					<td><strong>翼支付、和包支付</strong></td>
					<td>运营商数据+支付数据维度过大,手机号挂失补卡风险需警惕</td>
			</tr>
			<tr>
					<td>🔴 不推荐绑</td>
					<td>—</td>
					<td><strong>拼多多支付、携程支付、苏宁支付、唯品支付</strong></td>
					<td>历史安全事件严重(0-day恶意代码被下架/明文CVV/数据泄露)或安全投入不足</td>
			</tr>
			<tr>
					<td>🔴 不推荐绑</td>
					<td>—</td>
					<td><strong>通联支付、富友支付</strong></td>
					<td>B端机构,多次严重违规,个人用户无必要绑定</td>
			</tr>
	</tbody>
</table>
<h3 id="72-按使用场景的选择">7.2 按使用场景的选择</h3>
<table>
	<thead>
			<tr>
					<th>你的场景</th>
					<th>最优选择</th>
					<th>次优选择</th>
					<th>不要用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>iPhone 日常小额</td>
					<td>Apple Pay</td>
					<td>云闪付</td>
					<td>—</td>
			</tr>
			<tr>
					<td>安卓日常小额</td>
					<td>云闪付</td>
					<td>支付宝/微信</td>
					<td>拼多多支付</td>
			</tr>
			<tr>
					<td>大额交易(&gt;5000元)</td>
					<td>银联在线支付</td>
					<td>云闪付</td>
					<td>任何快捷支付</td>
			</tr>
			<tr>
					<td>抖音购物</td>
					<td>支付宝</td>
					<td>微信支付</td>
					<td>抖音支付(避免数据打通)</td>
			</tr>
			<tr>
					<td>美团外卖/到店</td>
					<td>支付宝</td>
					<td>微信支付</td>
					<td>美团支付(避免数据打通)</td>
			</tr>
			<tr>
					<td>拼多多购物</td>
					<td>微信支付</td>
					<td>支付宝</td>
					<td>拼多多支付</td>
			</tr>
			<tr>
					<td>携程订票订酒店</td>
					<td>微信支付</td>
					<td>支付宝</td>
					<td>携程支付</td>
			</tr>
			<tr>
					<td>京东自营购物</td>
					<td>微信支付</td>
					<td>支付宝</td>
					<td>—</td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="八如果你只看这一节">八、如果你只看这一节</h2>
<p>绑卡安全这件事上，有几个原则比任何技术分析都重要：</p>
<p><strong>1. 开一张 II 类账户专门做快捷支付。</strong> 招行 II 类账户每日进出限额 1 万元，把这张卡绑到各个支付平台上去。主力借记卡保持在所有外部渠道之外。最坏的情况下某个平台出问题，损失也卡在可控范围。</p>
<p><strong>2. 招行 App →「快捷支付管理」→ 每月看一遍。</strong> 把不认识或者已经不用的授权直接关掉。很多平台授权一次就永久有效，时间长了你自己都忘了绑过哪些。</p>
<p><strong>3. 大额付款统一用信用卡。</strong> 信用卡有 48 小时盗刷保障，争议交易可以拒付。借记卡是存款被直接划走，追讨流程慢得多——而且追回来之前这笔钱你用不了。</p>
<p><strong>4. 绑定新平台的同时立刻设限额。</strong> 招行 App 里可以按商户设单日单笔限额。就算平台出问题，损失的区间也是锁死的。</p>
<p><strong>5. 安全这件事，分散风险不如减少风险面。</strong> 不是铺开 17 个支付方式就能降低风险。少绑一个不靠谱的平台，比给 17 个平台都设好限额更安全。尤其是有历史污点的平台——第一次出问题是意外，多次出问题就是模式了。</p>
<hr>
<p><em>数据来源：中国人民银行总行及各地分支机构行政处罚信息公示、移动支付网公开统计、澎湃新闻、央广网、乌云漏洞平台（WooYun，已停止运营）、Google Android 安全团队公开披露。本文写于 2026 年 7 月，各平台安全状况动态变化中，建议每半年复查一次。如有事实错误或需补充的内容，欢迎指出。</em></p>
]]></content:encoded></item><item><title>Hermes 使用教程：从启动到故障排查的完整记录</title><link>https://raysre.com/posts/hermes-dingtalk-guide/</link><pubDate>Sat, 11 Jul 2026 00:00:00 +0000</pubDate><guid>https://raysre.com/posts/hermes-dingtalk-guide/</guid><description>Hermes AI 助手的部署与使用教程：网络架构、模型选择、多话题会话管理、CLI 命令速查和踩坑记录，基于实际运维经验整理。</description><content:encoded><![CDATA[<h2 id="环境概览">环境概览</h2>
<h3 id="网络架构">网络架构</h3>
<p>Hermes 的部署拓扑：</p>
<ul>
<li><strong>管理节点</strong>通过 WireGuard 接入公司内网，网段 <code>10.10.0.0/24</code></li>
<li><strong><code>10.10.0.4</code></strong> 上运行 Nginx 反向代理，监听 <code>0.0.0.0:8000</code>，后端接 DeepSeek 网关</li>
<li>所有 API 请求统一走 <code>http://10.10.0.4:8000/v1</code></li>
</ul>
<blockquote>
<p>这条链路踩过坑。Nginx 不传 Authorization 头、超时不够、Host 路由不对都遇到过，后面踩坑记录里展开。</p>
</blockquote>
<h3 id="可用模型">可用模型</h3>
<table>
	<thead>
			<tr>
					<th>模型名称</th>
					<th>Provider</th>
					<th>场景</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>deepseek-auto</code> ⭐</td>
					<td><code>company-deepseek-auto</code></td>
					<td>默认模型，智能路由，日常使用</td>
			</tr>
			<tr>
					<td><code>deepseek-semantic-auto</code></td>
					<td><code>company-deepseek-semantic-auto</code></td>
					<td>语义路由，需要精准理解意图时用</td>
			</tr>
			<tr>
					<td><code>deepseek-v4-pro</code></td>
					<td><code>company-deepseek-v4</code></td>
					<td>最强推理，复杂分析、长文档、架构设计</td>
			</tr>
			<tr>
					<td><code>deepseek-v4-flash</code></td>
					<td><code>company-deepseek-v4-flash</code></td>
					<td>快速响应，简单问答、翻译、代码片段</td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="日常启动与退出">日常启动与退出</h2>
<h3 id="启动">启动</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>hermes
</span></span></code></pre></div><p>在钉钉群里 @机器人 或在私聊里直接发消息即可交互。命令行环境下直接输入文本对话。</p>
<h3 id="对话中常用操作">对话中常用操作</h3>
<table>
	<thead>
			<tr>
					<th>操作</th>
					<th>命令</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>查看帮助</td>
					<td><code>/help</code></td>
			</tr>
			<tr>
					<td>退出会话</td>
					<td><code>/exit</code> 或 <code>/quit</code></td>
			</tr>
			<tr>
					<td>开新话题</td>
					<td><code>/new</code></td>
			</tr>
			<tr>
					<td>撤销上一步</td>
					<td><code>/undo</code></td>
			</tr>
			<tr>
					<td>重试回答</td>
					<td><code>/retry</code></td>
			</tr>
			<tr>
					<td>给会话取名</td>
					<td><code>/title 名称</code></td>
			</tr>
			<tr>
					<td>切换模型</td>
					<td><code>/model deepseek-v4-pro</code></td>
			</tr>
			<tr>
					<td>查看用量</td>
					<td><code>/usage</code></td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="模型选择与切换">模型选择与切换</h2>
<h3 id="规则引擎速查">规则引擎速查</h3>
<p>Hermes 支持通过 <code>config.yaml</code> 中的 <code>rules</code> 配置按关键词自动匹配模型。例如：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">keywords</span>: [<span style="color:#e6db74">&#34;架构&#34;</span>, <span style="color:#e6db74">&#34;架构设计&#34;</span>, <span style="color:#e6db74">&#34;系统设计&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">model</span>: <span style="color:#ae81ff">deepseek-v4-pro</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">provider</span>: <span style="color:#ae81ff">company-deepseek-v4</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">keywords</span>: [<span style="color:#e6db74">&#34;翻译&#34;</span>, <span style="color:#e6db74">&#34;translate&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">model</span>: <span style="color:#ae81ff">deepseek-v4-flash</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">provider</span>: <span style="color:#ae81ff">company-deepseek-v4-flash</span>
</span></span></code></pre></div><p>每条规则包括：</p>
<ul>
<li><strong><code>keywords</code></strong> — 匹配用户输入的关键词列表</li>
<li><strong><code>model</code></strong> — 指定使用的模型名</li>
<li><strong><code>provider</code></strong> — 对应的后端 provider（可选）</li>
<li><strong><code>priority</code></strong> — 规则优先级（数值越大越优先，默认 0）</li>
</ul>
<p>匹配后自动切换，不需要手动干预。</p>
<h3 id="临时切换模型">临时切换模型</h3>
<p>在对话里随时切，不影响上下文：</p>
<pre tabindex="0"><code>/model deepseek-v4-pro
</code></pre><p>也可以传 provider+model：</p>
<pre tabindex="0"><code>/model company-deepseek-v4/deepseek-v4-pro
</code></pre><hr>
<h2 id="多话题会话管理">多话题会话管理</h2>
<p>这是近期新增的功能：一个 Hermes 实例里同时跑多个独立会话，互不干扰。</p>
<h3 id="使用场景">使用场景</h3>
<p>假设你同时处理三个话题：</p>
<table>
	<thead>
			<tr>
					<th>话题</th>
					<th>会话名称</th>
					<th>推荐模型</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>技术架构设计</td>
					<td><code>技术架构</code></td>
					<td><code>deepseek-v4-pro</code></td>
			</tr>
			<tr>
					<td>项目日志分析</td>
					<td><code>项目日志</code></td>
					<td><code>deepseek-v4-pro</code></td>
			</tr>
			<tr>
					<td>孩子英语教育</td>
					<td><code>英语教育</code></td>
					<td><code>deepseek-auto</code></td>
			</tr>
	</tbody>
</table>
<p>每个会话有自己的对话历史、上下文和模型配置。启动新话题：</p>
<pre tabindex="0"><code>/new 技术架构
</code></pre><p>然后在对话中设置模型：</p>
<pre tabindex="0"><code>/model deepseek-v4-pro
/title 技术架构
</code></pre><p>这样下次通过 <code>hermes --continue 技术架构</code> 就能直接回到这个对话。</p>
<h3 id="查看所有会话">查看所有会话</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>hermes sessions list
</span></span></code></pre></div><p>输出会列出每个会话的 ID、名称、最后活跃时间。也可以用交互式界面浏览：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>hermes sessions browse
</span></span></code></pre></div><h3 id="会话操作速查">会话操作速查</h3>
<table>
	<thead>
			<tr>
					<th>操作</th>
					<th>命令</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>当前会话改名</td>
					<td><code>/title 新名称</code></td>
			</tr>
			<tr>
					<td>开新会话</td>
					<td><code>/new</code> 或 <code>/reset</code></td>
			</tr>
			<tr>
					<td>撤销上一条</td>
					<td><code>/undo</code></td>
			</tr>
			<tr>
					<td>分支会话</td>
					<td><code>/branch</code></td>
			</tr>
			<tr>
					<td>列出所有会话</td>
					<td><code>hermes sessions list</code></td>
			</tr>
			<tr>
					<td>交互式选会话</td>
					<td><code>hermes sessions browse</code></td>
			</tr>
			<tr>
					<td>恢复命名会话</td>
					<td><code>hermes --continue 名称</code></td>
			</tr>
			<tr>
					<td>导出会话记录</td>
					<td><code>hermes sessions export 名称.jsonl</code></td>
			</tr>
			<tr>
					<td>删除旧会话</td>
					<td><code>hermes sessions delete ID</code></td>
			</tr>
			<tr>
					<td>会话内搜索</td>
					<td><code>hermes sessions search 关键词</code></td>
			</tr>
	</tbody>
</table>
<h3 id="进阶profile-完全隔离">进阶：Profile 完全隔离</h3>
<p>如果希望话题之间完全隔离——独立的记忆、独立的配置、甚至独立的 API Key——可以创建多个 Profile：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 创建新 profile</span>
</span></span><span style="display:flex;"><span>hermes profile create 工作-架构设计
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 在该 profile 下配置不同的模型</span>
</span></span><span style="display:flex;"><span>hermes config set model deepseek-v4-pro --profile 工作-架构设计
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 使用指定 profile 启动</span>
</span></span><span style="display:flex;"><span>hermes --profile 工作-架构设计
</span></span></code></pre></div><p>每个 Profile 有自己独立的 skills、plugins、cron 和 memories。</p>
<hr>
<h2 id="常用命令速查">常用命令速查</h2>
<h3 id="基础命令">基础命令</h3>
<table>
	<thead>
			<tr>
					<th>命令</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>hermes</code></td>
					<td>启动交互模式</td>
			</tr>
			<tr>
					<td><code>hermes &quot;你的问题&quot;</code></td>
					<td>单次问答模式</td>
			</tr>
			<tr>
					<td><code>hermes --continue 名称</code></td>
					<td>恢复命名会话</td>
			</tr>
			<tr>
					<td><code>hermes --profile 名称</code></td>
					<td>指定 Profile 启动</td>
			</tr>
	</tbody>
</table>
<h3 id="模型切换">模型切换</h3>
<table>
	<thead>
			<tr>
					<th>命令</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>/model deepseek-v4-pro</code></td>
					<td>切换模型</td>
			</tr>
			<tr>
					<td><code>/model list</code></td>
					<td>查看可用模型</td>
			</tr>
			<tr>
					<td><code>/model current</code></td>
					<td>查看当前模型</td>
			</tr>
	</tbody>
</table>
<h3 id="会话管理">会话管理</h3>
<table>
	<thead>
			<tr>
					<th>命令</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>/new 名称</code></td>
					<td>新建命名会话</td>
			</tr>
			<tr>
					<td><code>/title 名称</code></td>
					<td>当前会话改名</td>
			</tr>
			<tr>
					<td><code>/branch</code></td>
					<td>从当前位置分支新会话</td>
			</tr>
			<tr>
					<td><code>/undo</code></td>
					<td>撤销上一条消息</td>
			</tr>
	</tbody>
</table>
<h3 id="系统维护">系统维护</h3>
<table>
	<thead>
			<tr>
					<th>命令</th>
					<th>作用</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>hermes update</code></td>
					<td>更新 Hermes</td>
			</tr>
			<tr>
					<td><code>hermes doctor</code></td>
					<td>运行健康检查</td>
			</tr>
			<tr>
					<td><code>/usage</code></td>
					<td>查看 Token 用量</td>
			</tr>
			<tr>
					<td><code>/status</code></td>
					<td>查看当前运行状态</td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="踩坑记录">踩坑记录</h2>
<p>以下是实际部署过程中踩过的坑。</p>
<h3 id="-nginx-反代导致-401-认证错误">❌ Nginx 反代导致 401 认证错误</h3>
<p><strong>现象</strong>：客户端配置了正确的 API Key，但请求始终返回 401。</p>
<p><strong>原因</strong>：Nginx 默认不会透传 <code>Authorization</code> 请求头。反向代理到后端时，这个头被丢了。</p>
<p><strong>修复</strong>：在 Nginx 的 location 块里显式加上：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">proxy_set_header</span> <span style="color:#e6db74">Authorization</span> $http_authorization;
</span></span></code></pre></div><p>这个坑踩了两次——正常对话没问题，但上下文压缩（生成大请求）时偶发 401，因为压缩请求走了不同的连接路径，偶尔触发同一根因。</p>
<h3 id="-realm-转发导致-404">❌ Realm 转发导致 404</h3>
<p><strong>现象</strong>：使用 Realm 做流量转发，所有请求返回 404。</p>
<p><strong>原因</strong>：Realm 不支持修改 <code>Host</code> 头，公司网关依赖 <code>Host</code> 做路由决策。</p>
<p><strong>解决</strong>：放弃 Realm，改为 Nginx <code>proxy_pass</code> 直接反代。</p>
<h3 id="-请求超时">❌ 请求超时</h3>
<p><strong>现象</strong>：长文本生成或复杂推理中途被断开，客户端收到超时错误。</p>
<p><strong>原因</strong>：Nginx 的 <code>proxy_read_timeout</code> 默认只有 60 秒，AI 后端生成长回复时经常超过这个时间。</p>
<p><strong>修复</strong>：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">proxy_read_timeout</span> <span style="color:#e6db74">600s</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">proxy_connect_timeout</span> <span style="color:#e6db74">60s</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">proxy_send_timeout</span> <span style="color:#e6db74">60s</span>;
</span></span></code></pre></div><p>确保同时使用 HTTP/1.1 长连接：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">proxy_set_header</span> <span style="color:#e6db74">Connection</span> <span style="color:#e6db74">&#34;&#34;</span>;
</span></span></code></pre></div><hr>
<h2 id="故障排查">故障排查</h2>
<h3 id="快速诊断">快速诊断</h3>
<p>遇到问题时按顺序执行：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 1. 检查 WireGuard 连通性</span>
</span></span><span style="display:flex;"><span>ping 10.10.0.4
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2. 检查 Nginx 是否存活</span>
</span></span><span style="display:flex;"><span>curl -s -o /dev/null -w <span style="color:#e6db74">&#34;%{http_code}&#34;</span> http://10.10.0.4:8000/v1/models
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3. 检查 Hermes 自身状态</span>
</span></span><span style="display:flex;"><span>hermes doctor
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 4. 查看最近日志</span>
</span></span><span style="display:flex;"><span>tail -100 ~/.hermes/logs/hermes.log
</span></span></code></pre></div><h3 id="常见问题">常见问题</h3>
<table>
	<thead>
			<tr>
					<th>问题</th>
					<th>可能原因</th>
					<th>怎么解决</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>无法连接</td>
					<td>WireGuard 断开</td>
					<td><code>ping 10.10.0.4</code> 检查连通性</td>
			</tr>
			<tr>
					<td>模型报错</td>
					<td>模型名拼写错误</td>
					<td>检查 <code>config.yaml</code> 中的模型名和 provider 名称</td>
			</tr>
			<tr>
					<td>Token 耗尽</td>
					<td>当月额度用完</td>
					<td>联系管理员续期</td>
			</tr>
			<tr>
					<td>响应超时</td>
					<td>Nginx 超时太短</td>
					<td>已将 <code>proxy_read_timeout</code> 优化为 600s</td>
			</tr>
			<tr>
					<td>会话丢失</td>
					<td>未命名就退出了</td>
					<td>用 <code>hermes sessions list</code> 查找</td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="附录-anginx-优化配置参考">附录 A：Nginx 优化配置参考</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">8000</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">_</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/v1</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://&lt;upstream&gt;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Connection</span> <span style="color:#e6db74">&#34;&#34;</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Authorization</span> $http_authorization;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_read_timeout</span> <span style="color:#e6db74">600s</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_connect_timeout</span> <span style="color:#e6db74">60s</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_send_timeout</span> <span style="color:#e6db74">60s</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>三个要点：超时设到 600 秒、显式转发 <code>Authorization</code> 头、用 HTTP/1.1 长连接避免频繁握手。</p>
<hr>
<blockquote>
<p>文档基于 Hermes Agent 公开发布版本及公司内部部署实践整理，配置参数和功能以实际部署版本为准。文档编号：DOC-2026-0708-HERMES-V2。</p>
</blockquote>
]]></content:encoded></item><item><title>Hugo + 腾讯云 COS + EdgeOne 静态博客部署全记录 — 从零到上线填过的坑</title><link>https://raysre.com/posts/hugo-cos-edgeone-deploy/</link><pubDate>Sat, 11 Jul 2026 00:00:00 +0000</pubDate><guid>https://raysre.com/posts/hugo-cos-edgeone-deploy/</guid><description>从零搭建 Hugo+PaperMod 静态博客，托管在腾讯云 COS 并通过 EdgeOne 边缘加速，覆盖 GitHub Actions 自动部署、HTTPS、ICP 备案展示、www 301 跳转和源站安全管控的完整记录。含 deploy.yml 逐行解读、AccessDenied 排查过程、COS/EdgeOne 核心概念速查。</description><content:encoded><![CDATA[<h2 id="写在前面">写在前面</h2>
<p>我有一个域名 <code>raysre.com</code>，放在那儿很久没什么用。最近打算把它变成个人博客——要求很简单：纯静态、不维护服务器、更新成本接近零。</p>
<p>选了 <a href="https://gohugo.io/">Hugo</a>（Go 写的静态站点生成器，<code>hugo build</code> 整个站点不到 200ms）+ <a href="https://github.com/adityatelange/hugo-PaperMod">PaperMod</a> 主题（极简，不需要 Webpack/PostCSS 那一套）。托管在腾讯云 COS（对象存储），套一层 EdgeOne（腾讯云的边缘加速产品，对标 Cloudflare），通过 GitHub Actions 推送代码自动上线。</p>
<p>这条路踩了几个坑。COS 的「公有读私有写」和 EdgeOne 的「私有访问授权」组合在一起会产生一个让人摸不着头脑的 AccessDenied。调试过程花了不少时间翻文档，顺带把 EdgeOne 和 COS 里那些没用到但经常被问到的功能也一并理了。</p>
<p>这篇文章是从零到上线的完整记录，加上排查过程、配置解读、概念速查。没用到腾讯云功能的内容放进进阶参考，最后附 FAQ。</p>
<hr>
<h2 id="整体架构">整体架构</h2>
<p>各组件分工：</p>
<table>
	<thead>
			<tr>
					<th>层级</th>
					<th>用什么</th>
					<th>负责</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>写作</td>
					<td>Markdown</td>
					<td>文章内容，存在 GitHub 仓库里</td>
			</tr>
			<tr>
					<td>构建</td>
					<td>Hugo 0.164.0 + PaperMod</td>
					<td>Markdown → HTML/CSS/JS，<code>hugo --minify</code> 压缩输出</td>
			</tr>
			<tr>
					<td>构建环境</td>
					<td>GitHub Actions</td>
					<td>每次 <code>git push main</code> 自动构建+上传</td>
			</tr>
			<tr>
					<td>存储</td>
					<td>腾讯云 COS（上海，标准存储）</td>
					<td>存 HTML/CSS/JS/图片等静态文件</td>
			</tr>
			<tr>
					<td>加速+安全</td>
					<td>腾讯云 EdgeOne</td>
					<td>边缘缓存、HTTPS 终结、DDoS 基础防护、www 跳转</td>
			</tr>
			<tr>
					<td>DNS</td>
					<td>腾讯云 DNSPod</td>
					<td><code>raysre.com</code> → EdgeOne CNAME</td>
			</tr>
	</tbody>
</table>
<p>数据流是单向的：</p>
<pre tabindex="0"><code>开发者 git push → GitHub Actions → hugo build → public/ → Python SDK 上传 COS → CDN 刷新
用户访问 raysre.com → EdgeOne 边缘节点（缓存命中）→ 返回缓存 → 用户
                           ｜缓存未命中 → 回源 COS → 返回
</code></pre><p>这个架构对个人博客来说有几个好处：不需要买云服务器，不需要维护 Nginx，COS 存储一个月几毛钱，EdgeOne 免费套餐 10GB/月流量够用。</p>
<hr>
<h2 id="基础概念速查">基础概念速查</h2>
<p>配置过程中反复出现的几个概念，放在这里免得后面看不懂。</p>
<table>
	<thead>
			<tr>
					<th>概念</th>
					<th>一句话</th>
					<th>你博客里的角色</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>COS 存储桶</td>
					<td>腾讯云的对象存储，类似 AWS S3</td>
					<td>存 HTML 等静态文件</td>
			</tr>
			<tr>
					<td>静态网站托管</td>
					<td>COS 的一个功能，让桶像 Web 服务器那样响应 HTTP 请求</td>
					<td>开了，但 EdgeOne 回源不用它</td>
			</tr>
			<tr>
					<td>默认域名</td>
					<td>COS 给每个桶分配的公网访问域名（<code>xxx.cos.ap-shanghai.myqcloud.com</code>）</td>
					<td>2024 年 1 月后新建的桶不能用它预览网页（官方限制）</td>
			</tr>
			<tr>
					<td>静态网站端点</td>
					<td>另一个域名（<code>xxx.cos-website.ap-shanghai.myqcloud.com</code>），专门用于静态网站访问</td>
					<td>开了，确认能用 HTTP 访问到页面</td>
			</tr>
			<tr>
					<td>EdgeOne 加速域名</td>
					<td>你自定义的域名接入 EdgeOne 后分配 CNAME（<code>raysre.com.eo.dnse2.com</code>）</td>
					<td>DNS CNAME 指到这儿</td>
			</tr>
			<tr>
					<td>回源</td>
					<td>CDN 节点缓存未命中时，向源站拉取文件</td>
					<td>EdgeOne → COS</td>
			</tr>
			<tr>
					<td>回源 HOST 头</td>
					<td>CDN 回源请求中带的 Host 头，决定源站识别哪个站点</td>
					<td>配置错误导致 AccessDenied 的关键字段</td>
			</tr>
			<tr>
					<td>CNAME</td>
					<td>DNS 里把域名指向另一个域名的记录</td>
					<td><code>raysre.com</code> CNAME → <code>raysre.com.eo.dnse2.com</code></td>
			</tr>
	</tbody>
</table>
<p>关键理解：COS 有两套域名体系。</p>
<table>
	<thead>
			<tr>
					<th>域名类型</th>
					<th>格式</th>
					<th>用在哪</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>默认域名（API 端点）</td>
					<td><code>bucket.cos.region.myqcloud.com</code></td>
					<td>SDK 上传文件、API 操作</td>
			</tr>
			<tr>
					<td>静态网站端点</td>
					<td><code>bucket.cos-website.region.myqcloud.com</code></td>
					<td>浏览器直接访问静态网站</td>
			</tr>
	</tbody>
</table>
<p>EdgeOne 如果用「对象存储源站」模式回源到默认域名，走的不是静态网站逻辑。你用浏览器访问 <code>https://raysre.com/index.html</code>，EdgeOne 回源时相当于向 COS 的 API 端点发起 GET 请求，这时候如果桶是私有读，会直接返回 AccessDenied——这就是后面排查过程中掉进去的坑。</p>
<hr>
<h2 id="step-1hugo-环境搭建">Step 1：Hugo 环境搭建</h2>
<p>Hugo 用的是 v0.164.0 extended 版 + PaperMod 主题。注意 PaperMod 是 submodule，克隆仓库时带 <code>--recurse-submodules</code>：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 新建站点</span>
</span></span><span style="display:flex;"><span>hugo new site raysre.com --format yaml
</span></span><span style="display:flex;"><span>cd raysre.com
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># PaperMod 主题</span>
</span></span><span style="display:flex;"><span>git submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
</span></span></code></pre></div><p><code>hugo.toml</code> 核心配置（60 行，去掉了默认配置的冗余项）：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span><span style="color:#a6e22e">baseURL</span> = <span style="color:#e6db74">&#39;https://raysre.com/&#39;</span>   <span style="color:#75715e"># 生产环境域名，决定所有绝对链接</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">locale</span> = <span style="color:#e6db74">&#39;zh-cn&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">title</span> = <span style="color:#e6db74">&#39;DevOps工作笔记&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">theme</span> = <span style="color:#e6db74">&#39;PaperMod&#39;</span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">params</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span> = <span style="color:#e6db74">&#39;工作与生活随记&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">author</span> = <span style="color:#e6db74">&#39;阿端&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">ShowToc</span> = <span style="color:#66d9ef">true</span>                  <span style="color:#75715e"># 显示文章目录</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">ShowBreadCrumbs</span> = <span style="color:#66d9ef">true</span>          <span style="color:#75715e"># 面包屑导航</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">ShowReadingTime</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">ShowWordCount</span> = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">markup</span>.<span style="color:#a6e22e">tableOfContents</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">startLevel</span> = <span style="color:#ae81ff">2</span>                  <span style="color:#75715e"># 目录只显示 h2/h3</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">endLevel</span> = <span style="color:#ae81ff">3</span>
</span></span></code></pre></div><p>构建命令：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>hugo --minify           <span style="color:#75715e"># 构建并压缩，输出到 public/</span>
</span></span><span style="display:flex;"><span>hugo server -D          <span style="color:#75715e"># 本地预览，--watch 默认开启</span>
</span></span></code></pre></div><p><strong>备案展示</strong>——PaperMod 默认的 footer.html 没有备案号。在 <code>layouts/partials/footer.html</code> 里加了：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">style</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text-align:center;font-size:12px;color:#888;margin-top:10px&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://beian.miit.gov.cn/&#34;</span> <span style="color:#a6e22e">target</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;_blank&#34;</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;noopener&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    沪ICP备2026021650号-1
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#f92672">a</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">span</span> <span style="color:#a6e22e">style</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;margin:0 8px&#34;</span>&gt;&lt;/<span style="color:#f92672">span</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=31011502406637&#34;</span>
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">target</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;_blank&#34;</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;noopener&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">img</span> <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/images/beian.png&#34;</span> <span style="color:#a6e22e">style</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;display:inline;height:14px;vertical-align:middle&#34;</span> <span style="color:#a6e22e">alt</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    沪公网安备 31011502406637 号
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#f92672">a</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">div</span>&gt;
</span></span></code></pre></div><p>公安备案图标 <code>beian.png</code> 放 <code>static/images/</code> 下，构建时会原样复制到 <code>public/images/</code>。</p>
<hr>
<h2 id="step-2cos-存储桶配置">Step 2：COS 存储桶配置</h2>
<p>腾讯云 COS 控制台操作清单：</p>
<table>
	<thead>
			<tr>
					<th>配置项</th>
					<th>设置</th>
					<th>原因</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>地域</td>
					<td>上海（ap-shanghai）</td>
					<td>和 GitHub Actions runner 延迟低，和 EdgeOne 节点同地域回源走内网</td>
			</tr>
			<tr>
					<td>访问权限</td>
					<td>公有读私有写</td>
					<td>最早设置；后面排查 AccessDenied 时临时改过，最终改回私有读写+EdgeOne 回源鉴权</td>
			</tr>
			<tr>
					<td>静态网站托管</td>
					<td>开启，默认首页 <code>index.html</code></td>
					<td>Hugo 生成的就是 index.html</td>
			</tr>
			<tr>
					<td>自定义源站域名</td>
					<td>不配置</td>
					<td>COS 的自定义域名和 EdgeOne 是两条路，用了 EdgeOne 就没必要在 COS 里再配</td>
			</tr>
			<tr>
					<td>防盗链</td>
					<td>Referer 白名单：空 Referer + <code>raysre.com</code></td>
					<td>防止其他网站直接盗链 COS 默认域名</td>
			</tr>
	</tbody>
</table>
<p>关于<strong>访问权限</strong>的最终方案：</p>
<blockquote>
<p>桶设为<strong>私有读写</strong>，在 EdgeOne 里开启<strong>私有访问授权</strong>。EdgeOne 回源 COS 时会带上内部鉴权签名，COS 认这个签名放行。用户在浏览器里直接打 COS 默认域名会返回 403。</p>
</blockquote>
<p>这样做的好处：COS 源站不暴露公网入口，只有 EdgeOne 能回源取文件。</p>
<hr>
<h2 id="step-3github-actions-自动部署">Step 3：GitHub Actions 自动部署</h2>
<p><a href="https://github.com/whherui/raysre.com/blob/main/.github/workflows/deploy.yml">deploy.yml</a> 总长 145 行，分 5 个步骤：</p>
<table>
	<thead>
			<tr>
					<th>步骤</th>
					<th>做什么</th>
					<th>耗时参考</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Checkout</td>
					<td>拉仓库 + PaperMod submodule</td>
					<td>3-5s</td>
			</tr>
			<tr>
					<td>Setup Hugo</td>
					<td>下载 Hugo 0.164.0 extended 二进制</td>
					<td>2-3s</td>
			</tr>
			<tr>
					<td>Build</td>
					<td><code>hugo --minify</code></td>
					<td>&lt;1s</td>
			</tr>
			<tr>
					<td>Deploy to COS</td>
					<td>Python SDK 遍历 public/ 上传，带 Content-Type</td>
					<td>10-20s</td>
			</tr>
			<tr>
					<td>Purge CDN Cache</td>
					<td>调 CDN API 刷新首页缓存</td>
					<td>1-2s</td>
			</tr>
	</tbody>
</table>
<p><strong>Setup Hugo 步骤</strong>：从 GitHub Releases 直接下载，不用 <code>peaceiris/actions-hugo</code> 这种第三方 Action——少依赖就是少隐患。指定版本 <code>0.164.0</code> 防止某天自动升版导致构建行为变化。</p>
<p><strong>Deploy 步骤逐段拆解</strong>：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># 第一段：MIME 类型映射表</span>
</span></span><span style="display:flex;"><span>MIME <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;.html&#39;</span>: <span style="color:#e6db74">&#39;text/html; charset=utf-8&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;.css&#39;</span>:  <span style="color:#e6db74">&#39;text/css; charset=utf-8&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;.js&#39;</span>:   <span style="color:#e6db74">&#39;application/javascript; charset=utf-8&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;.svg&#39;</span>:  <span style="color:#e6db74">&#39;image/svg+xml&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ... 16 种常见格式</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>不设 Content-Type 的话，COS 默认给所有文件 <code>application/octet-stream</code>。浏览器拿到不认识就当成下载，CSS/JS 不生效，页面直接崩。所以每个文件上传时必须带正确的 MIME。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># 第二段：遍历上传</span>
</span></span><span style="display:flex;"><span>local_dir <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;public&#39;</span>
</span></span><span style="display:flex;"><span>uploaded <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> root, dirs, files <span style="color:#f92672">in</span> os<span style="color:#f92672">.</span>walk(local_dir):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> f <span style="color:#f92672">in</span> files:
</span></span><span style="display:flex;"><span>        local_path <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>join(root, f)
</span></span><span style="display:flex;"><span>        cos_key <span style="color:#f92672">=</span> local_path[len(local_dir):]<span style="color:#f92672">.</span>lstrip(<span style="color:#e6db74">&#39;/&#39;</span>)
</span></span><span style="display:flex;"><span>        ext <span style="color:#f92672">=</span> os<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>splitext(f)[<span style="color:#ae81ff">1</span>]<span style="color:#f92672">.</span>lower()
</span></span><span style="display:flex;"><span>        content_type <span style="color:#f92672">=</span> MIME<span style="color:#f92672">.</span>get(ext, <span style="color:#e6db74">&#39;application/octet-stream&#39;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">with</span> open(local_path, <span style="color:#e6db74">&#39;rb&#39;</span>) <span style="color:#66d9ef">as</span> fp:
</span></span><span style="display:flex;"><span>            client<span style="color:#f92672">.</span>put_object(
</span></span><span style="display:flex;"><span>                Bucket<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;raysre-1377355589&#39;</span>,
</span></span><span style="display:flex;"><span>                Key<span style="color:#f92672">=</span>cos_key,
</span></span><span style="display:flex;"><span>                Body<span style="color:#f92672">=</span>fp,
</span></span><span style="display:flex;"><span>                ContentType<span style="color:#f92672">=</span>content_type
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        uploaded <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p><code>os.walk()</code> 递归遍历 <code>public/</code> 下所有文件，<code>cos_key</code> 去掉了 <code>public/</code> 前缀。比如 <code>public/posts/hello/index.html</code> 上传后 COS Key 就是 <code>posts/hello/index.html</code>，直接和 URL 路径对应。</p>
<p><strong>为什么用 Python SDK 而不是 <code>coscmd</code></strong>：</p>
<table>
	<thead>
			<tr>
					<th>方案</th>
					<th>问题</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>coscmd</code> CLI</td>
					<td>GitHub Actions 默认不带，需要 apt-get 装；配置文件要文本替换密钥，麻烦</td>
			</tr>
			<tr>
					<td><code>cos-python-sdk-v5</code></td>
					<td><code>pip install</code> 一行搞定，密钥走环境变量，代码即配置</td>
			</tr>
	</tbody>
</table>
<p><strong>Purge CDN 步骤</strong>：调腾讯云 CDN API <code>PurgeUrlsCache</code>，签 V3 签名算法（TC3-HMAC-SHA256），刷新首页和 www 两个 URL。Hugo 新构建的文件名含 hash（如 <code>main.abc123.css</code>），自然绕过旧缓存——所以不需要全站刷新，只刷首页让用户拿到新文件引用就够了。</p>
<p><strong>Secrets 配置</strong>：<code>TENCENT_SECRET_ID</code> 和 <code>TENCENT_SECRET_KEY</code> 必须放 GitHub Repository secrets（不是 Environment secrets）——走 Environment 的话 deploy.yml 里 <code>${{ secrets.XXX }}</code> 拿不到值。这个区分是排查早期构建失败时发现的。</p>
<hr>
<h2 id="step-4edgeone-配置--accessdenied-排查">Step 4：EdgeOne 配置 &amp; AccessDenied 排查</h2>
<h3 id="第一版配置翻车">第一版配置（翻车）</h3>
<table>
	<thead>
			<tr>
					<th>配置项</th>
					<th>值</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>加速域名</td>
					<td><code>raysre.com</code></td>
			</tr>
			<tr>
					<td>源站类型</td>
					<td>对象存储源站</td>
			</tr>
			<tr>
					<td>源站地址</td>
					<td>默认域名（<code>raysre-1377355589.cos.ap-shanghai.myqcloud.com</code>）</td>
			</tr>
			<tr>
					<td>私有访问授权</td>
					<td><strong>开启</strong></td>
			</tr>
			<tr>
					<td>回源 HOST 头</td>
					<td>使用源站域名</td>
			</tr>
	</tbody>
</table>
<p><code>git push</code> 后 Actions Run #9 成功上传 30+ 个文件但浏览器访问 <code>https://raysre.com/</code> 返回 AccessDenied XML：</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#f92672">&lt;Error&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;Code&gt;</span>AccessDenied<span style="color:#f92672">&lt;/Code&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;Message&gt;</span>Access Denied.<span style="color:#f92672">&lt;/Message&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;Resource&gt;</span>/<span style="color:#f92672">&lt;/Resource&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;RequestId&gt;</span>...<span style="color:#f92672">&lt;/RequestId&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/Error&gt;</span>
</span></span></code></pre></div><p>而直接访问 COS 静态网站端点 <code>http://raysre-1377355589.cos-website.ap-shanghai.myqcloud.com/</code> 能正常显示。</p>
<h3 id="定位过程">定位过程</h3>
<p>做了几个测试：</p>
<table>
	<thead>
			<tr>
					<th>测试</th>
					<th>结果</th>
					<th>结论</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>curl -sv https://raysre.com/</code></td>
					<td>403 AccessDenied</td>
					<td>EdgeOne 代理层面就返回了 403</td>
			</tr>
			<tr>
					<td><code>curl -sI http://cos-website域名/</code></td>
					<td>200 + HTML</td>
					<td>COS 静态网站本身正常</td>
			</tr>
			<tr>
					<td><code>curl -s https://默认域名/index.html</code></td>
					<td>200 + 完整 HTML</td>
					<td>默认域名公网可访问（当时桶是公有读）</td>
			</tr>
	</tbody>
</table>
<p><strong>根因</strong>：EdgeOne「私有访问授权」开启时，会构造一个鉴权签名放在回源请求中。但这个签名是针对 COS API 端点的——<strong>不是</strong>针对静态网站端点的。而用「对象存储源站」模式回源的地址恰恰好是 API 端点地址（默认域名）。COS 收到带签名的请求后发现签名和请求不匹配：API 端点期望的签名格式和静态网站的授权逻辑不一样。</p>
<p>换句话说：<strong>公有读桶 + 私有访问授权 = 冲突</strong>。公有读桶不需要鉴权就能访问，开了私有访问授权反而让 EdgeOne 构造了一个多此一举的签名，COS 校验失败直接拒。</p>
<h3 id="修复版本正确配置">修复版本（正确配置）</h3>
<table>
	<thead>
			<tr>
					<th>配置项</th>
					<th>旧值</th>
					<th>新值</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>COS 桶权限</td>
					<td>公有读</td>
					<td><strong>私有读写</strong></td>
			</tr>
			<tr>
					<td>EdgeOne 私有访问授权</td>
					<td>开启</td>
					<td><strong>保持开启</strong></td>
			</tr>
			<tr>
					<td>EdgeOne 回源 HOST 头</td>
					<td>源站域名</td>
					<td><strong>使用源站域名</strong>（不变）</td>
			</tr>
	</tbody>
</table>
<p><strong>修复逻辑</strong>：先让 EdgeOne 去 COS 完成授权绑定，等 COS 桶策略里出现一条允许 EdgeOne 服务访问的策略后，再把桶改为私有读写。这样 EdgeOne 回源带签名，COS 认这个签名放行。外面直接打 COS 默认域名因为没有签名，直接 403。</p>
<h3 id="附加规则">附加规则</h3>
<p><strong>URL 重写</strong>（解决 <code>/</code> 不自动返回 <code>index.html</code>）：</p>
<table>
	<thead>
			<tr>
					<th>匹配条件</th>
					<th>操作</th>
					<th>目标</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>URL Path 等于 <code>/</code></td>
					<td>URL 重写</td>
					<td><code>/index.html</code></td>
			</tr>
	</tbody>
</table>
<p>这一步是关键。EdgeOne 回源到 COS API 端点时 <code>GET /</code> 不等于 <code>GET /index.html</code>，不像 Nginx 那样有 <code>try_files</code>。不加这个规则首页永远 403。</p>
<p><strong>www 301 重定向</strong>：</p>
<table>
	<thead>
			<tr>
					<th>项</th>
					<th>配置</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>DNS</td>
					<td>添加 CNAME 记录 <code>www.raysre.com</code> → <code>raysre.com.eo.dnse2.com</code></td>
			</tr>
			<tr>
					<td>EdgeOne 域名</td>
					<td>添加加速域名 <code>www.raysre.com</code>，源站和 <code>raysre.com</code> 相同</td>
			</tr>
			<tr>
					<td>EdgeOne 规则引擎</td>
					<td>匹配 HOST = <code>www.raysre.com</code> → 访问 URL 重定向 301 → <code>https://raysre.com${uri}</code></td>
			</tr>
	</tbody>
</table>
<p><code>${uri}</code> 是 EdgeOne 内置变量，保留原始路径。比如 <code>www.raysre.com/posts/xxx</code> → <code>raysre.com/posts/xxx</code>，不会全部丟到首页。</p>
<hr>
<h2 id="step-5dns-切换">Step 5：DNS 切换</h2>
<p>DNSPod 控制台里把 <code>raysre.com</code> 和 <code>www.raysre.com</code> 的 A 记录删掉（如果之前指向了服务器 IP），改成 CNAME：</p>
<table>
	<thead>
			<tr>
					<th>记录类型</th>
					<th>主机记录</th>
					<th>记录值</th>
					<th>TTL</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>CNAME</td>
					<td><code>@</code></td>
					<td><code>raysre.com.eo.dnse2.com</code></td>
					<td>600</td>
			</tr>
			<tr>
					<td>CNAME</td>
					<td><code>www</code></td>
					<td><code>raysre.com.eo.dnse2.com</code></td>
					<td>600</td>
			</tr>
	</tbody>
</table>
<p>TTL 设 600 秒（10 分钟），初次调试时改配置能更快生效。稳定运行后可以调回 3600。</p>
<hr>
<h2 id="成本估算">成本估算</h2>
<p>以我这个站的实际用量（文章 3 篇、访问量可以忽略）：</p>
<table>
	<thead>
			<tr>
					<th>计费项</th>
					<th>月费（估算）</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>COS 存储（标准存储，&lt;1GB）</td>
					<td>¥0.10</td>
			</tr>
			<tr>
					<td>COS 请求费（几千次/月）</td>
					<td>¥0.01</td>
			</tr>
			<tr>
					<td>CDN 流量（&lt;10GB/月）</td>
					<td>免费套餐覆盖</td>
			</tr>
			<tr>
					<td>CDN 请求费</td>
					<td>免费套餐覆盖</td>
			</tr>
			<tr>
					<td>DNS 解析</td>
					<td>免费</td>
			</tr>
			<tr>
					<td>总计</td>
					<td>≈ ¥0.11/月</td>
			</tr>
	</tbody>
</table>
<p>对个人博客来说几乎免费。唯一需要留意的：CDN 免费套餐每月 10GB 国内流量，如果哪天文章被大量访问超额，按量计费部分 ¥0.21/GB——设置好「用量封顶」防止意外。</p>
<hr>
<h2 id="进阶参考没用到的腾讯云功能">进阶参考：没用到的腾讯云功能</h2>
<p>下面这些是 EdgeOne 和 COS 提供的功能，个人博客场景用不到，但如果你要从静态博客拓展到更复杂的场景，可以参考。</p>
<h3 id="edgeone-四层代理l4-proxy">EdgeOne 四层代理（L4 Proxy）</h3>
<p>用 TCP/UDP 协议加速，适用场景是游戏加速、IoT 设备通信、实时音视频——不走 HTTP。静态博客只用七层（HTTP/HTTPS）。</p>
<h3 id="edgeone-边缘函数edge-functions">EdgeOne 边缘函数（Edge Functions）</h3>
<p>在 EdgeOne 边缘节点上跑 JavaScript 代码，类似 Cloudflare Workers。可以做的事：自定义鉴权、A/B 分流、响应头注入、内部重定向。比规则引擎灵活，但需要写代码。如果你要实现「特定 IP 段跳转到维护页」这种逻辑，用边缘函数比规则引擎方便。</p>
<h3 id="回源限频">回源限频</h3>
<p>限制 EdgeOne 向源站发起的请求频率。作用是高并发时保护源站不被击穿。文档里典型的参数是「同一 URL 每秒最多回源 1 次」。个人博客流量小，可以不开。</p>
<h3 id="edgeone-用量封顶">EdgeOne 用量封顶</h3>
<p>设置流量 / 带宽 / QPS 上限，达到后自动限速或返回提示页。<strong>建议设一下</strong>——免费套餐有额度限制，防止某个月被刷流量产生天价账单。</p>
<h3 id="cos-回源设置">COS 回源设置</h3>
<p>当请求的对象在 COS 桶里找不到时，自动去一个预设地址拉取内容。用于数据热迁移场景（如从旧服务器逐步搬到 COS）。不用于静态博客——所有文件由 GitHub Actions 上传，不会出现 404。</p>
<h3 id="cos-生命周期管理">COS 生命周期管理</h3>
<p>自动把对象从标准存储切换到低频 / 归档 / 深度归档，或到期后自动删除。如果你的桶存了大量日志需要定期清理，这是一个有用的功能。</p>
<h3 id="cos-智能分层存储">COS 智能分层存储</h3>
<p>自动在「高频」和「低频」两个访问层之间迁移对象，不需要手动配生命周期。访问模式不可预测时比生命周期省心。</p>
<h3 id="数据万象-ci图片音视频处理">数据万象 CI（图片/音视频处理）</h3>
<p>和 COS 绑定的数据处理服务，提供实时图片裁剪、水印、格式转换（比如 <code>?imageMogr2/thumbnail/200x</code>），以及视频转码、截帧等能力。如果你以后需要在文章里自动生成缩略图、加水印，数据万象是实现方案之一。</p>
<h3 id="cos-标签管理">COS 标签管理</h3>
<p>给对象打标签（如 <code>env=prod</code>, <code>category=blog</code>），方便分类、成本核算和批量操作。</p>
<hr>
<h2 id="faq">FAQ</h2>
<h3 id="q为什么-hugotoml-不用-yaml-格式">Q：为什么 hugo.toml 不用 yaml 格式？</h3>
<p><code>hugo new site --format yaml</code> 生成的就是 TOML（这个参数有点误导，实际上 Hugo 默认生成 TOML）。TOML 比 YAML 更不容易出现缩进错误，而且 Hugo 文档里的示例代码基本是 TOML，复制粘贴改起来方便。</p>
<h3 id="qdeployyml-在-github-actions-里报-modulenotfounderror-no-module-named-qcloud_cos-怎么处理">Q：deploy.yml 在 GitHub Actions 里报 <code>ModuleNotFoundError: No module named 'qcloud_cos'</code> 怎么处理？</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">run</span>: <span style="color:#ae81ff">pip install cos-python-sdk-v5</span>
</span></span></code></pre></div><p>加在 Deploy 步骤最前面。注意每次 step 的 shell 环境可能不同，<code>pip install</code> 放在同一个 <code>run:</code> 块里，不要跨 step。</p>
<h3 id="q如何验证-cos-静态网站端点正常工作">Q：如何验证 COS 静态网站端点正常工作？</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -sI http://raysre-1377355589.cos-website.ap-shanghai.myqcloud.com/index.html
</span></span></code></pre></div><p>预期返回 <code>200 OK</code> + <code>Content-Type: text/html</code>。如果不是 200，去 COS 控制台检查静态网站托管是否开启、默认首页是否设了 <code>index.html</code>。</p>
<h3 id="qhttps-证书怎么搞">Q：HTTPS 证书怎么搞？</h3>
<p>EdgeOne 免费提供 DV SSL 证书，接入域名后自动申请，不需要额外操作。如果 <code>www</code> 也需要扫码 HTTPS，申请时勾选多域名即可。</p>
<h3 id="qpapermod-怎么加备案号">Q：PaperMod 怎么加备案号？</h3>
<p>在 <code>layouts/partials/footer.html</code> 里覆盖默认模板。PaperMod 的默认 footer 位于 <code>themes/PaperMod/layouts/partials/footer.html</code>，把内容复制过来，底部追加备案信息。Hugo 发现项目 <code>layouts/</code> 下同名文件会优先用你的，不会动主题原文件。</p>
<h3 id="q文章更新后旧页面能看到吗">Q：文章更新后旧页面能看到吗？</h3>
<p>能看到。Hugo 构建时 CSS/JS 文件名含内容 hash，新版本文件名不同。旧文件还在 COS 上，EdgeOne 缓存里旧的 HTML 引用旧 JS——所以 deploy 后加了一步 CDN 刷新。</p>
<h3 id="qcos-默认域名能被直接访问吗">Q：COS 默认域名能被直接访问吗？</h3>
<p>取决于桶权限。如果你设了私有读写+EdgeOne 私有访问授权，直接打 COS 默认域名会 403。顺手加 Referer 白名单可以多做一层防护。</p>
<h3 id="q为啥不用-cloudflare-pages--vercel">Q：为啥不用 Cloudflare Pages / Vercel？</h3>
<p>Cloudflare Pages 免费 500 次/月构建，Vercel 免费 6000 分钟/月。两家托管在国内的访问稳定性时好时坏——不是产品不行，是物理距离在那儿。腾讯云 COS + EdgeOne 面向国内用户，延迟 &lt;50ms，在线率有 SLA。如果你的受众主要在海外，那 Cloudflare Pages 反倒更合适。</p>
<h3 id="q文章多了以后构建和上传会变慢吗">Q：文章多了以后构建和上传会变慢吗？</h3>
<p>Hugo 官方的<a href="https://gohugo.io/about/benefits/">基准测试数据</a>：5000 篇文章的站点，<code>hugo build</code> 在 1 秒内完成。瓶颈在上传：<code>os.walk()</code> 顺序串行，文件数到几千时可以考虑用 <code>ThreadPoolExecutor</code> 并行化上传。目前几十个文件串行够用。</p>
<hr>
<h2 id="写在最后">写在最后</h2>
<p>整个过程踩的最大坑是 EdgeOne 的「私有访问授权」和「公有读桶」的组合——文档里散落在不同页面，没有明确的「桶公有读时不要开私有访问授权」的警告。这篇记录把配置关系理成表格，方便以后查。</p>
<p>不用管服务器、不用打安全补丁、不用半夜被告警吵醒——COS 存文件，EdgeOne 扛流量，GitHub Actions 管发布。代价是没法跑动态内容（评论、登录、后台）。如果你需要这些，WordPress + 云服务器或者 Hugo + Serverless 评论更适合。</p>
<p>如果你也在搭类似的个人博客，上面的 deploy.yml 可以直接拿走用，改个 bucket 名字就行。</p>
]]></content:encoded></item><item><title>Jira Data Center NFS 存储迁移实战：7TB 数据从 NVMe 到机械盘的无损切割</title><link>https://raysre.com/posts/jira-nfs-storage-migration/</link><pubDate>Sat, 11 Jul 2026 00:00:00 +0000</pubDate><guid>https://raysre.com/posts/jira-nfs-storage-migration/</guid><description>Atlassian Jira Data Center v10.3.3 集群环境下，NFS 共享存储从企业级 NVMe SSD 到 45TB 机械硬盘的完整迁移记录，含 rsync 参数逐项解读、迁移耗时实测数据、停服回滚方案、目标盘 ext4 分区策略，以及迁移完成后的权限故障排查与解决。附带多节点集群架构拓扑、日常运维速查。</description><content:encoded><![CDATA[<h2 id="背景">背景</h2>
<p>公司 Jira Data Center v10.3.3 集群的共享存储（NFS）跑在一块 7.8TB 的企业级 NVMe SSD 上，挂载路径 <code>/data</code>，实际数据量 7.0TB——全是 Jira 附件，不含数据库。</p>
<p>要腾出这块 SSD 给别的业务，替换盘是一块 45.5TB 机械硬盘（<code>/dev/sda</code>，未初始化）。要求在<strong>下一个工作日 9:00 前</strong>完成迁移，尽可能减少停服时间。</p>
<h2 id="集群拓扑">集群拓扑</h2>
<pre tabindex="0"><code>10.177.100.25  Nginx (反向代理/负载均衡)
       │
       ├──→ 10.177.100.26  Jira Node 1
       └──→ 10.177.100.27  Jira Node 2
                │                      │
                ├──→ 10.177.100.28  MySQL 主
                ├──→ 10.177.100.29  MySQL 从
                │
                └──→ 10.177.100.30  NFS Server  ← 本次迁移对象
</code></pre><table>
	<thead>
			<tr>
					<th>节点</th>
					<th>角色</th>
					<th>操作系统</th>
					<th>说明</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>100.25</td>
					<td>Nginx</td>
					<td>Ubuntu 24.04</td>
					<td>nginx/1.31.2，反向代理 + 负载均衡</td>
			</tr>
			<tr>
					<td>100.26</td>
					<td>Jira Node 1</td>
					<td>Ubuntu 24.04</td>
					<td>应用服务器，su - jira 操作</td>
			</tr>
			<tr>
					<td>100.27</td>
					<td>Jira Node 2</td>
					<td>Ubuntu 24.04</td>
					<td>应用服务器，与 Node 1 组成集群</td>
			</tr>
			<tr>
					<td>100.28</td>
					<td>MySQL 主库</td>
					<td>Ubuntu 24.04</td>
					<td>MySQL 8.0.33，socket 连接</td>
			</tr>
			<tr>
					<td>100.29</td>
					<td>MySQL 从库</td>
					<td>Ubuntu 24.04</td>
					<td>MySQL 8.0.33，主从同步</td>
			</tr>
			<tr>
					<td><strong>100.30</strong></td>
					<td><strong>NFS Server</strong></td>
					<td><strong>Ubuntu 24.04</strong></td>
					<td><strong>Jira 共享存储，全部附件</strong></td>
			</tr>
	</tbody>
</table>
<p>所有 IP 为企业内网地址，仅运维团队可达。</p>
<h2 id="存储现状">存储现状</h2>
<p>迁移前的磁盘布局：</p>
<pre tabindex="0"><code>vdb  7.8T  NVMe SSD  ── vdb1  ext4  /dev/vdb1  →  /data   (7.0T 已用)
sda  45.5T HDD       ── 未分区，未格式化
</code></pre><p>迁移后的目标布局：</p>
<pre tabindex="0"><code>vdb  7.8T  NVMe SSD  ── 腾出
sda  45.5T HDD       ── sda1  ext4  /dev/sda1  →  /data   (7.0T 已用)
</code></pre><p>机械盘容量是 NVMe 的近 6 倍，写入速度约 110~140 MB/s——这是全流程的硬瓶颈。</p>
<h2 id="方案选型">方案选型</h2>
<table>
	<thead>
			<tr>
					<th>方案</th>
					<th>做法</th>
					<th style="text-align: center">迁移总耗时</th>
					<th style="text-align: center">停服窗口</th>
					<th style="text-align: center">文件数影响</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>rsync 两遍</td>
					<td>第一遍不停服、第二遍停服增量</td>
					<td style="text-align: center">两遍扫描，文件多则 20h+</td>
					<td style="text-align: center">第二遍扫描时间</td>
					<td style="text-align: center"><strong>大</strong> — 百万文件扫描几小时</td>
			</tr>
			<tr>
					<td>rsync 一遍</td>
					<td>停服后直接拷</td>
					<td style="text-align: center">一次性扫完写完</td>
					<td style="text-align: center">全程</td>
					<td style="text-align: center"><strong>大</strong> — 同上</td>
			</tr>
			<tr>
					<td><strong>rsync 一遍（本方案）</strong></td>
					<td><strong>停服后一次性拷，不做增量</strong></td>
					<td style="text-align: center"><strong>10-14h</strong></td>
					<td style="text-align: center"><strong>全程</strong></td>
					<td style="text-align: center"><strong>小</strong> — 10~14h 内文件数不构成瓶颈</td>
			</tr>
	</tbody>
</table>
<p>三选一，选了<strong>停服 rsync 一遍</strong>。</p>
<p>理由是 <code>/data</code> 目录下只有 Jira 附件、索引缓存等静态文件，不含写入中的数据库。停服后数据完全冻结，不存在 rsync 过程中文件被修改导致不一致的问题。7.0TB 数据，机械盘顺序写入 110+ MB/s，数学上 10~14 小时能跑完，停服窗口 24 小时绰绰有余。</p>
<p><strong>没有用 <code>dd</code></strong>。dd 会复制整块 7.8TB 盘（含空闲空间），额外消耗约 1 小时且浪费目标盘空间。rsync 只传实际文件，且参数 <code>-aHAX</code> 能完整保留软链接、硬链接、ACL 和扩展属性。</p>
<h2 id="完整操作记录">完整操作记录</h2>
<p>所有命令在 NFS Server（10.177.100.30，root 用户）上执行。</p>
<h3 id="第一步开-tmux-防断连">第一步：开 tmux 防断连</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>tmux new -s jira-migration
</span></span></code></pre></div><p>SSH 连接意外断开时，<code>tmux attach -t jira-migration</code> 恢复会话。</p>
<h3 id="第二步停服">第二步：停服</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 停 Jira 所有节点</span>
</span></span><span style="display:flex;"><span>systemctl stop jira
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 确认 /data 无进程占用</span>
</span></span><span style="display:flex;"><span>lsof +D /data 2&gt;/dev/null
</span></span></code></pre></div><h3 id="第三步sda-分区与格式化">第三步：sda 分区与格式化</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># GPT 分区表 + 单分区</span>
</span></span><span style="display:flex;"><span>parted /dev/sda --script mklabel gpt
</span></span><span style="display:flex;"><span>parted /dev/sda --script mkpart primary ext4 0% 100%
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ext4 格式化，-m 0 不给 root 预留空间</span>
</span></span><span style="display:flex;"><span>mkfs.ext4 -m <span style="color:#ae81ff">0</span> -L jira-data /dev/sda1
</span></span></code></pre></div><p><code>-m 0</code> 是 45TB 大容量盘的关键参数。默认 <code>-m 5</code> 会预留 5% 给 root，45.5TB × 5% = <strong>2.3TB 浪费</strong>。NFS 共享盘不需要这层保护，<code>-m 0</code> 全部给数据。</p>
<h3 id="第四步执行迁移">第四步：执行迁移</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir -p /mnt/sda1
</span></span><span style="display:flex;"><span>mount /dev/sda1 /mnt/sda1
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 核心传输命令</span>
</span></span><span style="display:flex;"><span>rsync -aHAX --info<span style="color:#f92672">=</span>progress2 --numeric-ids /data/ /mnt/sda1/
</span></span></code></pre></div><p>参数逐项说明：</p>
<table>
	<thead>
			<tr>
					<th>选项</th>
					<th>作用</th>
					<th>为什么重要</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>-a</code></td>
					<td>archive 模式，等同 <code>-rlptgoD</code></td>
					<td>保留权限、时间戳、软链接、属主/属组、设备文件</td>
			</tr>
			<tr>
					<td><code>-H</code></td>
					<td>保留硬链接</td>
					<td>Jira 附件目录可能有硬链接，不保留会导致空间膨胀</td>
			</tr>
			<tr>
					<td><code>-A</code></td>
					<td>保留 ACL</td>
					<td>NFS 客户端可能依赖 ACL 做访问控制</td>
			</tr>
			<tr>
					<td><code>-X</code></td>
					<td>保留扩展属性</td>
					<td>安全上下文（SELinux/AppArmor）和 NFS xattr</td>
			</tr>
			<tr>
					<td><code>--info=progress2</code></td>
					<td>全局进度条</td>
					<td>7TB 传输不显示整体进度没法预估完成时间</td>
			</tr>
			<tr>
					<td><code>--numeric-ids</code></td>
					<td>数字 UID/GID</td>
					<td>不解析用户名，避免目标机器不存在同名用户导致 ID 映射错误</td>
			</tr>
			<tr>
					<td><code>/data/</code> 末尾斜杠</td>
					<td>复制目录<strong>内容</strong></td>
					<td>不加斜杠会在目标盘创建 <code>/mnt/sda1/data/</code> 额外层级</td>
			</tr>
	</tbody>
</table>
<h3 id="第五步实际耗时记录">第五步：实际耗时记录</h3>
<table>
	<thead>
			<tr>
					<th>时间节点</th>
					<th>状态</th>
					<th style="text-align: center">已传输</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>周六 23:48</td>
					<td>启动 rsync</td>
					<td style="text-align: center">0</td>
			</tr>
			<tr>
					<td>周日 11:03</td>
					<td>运行 11h15m</td>
					<td style="text-align: center">4.4 TB</td>
			</tr>
			<tr>
					<td>周日 16:20</td>
					<td>运行 16h32m</td>
					<td style="text-align: center">6.3 TB</td>
			</tr>
			<tr>
					<td>周日 18:10</td>
					<td><strong>完成</strong></td>
					<td style="text-align: center"><strong>7.0 TB</strong></td>
			</tr>
	</tbody>
</table>
<p>实际耗时约 <strong>18 小时 22 分钟</strong>，平均速度 111 MB/s。比预估的 10-14 小时慢，原因是 Jira 附件目录中夹杂数十万个小文件（图标、缩略图、日志片段），随机寻道拉低了机械盘的实际写入吞吐。</p>
<h3 id="第六步校验">第六步：校验</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 数据量对比</span>
</span></span><span style="display:flex;"><span>du -sh /data /mnt/sda1
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 两边均为 7.0T</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 文件数对比</span>
</span></span><span style="display:flex;"><span>find /data -type f | wc -l
</span></span><span style="display:flex;"><span>find /mnt/sda1 -type f | wc -l
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 两边一致</span>
</span></span></code></pre></div><h3 id="第七步切换挂载">第七步：切换挂载</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 卸载旧盘</span>
</span></span><span style="display:flex;"><span>umount /data
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 挂载新盘</span>
</span></span><span style="display:flex;"><span>mount /dev/sda1 /data
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 更新 fstab</span>
</span></span><span style="display:flex;"><span>cp /etc/fstab /etc/fstab.bak.<span style="color:#66d9ef">$(</span>date +%Y%m%d<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>blkid /dev/sda1
</span></span><span style="display:flex;"><span><span style="color:#75715e"># UUID=a1b2c3...  记录这个值</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>vi /etc/fstab
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 删除 vdb 那行，添加：</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># UUID=a1b2c3...  /data  ext4  defaults,nofail  0  2</span>
</span></span></code></pre></div><h3 id="第八步启动与排错">第八步：启动与排错</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 重启 NFS 服务（挂载点切换后 NFS 需要刷新文件句柄）</span>
</span></span><span style="display:flex;"><span>exportfs -ra <span style="color:#f92672">&amp;&amp;</span> systemctl restart nfs-server
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 启动 Jira</span>
</span></span><span style="display:flex;"><span>systemctl start jira
</span></span></code></pre></div><p>Jira 启动后报错：</p>
<pre tabindex="0"><code>JIRA couldn&#39;t create the jira.home directory
Ensure JIRA has permission to create and write to the
jira.home directory /data/jirasoftware/jira-home/shared_home.
</code></pre><p>原因：NFS 内核模块缓存了旧 vdb1 的文件句柄，Jira Node 在挂载点切换后看到的仍是旧设备。<code>exportfs -ra</code> 刷新了 NFS 导出表，但 Jira Node 的 NFS 客户端缓存未清理。</p>
<p><strong>解决</strong>：重启 Jira Node 1 和 Node 2，强制刷新 NFS 客户端缓存。</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 在 10.177.100.26 和 10.177.100.27 上执行</span>
</span></span><span style="display:flex;"><span>reboot
</span></span></code></pre></div><p>重启后 Jira 正常启动，附件可访问，集群状态恢复。</p>
<h2 id="日常运维速查">日常运维速查</h2>
<p>以下命令在日常运维中高频出现，按场景归类备查。</p>
<h3 id="jira-启停">Jira 启停</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 启动</span>
</span></span><span style="display:flex;"><span>systemctl start jira
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 停止</span>
</span></span><span style="display:flex;"><span>systemctl stop jira
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 或者用 Jira 自带脚本</span>
</span></span><span style="display:flex;"><span>/data/jirasoftware/jira/bin/start-jira.sh
</span></span><span style="display:flex;"><span>/data/jirasoftware/jira/bin/stop-jira.sh
</span></span></code></pre></div><h3 id="mysql-启停">MySQL 启停</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl start mysql
</span></span><span style="display:flex;"><span>systemctl stop mysql
</span></span><span style="display:flex;"><span>systemctl restart mysql
</span></span></code></pre></div><h3 id="nfs-服务">NFS 服务</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl start nfs-server
</span></span><span style="display:flex;"><span>systemctl stop nfs-server
</span></span><span style="display:flex;"><span>systemctl restart nfs-server
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 刷新导出表（修改 /etc/exports 后必须执行）</span>
</span></span><span style="display:flex;"><span>exportfs -ra
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 查看客户端挂载情况</span>
</span></span><span style="display:flex;"><span>showmount -e 10.177.100.30
</span></span></code></pre></div><h3 id="磁盘检查">磁盘检查</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 查看挂载点与磁盘对应关系</span>
</span></span><span style="display:flex;"><span>lsblk
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 查看 /data 使用量</span>
</span></span><span style="display:flex;"><span>df -h /data
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 查看 /data 内容概览</span>
</span></span><span style="display:flex;"><span>du -sh /data
</span></span></code></pre></div><h2 id="关键配置文件位置">关键配置文件位置</h2>
<table>
	<thead>
			<tr>
					<th>文件</th>
					<th>路径</th>
					<th>用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Jira 集群配置</td>
					<td><code>/data/jirasoftware/jira-home/cluster.properties</code></td>
					<td>定义集群节点、共享目录路径</td>
			</tr>
			<tr>
					<td>Jira 服务配置</td>
					<td><code>/data/jirasoftware/jira/conf/server.xml</code></td>
					<td>端口、上下文路径、连接池</td>
			</tr>
			<tr>
					<td>Jira JVM 参数</td>
					<td><code>/data/jirasoftware/jira/bin/setenv.sh</code></td>
					<td>堆内存、GC 策略、JVM 选项</td>
			</tr>
			<tr>
					<td>MySQL 配置</td>
					<td><code>/data/mysql/conf/my.cnf</code></td>
					<td>数据库引擎、缓冲池、二进制日志</td>
			</tr>
			<tr>
					<td>NFS 导出</td>
					<td><code>/etc/exports</code></td>
					<td>允许哪些 IP 挂载 /data</td>
			</tr>
			<tr>
					<td>自动挂载</td>
					<td><code>/etc/fstab</code></td>
					<td>系统启动时挂载磁盘</td>
			</tr>
	</tbody>
</table>
<h2 id="注意事项">注意事项</h2>
<ol>
<li>
<p><strong>停服前做最终状态确认</strong>。生产环境停服不是小事。除了 <code>systemctl stop jira</code>，还要 <code>lsof +D /data</code> 确认无一进程占用，NFS 客户端缓存可能导致静默写入。</p>
</li>
<li>
<p><strong>rsync 源路径末尾斜杠是坑</strong>。<code>/data/</code> 和 <code>/data</code> 的区别是一层目录结构。用错了会让目标盘里塞一个多余的 <code>/data/</code> 子目录，Jira 的 <code>jira.home</code> 配置指向的路径就找不到了。</p>
</li>
<li>
<p><strong><code>-m 0</code> 不是默认参数</strong>。<code>mkfs.ext4</code> 不给 <code>-m</code> 参数时默认预留 5% 给 root。45TB 上的 5% 是 2.3TB——大到摸得着。NFS 数据盘生产用途明确，不需要反碎片化保护，全部空间给业务数据。</p>
</li>
<li>
<p><strong>NFS 切换后必须刷新两端缓存</strong>。服务端 <code>exportfs -ra</code> 刷新导出表，客户端重启或重新挂载清除内核 NFS 句柄缓存。只做一端的效果是 Jira 无法写入 <code>shared_home</code> 目录。</p>
</li>
<li>
<p><strong>Jira Data Center 的 shared_home 路径不能变</strong>。<code>cluster.properties</code> 里硬编码了 <code>/data/jirasoftware/jira-home/shared_home</code>。迁移时目标盘挂载点必须完全一致（<code>/data</code>），否则需要停机修改集群配置并重启所有节点。</p>
</li>
<li>
<p><strong>回滚路径要保持到验证结束</strong>。迁移完成后 vdb 上的数据不要立刻清除。等 Jira 跑 2~3 个工作日，确认附件上传、全文搜索、集群同步都没问题，再考虑回收旧盘。遇到问题只需 <code>umount /data &amp;&amp; mount /dev/vdb1 /data &amp;&amp; systemctl start jira</code> 即可回退。</p>
</li>
</ol>
<h2 id="小结">小结</h2>
<p>从一块 NVMe SSD 到一块机械盘，迁移 7TB Jira 附件数据，核心就一条 <code>rsync -aHAX --info=progress2 --numeric-ids /data/ /mnt/sda1/</code>。参数选对了，数据属性一件不少；停服窗口估准了，团队沟通在先，操作过程没有意外。</p>
<p>机械盘写入带宽是硬天花板——111 MB/s 对 7TB 来说跑 18 小时无可避免。真正把时间从&quot;超过 24 小时&quot;压缩到 18 小时的是文件系统参数：<code>-m 0</code> 省了 2.3TB 无用写入，GPT 单分区省了 MBR 扩展分区链的开销。这些微调单独看都不起眼，叠在一起就是停服窗口能不能按时交付的差距。</p>
]]></content:encoded></item></channel></rss>