<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://muxui.com/</id>
  <title type="text">Muxui</title>
  <subtitle type="text">记录开发与折腾的个人技术博客</subtitle>
  <updated>2026-09-23T00:00:00.000Z</updated>
  <author><name>Muxui</name></author>
  <link rel="alternate" href="https://muxui.com/"/>
  <link rel="self" href="https://muxui.com/atom.xml"/>
  <generator uri="https://github.com/CuteLeaf/Firefly">Firefly v6.16.8</generator>
    <entry>
      <id>https://muxui.com/posts/pichost-cloudflare-r2-image-host/</id>
      <title type="text">[开源] PicHost - 自托管个人轻量图床</title>
      <published>2026-08-03T00:00:00.000Z</published>
      <updated>2026-09-23T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/pichost-cloudflare-r2-image-host/"/>
      <summary type="text">自用开源图床 PicHost v1.4.2：Docker 一条命令、标签与备份、上传策略与保留原图、API Token 与 tagNames、Twikoo 配图——推广文，不是说明书。</summary>
      <content type="html"><![CDATA[<p>博客写久了，配图是刚需。市面上图床功能越堆越满——相册、社交、付费套餐——对我这种<strong>只给自己和评论区用</strong>的场景来说反而太重。我更在意几件事：<strong>数据在自己手里</strong>、<strong>不绑某家图床的政策和续费</strong>、<strong>自己不删就尽量一直在</strong>。</p>
<p>PicHost 就是为此做的：<strong>个人轻量图床，图片放好，管理也要顺手</strong>。从最早蹭 Cloudflare R2 的实验版，到推倒重来的 Docker 自托管，再到现在的 <strong>v1.4.2</strong>——标签、备份、多后端、Twikoo 兼容、上传策略与 API 打标都齐了，一条 <code>docker run</code> 就能跑。部署细节、环境变量、API 字段见用户文档（文末链接）；这篇只聊<strong>它是什么、为什么值得试、现在长什么样</strong>。</p>
<section><h2>它解决什么问题<a href="#它解决什么问题"><span>#</span></a></h2>

<table><thead><tr><th>阶段</th><th>当时的痛点</th><th>PicHost 的回应</th></tr></thead><tbody><tr><td><strong>v0（CF 版，已归档）</strong></td><td>博客配图，想蹭 CF 免费额度</td><td>R2 + Worker 双域出图，Referer 防盗链</td></tr><tr><td><strong>接上 Twikoo</strong></td><td>评论发图不想绑收费官方图床</td><td>EasyImage 2.0 兼容 + 独立上传 Token</td></tr><tr><td><strong>v1.0</strong></td><td>CF Dashboard 运维成本高、配置易被 deploy 覆盖</td><td>Docker 零配置引导、本地盘 + SQLite、多用户</td></tr><tr><td><strong>v1.1–v1.2</strong></td><td>想接回对象存储，目录模型要收拢</td><td>S3 多后端、双域名分离、统一 <code>images/</code></td></tr><tr><td><strong>v1.3–v1.4</strong></td><td>图多了要分类、数据要能整包带走</td><td>标签打标、<code>.phost.tar.gz</code> 备份恢复、跨后端同步</td></tr><tr><td><strong>v1.4.1</strong></td><td>上传要可管、博客要原图</td><td>上传策略（大小/格式/限流）、<strong>保留原图</strong>、API <code>tagNames</code></td></tr><tr><td><strong>v1.4.2（当前）</strong></td><td>API/标签要更顺手、脚本鉴权要清晰</td><td>每账号单一 API Token；上传 API 统一 <code>tagNames</code>；标签管理排序与布局；批量打标与 <code>Auth-Token</code> 鉴权修复</td></tr></tbody></table><p>CF 版教会我防盗链和双 Token；Docker 版让我把图床和数据一起挂在 NAS 或 VPS 上，<strong>不必再跟 Cloudflare Dashboard 较劲</strong>。<code>cloudflare</code> 分支仍保留 R2 专版，在线演示见 <a href="https://pic.roven.cc" target="_blank">pic.roven.cc</a>。</p><hr /></section>
<section><h2>现在长什么样<a href="#现在长什么样"><span>#</span></a></h2><p>后台基于 <strong>Nuxt 4 + Nuxt UI 4</strong>，默认暗黑、中英文切换。登录支持本地滑块、<strong>Cloudflare Turnstile</strong> 或 <strong>Cap</strong> 人机验证——管理员在设置里一键切换。上传区支持拖拽、点击、Ctrl+V 粘贴；默认入库 <strong>sharp → WebP</strong>，<strong>v1.4.1</strong> 起可在设置里打开<strong>保留原图</strong>关闭服务端转换。</p><section><h3>上传<a href="#上传"><span>#</span></a></h3><p>上传完一键复制直链 / Markdown / HTML；卡片可调客户端预压缩、自动复制链接、按用户自动删除；上传后可直接<strong>批量打标签</strong>。</p><p><figure><img src="https://s.muxui.com:8015/images/2026/09/A37g7XclOd9d.webp" alt="PicHost 上传页" /><figcaption>PicHost 上传页</figcaption></figure></p></section><section><h3>图库<a href="#图库"><span>#</span></a></h3><p><code>/gallery</code> 替代了旧版统计页：网格 / 列表切换，按存储、来源、<strong>标签</strong>筛选（OR / AND），批量删除与批量打标；点图弹出详情层，尺寸、上传方式、多格式链接一目了然。</p><p><figure><img src="https://s.muxui.com:8015/images/2026/09/BUkX1BJafO0f.webp" alt="PicHost 图库" /><figcaption>PicHost 图库</figcaption></figure></p><p><figure><img src="https://s.muxui.com:8015/images/2026/09/1HagoV0RVQKW.webp" alt="PicHost 图库详情" /><figcaption>PicHost 图库详情</figcaption></figure></p></section><section><h3>标签<a href="#标签"><span>#</span></a></h3><p>v1.4 的重头戏。每用户独立标签、彩色标记；上传后单张或批量打标，设置页里新建、改色、合并、删除。<strong>v1.4.2</strong> 起标签列表可按图片数 / 最近使用排序，桌面端列布局、窄屏卡片列表更易扫。图库堆了几百张截图之后，<strong>按标签筛</strong>比按文件名搜省心太多。</p><p><figure><img src="https://s.muxui.com:8015/images/2026/09/6YUQRhvF4Fe9.webp" alt="PicHost 批量打标签" /><figcaption>PicHost 批量打标签</figcaption></figure></p></section><section><h3>存储与备份<a href="#存储与备份"><span>#</span></a></h3><p>本地磁盘和 S3 兼容后端（R2 / COS / OSS / AWS）可混用；存储页导出整站 <code>.phost.tar.gz</code>、从备份恢复、跨后端同步，任务中心看进度。换机器或升大版本，<strong>整包带走</strong>比对着旧目录手工 <code>migrate</code> 踏实——v1.3 起遗留迁移 CLI 已移除，备份恢复是官方推荐路径。</p><p><figure><img src="https://s.muxui.com:8015/images/2026/09/VdcVRxHWflY1.webp" alt="PicHost 存储管理" /><figcaption>PicHost 存储管理</figcaption></figure></p></section><section><h3>设置、API 与日志<a href="#设置api-与日志"><span>#</span></a></h3><p><strong>v1.4.1</strong> 把全站上传策略收进「基础设置」：单文件大小、允许格式、上传/登录限流、<strong>保留原图</strong>（也可用 <code>.env</code> 覆盖）。Referer 白名单、双域名分离仍在设置里；<strong>v1.4.2</strong> 起每账号<strong>单一 API Token</strong>（界面与文档一致），API 页带在线调试，上传接口以 <strong><code>tagNames</code></strong> 按名创建或复用标签（Twikoo 兼容路由同步）；脚本上传时 <strong><code>Auth-Token</code> / 表单 <code>token</code> 优先于会话</strong>，并兼容全局 <code>API_UPLOAD_TOKEN</code>。</p><p><figure><img src="https://s.muxui.com:8015/images/2026/09/a7iZlFBuhJA1.webp" alt="PicHost 系统设置" /><figcaption>PicHost 系统设置</figcaption></figure></p><p><figure><img src="https://s.muxui.com:8015/images/2026/09/9lfLphlnNlhG.webp" alt="PicHost API 页" /><figcaption>PicHost API 页</figcaption></figure></p><p><figure><img src="https://s.muxui.com:8015/images/2026/09/CAVualHW5Pen.webp" alt="PicHost 操作日志" /><figcaption>PicHost 操作日志</figcaption></figure></p><p>首次部署走 Web 引导创建管理员，也支持从备份包直接恢复——不用预先写密钥，容器起来就能用。</p><hr /></section></section>
<section><h2>几个我自己用得上的点<a href="#几个我自己用得上的点"><span>#</span></a></h2><ul>
<li><strong>多用户</strong>：管理员可开放注册，普通用户只看到自己的图；每人一个 API Token，脚本上传归属清晰。</li>
<li><strong>Twikoo 评论配图</strong>：<code>POST /api/index.php</code> 兼容 EasyImage 2.0，评论发图和正文配图走同一套图床；Referer 白名单记得把博客域名（含 <code>www</code>）都加上。</li>
<li><strong>双域名分离</strong>：管理域和图片域拆开，中间件自动隔离；配错了有 <code>clear-domains</code> CLI 兜底。</li>
<li><strong>服务端 WebP + 防盗链</strong>：默认入库压缩 + 可选 Referer 白名单；要原图可在 <strong>v1.4.1</strong> 打开保留原图。</li>
<li><strong>GPL-3.0 开源</strong>：欢迎 Star、Issue 和 PR（仓库见文末链接）。</li>
</ul><hr /></section>
<section><h2>想试的话<a href="#想试的话"><span>#</span></a></h2><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>run</span><span> </span><span>-d</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>--name</span><span> </span><span>pichost</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-p</span><span> </span><span>6892:6892</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-v</span><span> </span><span>./data:/data</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>--restart</span><span> </span><span>unless-stopped</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>muxui/pichost:latest</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>浏览器打开 <code>http://&lt;主机IP&gt;:6892</code>，按引导完成设置即可。反代、HTTPS、双域名、Twikoo 对接、cURL 示例见用户文档，不在这篇重复抄说明书。</p><hr /></section>
<section><h2>版本轨迹<a href="#版本轨迹"><span>#</span></a></h2><div><ol><li><div><span></span></div><div><div>2026-09-23</div><div>v1.4.2</div><div><p>每账号单一 API Token；上传 API 仅 <code>tagNames</code>（Twikoo 路由同步）。标签管理按使用量排序、桌面列 / 窄屏卡片布局。修复图库批量打标（管理员/跨归属）与上传鉴权（<code>Auth-Token</code>、表单 <code>token</code>、<code>API_UPLOAD_TOKEN</code>）。</p></div></div></li><li><div></div><div><div>2026-09-16</div><div>v1.4.1</div><div><p>上传策略（设置 → 基础）：单文件大小、允许格式、上传/登录限流、<strong>保留原图</strong>；上传 API 支持 <code>tagNames</code>（与 <code>tagIds</code> 可并用）。</p></div></div></li><li><div><span></span></div><div><div>2026-09-09</div><div>v1.4.0</div><div><p>标签系统：每用户独立标签、彩色标记；上传后单张/批量打标；图库 OR/AND 筛选；操作日志扩展至登录、设置变更、标签操作；API 支持标签 CRUD 与 <code>tagIds</code>。</p></div></div></li><li><div></div><div><div>2026-09-07</div><div>v1.3.0</div><div><p>整站导出/恢复 <code>.phost.tar.gz</code>；跨后端同步与任务中心；setup 未初始化时可从备份恢复。移除遗留 <code>migrate</code> CLI，由备份恢复与启动索引同步替代。</p></div></div></li><li><div></div><div><div>2026-09-01</div><div>v1.2.6–v1.2.8</div><div><p>设置/图库/存储/API 页 UI 重构；登录验证支持 Turnstile / Cap；双域名保存风险确认与 <code>clear-domains</code> CLI。</p></div></div></li><li><div></div><div><div>2026-08-31</div><div>v1.2.5</div><div><p>图库页（<code>/gallery</code>）替代原统计导航；图片详情弹层、全选/反选；移除 <code>/stats</code> 路由。</p></div></div></li><li><div></div><div><div>2026-08-30</div><div>v1.2.2</div><div><p>双域名设置保存误清空管理域、未知 Host 绕过隔离等修复；文档站新增更新日志。</p></div></div></li><li><div></div><div><div>2026-08-29</div><div>v1.2.1</div><div><p>登录滑动拼图验证；VitePress 文档站发布。</p></div></div></li><li><div></div><div><div>2026-08-28</div><div>v1.2.0</div><div><p>本地图片统一进 <code>data/images/</code>；启动时自动扫描补索引。</p></div></div></li><li><div><span></span></div><div><div>2026-08-26</div><div>v1.1.0</div><div><p>S3 多后端、<code>/storage</code> 存储管理页；操作日志与上传限流。</p></div></div></li><li><div><span></span></div><div><div>2026-08-23</div><div>v1.0</div><div><p>Docker 自托管首发：多用户、Web 引导、Twikoo 兼容、服务端 WebP。CF R2 版归档至 <code>cloudflare</code> 分支。</p></div></div></li><li><div></div><div><div>2025 前后</div><div>v0（CF 版，已归档）</div><div><p>Nuxt + R2 + Worker；运维成本高，后推倒为 Docker 版。</p></div></div></li></ol><div></div></div><span>展开</span><span>收起</span><hr /></section>
<section><h2>写在最后<a href="#写在最后"><span>#</span></a></h2><p>PicHost 的演进很直白：<strong>先为博客配图</strong>，<strong>再为 Twikoo 评论配图</strong>，<strong>把运维成本打下来</strong>，<strong>把目录和备份模型理顺</strong>，<strong>用标签让图库真正能管</strong>，<strong>v1.4.1 补齐上传策略与原图</strong>，<strong>v1.4.2 把 API Token 与 <code>tagNames</code> 打标链路收顺</strong>。</p><p>若你也在 Firefly + Twikoo 这条路上，或者只是想找一套能 <code>docker run</code>、数据能整包带走的自托管图床，欢迎 Star、Issue 或评论区聊。</p></section>
<section><h2>相关链接<a href="#相关链接"><span>#</span></a></h2><a href="https://github.com/O96u/PicHost" target="_blank"><div><div><div><div></div><div>O96u</div></div><div>/</div><div>PicHost</div></div><div></div></div><div>Lightweight self-hosted image hosting — Docker, multi-user, gallery &amp; API, local disk or S3-compatible storage.</div><div><div>—</div><div>—</div><div>GPL-3.0</div><span>TypeScript</span></div></a><a href="https://hub.docker.com/r/muxui/pichost" target="_blank"><div><div><div>muxui</div><div>/</div><div>pichost</div></div><div></div></div><div>轻量自托管图床，WebP压缩，防盗链，适合NAS与个人博客</div><div><div>5.5K</div><div>0</div></div></a><ul>
<li><a href="https://o96u.github.io/PicHost/" target="_blank">用户文档</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/fonu-fnos-ddns-https-reverse-proxy/</id>
      <title type="text">[开源] Fonu - 专为 fnOS 准备的轻量 DDNS + HTTPS 证书 + 反向代理工具</title>
      <published>2026-09-15T00:00:00.000Z</published>
      <updated>2026-09-23T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/fonu-fnos-ddns-https-reverse-proxy/"/>
      <summary type="text">自用开源 Fonu：面向飞牛 fnOS 的轻量公网访问工具，DDNS、HTTPS 证书、独立 Nginx 反代、FRP 内网穿透一站搞定——推广文，不是说明书。</summary>
      <content type="html"><![CDATA[<p>最近一直在折腾飞牛 fnOS 的公网访问。平时真正用得比较多的，其实就几件事：<strong>DDNS</strong>、<strong>HTTPS 证书</strong>、<strong>反向代理</strong>；没有公网 IP 时还得加一层 <strong>内网穿透</strong>。</p>
<p>社区里已有 Lucky 等成熟方案，功能非常强。但对我自己来说，长期打开的也就这几项，很多高级能力基本用不到，整体略重。所以干脆写了一个更轻量的：<strong>Fonu</strong>——面向 fnOS 公网访问，把「域名 → DDNS → 证书 → 反代 → 内网服务」这条链路尽量做简单；没有公网 IP 时还能接上 <strong>FRP</strong> 把本地 Nginx 网关穿透出去。环境变量、端口说明、Compose 示例见仓库 README（文末链接）；这篇是推广，不是说明书。</p>
<section><h2>Fonu 是做什么的<a href="#fonu-是做什么的"><span>#</span></a></h2><p>Fonu 不是「大而全」的网络工具，而是把 NAS 公网访问最常用的几块收进一个 Web 后台：</p><ul>
<li><strong>DDNS</strong>：IPv4 / IPv6，多域名，Cloudflare、DNSPod、阿里云、腾讯云 DNS</li>
<li><strong>HTTPS 证书</strong>：ACME 申请与自动续签（Let’s Encrypt、ZeroSSL），多域名与通配符</li>
<li><strong>反向代理</strong>：HTTP / HTTPS、WebSocket，流量统计、当前连接数、规则详情与访问日志</li>
<li><strong>内网穿透</strong>：内置 frpc，Nginx 网关模式，把本地反代同步到 VPS 上的 frps</li>
<li><strong>访问保护</strong>：IP 黑白名单、大陆 IP 限制、Basic Auth、限流、自定义错误页</li>
</ul><p>有公网 IP 时：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>域名 → DDNS → HTTPS 证书 → 反向代理 → fnOS / MoviePilot / Jellyfin / 青龙 / …</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>没有公网 IP 时，可在 Fonu 里接上 FRP：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>外部用户 → frps（VPS）→ frpc → Fonu Nginx → 内网服务</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>和 <a href="/posts/pichost-cloudflare-r2-image-host/">PicHost</a> 一样，都是我自己在 NAS 上长期跑着、顺手开源出来的小工具；Fonu 管公网入口，PicHost 管博客配图。</p><hr /></section>
<section><h2>为什么又写一个反代工具<a href="#为什么又写一个反代工具"><span>#</span></a></h2><p>估计很多人第一反应是：Lucky 已经很成熟了，为什么还要写？</p><p>原因很直白：<strong>我自己的需求比较单一</strong>——就是 <code>DDNS + 证书 + 反代</code>（偶尔加 FRP）。Lucky 功能全面，但很多我用不到；Fonu 的方向不是替代 Lucky，而是：</p><blockquote><p>只把飞牛 NAS 公网访问常用的几项单独做好：简单、轻量、配置直观。</p></blockquote><p>需要非常完整、非常高级的网络能力，Lucky 肯定更合适；如果只是给自己的 fnOS 配域名、证书、反代，可以试试 Fonu。</p><hr /></section>
<section><h2>专为 fnOS，独立 Nginx<a href="#专为-fnos独立-nginx"><span>#</span></a></h2><p>Fonu 目前按 <strong>飞牛 fnOS</strong> 场景开发和测试，我自己也主要在 fnOS 上用。群晖、威联通、自建 Linux 等环境理论上能跑 Docker 就可能能用，但<strong>没有专门做过完整测试</strong>，现阶段优先推荐 fnOS 用户。</p><p>另一个重要设计：<strong>Fonu 跑自己独立的一套 Nginx</strong>，不会去改飞牛系统自带的 Nginx 配置。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>fnOS</span></div></div><div><div><div>2</div></div><div><span>├── 飞牛自己的 Nginx</span></div></div><div><div><div>3</div></div><div><span>└── Fonu</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>├── DDNS</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>├── HTTPS 证书</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>├── 独立 Nginx → 反向代理</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>└── frpc → 内网穿透（可选）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>增加一条反代规则，不必动系统 Nginx；两边正常情况下互不干扰。飞牛已占用 80/443 时，Fonu 默认用高位端口（如 <code>18080</code> / <code>9443</code>）做反代，管理后台在 <code>:6893</code>。</p><hr /></section>
<section><h2>产品展示<a href="#产品展示"><span>#</span></a></h2><p>后台基于 <strong>Vue 3 + Naive UI</strong>，支持浅色 / 深色主题。下面是我自己在 fnOS 上实际在用的界面截图。</p><section><h3>仪表盘<a href="#仪表盘"><span>#</span></a></h3><p>公网 IP、域名、证书、服务状态一屏看完；今日请求按 0–24 时固定坐标展示，实时流量、最近访问与系统日志也集中在这里。</p><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/s7ehnVfHUcoq.webp" alt="Fonu 仪表盘" /><figcaption>Fonu 仪表盘</figcaption></figure><p></p></section><section><h3>DDNS<a href="#ddns"><span>#</span></a></h3><p>多服务商、多根域名，解析记录与同步状态一页管理；支持 IPv4 / IPv6，同步间隔可配。启用 FRP 时会提示把域名解析到 VPS 公网 IP。</p><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/FA0CSffXpHwg.webp" alt="Fonu DDNS" /><figcaption>Fonu DDNS</figcaption></figure><p></p></section><section><h3>HTTPS 证书<a href="#https-证书"><span>#</span></a></h3><p>ACME 申请与自动续签，支持 Let’s Encrypt / ZeroSSL；证书、私钥、ZIP 下载，申请过程有实时日志，快到期会在仪表盘提示。</p><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/qkQkdfuR7DcV.webp" alt="Fonu 证书管理" /><figcaption>Fonu 证书管理</figcaption></figure><p></p></section><section><h3>反向代理<a href="#反向代理"><span>#</span></a></h3><p>规则列表可看每条服务的运行状态、当前 / 总上下行、连接数；支持扫描内网服务、列表内直接启停规则；新建时会校验监听端口是否已被占用。全局汇总卡片一眼看整体流量。</p><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/pxmxdb6FKUxx.webp" alt="Fonu 反向代理列表" /><figcaption>Fonu 反向代理列表</figcaption></figure><p></p><p>新建规则尽量保持简单：填前端域名、监听端口、目标地址，勾选 HTTPS 和跳转即可。例如 <code>mp.example.com → http://192.168.1.100:3000</code>。</p><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/XfCJ6xzn8TTG.webp" alt="Fonu 新增反代规则" /><figcaption>Fonu 新增反代规则</figcaption></figure><p></p><p>点进单条规则可看实时流量曲线、累计统计和 SSE 访问日志；列表里「复制」会打开预填好的表单，确认后再保存（不会悄悄改域名后缀）；域名旁可直接复制或打开链接，拖动排序调整优先级。</p><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/nvHEnQEim1Mn.webp" alt="Fonu 规则详情" /><figcaption>Fonu 规则详情</figcaption></figure><p></p><p>每条规则还可切换 <strong>Nginx</strong> 页签：默认按基础配置与安全策略<strong>自动生成</strong>带中文注释的 <code>server</code> 块，也支持手动编辑、保存前备份与回滚。</p><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/Y0TPwnhPSHJF.webp" alt="Fonu 规则 Nginx 配置" /><figcaption>Fonu 规则 Nginx 配置</figcaption></figure><p></p></section><section><h3>访问保护<a href="#访问保护"><span>#</span></a></h3><p>反代出去的可能是 fnOS 后台、青龙、MoviePilot、qBittorrent、Alist 等管理界面。每条规则可单独配安全策略：大陆 IP 限制（内网段默认放行）、黑白名单、Basic Auth、请求限流、连接数限制、安全响应头等；也有「一键推荐」快捷开启常用组合。</p><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/Zedz3bPmdxnH.webp" alt="Fonu 反代安全设置" /><figcaption>Fonu 反代安全设置</figcaption></figure><p></p></section><section><h3>内网穿透<a href="#内网穿透"><span>#</span></a></h3><p>独立「内网穿透」页：配置 frps 地址、端口与 Token，一键启用后把本地 Nginx 的 HTTP/HTTPS 网关同步到 FRP；TCP 隧道可手动指定远程端口（1–65535，留空则自动分配）。Docker 镜像内置 frpc，页面会按连接设置生成 <code>frps.toml</code> 示例。</p><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/N9l3IjBayN3t.webp" alt="Fonu 内网穿透" /><figcaption>Fonu 内网穿透</figcaption></figure><p></p></section><section><h3>日志中心<a href="#日志中心"><span>#</span></a></h3><p>系统日志、反代访问日志与 Nginx 错误日志分 tab 查看，支持关键词搜索与自动刷新，排障时不用 SSH 翻文件。</p><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/fcgYhC2Uzqh2.webp" alt="Fonu 日志中心" /><figcaption>Fonu 日志中心</figcaption></figure><p></p></section><section><h3>设置<a href="#设置"><span>#</span></a></h3><p>外观与时区、DDNS / 证书自动任务、反代可信代理与中国 IP 库更新、全局 IP 白名单、管理密码、Telegram / Webhook / SMTP 等通知（各通道独立配置，单条密钥异常不影响其它通道）、ACME 邮箱、配置导出与恢复等；<strong>高级</strong>里还可编辑 Nginx <code>http</code> 块全局片段（如 <code>client_max_body_size</code>）。</p><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/I27bp1Kwfl8N.webp" alt="Fonu 设置" /><figcaption>Fonu 设置</figcaption></figure><p></p><hr /></section></section>
<section><h2>想试的话<a href="#想试的话"><span>#</span></a></h2><p>现阶段只提供 <strong>Docker</strong> 部署，暂无 fnOS 原生 <code>.fpk</code>；Host 网络模式在飞牛上最省事：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>run</span><span> </span><span>-d</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>--name</span><span> </span><span>fonu</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>--network</span><span> </span><span>host</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-v</span><span> </span><span>./data:/data</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>--restart</span><span> </span><span>unless-stopped</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>muxui/fonu:latest</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>数据统一在 <code>/data</code>，<strong>务必挂载 volume</strong>，更新容器配置才不会丢。启动后看日志拿 admin 初始密码，访问 <code>http://&lt;主机IP&gt;:6893/login</code> 登录，进设置立刻改密码。</p><hr /></section>
<section><h2>写在最后<a href="#写在最后"><span>#</span></a></h2><p>公网访问这件事，对我来说不需要一个「全能网络中枢」，只需要把<strong>域名、证书、反代</strong>（以及偶尔需要的 <strong>FRP 穿透</strong>）做得顺手、别跟系统 Nginx 打架。Fonu 就是按这个思路写的；若你也在 fnOS 上折腾同类需求，欢迎 Star、Issue 或评论区聊。</p></section>
<section><h2>相关链接<a href="#相关链接"><span>#</span></a></h2><a href="https://github.com/O96u/Fonu" target="_blank"><div><div><div><div></div><div>O96u</div></div><div>/</div><div>Fonu</div></div><div></div></div><div>面向飞牛fnOS的轻量公网访问管理工具</div><div><div>—</div><div>—</div><div>GPL-3.0</div><span>Go</span></div></a><a href="https://hub.docker.com/r/muxui/fonu" target="_blank"><div><div><div>muxui</div><div>/</div><div>fonu</div></div><div></div></div><div>面向飞牛fnOS的轻量公网访问管理工具</div><div><div>5.8K</div><div>0</div></div></a><ul>
<li><a href="https://github.com/O96u/Fonu/releases" target="_blank">Releases</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/dbx-open-source-database-client/</id>
      <title type="text">[开源自荐] DBX - 20MB 开源数据库客户端</title>
      <published>2026-08-17T00:00:00.000Z</published>
      <updated>2026-08-17T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/dbx-open-source-database-client/"/>
      <summary type="text">推荐开源数据库客户端 DBX：约 20MB、Apache-2.0、一个窗口连 80+ 种库。和 Navicat 比，差在许可、体积、部署和 AI/MCP。日常查改够用，企业建模报表还是 Navicat 更稳。</summary>
      <content type="html"><![CDATA[<p>Navicat 好用，这没得洗。界面熟、功能全，公司电脑上几乎是标配。</p>
<p>但个人用、自己折腾服务器的时候，账很难算：授权要钱，安装包不小，想连 Redis 再连 MySQL，还得纠结买 Premium 还是装两个产品。正版价格看一眼就想关掉页面。</p>
<p>后来翻到 <strong>DBX</strong>：大约 <strong>20MB</strong>，<strong>Apache-2.0</strong>，一个窗口能连 <strong>80+</strong> 种库。装完第一感觉不是「又一个客户端」，而是——这东西怎么这么轻。</p>
<a href="https://github.com/t8y2/dbx" target="_blank"><div><div><div><div></div><div>t8y2</div></div><div>/</div><div>dbx</div></div><div></div></div><div>25 MB lightweight cross-platform database client for 90+ databases, including MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, SQL Server, and Dameng. Built-in AI, MCP Server, CLI, desktop and Docker. | 轻量级跨平台数据库管理工具，支持 MySQL、PostgreSQL、SQLite、Redis、MongoDB、达梦等 90+ 数据库，提供桌面端、Docker、CLI、内置 AI 助手和 MCP Server。</div><div><div>—</div><div>—</div><div>Apache-2.0</div><span>Rust</span></div></a>
<p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/V7hMyEKFCrHl.webp" alt="DBX 主界面" /><figcaption>DBX 主界面</figcaption></figure><p></p>
<section><h2>先说结论<a href="#先说结论"><span>#</span></a></h2><p>日常连库、写 SQL、看表、导一点数据，<strong>DBX 够用，而且更轻</strong>。你要的是企业级建模、定时任务、报表和原厂支持，<strong>Navicat 仍然更稳</strong>。两者可以并存，不必站队。</p><div><div><div></div><div>Tip</div></div><div><p>已经有 Navicat / DBeaver 连接的话，DBX 支持直接导入，不用一个个重填。试错成本接近为零。</p></div></div></section>
<section><h2>它到底是什么<a href="#它到底是什么"><span>#</span></a></h2><p>Rust + Tauri 2 做的跨平台客户端，不内嵌 Chromium，也不绑 Java。官方文档：<a href="https://dbxio.com" target="_blank">https://dbxio.com</a></p><p>MySQL、PostgreSQL、SQLite、SQL Server、Oracle、Redis、MongoDB 这些日常货都在；ClickHouse、DuckDB、TiDB、OceanBase，以及达梦、人大金仓、GaussDB 也能连。一部分冷门库走 JDBC Agent，不是每种都同等深度，但「先连上」这件事，比 Navicat 拆一堆版本省心。</p><p>桌面端、Docker Web、CLI 是同一套连接。公司机器不让装客户端时，丢一行 Docker 也能在浏览器里干活。</p></section>
<section><h2>为什么值得装<a href="#为什么值得装"><span>#</span></a></h2><p><strong>真免费，不是试用倒计时。</strong> Apache-2.0，功能不锁付费档。仓库写明不收集遥测，自动更新只去 GitHub Releases 看版本，设置里能关。</p><p><strong>体积对得上「随手打开」。</strong> 大约 20MB。DBeaver 要 Java，一堆 Electron 客户端还带着整颗 Chromium。DBX 是下载、安装、连库，流程短。</p><p><strong>一个窗口覆盖你会碰到的库。</strong> 业务用 MySQL、缓存看 Redis、文档翻 MongoDB，再加一两套国产库——Navicat 这边通常要 Premium。DBX 塞进同一个侧栏。</p><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/rYSWTdJWhf6O.webp" alt="DBX 特性与支持的数据库" /><figcaption>DBX 特性与支持的数据库</figcaption></figure><p></p><p><strong>AI 和 MCP 是写进产品里的。</strong> 选中一张表，用自然语言生成、解释、优化 SQL，支持 Claude、OpenAI，以及 Ollama 本地模型。生成的 SQL 执行前有安全检查。Cursor、Claude Code 可以通过 MCP 直接走你已经配好的连接，不用在编辑器和客户端之间复制粘贴。</p><p>界面也更接近现在常见的编辑器，而不是传统客户端那套皮肤：</p><div><figure><img src="https://s.muxui.com:8015/images/2026/09/vZDFvewqn7ZJ.webp" alt="浅色界面" /><figcaption>浅色界面</figcaption></figure><figure><img src="https://s.muxui.com:8015/images/2026/09/JAtH3Ts40ryC.webp" alt="深色界面" /><figcaption>深色界面</figcaption></figure></div><p>数据表格是虚拟滚动，大结果集不容易把窗口卡死；ER 图、Schema 对比、执行计划这些「打开就能看结构」的能力也有：</p><div><figure><img src="https://s.muxui.com:8015/images/2026/09/CBvakH0X3SWc.webp" alt="数据表格" /><figcaption>数据表格</figcaption></figure><figure><img src="https://s.muxui.com:8015/images/2026/09/qB5mqwi4577m.webp" alt="ER 关系图" /><figcaption>ER 关系图</figcaption></figure></div></section>
<section><h2>和 Navicat 到底差在哪<a href="#和-navicat-到底差在哪"><span>#</span></a></h2><p>Navicat 是<strong>付费、成熟、偏传统桌面</strong>；DBX 是<strong>免费、轻量、偏现在这套工作流</strong>。</p>

<table><thead><tr><th></th><th>Navicat</th><th>DBX</th></tr></thead><tbody><tr><td>许可</td><td>商业软件，按版本 / 订阅收费</td><td>Apache-2.0，功能免费</td></tr><tr><td>体积</td><td>安装包通常大一个数量级</td><td>约 20 MB，不内嵌 Chromium</td></tr><tr><td>数据库</td><td>分产品线，全开通常要 Premium</td><td>一个客户端 80+，含不少国产库</td></tr><tr><td>用法</td><td>以桌面 GUI 为主</td><td>桌面 + Docker Web + CLI</td></tr><tr><td>AI</td><td>产品内有加，偏套件功能</td><td>编辑器内 AI + MCP 给编程助手用</td></tr><tr><td>成熟度</td><td>十几年打磨，建模、报表、计划任务更全</td><td>新项目，日常查、改、导够用</td></tr><tr><td>迁移</td><td>—</td><td>可导入 Navicat / DBeaver 连接</td></tr></tbody></table><p>不是「DBX 全面赢了」，是<strong>你要解决的问题不一样</strong>。个人开发者那笔 Navicat 授权，很多时候买的是习惯，不是刚需。</p></section>
<section><h2>怎么装<a href="#怎么装"><span>#</span></a></h2><p>安装包在 <a href="https://github.com/t8y2/dbx/releases/latest" target="_blank">Releases</a>。Windows 用包管理器更省事：</p><div><div>WinGetScoop</div><div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>winget install t8y2.dbx</span></div></div></code></pre><div><div></div><div></div></div></figure></div></div><div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>scoop bucket add dbx https:</span><span>//</span></span><span>github.com</span><span><span>/</span><span>t8y2</span><span>/</span><span>scoop</span><span>-</span><span>bucket</span></span></div></div><div><div><div>2</div></div><div><span>scoop install dbx</span></div></div></code></pre><div><div></div><div></div></div></figure></div></div></div><p>macOS：<code>brew install --cask dbx</code>。给团队一个网页入口的话：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>docker</span><span> </span><span>run</span><span> </span><span>-d</span><span> </span><span>--pull=always</span><span> </span><span>--name</span><span> </span><span>dbx</span><span> </span><span>-p</span><span> </span><span>4224:4224</span><span> </span><span>-v</span><span> </span><span>dbx-data:/app/data</span><span> </span><span>t8y2/dbx:latest</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>大陆可以换成 <code>docker.cnb.cool/dbxio.com/dbx:latest</code>，浏览器打开 <code>http://localhost:4224</code>。</p><p>已经在用 Cursor 的话，MCP 大致是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"mcpServers"</span><span>: {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>"dbx"</span><span>: { </span><span>"command"</span><span>: </span><span>"npx"</span><span>, </span><span>"args"</span><span>: [</span><span>"-y"</span><span>, </span><span>"@dbx-app/mcp-server"</span><span>] }</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>连接白名单和只读 / 读写权限在 <strong>DBX 设置 → MCP</strong> 里管。Windows 便携版还要加 <code>DBX_DATA_DIR</code>，指到 <code>DBX.exe</code> 旁边那个包含 <code>dbx.db</code> 的 <code>data</code> 目录。</p></section>
<section><h2>别抱太高期望<a href="#别抱太高期望"><span>#</span></a></h2><div><div><div></div><div>Warning</div></div><div><p>公司强依赖 Navicat 的建模、报表、自动化，别指望一周内无痛替换。80+ 也不等于每种库一样深——主流库最好用，冷门库有的靠 JDBC Agent，边角功能要自己试。</p></div></div><p>项目还年轻。GitHub 已经一万多星，迭代很快，但稳定性和文档厚度跟卖了十几年的商业软件比，仍有差距。AI 也不是开箱即用的魔法，得自己配模型端点；完全离线也可以，只用 Ollama，或者干脆关掉。</p></section>
<section><h2>下手试试<a href="#下手试试"><span>#</span></a></h2><p>还在为授权、体积、或者「再装一个只为 Redis」发愁的话，先连自己的库看一天。旧连接能导入，不行就卸，没什么沉没成本。</p><p>它解决的是：<strong>免费、轻、一个窗口覆盖日常会碰到的库，还能把连接交给编辑器里的 AI。</strong> Navicat 解决的是另一套问题。</p><a href="https://github.com/t8y2/dbx" target="_blank"><div><div><div><div></div><div>t8y2</div></div><div>/</div><div>dbx</div></div><div></div></div><div>25 MB lightweight cross-platform database client for 90+ databases, including MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, SQL Server, and Dameng. Built-in AI, MCP Server, CLI, desktop and Docker. | 轻量级跨平台数据库管理工具，支持 MySQL、PostgreSQL、SQLite、Redis、MongoDB、达梦等 90+ 数据库，提供桌面端、Docker、CLI、内置 AI 助手和 MCP Server。</div><div><div>—</div><div>—</div><div>Apache-2.0</div><span>Rust</span></div></a></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/wordpress-to-astro-migration/</id>
      <title type="text">从 WordPress 到 Astro迁移实战全记录</title>
      <published>2026-07-23T00:00:00.000Z</published>
      <updated>2026-07-23T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/wordpress-to-astro-migration/"/>
      <summary type="text">记录将 Muxui 从 WordPress（B2 Pro）完整迁移到 Astro 静态博客主题 Firefly 的全过程：导出 WXR、HTML 转 Markdown、图床改写、友链与导航定制、Twikoo 评论迁移，以及迁移中踩过的坑。</summary>
      <content type="html"><![CDATA[<p>这篇文章记录了把 <a href="https://muxui.com">Muxui</a> 从 <strong>WordPress（B2 Pro）</strong> 迁到 <strong>Astro + Firefly</strong> 的完整过程。不是「理论科普」，而是一次可复现的实操：文章怎么导、图片怎么改、评论怎么接到 Twikoo、导航怎么拆、友链怎么迁、踩了哪些坑。</p>
<p>如果你也在考虑离开 WordPress，或者正在把内容搬到 Astro / Fuwari / Firefly 一类静态主题，可以直接对照本文的步骤做。</p>
<hr />
<section><h2>为什么离开 WordPress<a href="#为什么离开-wordpress"><span>#</span></a></h2><p>WordPress 很强，但对我这种以内容为主、不需要商城和复杂会员体系的个人站来说，日常成本越来越明显：</p><ul>
<li><strong>性能与安全</strong>：PHP + 数据库 + 插件栈，更新、备份、防篡改都要花精力</li>
<li><strong>主题耦合深</strong>：B2 Pro 功能丰富，但短代码、付费隐藏、圈子等和主题绑死，换主题等于重做</li>
<li><strong>静态化红利</strong>：Astro 构建后是纯静态（或边缘静态），CDN 分发简单，运维接近零</li>
</ul><p>最终选择 <strong>Firefly</strong>——基于 Astro 的个人博客主题，Markdown / MDX 写作，配置全在 TypeScript 文件里，改起来清晰。</p><hr /></section>
<section><h2>目标架构一览<a href="#目标架构一览"><span>#</span></a></h2>

<table><thead><tr><th>项目</th><th>迁移前</th><th>迁移后</th></tr></thead><tbody><tr><td>运行时</td><td>WordPress + PHP + MySQL</td><td>Astro 静态构建</td></tr><tr><td>主题</td><td>B2 Pro</td><td>Firefly（fork 自用）</td></tr><tr><td>内容</td><td>后台编辑器 / Gutenberg</td><td><code>src/content/posts/*.md</code></td></tr><tr><td>图片</td><td><code>muxui.com/wp-content/uploads/...</code></td><td><code>muxui.com/uploads/...</code></td></tr><tr><td>评论</td><td>WordPress 数据库 / 主题评论</td><td>Twikoo（独立服务 + 导入）</td></tr><tr><td>域名</td><td>muxui.com</td><td>仍为 muxui.com</td></tr></tbody></table><p>仓库结构大致是：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Firefly/</span></div></div><div><div><div>2</div></div><div><span>├── muxui.WordPress.2026-07-23.xml   # WXR 导出（本地用，不必提交）</span></div></div><div><div><div>3</div></div><div><span>├── scripts/</span></div></div><div><div><div>4</div></div><div><span>│   ├── migrate-wordpress.mjs        # 一次性迁移脚本</span></div></div><div><div><div>5</div></div><div><span>│   └── lib/</span></div></div><div><div><div>6</div></div><div><span>│       ├── wxr-parse.mjs            # 解析 WXR</span></div></div><div><div><div>7</div></div><div><span>│       └── html-to-md.mjs           # HTML → Markdown + 图床改写</span></div></div><div><div><div>8</div></div><div><span>├── src/config/                      # 站点 / 导航 / 友链等配置</span></div></div><div><div><div>9</div></div><div><span>└── src/content/posts/               # 迁过来的文章</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><div><div><figure><figcaption><span>migrate-wordpress.mjs</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> </span><span>fs</span><span> </span><span>from</span><span> </span><span>"node:fs"</span><span>;</span></div></div><div><div><div>2</div></div><div><span>import</span><span> </span><span>path</span><span> </span><span>from</span><span> </span><span>"node:path"</span><span>;</span></div></div><div><div><div>3</div></div><div><span>import</span><span> { </span><span>parseWxr</span><span> } </span><span>from</span><span> </span><span>"./lib/wxr-parse.mjs"</span><span>;</span></div></div><div><div><div>4</div></div><div><span>import</span><span> { </span><span>htmlToMarkdown</span><span>, </span><span>rewriteImageUrls</span><span> } </span><span>from</span><span> </span><span>"./lib/html-to-md.mjs"</span><span>;</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>const</span><span> </span><span>ROOT</span><span> </span><span>=</span><span><span> </span><span>process</span><span>.</span></span><span>cwd</span><span>();</span></div></div><div><div><div>7</div></div><div><span>const</span><span> </span><span>XML</span><span> </span><span>=</span><span><span> </span><span>path</span><span>.</span></span><span>join</span><span>(</span><span>ROOT</span><span>, </span><span>"muxui.WordPress.2026-07-23.xml"</span><span>);</span></div></div><div><div><div>8</div></div><div><span>const</span><span> </span><span>POSTS_DIR</span><span> </span><span>=</span><span><span> </span><span>path</span><span>.</span></span><span>join</span><span>(</span><span>ROOT</span><span>, </span><span>"src/content/posts"</span><span>);</span></div></div><div><div><div>9</div></div><div><span>const</span><span> </span><span>SITE_CONFIG</span><span> </span><span>=</span><span><span> </span><span>path</span><span>.</span></span><span>join</span><span>(</span><span>ROOT</span><span>, </span><span>"src/config/siteConfig.ts"</span><span>);</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>function</span><span> </span><span>decodeSlug</span><span><span>(</span><span>raw</span><span>) {</span></span></div></div><div><div><div>12</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span><span>raw</span><span>) </span></span><span>return</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>13</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>14</div></div><div><span>    </span><span>return</span><span> </span><span>decodeURIComponent</span><span><span>(</span><span>raw</span><span>);</span></span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> {</span></div></div><div><div><div>16</div></div><div><span>    </span><span>return</span><span><span> </span><span>raw</span><span>;</span></span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>function</span><span> </span><span>safeSlug</span><span><span>(</span><span>title</span><span>, </span><span>id</span><span>) {</span></span></div></div><div><div><div>21</div></div><div><span>  </span><span>const</span><span> </span><span>base</span><span> </span><span>=</span><span><span> </span><span>title</span></span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>.</span><span>toLowerCase</span><span>()</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span>/</span><span>[</span><span><span>^</span><span>\p</span></span><span>{L}</span><span>\p</span><span>{N}]</span><span><span>+</span><span>/</span></span><span>gu</span><span>, </span><span>"-"</span><span>)</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span>/</span><span>^</span><span><span>-</span><span>+</span><span>|</span><span>-</span><span>+</span></span><span>$</span><span>/</span><span>g</span><span>, </span><span>""</span><span>)</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>.</span><span>slice</span><span>(</span><span>0</span><span>, </span><span>80</span><span>);</span></div></div><div><div><div>26</div></div><div><span>  </span><span>return</span><span><span> </span><span>base</span><span> </span></span><span>||</span><span> </span><span>`post-</span><span>${</span><span>id</span><span>}</span><span>`</span><span>;</span></div></div><div><div><div>27</div></div><div><span>}</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>function</span><span> </span><span>pickCategory</span><span><span>(</span><span>cats</span><span>) {</span></span></div></div><div><div><div>30</div></div><div><span>  </span><span>const</span><span> </span><span>specific</span><span> </span><span>=</span><span><span> </span><span>cats</span><span>.</span></span><span>filter</span><span><span>((</span><span>c</span><span>) </span></span><span>=&gt;</span><span><span> </span><span>c</span><span> </span></span><span>!==</span><span> </span><span>"文章"</span><span>);</span></div></div><div><div><div>31</div></div><div><span>  </span><span>return</span><span><span> </span><span>specific</span><span>[</span></span><span>0</span><span>] </span><span>||</span><span><span> </span><span>cats</span><span>[</span></span><span>0</span><span>] </span><span>||</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>32</div></div><div><span>}</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span>function</span><span> </span><span>yamlEscape</span><span><span>(</span><span>s</span><span>) {</span></span></div></div><div><div><div>35</div></div><div><span>  </span><span>return</span><span> </span><span>String</span><span><span>(</span><span>s</span><span>).</span></span><span>replace</span><span>(</span><span><span>/</span><span>\\</span><span>/</span></span><span>g</span><span>, </span><span>"</span><span>\\\\</span><span>"</span><span>).</span><span>replace</span><span>(</span><span>/"/</span><span>g</span><span>, </span><span>'</span><span>\\</span><span>"'</span><span>);</span></div></div><div><div><div>36</div></div><div><span>}</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>function</span><span> </span><span>toIsoDate</span><span><span>(</span><span>wpDate</span><span>) {</span></span></div></div><div><div><div>39</div></div><div><span>  </span><span>return</span><span><span> </span><span>wpDate</span><span>.</span></span><span>slice</span><span>(</span><span>0</span><span>, </span><span>10</span><span>);</span></div></div><div><div><div>40</div></div><div><span>}</span></div></div><div><div><div>41</div></div><div>
</div></div><div><div><div>42</div></div><div><span>function</span><span> </span><span>excerptFrom</span><span><span>(</span><span>md</span><span>, </span><span>excerpt</span><span>) {</span></span></div></div><div><div><div>43</div></div><div><span>  </span><span>if</span><span><span> (</span><span>excerpt</span><span>?.</span></span><span>trim</span><span>()) </span><span>return</span><span><span> </span><span>excerpt</span><span>.</span></span><span>trim</span><span>().</span><span>slice</span><span>(</span><span>0</span><span>, </span><span>160</span><span>);</span></div></div><div><div><div>44</div></div><div><span>  </span><span>const</span><span> </span><span>plain</span><span> </span><span>=</span><span><span> </span><span>md</span></span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span><span>/!</span><span>\[</span></span><span>[</span><span><span>^</span><span>\]</span></span><span>]</span><span><span>*</span><span>\]\(</span></span><span>[</span><span>^</span><span>)]</span><span><span>*</span><span>\)</span><span>/</span></span><span>g</span><span>, </span><span>""</span><span>)</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\[</span></span><span>[</span><span><span>^</span><span>\]</span></span><span>]</span><span><span>*</span><span>\]\(</span></span><span>[</span><span>^</span><span>)]</span><span><span>*</span><span>\)</span><span>/</span></span><span>g</span><span>, </span><span>"$1"</span><span>)</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span>/</span><span>[#&gt;*`</span><span>\[\]</span><span>()\-_!]</span><span>/</span><span>g</span><span>, </span><span>" "</span><span>)</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span><span>/https</span><span>?</span><span>:</span><span>\/\/</span></span><span>\S</span><span><span>+</span><span>/</span></span><span>g</span><span>, </span><span>""</span><span>)</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span>/</span><span>\s</span><span><span>+</span><span>/</span></span><span>g</span><span>, </span><span>" "</span><span>)</span></div></div><div><div><div>50</div></div><div><span><span>    </span></span><span>.</span><span>trim</span><span>();</span></div></div><div><div><div>51</div></div><div><span>  </span><span>return</span><span><span> </span><span>plain</span><span>.</span></span><span>slice</span><span>(</span><span>0</span><span>, </span><span>120</span><span>);</span></div></div><div><div><div>52</div></div><div><span>}</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span>function</span><span> </span><span>clearPostsDir</span><span><span>(</span><span>dir</span><span>) {</span></span></div></div><div><div><div>55</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span><span>fs</span><span>.</span></span><span>existsSync</span><span><span>(</span><span>dir</span><span>)) {</span></span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>fs</span><span>.</span><span>mkdirSync</span><span><span>(</span><span>dir</span><span>, { </span></span><span>recursive</span><span>:</span><span> </span><span>true</span><span> });</span></div></div><div><div><div>57</div></div><div><span>    </span><span>return</span><span>;</span></div></div><div><div><div>58</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>59</div></div><div><span>  </span><span>for</span><span> (</span><span>const</span><span> </span><span>name</span><span> </span><span>of</span><span><span> </span><span>fs</span><span>.</span></span><span>readdirSync</span><span><span>(</span><span>dir</span><span>)) {</span></span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>fs</span><span>.</span><span>rmSync</span><span><span>(</span><span>path</span><span>.</span></span><span>join</span><span><span>(</span><span>dir</span><span>, </span><span>name</span><span>), { </span></span><span>recursive</span><span>:</span><span> </span><span>true</span><span>, </span><span>force</span><span>:</span><span> </span><span>true</span><span> });</span></div></div><div><div><div>61</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>62</div></div><div><span>}</span></div></div><div><div><div>63</div></div><div>
</div></div><div><div><div>64</div></div><div><span>function</span><span> </span><span>buildFrontmatter</span><span><span>(</span><span>post</span><span>, </span><span>image</span><span>, </span><span>body</span><span>, </span><span>slug</span><span>) {</span></span></div></div><div><div><div>65</div></div><div><span>  </span><span>const</span><span> </span><span>tags</span><span> </span><span>=</span><span><span> </span><span>post</span><span>.</span></span><span>tags</span><span>.</span><span>map</span><span><span>((</span><span>t</span><span>) </span></span><span>=&gt;</span><span> </span><span>`"</span><span>${</span><span>yamlEscape</span><span><span>(</span><span>t</span><span>)</span></span><span>}</span><span>"`</span><span>).</span><span>join</span><span>(</span><span>", "</span><span>);</span></div></div><div><div><div>66</div></div><div><span>  </span><span>const</span><span> </span><span>desc</span><span> </span><span>=</span><span> </span><span>excerptFrom</span><span><span>(</span><span>body</span><span>, </span><span>post</span><span>.</span></span><span>excerpt</span><span>);</span></div></div><div><div><div>67</div></div><div><span>  </span><span>return</span><span> </span><span>`---</span></div></div><div><div><div>68</div></div><div><span>title: "</span><span>${</span><span>yamlEscape</span><span><span>(</span><span>post</span></span><span>.</span><span>title</span><span>)</span><span>}</span><span>"</span></div></div><div><div><div>69</div></div><div><span>published: </span><span>${</span><span>toIsoDate</span><span><span>(</span><span>post</span></span><span>.</span><span>date</span><span>)</span><span>}</span></div></div><div><div><div>70</div></div><div><span>updated: </span><span>${</span><span>toIsoDate</span><span><span>(</span><span>post</span></span><span>.</span><span>modified</span><span>)</span><span>}</span></div></div><div><div><div>71</div></div><div><span>description: "</span><span>${</span><span>yamlEscape</span><span><span>(</span><span>desc</span><span>)</span></span><span>}</span><span>"</span></div></div><div><div><div>72</div></div><div><span>image: "</span><span>${</span><span>yamlEscape</span><span><span>(</span><span>image</span><span>)</span></span><span>}</span><span>"</span></div></div><div><div><div>73</div></div><div><span>tags: [</span><span>${</span><span>tags</span><span>}</span><span>]</span></div></div><div><div><div>74</div></div><div><span>category: "</span><span>${</span><span>yamlEscape</span><span>(</span><span>pickCategory</span><span><span>(</span><span>post</span></span><span>.</span><span>cats</span><span>))</span><span>}</span><span>"</span></div></div><div><div><div>75</div></div><div><span>draft: false</span></div></div><div><div><div>76</div></div><div><span>comment: true</span></div></div><div><div><div>77</div></div><div><span>slug: "</span><span>${</span><span>yamlEscape</span><span><span>(</span><span>slug</span><span>)</span></span><span>}</span><span>"</span></div></div><div><div><div>78</div></div><div><span>---</span></div></div><div><div><div>79</div></div><div>
</div></div><div><div><div>80</div></div><div><span>`</span><span>;</span></div></div><div><div><div>81</div></div><div><span>}</span></div></div><div><div><div>82</div></div><div>
</div></div><div><div><div>83</div></div><div><span>function</span><span> </span><span>patchSiteConfig</span><span><span>(</span><span>filePath</span><span>) {</span></span></div></div><div><div><div>84</div></div><div><span>  </span><span>let</span><span><span> </span><span>src</span><span> </span></span><span>=</span><span><span> </span><span>fs</span><span>.</span></span><span>readFileSync</span><span><span>(</span><span>filePath</span><span>, </span></span><span>"utf8"</span><span>);</span></div></div><div><div><div>85</div></div><div><span><span>  </span></span><span>src</span><span> </span><span>=</span><span><span> </span><span>src</span><span>.</span></span><span>replace</span><span>(</span><span>/title: "Firefly"/</span><span>, </span><span>'title: "Muxui"'</span><span>);</span></div></div><div><div><div>86</div></div><div><span><span>  </span></span><span>src</span><span> </span><span>=</span><span><span> </span><span>src</span><span>.</span></span><span>replace</span><span>(</span><span>/subtitle: "Demo site"/</span><span>, </span><span>'subtitle: "记录美好生活！"'</span><span>);</span></div></div><div><div><div>87</div></div><div><span><span>  </span></span><span>src</span><span> </span><span>=</span><span><span> </span><span>src</span><span>.</span></span><span>replace</span><span>(</span></div></div><div><div><div>88</div></div><div><span>    </span><span><span>/site_url: "https:</span><span>\/\/</span><span>firefly</span><span>\.</span><span>cuteleaf</span><span>\.</span><span>cn"/</span></span><span>,</span></div></div><div><div><div>89</div></div><div><span>    </span><span>'site_url: "https://muxui.com"'</span><span>,</span></div></div><div><div><div>90</div></div><div><span><span>  </span></span><span>);</span></div></div><div><div><div>91</div></div><div><span><span>  </span></span><span>src</span><span> </span><span>=</span><span><span> </span><span>src</span><span>.</span></span><span>replace</span><span>(</span></div></div><div><div><div>92</div></div><div><span>    </span><span>/description:</span><span>\s</span><span>*</span><span>\n\s</span><span><span>*</span><span>"</span></span><span><span>[</span><span>\s\S</span><span>]</span></span><span><span>*?</span><span>",/</span></span><span>,</span></div></div><div><div><div>93</div></div><div><span>    </span><span>'description:</span><span>\n\t\t</span><span>"记录美好生活！",'</span><span>,</span></div></div><div><div><div>94</div></div><div><span><span>  </span></span><span>);</span></div></div><div><div><div>95</div></div><div><span><span>  </span></span><span>src</span><span> </span><span>=</span><span><span> </span><span>src</span><span>.</span></span><span>replace</span><span>(</span></div></div><div><div><div>96</div></div><div><span>    </span><span><span>/keywords: </span><span>\[</span></span><span><span>[</span><span>\s\S</span><span>]</span></span><span><span>*?</span><span>\]</span><span>,/</span></span><span>,</span></div></div><div><div><div>97</div></div><div><span>    </span><span>`keywords: [</span></div></div><div><div><div>98</div></div><div><span>\t\t</span><span>"Muxui",</span></div></div><div><div><div>99</div></div><div><span>\t\t</span><span>"博客",</span></div></div><div><div><div>100</div></div><div><span>\t\t</span><span>"技术博客",</span></div></div><div><div><div>101</div></div><div><span>\t\t</span><span>"muxui.com",</span></div></div><div><div><div>102</div></div><div><span>\t</span><span>],`</span><span>,</span></div></div><div><div><div>103</div></div><div><span><span>  </span></span><span>);</span></div></div><div><div><div>104</div></div><div><span><span>  </span></span><span>src</span><span> </span><span>=</span><span><span> </span><span>src</span><span>.</span></span><span>replace</span><span>(</span><span><span>/(navbar: </span><span>\{</span></span><span><span>[</span><span>\s\S</span><span>]</span></span><span><span>*?</span><span>title: )"Firefly"/</span></span><span>, </span><span>'$1"Muxui"'</span><span>);</span></div></div><div><div><div>105</div></div><div><span><span>  </span></span><span>fs</span><span>.</span><span>writeFileSync</span><span><span>(</span><span>filePath</span><span>, </span><span>src</span><span>);</span></span></div></div><div><div><div>106</div></div><div><span>}</span></div></div><div><div><div>107</div></div><div>
</div></div><div><div><div>108</div></div><div><span>function</span><span> </span><span>main</span><span>() {</span></div></div><div><div><div>109</div></div><div><span>  </span><span>if</span><span> (</span><span>!</span><span><span>fs</span><span>.</span></span><span>existsSync</span><span>(</span><span>XML</span><span>)) {</span></div></div><div><div><div>110</div></div><div><span><span>    </span></span><span>console</span><span>.</span><span>error</span><span>(</span><span>"Missing WXR:"</span><span>, </span><span>XML</span><span>);</span></div></div><div><div><div>111</div></div><div><span><span>    </span></span><span>process</span><span>.</span><span>exit</span><span>(</span><span>1</span><span>);</span></div></div><div><div><div>112</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>113</div></div><div>
</div></div><div><div><div>114</div></div><div><span>  </span><span>const</span><span> { </span><span>posts</span><span>, </span><span>attachmentsById</span><span> } </span><span>=</span><span> </span><span>parseWxr</span><span>(</span><span>XML</span><span>);</span></div></div><div><div><div>115</div></div><div><span><span>  </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>`Parsed </span><span>${</span><span>posts</span><span>.</span><span>length</span><span>}</span><span> published posts`</span><span>);</span></div></div><div><div><div>116</div></div><div>
</div></div><div><div><div>117</div></div><div><span>  </span><span>clearPostsDir</span><span>(</span><span>POSTS_DIR</span><span>);</span></div></div><div><div><div>118</div></div><div>
</div></div><div><div><div>119</div></div><div><span>  </span><span>const</span><span> </span><span>used</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Set</span><span>();</span></div></div><div><div><div>120</div></div><div><span>  </span><span>for</span><span> (</span><span>const</span><span> </span><span>post</span><span> </span><span>of</span><span><span> </span><span>posts</span><span>) {</span></span></div></div><div><div><div>121</div></div><div><span>    </span><span>const</span><span> </span><span>body</span><span> </span><span>=</span><span> </span><span>htmlToMarkdown</span><span><span>(</span><span>post</span><span>.</span></span><span>content</span><span>);</span></div></div><div><div><div>122</div></div><div><span>    </span><span>let</span><span><span> </span><span>image</span><span> </span></span><span>=</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>123</div></div><div><span>    </span><span>if</span><span><span> (</span><span>post</span><span>.</span></span><span>thumbnailId</span><span> </span><span>&amp;&amp;</span><span><span> </span><span>attachmentsById</span><span>.</span></span><span>has</span><span><span>(</span><span>post</span><span>.</span></span><span>thumbnailId</span><span>)) {</span></div></div><div><div><div>124</div></div><div><span><span>      </span></span><span>image</span><span> </span><span>=</span><span> </span><span>rewriteImageUrls</span><span><span>(</span><span>attachmentsById</span><span>.</span></span><span>get</span><span><span>(</span><span>post</span><span>.</span></span><span>thumbnailId</span><span>));</span></div></div><div><div><div>125</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>126</div></div><div><span>    </span><span>let</span><span><span> </span><span>slug</span><span> </span></span><span>=</span><span> </span><span>decodeSlug</span><span><span>(</span><span>post</span><span>.</span></span><span>slug</span><span>) </span><span>||</span><span> </span><span>safeSlug</span><span><span>(</span><span>post</span><span>.</span></span><span>title</span><span><span>, </span><span>post</span><span>.</span></span><span>id</span><span>);</span></div></div><div><div><div>127</div></div><div><span>    </span><span>// Windows-safe filenames: avoid reserved chars</span></div></div><div><div><div>128</div></div><div><span><span>    </span></span><span>slug</span><span> </span><span>=</span><span><span> </span><span>slug</span><span>.</span></span><span>replace</span><span>(</span><span>/</span><span>[&lt;&gt;:"/</span><span>\\</span><span>|?*]</span><span>/</span><span>g</span><span>, </span><span>"-"</span><span>).</span><span>replace</span><span>(</span><span><span>/</span><span>\.</span><span>+</span></span><span>$</span><span>/</span><span>g</span><span>, </span><span>""</span><span>);</span></div></div><div><div><div>129</div></div><div><span>    </span><span>if</span><span><span> (</span><span>used</span><span>.</span></span><span>has</span><span><span>(</span><span>slug</span><span>)) </span><span>slug</span><span> </span></span><span>=</span><span> </span><span>`</span><span>${</span><span>slug</span><span>}</span><span>-</span><span>${</span><span>post</span><span>.</span><span>id</span><span>}</span><span>`</span><span>;</span></div></div><div><div><div>130</div></div><div><span><span>    </span></span><span>used</span><span>.</span><span>add</span><span><span>(</span><span>slug</span><span>);</span></span></div></div><div><div><div>131</div></div><div><span>    </span><span>const</span><span> </span><span>fm</span><span> </span><span>=</span><span> </span><span>buildFrontmatter</span><span><span>(</span><span>post</span><span>, </span><span>image</span><span>, </span><span>body</span><span>, </span><span>slug</span><span>);</span></span></div></div><div><div><div>132</div></div><div><span>    </span><span>const</span><span> </span><span>out</span><span> </span><span>=</span><span><span> </span><span>path</span><span>.</span></span><span>join</span><span>(</span><span>POSTS_DIR</span><span>, </span><span>`</span><span>${</span><span>slug</span><span>}</span><span>.md`</span><span>);</span></div></div><div><div><div>133</div></div><div><span><span>    </span></span><span>fs</span><span>.</span><span>writeFileSync</span><span><span>(</span><span>out</span><span>, </span><span>fm</span><span> </span></span><span>+</span><span><span> </span><span>body</span><span> </span></span><span>+</span><span> </span><span>"</span><span>\n</span><span>"</span><span>, </span><span>"utf8"</span><span>);</span></div></div><div><div><div>134</div></div><div><span><span>    </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>"Wrote"</span><span><span>, </span><span>path</span><span>.</span></span><span>relative</span><span>(</span><span>ROOT</span><span><span>, </span><span>out</span><span>));</span></span></div></div><div><div><div>135</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>136</div></div><div>
</div></div><div><div><div>137</div></div><div><span>  </span><span>patchSiteConfig</span><span>(</span><span>SITE_CONFIG</span><span>);</span></div></div><div><div><div>138</div></div><div><span><span>  </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>"Patched"</span><span><span>, </span><span>path</span><span>.</span></span><span>relative</span><span>(</span><span>ROOT</span><span>, </span><span>SITE_CONFIG</span><span>));</span></div></div><div><div><div>139</div></div><div><span><span>  </span></span><span>console</span><span>.</span><span>log</span><span>(</span><span>"Done."</span><span>);</span></div></div><div><div><div>140</div></div><div><span>}</span></div></div><div><div><div>141</div></div><div>
</div></div><div><div><div>142</div></div><div><span>main</span><span>();</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption><span>wxr-parse.mjs</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> </span><span>fs</span><span> </span><span>from</span><span> </span><span>"node:fs"</span><span>;</span></div></div><div><div><div>2</div></div><div><span>import</span><span> { </span><span>XMLParser</span><span> } </span><span>from</span><span> </span><span>"fast-xml-parser"</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>const</span><span> </span><span>parser</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>XMLParser</span><span>({</span></div></div><div><div><div>5</div></div><div><span>  </span><span>ignoreAttributes</span><span>:</span><span> </span><span>false</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>attributeNamePrefix</span><span>:</span><span> </span><span>"@_"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>cdataPropName</span><span>:</span><span> </span><span>"__cdata"</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>isArray</span><span>:</span><span><span> (</span><span>name</span><span>) </span></span><span>=&gt;</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>[</span><span>"item"</span><span>, </span><span>"category"</span><span>, </span><span>"wp:postmeta"</span><span>, </span><span>"wp:comment"</span><span>].</span><span>includes</span><span><span>(</span><span>name</span><span>),</span></span></div></div><div><div><div>10</div></div><div><span>});</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>function</span><span> </span><span>textOf</span><span><span>(</span><span>node</span><span>) {</span></span></div></div><div><div><div>13</div></div><div><span>  </span><span>if</span><span><span> (</span><span>node</span><span> </span></span><span>==</span><span> </span><span>null</span><span>) </span><span>return</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>14</div></div><div><span>  </span><span>if</span><span> (</span><span>typeof</span><span><span> </span><span>node</span><span> </span></span><span>===</span><span> </span><span>"string"</span><span> </span><span>||</span><span> </span><span>typeof</span><span><span> </span><span>node</span><span> </span></span><span>===</span><span> </span><span>"number"</span><span>) </span><span>return</span><span> </span><span>String</span><span><span>(</span><span>node</span><span>);</span></span></div></div><div><div><div>15</div></div><div><span>  </span><span>if</span><span> (</span><span>typeof</span><span><span> </span><span>node</span><span> </span></span><span>===</span><span> </span><span>"object"</span><span> </span><span>&amp;&amp;</span><span> </span><span>"__cdata"</span><span> </span><span>in</span><span><span> </span><span>node</span><span>)</span></span></div></div><div><div><div>16</div></div><div><span>    </span><span>return</span><span> </span><span>String</span><span><span>(</span><span>node</span><span>.</span></span><span>__cdata</span><span> </span><span>??</span><span> </span><span>""</span><span>);</span></div></div><div><div><div>17</div></div><div><span>  </span><span>return</span><span> </span><span>String</span><span><span>(</span><span>node</span><span>);</span></span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>function</span><span> </span><span>metaMap</span><span><span>(</span><span>postmeta</span><span>) {</span></span></div></div><div><div><div>21</div></div><div><span>  </span><span>const</span><span> </span><span>map</span><span> </span><span>=</span><span> {};</span></div></div><div><div><div>22</div></div><div><span>  </span><span>for</span><span> (</span><span>const</span><span> </span><span>m</span><span> </span><span>of</span><span><span> </span><span>postmeta</span><span> </span></span><span>||</span><span> []) {</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>map</span><span>[</span><span>textOf</span><span><span>(</span><span>m</span><span>[</span></span><span>"wp:meta_key"</span><span>])] </span><span>=</span><span> </span><span>textOf</span><span><span>(</span><span>m</span><span>[</span></span><span>"wp:meta_value"</span><span>]);</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>25</div></div><div><span>  </span><span>return</span><span><span> </span><span>map</span><span>;</span></span></div></div><div><div><div>26</div></div><div><span>}</span></div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>function</span><span> </span><span>categoriesOf</span><span><span>(</span><span>item</span><span>) {</span></span></div></div><div><div><div>29</div></div><div><span>  </span><span>const</span><span> </span><span>cats</span><span> </span><span>=</span><span> [];</span></div></div><div><div><div>30</div></div><div><span>  </span><span>const</span><span> </span><span>tags</span><span> </span><span>=</span><span> [];</span></div></div><div><div><div>31</div></div><div><span>  </span><span>for</span><span> (</span><span>const</span><span> </span><span>c</span><span> </span><span>of</span><span><span> </span><span>item</span><span>.</span></span><span>category</span><span> </span><span>||</span><span> []) {</span></div></div><div><div><div>32</div></div><div><span>    </span><span>const</span><span> </span><span>name</span><span> </span><span>=</span><span> </span><span>textOf</span><span><span>(</span><span>c</span><span>);</span></span></div></div><div><div><div>33</div></div><div><span>    </span><span>const</span><span> </span><span>domain</span><span> </span><span>=</span><span><span> </span><span>c</span><span>[</span></span><span>"@_domain"</span><span>];</span></div></div><div><div><div>34</div></div><div><span>    </span><span>if</span><span><span> (</span><span>domain</span><span> </span></span><span>===</span><span> </span><span>"post_tag"</span><span><span>) </span><span>tags</span><span>.</span></span><span>push</span><span><span>(</span><span>name</span><span>);</span></span></div></div><div><div><div>35</div></div><div><span>    </span><span>else</span><span> </span><span>if</span><span><span> (</span><span>domain</span><span> </span></span><span>===</span><span> </span><span>"category"</span><span><span>) </span><span>cats</span><span>.</span></span><span>push</span><span><span>(</span><span>name</span><span>);</span></span></div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span>  </span><span>return</span><span><span> { </span><span>cats</span><span>, </span><span>tags</span><span> };</span></span></div></div><div><div><div>38</div></div><div><span>}</span></div></div><div><div><div>39</div></div><div>
</div></div><div><div><div>40</div></div><div><span>export</span><span> </span><span>function</span><span> </span><span>parseWxr</span><span><span>(</span><span>xmlPath</span><span>) {</span></span></div></div><div><div><div>41</div></div><div><span>  </span><span>const</span><span> </span><span>xml</span><span> </span><span>=</span><span><span> </span><span>fs</span><span>.</span></span><span>readFileSync</span><span><span>(</span><span>xmlPath</span><span>, </span></span><span>"utf8"</span><span>);</span></div></div><div><div><div>42</div></div><div><span>  </span><span>const</span><span> </span><span>doc</span><span> </span><span>=</span><span><span> </span><span>parser</span><span>.</span></span><span>parse</span><span><span>(</span><span>xml</span><span>);</span></span></div></div><div><div><div>43</div></div><div><span>  </span><span>const</span><span> </span><span>channel</span><span> </span><span>=</span><span><span> </span><span>doc</span><span>.</span></span><span>rss</span><span>.</span><span>channel</span><span>;</span></div></div><div><div><div>44</div></div><div><span>  </span><span>const</span><span> </span><span>attachmentsById</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>Map</span><span>();</span></div></div><div><div><div>45</div></div><div><span>  </span><span>const</span><span> </span><span>posts</span><span> </span><span>=</span><span> [];</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>  </span><span>for</span><span> (</span><span>const</span><span> </span><span>item</span><span> </span><span>of</span><span><span> </span><span>channel</span><span>.</span></span><span>item</span><span> </span><span>||</span><span> []) {</span></div></div><div><div><div>48</div></div><div><span>    </span><span>const</span><span> </span><span>type</span><span> </span><span>=</span><span> </span><span>textOf</span><span><span>(</span><span>item</span><span>[</span></span><span>"wp:post_type"</span><span>]);</span></div></div><div><div><div>49</div></div><div><span>    </span><span>const</span><span> </span><span>id</span><span> </span><span>=</span><span> </span><span>textOf</span><span><span>(</span><span>item</span><span>[</span></span><span>"wp:post_id"</span><span>]);</span></div></div><div><div><div>50</div></div><div><span>    </span><span>if</span><span><span> (</span><span>type</span><span> </span></span><span>===</span><span> </span><span>"attachment"</span><span>) {</span></div></div><div><div><div>51</div></div><div><span>      </span><span>const</span><span> </span><span>url</span><span> </span><span>=</span><span> </span><span>textOf</span><span><span>(</span><span>item</span><span>[</span></span><span>"wp:attachment_url"</span><span>]) </span><span>||</span><span> </span><span>textOf</span><span><span>(</span><span>item</span><span>.</span></span><span>guid</span><span>) </span><span>||</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>52</div></div><div><span><span>      </span></span><span>attachmentsById</span><span>.</span><span>set</span><span><span>(</span><span>id</span><span>, </span><span>url</span><span>);</span></span></div></div><div><div><div>53</div></div><div><span>      </span><span>continue</span><span>;</span></div></div><div><div><div>54</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>55</div></div><div><span>    </span><span>if</span><span><span> (</span><span>type</span><span> </span></span><span>!==</span><span> </span><span>"post"</span><span>) </span><span>continue</span><span>;</span></div></div><div><div><div>56</div></div><div><span>    </span><span>if</span><span> (</span><span>textOf</span><span><span>(</span><span>item</span><span>[</span></span><span>"wp:status"</span><span>]) </span><span>!==</span><span> </span><span>"publish"</span><span>) </span><span>continue</span><span>;</span></div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span>    </span><span>const</span><span> { </span><span>cats</span><span>, </span><span>tags</span><span> } </span><span>=</span><span> </span><span>categoriesOf</span><span><span>(</span><span>item</span><span>);</span></span></div></div><div><div><div>59</div></div><div><span>    </span><span>const</span><span> </span><span>meta</span><span> </span><span>=</span><span> </span><span>metaMap</span><span><span>(</span><span>item</span><span>[</span></span><span>"wp:postmeta"</span><span>]);</span></div></div><div><div><div>60</div></div><div><span><span>    </span></span><span>posts</span><span>.</span><span>push</span><span>({</span></div></div><div><div><div>61</div></div><div><span><span>      </span></span><span>id</span><span>,</span></div></div><div><div><div>62</div></div><div><span>      </span><span>title</span><span>:</span><span> </span><span>textOf</span><span><span>(</span><span>item</span><span>.</span></span><span>title</span><span>),</span></div></div><div><div><div>63</div></div><div><span>      </span><span>slug</span><span>:</span><span> </span><span>textOf</span><span><span>(</span><span>item</span><span>[</span></span><span>"wp:post_name"</span><span>]),</span></div></div><div><div><div>64</div></div><div><span>      </span><span>date</span><span>:</span><span> </span><span>textOf</span><span><span>(</span><span>item</span><span>[</span></span><span>"wp:post_date"</span><span>]),</span></div></div><div><div><div>65</div></div><div><span>      </span><span>modified</span><span>:</span><span> </span><span>textOf</span><span><span>(</span><span>item</span><span>[</span></span><span>"wp:post_modified"</span><span>]),</span></div></div><div><div><div>66</div></div><div><span>      </span><span>excerpt</span><span>:</span><span> </span><span>textOf</span><span><span>(</span><span>item</span><span>[</span></span><span>"excerpt:encoded"</span><span>]),</span></div></div><div><div><div>67</div></div><div><span>      </span><span>content</span><span>:</span><span> </span><span>textOf</span><span><span>(</span><span>item</span><span>[</span></span><span>"content:encoded"</span><span>]),</span></div></div><div><div><div>68</div></div><div><span><span>      </span></span><span>cats</span><span>,</span></div></div><div><div><div>69</div></div><div><span><span>      </span></span><span>tags</span><span>,</span></div></div><div><div><div>70</div></div><div><span>      </span><span>thumbnailId</span><span>:</span><span><span> </span><span>meta</span><span>.</span></span><span>_thumbnail_id</span><span> </span><span>||</span><span> </span><span>""</span><span>,</span></div></div><div><div><div>71</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>72</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>73</div></div><div>
</div></div><div><div><div>74</div></div><div><span>  </span><span>return</span><span> {</span></div></div><div><div><div>75</div></div><div><span>    </span><span>site</span><span>:</span><span> {</span></div></div><div><div><div>76</div></div><div><span>      </span><span>title</span><span>:</span><span> </span><span>textOf</span><span><span>(</span><span>channel</span><span>.</span></span><span>title</span><span>),</span></div></div><div><div><div>77</div></div><div><span>      </span><span>link</span><span>:</span><span> </span><span>textOf</span><span><span>(</span><span>channel</span><span>.</span></span><span>link</span><span>),</span></div></div><div><div><div>78</div></div><div><span>      </span><span>description</span><span>:</span><span> </span><span>textOf</span><span><span>(</span><span>channel</span><span>.</span></span><span>description</span><span>),</span></div></div><div><div><div>79</div></div><div><span><span>    </span></span><span>},</span></div></div><div><div><div>80</div></div><div><span><span>    </span></span><span>attachmentsById</span><span>,</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>posts</span><span>,</span></div></div><div><div><div>82</div></div><div><span><span>  </span></span><span>};</span></div></div><div><div><div>83</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><div><div><div><figure><figcaption><span>html-to-md.mjs</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> </span><span>TurndownService</span><span> </span><span>from</span><span> </span><span>"turndown"</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>const</span><span> </span><span>TARGET_PREFIX</span><span> </span><span>=</span><span> </span><span>"https://muxui.com/uploads/"</span><span>;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>export</span><span> </span><span>function</span><span> </span><span>rewriteImageUrls</span><span><span>(</span><span>text</span><span>) {</span></span></div></div><div><div><div>6</div></div><div><span>  </span><span>return</span><span><span> </span><span>text</span></span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span><span>/https</span><span>?</span><span>:</span><span>\/\/</span><span>img</span><span>\.</span><span>roven</span><span>\.</span><span>cc</span><span>\/</span><span>images</span><span>\/</span><span>/</span></span><span>g</span><span>, </span><span>TARGET_PREFIX</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span><span>/https</span><span>?</span><span>:</span><span>\/\/</span><span>muxui</span><span>\.</span><span>com</span><span>\/</span><span>wp-content</span><span>\/</span><span>uploads</span><span>\/</span><span>/</span></span><span>g</span><span>, </span><span>TARGET_PREFIX</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span>/(</span><span><span>["'(=</span><span>\s</span><span>]</span></span><span>|</span><span>^</span><span><span>)</span><span>\/</span><span>wp-content</span><span>\/</span><span>uploads</span><span>\/</span><span>/</span></span><span>g</span><span>, </span><span>`$1</span><span>${</span><span>TARGET_PREFIX</span><span>}</span><span>`</span><span>);</span></div></div><div><div><div>10</div></div><div><span>}</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>function</span><span> </span><span>stripShortcodes</span><span><span>(</span><span>html</span><span>) {</span></span></div></div><div><div><div>13</div></div><div><span>  </span><span>return</span><span><span> </span><span>html</span></span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\[\/</span><span>?</span><span>b2</span></span><span>[</span><span><span>^</span><span>\]</span></span><span>]</span><span><span>*</span><span>\]</span><span>/</span></span><span>gi</span><span>, </span><span>""</span><span>)</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\[</span><span>hide</span></span><span>[</span><span><span>^</span><span>\]</span></span><span>]</span><span><span>*</span><span>\]</span><span>(</span></span><span><span>[</span><span>\s\S</span><span>]</span></span><span><span>*?</span><span>)</span><span>\[\/</span><span>hide</span><span>\]</span><span>/</span></span><span>gi</span><span>, </span><span>"$1"</span><span>)</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>.</span><span>replace</span><span>(</span><span><span>/</span><span>\[</span></span><span>.</span><span><span>*?</span><span>\]</span><span>/</span></span><span>g</span><span><span>, (</span><span>m</span><span>) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>17</div></div><div><span>      </span><span>const</span><span> </span><span>href</span><span> </span><span>=</span><span><span> </span><span>m</span><span>.</span></span><span>match</span><span>(</span><span><span>/https</span><span>?</span><span>:</span><span>\/\/</span></span><span>[</span><span>^</span><span>\s</span><span>\]</span><span>]</span><span><span>+</span><span>/</span></span><span>);</span></div></div><div><div><div>18</div></div><div><span>      </span><span>return</span><span><span> </span><span>href</span><span> </span></span><span>?</span><span> </span><span>`&lt;p&gt;&lt;a href="</span><span>${</span><span><span>href</span><span>[</span></span><span>0</span><span>]</span><span>}</span><span>"&gt;</span><span>${</span><span><span>href</span><span>[</span></span><span>0</span><span>]</span><span>}</span><span>&lt;/a&gt;&lt;/p&gt;`</span><span> </span><span>:</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>});</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>export</span><span> </span><span>function</span><span> </span><span>htmlToMarkdown</span><span><span>(</span><span>html</span><span>) {</span></span></div></div><div><div><div>23</div></div><div><span>  </span><span>const</span><span> </span><span>cleaned</span><span> </span><span>=</span><span> </span><span>stripShortcodes</span><span>(</span><span>rewriteImageUrls</span><span><span>(</span><span>html</span><span>));</span></span></div></div><div><div><div>24</div></div><div><span>  </span><span>const</span><span> </span><span>td</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>TurndownService</span><span>({</span></div></div><div><div><div>25</div></div><div><span>    </span><span>headingStyle</span><span>:</span><span> </span><span>"atx"</span><span>,</span></div></div><div><div><div>26</div></div><div><span>    </span><span>codeBlockStyle</span><span>:</span><span> </span><span>"fenced"</span><span>,</span></div></div><div><div><div>27</div></div><div><span>    </span><span>bulletListMarker</span><span>:</span><span> </span><span>"-"</span><span>,</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>});</span></div></div><div><div><div>29</div></div><div><span><span>  </span></span><span>td</span><span>.</span><span>keep</span><span>([</span><span>"iframe"</span><span>, </span><span>"video"</span><span>, </span><span>"table"</span><span>]);</span></div></div><div><div><div>30</div></div><div><span>  </span><span>return</span><span><span> </span><span>td</span><span>.</span></span><span>turndown</span><span><span>(</span><span>cleaned</span><span>).</span></span><span>trim</span><span>();</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section>
<section><h2>第一步：从 WordPress 导出内容<a href="#第一步从-wordpress-导出内容"><span>#</span></a></h2><p>后台路径：<strong>工具 → 导出 → 全部内容</strong>，得到 WXR（WordPress eXtended RSS）XML。</p><p>我这份导出里大致有：</p><ul>
<li>已发布文章 <strong>23</strong> 篇（另有 1 篇草稿跳过）</li>
<li>附件约 <strong>181</strong> 个（主要用于封面 ID → URL 映射）</li>
<li>以及页面、友链、圈子、快讯等 B2 扩展类型</li>
</ul><p>第一期只迁 <strong>已发布文章 + 站点基础信息</strong>；友链、页脚备案、导航定制是迁完内容后再单独处理的。</p><blockquote><p>建议：导出后先把 XML 放到仓库根目录本地用，<strong>不要轻易提交到公开仓库</strong>（体积大，也可能含邮件等隐私字段）。</p></blockquote><hr /></section>
<section><h2>第二步：写一个可复跑的迁移脚本<a href="#第二步写一个可复跑的迁移脚本"><span>#</span></a></h2><p>手工复制 23 篇不现实，所以用 Node 脚本一次性完成：</p><ol>
<li>用 <code>fast-xml-parser</code> 解析 WXR</li>
<li>建立 <code>attachment_id → URL</code> 索引（封面 <code>_thumbnail_id</code>）</li>
<li>过滤 <code>post_type=post</code> 且 <code>status=publish</code></li>
<li>Gutenberg HTML → Markdown（<code>turndown</code>）</li>
<li>改写图片域名</li>
<li>写出 Firefly frontmatter + <code>.md</code> 文件</li>
<li>更新 <code>siteConfig.ts</code>，删掉主题自带 Demo 文</li>
</ol><p>核心映射关系：</p>

<table><thead><tr><th>WordPress</th><th>Firefly</th></tr></thead><tbody><tr><td>title</td><td><code>title</code></td></tr><tr><td>post_name（URL 解码）</td><td>文件名 / <code>slug</code></td></tr><tr><td>post_date</td><td><code>published</code></td></tr><tr><td>post_modified</td><td><code>updated</code></td></tr><tr><td>category</td><td><code>category</code>（优先具体分类，弱化泛化的「文章」）</td></tr><tr><td>post_tag</td><td><code>tags</code></td></tr><tr><td>excerpt / 正文截取</td><td><code>description</code></td></tr><tr><td><code>_thumbnail_id</code> → attachment</td><td><code>image</code>（绝对 URL）</td></tr><tr><td>content</td><td>Markdown 正文</td></tr></tbody></table><p>图片统一规则：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>https://muxui.com/wp-content/uploads/{path}</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>→ https://muxui.com/uploads/{path}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>相对路径 <code>/wp-content/uploads/...</code> 同样改写；<strong>外站</strong>自带的 <code>/wp-content/uploads/</code>（例如引用别人站点的图）不要动，否则会出现：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>https://www.example.comhttps://muxui.com/uploads/...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这种拼接错乱——我第一次写正则时就踩过。</p><p>运行方式：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>pnpm</span><span> </span><span>add</span><span> </span><span>-D</span><span> </span><span>turndown</span><span> </span><span>fast-xml-parser</span></div></div><div><div><div>2</div></div><div><span>node</span><span> </span><span>scripts/migrate-wordpress.mjs</span></div></div><div><div><div>3</div></div><div><span>pnpm</span><span> </span><span>check</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section>
<section><h2>第三步：处理 B2 / Gutenberg 的「脏内容」<a href="#第三步处理-b2--gutenberg-的脏内容"><span>#</span></a></h2><p>纯 Markdown 博客迁出去通常很干净；B2 + Gutenberg 会带上：</p><ul>
<li>区块注释：<code>&lt;!-- wp:paragraph --&gt;</code></li>
<li>短代码：隐藏内容、下载框、卡片等</li>
<li>偶发残缺 HTML、外链图片</li>
</ul><p>策略是 <strong>可读优先，不追求像素级还原</strong>：</p><ul>
<li>Turndown 处理常见标题、列表、链接、代码块、表格</li>
<li><code>[hide]...[/hide]</code> 之类尽量展开正文</li>
<li>无法可靠还原的短代码，降级成链接或直接丢掉，保证构建不炸</li>
</ul><p>迁完后务必抽查：</p><ul>
<li>图多的文章（封面 + 正文图是否都是 <code>muxui.com/uploads</code>）</li>
<li>代码长文（Markdown 代码围栏是否完整）</li>
<li>带外链配图的文章（外站 URL 是否被误伤）</li>
</ul><hr /></section>
<section><h2>第四步：站点身份与页脚<a href="#第四步站点身份与页脚"><span>#</span></a></h2><p><code>siteConfig.ts</code> 里至少改这些：</p><ul>
<li><code>title</code> / <code>navbar.title</code> → Muxui</li>
<li><code>subtitle</code> / <code>description</code> → 「记录美好生活！」</li>
<li><code>site_url</code> → <code>https://muxui.com</code></li>
</ul><p>页脚版权与备案：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Copyright © 2026 Muxui ・ 蜀ICP备2024080231号-1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>备案号放在 <code>FooterConfig.html</code>，并链到工信部查询页 <code>https://beian.miit.gov.cn/</code>。侧栏显示名同步改成 Muxui。</p><hr /></section>
<section><h2>第五步：导航按「文章 / 软件」分组<a href="#第五步导航按文章--软件分组"><span>#</span></a></h2><p>WordPress 时代分类较杂。迁到 Firefly 后，导航做成：</p><p><strong>主要 · 文章 · 软件 · 工具 · 友链 · 留言 · 关于我</strong></p><p>其中：</p><ul>
<li><strong>文章</strong> 下拉：归档 / 分类 / 标签 —— 只覆盖<br />
<code>文章、Java、Python、PHP、Vue、jQuery</code></li>
<li><strong>软件</strong> 下拉：归档 / 分类 / 标签 —— 只覆盖<br />
<code>软件、Windows、Mac、IOS</code></li>
<li><strong>工具</strong>：外链 <code>https://muxui.com/tools</code>，新窗口打开</li>
</ul><p>实现上用配置文件声明分组，例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>export</span><span> </span><span>const</span><span> </span><span>CONTENT_GROUPS</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>article</span><span>:</span><span> {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>id</span><span>:</span><span> </span><span>"article"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>    </span><span>name</span><span>:</span><span> </span><span>"文章"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>categories</span><span>:</span><span> [</span><span>"文章"</span><span>, </span><span>"Java"</span><span>, </span><span>"Python"</span><span>, </span><span>"PHP"</span><span>, </span><span>"Vue"</span><span>, </span><span>"jQuery"</span><span>],</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>7</div></div><div><span>  </span><span>software</span><span>:</span><span> {</span></div></div><div><div><div>8</div></div><div><span>    </span><span>id</span><span>:</span><span> </span><span>"software"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>name</span><span>:</span><span> </span><span>"软件"</span><span>,</span></div></div><div><div><div>10</div></div><div><span>    </span><span>categories</span><span>:</span><span> [</span><span>"软件"</span><span>, </span><span>"Windows"</span><span>, </span><span>"Mac"</span><span>, </span><span>"IOS"</span><span>],</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>12</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>归档：<code>/archive/?category=A&amp;category=B&amp;...</code>（Firefly 的 ArchivePanel 支持多 <code>category</code> OR 筛选）</li>
<li>分类页：<code>/categories/article/</code>、<code>/categories/software/</code></li>
<li>标签页：<code>/tags/article/</code>、<code>/tags/software/</code>（标签点击时带上同组 category，避免串到另一边）</li>
</ul><p>相册、追番、番组、打赏、音乐等与个人站无关的入口全部关掉，避免空页面。</p><hr /></section>
<section><h2>第六步：友链迁移与入驻说明<a href="#第六步友链迁移与入驻说明"><span>#</span></a></h2><p>WordPress 里 <code>post_type=links</code> 很多，真正「友情链接」分类只有几条；AI / 网络工具那些不算友链，不要混进 Firefly 的 <code>friendsConfig</code>。</p><p>最终保留例如：</p><ul>
<li>朱某的生活印记</li>
<li>小栋博客</li>
<li>handsome</li>
</ul><p>并在 <code>src/content/spec/friends.mdx</code> 写清：</p><p><strong>本站信息</strong></p><ul>
<li>名称：Muxui</li>
<li>介绍：记录个人生活点滴与技术笔记……</li>
<li>地址：<code>https://muxui.com</code></li>
<li>Logo：<code>https://muxui.com/favicon.ico</code></li>
</ul><p><strong>入驻说明要点</strong></p><ol>
<li>必须完成国内 ICP 备案</li>
<li>欢迎个人博客、WordPress 类同类站互换</li>
<li>首页链接需双方同等待遇</li>
<li>提交后请联系博主；未通过会在留言板回应</li>
<li>内容须合法合规</li>
<li>定期检查死链；对方取消互链则删除且不另行通知</li>
<li>请先在贵站加好本站，再按模板申请</li>
</ol><hr /></section>
<section><h2>第七步：评论迁移（Twikoo）<a href="#第七步评论迁移twikoo"><span>#</span></a></h2><p>文章迁到 Markdown <strong>不会</strong>带走 WordPress 评论。评论在 MySQL 里（或通过 WXR 里的 <code>wp:comment</code> 节点导出），需要<strong>单独</strong>接到静态站用的评论服务上。</p><p>Muxui 选用 <strong><a href="https://twikoo.js.org/" target="_blank">Twikoo</a></strong>：后端自建（Docker），前端在 Firefly 里通过 <code>commentConfig.ts</code> 加载；评论数据与 Astro 构建产物无关，换主题、重新部署静态站都不会冲掉评论库。</p><section><h3>7.1 先搞清楚「评论挂在哪」<a href="#71-先搞清楚评论挂在哪"><span>#</span></a></h3><p>Firefly 给 Twikoo 传的 <strong><code>path</code></strong> 规则是（见 <code>src/components/comment/index.astro</code>）：</p><ul>
<li>普通文章：<code>/posts/{文件名去掉.md}</code>，例如 <code>src/content/posts/hello-world.md</code> → <code>/posts/hello-world</code></li>
<li>留言板等特殊页：用 <code>customPath</code>（如 <code>/guestbook/</code>）</li>
</ul><p>Twikoo 按这个 <strong>path（或与之对应的完整 URL）</strong> 把评论和页面绑在一起。<br />
因此导入历史评论时，每条记录的 <strong><code>url</code> / 页面标识</strong> 必须和<strong>现在线上文章地址一致</strong>。若你后来改过 Markdown 文件名（标题重命名），旧 WordPress 链接 <code>https://muxui.com/某旧slug/</code> 和新站 <code>/posts/某新文件名/</code> 对不上，评论会「挂在旧地址上」，页面上就看不到——需要在导入 JSON 里做 <strong>slug 映射</strong>，或导入后再在 Twikoo 后台/database 里批量改 <code>url</code>（后者更折腾，建议导入前改好）。</p><p>WordPress 常见 permalink 是 <code>/%postname%/</code>；迁到 Firefly 后多是 <code>/posts/{slug}/</code>。<strong>permalink 结构变了，评论迁移的核心就是 path 映射表。</strong></p></section><section><h3>7.2 静态站侧要做的配置<a href="#72-静态站侧要做的配置"><span>#</span></a></h3><ol>
<li>部署 Twikoo 服务（官方 Docker 即可；可用 <strong>LokiJS 本地库</strong>，不必强上 MongoDB，个人站够用）。</li>
<li><code>src/config/commentConfig.ts</code>：</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>export</span><span> </span><span>const</span><span> </span><span>commentConfig</span><span>:</span><span> </span><span>CommentConfig</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>type</span><span>:</span><span> </span><span>"twikoo"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>twikoo</span><span>:</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>envId</span><span>:</span><span> </span><span>"https://你的-twikoo-域名或IP:端口"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>    </span><span>lang</span><span>:</span><span> </span><span>"zh-CN"</span><span>,</span></div></div><div><div><div>6</div></div><div><span>    </span><span>visitorCount</span><span>:</span><span> </span><span>true</span><span>,</span></div></div><div><div><div>7</div></div><div><span>    </span><span>jsUrl</span><span>:</span></div></div><div><div><div>8</div></div><div><span>      </span><span>"https://registry.npmmirror.com/twikoo/1.7.14/files/dist/twikoo.min.js"</span><span>,</span></div></div><div><div><div>9</div></div><div><span>    </span><span>cssUrl</span><span>:</span><span> </span><span>"/assets/css/twikoo-custom.css"</span><span>,</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>11</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ol>
<li>文章 frontmatter 里 <code>comment: true</code>（迁移脚本里已默认写上）才会显示评论区。</li>
<li>本站使用 <strong>Swup</strong> 无刷新切换时，需保证 Twikoo 在换页后按<strong>当前文章 path</strong> 重新 <code>init</code>（Firefly 已在 <code>Twikoo.astro</code> 里用 <code>data-twikoo-path</code> 处理；若自改主题，注意不要沿用上一篇的 path）。</li>
</ol></section><section><h3>7.3 从 WordPress 导出评论数据<a href="#73-从-wordpress-导出评论数据"><span>#</span></a></h3><p>WXR 里每个 <code>&lt;item&gt;</code>（文章）下可带多条 <code>wp:comment</code>，字段大致包括：</p><ul>
<li>评论内容、<code>wp:comment_author</code>、<code>wp:comment_author_email</code>、<code>wp:comment_author_url</code></li>
<li><code>wp:comment_date</code>、<code>wp:comment_parent</code>（楼中楼）</li>
<li>所属文章由该 <code>&lt;item&gt;</code> 的 <code>wp:post_id</code> / <code>wp:post_name</code> 决定</li>
</ul><p><strong>做法</strong>：在解析 WXR 时除了正文，再扫一遍 <code>wp:comment</code>，按 <code>post_name</code>（或你映射后的新 slug）分组，转成 Twikoo 可导入的 JSON。<br />
（与正文迁移一样，这一步适合写一次性 Node 脚本；不必进 Git，本地跑完即可。）</p><p>Twikoo 侧常见导入方式：</p>

<table><thead><tr><th>方式</th><th>说明</th></tr></thead><tbody><tr><td>后台「导入」</td><td>若你<strong>以前就用 Twikoo / Valine / Waline</strong> 等，可直接按官方文档选对应格式导入</td></tr><tr><td><strong>自写 JSON + 导入接口 / 后台</strong></td><td>从 WordPress 转出来的数据，一般要按 Twikoo 文档的字段整理；Muxui 走的是「WXR → 转换 → Twikoo JSON → 再导入」</td></tr><tr><td>放弃历史评论</td><td>只保留新评论，最省事</td></tr></tbody></table><p>没有「官方一键 WordPress → Twikoo」按钮；<strong>不能完美自动</strong>时，接受只迁近 N 条或只迁留言板，也合理。</p></section><section><h3>7.4 导入时要注意的坑（实测）<a href="#74-导入时要注意的坑实测"><span>#</span></a></h3><p>下面这些是我迁 Muxui 时踩过的，和「能不能在页面上看到评论」直接相关：</p><ol>
<li>
<p><strong><code>url</code> / path 必须与线上一致</strong><br />
少写 <code>/posts</code> 前缀、多了尾部斜杠不一致、或仍用 WordPress 旧 slug，都会导致评论列表为空。</p>
</li>
<li>
<p><strong>楼中楼字段 <code>rid</code>（回复目标 id）</strong><br />
使用 LokiJS 存储时，若子评论的 <code>rid</code> 留成非法空串，可能导致接口报错、整页评论拉不出来。导入时要嘛按父子关系填对目标评论 id，要嘛先把历史评论压平成一层（无回复），再慢慢补。</p>
</li>
<li>
<p><strong>Twikoo 版本与 <code>jsUrl</code> 对齐</strong><br />
前端 <code>commentConfig.twikoo.jsUrl</code> 建议与后端镜像版本一致（例如 <strong>1.7.14</strong>），避免接口字段不匹配。</p>
</li>
<li>
<p><strong>人机验证</strong><br />
若在 Twikoo 管理端开了 <strong>Turnstile</strong> 等验证，必须在后台配好 Site Key / Secret；否则前台会一直报错。个人站可先关验证，稳定后再开。</p>
</li>
<li>
<p><strong>邮箱、头像、IP</strong><br />
WordPress 里的 Gravatar、B2 扩展字段不会自动变成 Twikoo 头像；能迁的主要是昵称、邮箱（可选）、正文、时间。敏感信息导入前可自行脱敏。</p>
</li>
</ol></section><section><h3>7.5 验证清单<a href="#75-验证清单"><span>#</span></a></h3><ul>
<li> 任选一篇<strong>确定有旧评论</strong>的文章，打开 <code>/posts/xxx/</code>，能看到列表</li>
<li> 新开一条测试评论，刷新后仍在（说明写库正常）</li>
<li> Swup 从首页点进另一篇文章，评论区换成该文（不是上一篇残留）</li>
<li> 留言板 <code>/guestbook/</code> 若启用评论，path 为 <code>customPath</code>，与文章不同</li>
<li> 管理端 <code>https://你的-envId</code> 能登录，评论数与预期接近</li>
</ul><p>若某篇文章 WordPress 时期评论为 0，迁完后为空是正常现象，不是失败。</p></section><section><h3>7.6 做不到或不值得做的<a href="#76-做不到或不值得做的"><span>#</span></a></h3><ul>
<li><strong>和 WP 后台一模一样的审核流、通知邮件、会员等级评论</strong>：Twikoo 另一套逻辑，别指望 1&lt;1&gt;。</li>
<li><strong>B2 付费隐藏、仅登录可见评论</strong>：静态站 + Twikoo 需重新设计，不能靠迁库解决。</li>
<li><strong>spam 评论、垃圾数据</strong>：建议在 WXR 转 JSON 时过滤 <code>comment_approved != 1</code> 的条目。</li>
<li><strong>精确还原每一条楼中楼</strong>：父评论 id 映射写错时，宁可先扁平化，也不要导入半残数据把库搞挂。</li>
</ul><hr /></section></section>
<section><h2>迁移清单（可直接照抄）<a href="#迁移清单可直接照抄"><span>#</span></a></h2><ul>
<li> WordPress 导出 WXR，本地备份数据库与 <code>uploads</code></li>
<li> Fork / Clone Firefly，建自己的分支</li>
<li> 图床已托管旧路径（或准备批量同步 <code>uploads</code> → 图床）</li>
<li> 写迁移脚本：解析 → 转 MD → 改图床 → 写 frontmatter</li>
<li> 删除 Demo 文，更新 <code>siteConfig</code> / <code>profileConfig</code></li>
<li> <code>pnpm check</code> / <code>pnpm build</code> 通过</li>
<li> 抽查图多文、代码文、外链图</li>
<li> 导航、友链、页脚备案、无用页面开关</li>
<li> Twikoo 部署 + <code>commentConfig</code>；核对每篇文章 <code>comment: true</code></li>
<li> WXR 评论 → Twikoo JSON（含 <strong>旧 slug → 新 <code>/posts/...</code> path</strong> 映射）</li>
<li> 抽查有历史评论的文章 + Swup 换页后评论区</li>
<li> DNS / CDN 切到静态托管（Vercel / Cloudflare Pages / 自建 Nginx 等）</li>
<li> 旧链接 301（若 URL 结构变化）</li>
</ul><hr /></section>
<section><h2>踩坑摘要<a href="#踩坑摘要"><span>#</span></a></h2><ol>
<li>
<p><strong>图片正则过宽</strong><br />
不要匹配所有站点的 <code>/wp-content/uploads/</code>，只改自己的域名和站内相对路径。</p>
</li>
<li>
<p><strong>单分类限制</strong><br />
Firefly 一篇文章一个 <code>category</code>。WordPress 多分类时要定优先级（例如去掉泛化「文章」后取第一个具体分类），否则导航分组会对不齐。</p>
</li>
<li>
<p><strong>中文 slug</strong><br />
<code>post_name</code> 可能是百分号编码，写入文件前要 <code>decodeURIComponent</code>；Windows 文件名还要避开非法字符。</p>
</li>
<li>
<p><strong>短代码不可完美还原</strong><br />
付费隐藏、下载框等接受降级，比硬解析到构建失败更划算。</p>
</li>
<li>
<p><strong>主题功能默认全开</strong><br />
Firefly Demo 带追番、相册、音乐等，个人站要主动关，不然导航和侧栏很「热闹」。</p>
</li>
<li>
<p><strong>评论 path 与文件名绑定</strong><br />
改 Markdown 文件名会改 URL，历史评论若仍挂在旧 path 上会「消失」；迁移或重命名后要统一改 Twikoo 里的 <code>url</code>，或导入时就用新 path。</p>
</li>
</ol><hr /></section>
<section><h2>结果与后续<a href="#结果与后续"><span>#</span></a></h2><p>迁移完成后：</p><ul>
<li>23 篇正文以 Markdown 落在 Git 里，可版本管理、可 PR 审稿</li>
<li>图片走自有图床，站点本体静态分发</li>
<li>导航按「文章 / 软件」内容域拆分，比 WordPress 默认分类页更符合阅读习惯</li>
<li>友链与入驻规则与旧站对齐</li>
<li>评论挂在 Twikoo，与静态构建解耦；历史评论靠 WXR + 字段映射导入</li>
</ul><p>后续还可以做：</p><ul>
<li>旧 permalink → 新 <code>/posts/{slug}/</code> 的 301 表（<strong>评论 path 映射最好与 301 表共用同一份 slug 对照</strong>）</li>
<li>把「WXR 评论 → Twikoo JSON」收成可复用脚本（若你还会再迁站）</li>
</ul><hr /></section>
<section><h2>结语<a href="#结语"><span>#</span></a></h2><p>WordPress 适合「什么都想装一点」的阶段；当站点目标收敛成 <strong>写文章、存笔记、换友链</strong> 时，Astro 静态站往往更省心。</p><p>这次 Muxui 的路径可以概括成一句话：</p><blockquote><p><strong>WXR 导出 → 脚本转 Markdown → 图床改写 → Firefly 配置本地化 → Twikoo 接评论（path 对齐 + 导入）。</strong></p></blockquote><p>脚本和配置都在自己的 Git 仓库里，下次换机器或重建环境，只要拉代码、装依赖、构建即可。</p><p>如果你也在迁站，欢迎在留言板交流你的导出格式和坑点——尤其是不同主题的短代码，大家踩的都不太一样。</p></section>
<section><h2>相关阅读<a href="#相关阅读"><span>#</span></a></h2><a href="/posts/alipay-face-to-face-donation-page/"><div><div>支付宝面对面对接个人赞赏页</div><div>📌 项目背景 ​ 在个人博客/自媒体场景中，接收读者小额赞赏是常见的互动方式。基于支付宝的「面对面支付」接口，我开发了一个轻量级赞赏页，支持金额选择、动态二维码生成、捐赠记录展示等功能。核心代码基于PHP+MySQL实现，无需依赖复杂框架</div><div><span>2025-02-25</span><span>原创</span><span><span>#Alipay</span><span>#PHP</span><span>#jQuery</span></span></div></div><div><img src="https://s.muxui.com:8015/images/2026/09/UO34wYhfT5KT.webp" alt="" loading="lazy" /></div></a><a href="https://github.com/CuteLeaf/Firefly" target="_blank"><div><div><div><div></div><div>CuteLeaf</div></div><div>/</div><div>Firefly</div></div><div></div></div><div>🍀Firefly, fresh and aesthetic Astro blog theme template. </div><div><div>—</div><div>—</div><div>MIT</div><span>Astro</span></div></a></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/yolo-mahjong-onnx-export/</id>
      <title type="text">YOLO训练麻将(Mahjong)识别,导出ONNX</title>
      <published>2026-04-20T00:00:00.000Z</published>
      <updated>2026-04-24T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/yolo-mahjong-onnx-export/"/>
      <summary type="text">本文基于 Roboflow Universe 数据集（例如 $1 ）与仓库内脚本，在 Windows 上完成训练，并导出 ONNX 供后端推理。下文配图均为本机实测截图，便于对照。快速体验 小程序 1\. 环境要求 \ Windows 10</summary>
      <content type="html"><![CDATA[<blockquote><p>本文基于 <strong>Roboflow Universe</strong> 数据集（例如</p><p>[http://(<a href="https://universe.roboflow.com/jon-chan-gnsoa/mahjong-baq4s%22%5D(http://(" target="_blank">https://universe.roboflow.com/jon-chan-gnsoa/mahjong-baq4s”](http://(</a><a href="https://universe.roboflow.com/jon-chan-gnsoa/mahjong-baq4s" target="_blank">https://universe.roboflow.com/jon-chan-gnsoa/mahjong-baq4s</a>)</p><p>）与仓库内脚本，在 <strong>Windows</strong> 上完成训练，并导出 <strong>ONNX</strong> 供后端推理。下文配图均为本机实测截图，便于对照。快速体验</p><p><img src="https://s.muxui.com:8015/images/2026/09/06WJKxg0YT8t.webp" alt="" /></p><p>小程序</p></blockquote>
<section><h2><strong>1. 环境要求</strong><a href="#1-环境要求"><span>#</span></a></h2><p>- Windows 10 / 11  </p><p>- Python <strong>3.10 或 3.11</strong> </p><p>- <strong>NVIDIA 显卡</strong>：建议已安装 CUDA 驱动；训练阶段 GPU 会持续高占用  </p><p>命令行自检：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>nvidia-smi</span></div></div><div><div><div>2</div></div><div><span>python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'no gpu')"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>训练中若希望直观确认显卡是否在干活，可打开 <strong>**任务管理器 → 性能 → GPU**</strong>。下图为本机训练时 <strong>**RTX 3070**</strong> 的占用：<strong>**利用率接近满载、专用显存占用约数 GB**</strong>，说明计算确实在显卡上跑（温度因机型与散热而异，只要在合理区间即可）。</p><p><img src="https://s.muxui.com:8015/images/2026/09/VMf6QansJiTP.webp" alt="" /></p></section>
<section><h2><strong>2. 安装依赖</strong><a href="#2-安装依赖"><span>#</span></a></h2><p>在 <strong>Mahjong_YOLO 根目录</strong>：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>python -m venv .venv</span></div></div><div><div><div>2</div></div><div><span>.\.venv\Scripts\activate</span></div></div><div><div><div>3</div></div><div><span>python -m pip install --upgrade pip</span></div></div><div><div><div>4</div></div><div><span>pip install -r requirements.txt</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2><strong>3. 数据与类别</strong><a href="#3-数据与类别"><span>#</span></a></h2><p>目录约定（Roboflow 导出常见结构）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>dataset/</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>train/images/    train/labels/</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>valid/images/    valid/labels/</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>test/images/     test/labels/</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>data.yaml</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>- <strong>类别数与名称</strong>以 `dataset/data.yaml` 为准（示例工程为 <strong>42 类</strong>，`nc: 42`）。  </p><p>- 标签为 <strong>YOLO 检测格式</strong>，每行：`class_id x_center y_center width height`（均为 0～1 归一化坐标）。</p></section>
<section><h2><strong>4. 训练前：标签体检</strong><a href="#4-训练前标签体检"><span>#</span></a></h2><p>在根目录、已激活 `.venv`：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>python scripts/check_labels.py --data dataset/data.yaml --num-classes 42 --show-bad-samples 15</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>若误用 <strong>分割标注</strong> 当检测，可能出现大量 `invalid_lines`，可先：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>python scripts/convert_seg_labels_to_det.py --dataset-root dataset</span></div></div><div><div><div>2</div></div><div><span>python scripts/check_labels.py --data dataset/data.yaml --num-classes 42 --show-bad-samples 15</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2><strong>5. 第一轮训练：YOLOv8n 跑通</strong><a href="#5-第一轮训练yolov8n-跑通"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>python scripts/train.py --data dataset/data.yaml --model yolov8n.pt --imgsz 960 --epochs 80 --batch 8 --workers 2 --device 0 --name mahjong_42cls_n</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>显存紧张时可改为更小输入、更小 batch，例如：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>python scripts/train.py --data dataset/data.yaml --model yolov8n.pt --imgsz 640 --epochs 80 --batch 4 --workers 0 --device 0 --name mahjong_42cls_n_safe</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>终端里应能看到：<strong>验证集路径、`save_dir`、优化器、TensorBoard 提示、当前 epoch 进度条、GPU 显存占用、各项 loss</strong> 等。下图示例为 <strong>第 1 轮训练进行中</strong>（路径中的 `mahjong_42cls_n` 即本次 `—name`，具体以你机器打印为准）。</p><p><img src="https://s.muxui.com:8015/images/2026/09/EAyIJ2fQSmNp.webp" alt="" /></p></section>
<section><h2><strong>6. 训练输出目录（务必核对）</strong><a href="#6-训练输出目录务必核对"><span>#</span></a></h2><p>默认输出在：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>runs/detect/&lt;run_name&gt;/</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>`&lt;run_name&gt;` 对应你传入的 `—name`。若同名目录已存在，Ultralytics 往往会自动顺延为 `mahjong_42cls_n2`、`mahjong_42cls_n3` 等，<strong>请以终端里的 `save_dir` 或 `runs/detect` 下实际文件夹为准</strong>。</p><p>常用文件：</p><table><tbody><tr><td>路径</td><td>说明</td></tr><tr><td><code>runs/detect/&lt;run_name&gt;/weights/best.pt</code></td><td>验证集上表现最佳的权重，<strong>导出 ONNX 首选</strong></td></tr><tr><td><code>runs/detect/&lt;run_name&gt;/results.csv</code></td><td>每 epoch 指标，便于脚本汇总或自行画图</td></tr></tbody></table><p>训练结束后可看摘要：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>python scripts/show_train_summary.py --results-csv runs/detect/&lt;run_name&gt;/results.csv</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>训练中另开终端可选：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>python scripts/watch_train.py --results-csv runs/detect/&lt;run_name&gt;/results.csv --interval 5 --idle-timeout 180</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2><strong>7. TensorBoard：曲线与计算图</strong><a href="#7-tensorboard曲线与计算图"><span>#</span></a></h2><p>安装并开启 TensorBoard 日志（若尚未配置）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>pip install tensorboard</span></div></div><div><div><div>2</div></div><div><span>yolo settings tensorboard=True</span></div></div><div><div><div>3</div></div><div><span>tensorboard --logdir runs\detect\&lt;run_name&gt; --port 6006</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>浏览器访问：`<a href="http://localhost:6006%5C%60%E3%80%82%E5%B7%A6%E4%BE%A7" target="_blank">http://localhost:6006\`。左侧</a> <strong>Runs</strong> 中选择对应目录（例如界面中的 `detect/mahjong_42cls_n`）。</p><p><strong>GRAPHS</strong> 页可查看模型数据流概览（input → DetectionModel → output），用于确认当前跑的是检测结构、日志是否挂在本次 run 上：</p><p><img src="https://s.muxui.com:8015/images/2026/09/lN749Hh67gQG.webp" alt="" /></p><p><strong>SCALARS</strong> 页重点看 <strong>lr</strong>（学习率调度是否如预期）、<strong>metrics</strong>（mAP50、mAP50-95、precision、recall）。训练初期曲线通常整体向上；若长期横盘或发散，再回头查数据与超参。</p><p><img src="https://s.muxui.com:8015/images/2026/09/VynrLCvXNN9w.webp" alt="" /></p><p>同一 run 在训练后期，指标会趋于稳定。下图示例中可见 <strong>mAP50、mAP50-95、precision、recall</strong> 等读数（具体数值随数据与随机种子变化，以你本地 `results.csv` 为准）。</p><p><img src="https://s.muxui.com:8015/images/2026/09/IiQ6j1Wn5KH5.webp" alt="" /></p></section>
<section><h2><strong>8. 第二轮（可选）：换更大模型提精度</strong><a href="#8-第二轮可选换更大模型提精度"><span>#</span></a></h2><p>第一轮流程与指标正常后，可再跑例如 YOLOv8s、加长 epoch：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>python scripts/train.py --data dataset/data.yaml --model yolov8s.pt --imgsz 960 --epochs 150 --batch 8 --workers 2 --device 0 --name mahjong_42cls_s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同样注意 <strong>输出目录名是否带 `2` 后缀</strong>。</p></section>
<section><h2><strong>9. 导出 ONNX</strong><a href="#9-导出-onnx"><span>#</span></a></h2><p>将 `—weights` 换成你选定的那次训练的 `best.pt`，<strong>`—imgsz` 与训练一致</strong>，便于与后端输入尺寸（如 `MAHJONG_OCR_IMGSZ`）对齐：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>python scripts/export_onnx.py --weights runs/detect/mahjong_42cls_s/weights/best.pt --imgsz 960 --opset 12 --dynamic</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>产物示例：`artifacts/best.onnx`。类别文件复制给后端使用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>copy tools\label_classes.txt artifacts\classes.txt</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2><strong>## 10. ONNX 快速验证</strong><a href="#-10-onnx-快速验证"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>python scripts/predict_onnx_check.py --onnx artifacts/best.onnx --source dataset/test/images --imgsz 960 --conf 0.35 --iou 0.55 --device 0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>可视化一般在 `artifacts/onnx_check/pred/`。若几乎全空或误检爆炸，先调 `conf`/`iou`，再核对预处理与类别顺序是否与训练一致。</p></section>
<section><h2>开源地址<a href="#开源地址"><span>#</span></a></h2><a href="https://github.com/O96u/Mahjong_YOLO" target="_blank"><div><div><div><div></div><div>O96u</div></div><div>/</div><div>Mahjong_YOLO</div></div><div></div></div><div>YOLO训练麻将(Mahjong)识别,并导出ONNX</div><div><div>—</div><div>—</div><div>no-license</div><span>Python</span></div></a></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/headscale-derp-self-hosted-guide/</id>
      <title type="text">自建 Headscale + DERP 全流程实战记录</title>
      <published>2025-12-25T00:00:00.000Z</published>
      <updated>2026-01-07T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/headscale-derp-self-hosted-guide/"/>
      <summary type="text">记录一次完整、可上线、可长期运行的 Headscale + 自建 DERP 搭建过程。 本文不是“能跑就行”的教程，而是 生产可用、已多客户端验证 的配置方案。 简介 Tailscale（Headscale）就是组建一个大的局域网，可以将你</summary>
      <content type="html"><![CDATA[<blockquote><p>记录一次完整、可上线、可长期运行的 Headscale + 自建 DERP 搭建过程。<br />
本文不是“能跑就行”的教程，而是<strong>生产可用、已多客户端验证</strong>的配置方案。</p></blockquote>
<section><h2>简介<a href="#简介"><span>#</span></a></h2><ul>
<li>Tailscale（Headscale）就是组建一个大的局域网，可以将你手里头的所有设备都拉到这个局域网内，进而使用局域网IP进行互联。</li>
<li>此外，Tailscale（Headscale）还有一个作用就是内网穿透，局域网内的设备之间的访问会通过穿透进行打洞（成功概率挺高），进而实现公网的点对点互联。而且就算打洞失败，也可以利用中转服务器进行互联。</li>
<li>应用场景举例：家里没有公网IP，但是可以通过Tailscale组网和穿透的方式实现在任意网络下对家里设备的访问。</li>
</ul></section>
<section><h2>整体架构说明<a href="#整体架构说明"><span>#</span></a></h2><ul>
<li>使用 <strong>Headscale</strong> 作为 Tailscale 控制平面</li>
<li>自建 <strong>DERP Server</strong>，不依赖官方 DERP</li>
<li>使用 <strong>宝塔 / Nginx 统一终止 TLS</strong></li>
<li>开启 <strong>DERP 防白嫖（verify-clients）</strong></li>
<li>多客户端 <strong>tailscale netcheck</strong> 验证通过</li>
</ul><section><h3>最终架构如下：<a href="#最终架构如下"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Tailscale Client</span></div></div><div><div><div>2</div></div><div><span><span>   </span></span><span>↓</span></div></div><div><div><div>3</div></div><div><span>Headscale（控制平面）</span></div></div><div><div><div>4</div></div><div><span><span>   </span></span><span>↓ 下发 DERP Map</span></div></div><div><div><div>5</div></div><div><span>https://derp.muxui.com/derp.json</span></div></div><div><div><div>6</div></div><div><span><span>   </span></span><span>↓</span></div></div><div><div><div>7</div></div><div><span>DERP Server（derper，HTTP 本地端口）</span></div></div><div><div><div>8</div></div><div><span><span>   </span></span><span>↓</span></div></div><div><div><div>9</div></div><div><span>宝塔 / Nginx（HTTPS 443）</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>DERP Server 搭建<a href="#derp-server-搭建"><span>#</span></a></h2><section><h3>安装最新版GO<a href="#安装最新版go"><span>#</span></a></h3><ul>
<li>更新软件包，安装依赖</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>apt update &amp;&amp; apt upgrade</span></div></div><div><div><div>2</div></div><div><span>apt install -y wget git openssl curl</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>下载最新版GO</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>wget https://go.dev/dl/go1.25.5.linux-amd64.tar.gz</span></div></div><div><div><div>2</div></div><div><span>rm -rf /usr/local/go &amp;&amp; tar -C /usr/local -xzf go1.25.5.linux-amd64.tar.gz</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>目前最新版为1.25.5，后续若有更新则可以去 <a href="https://go.dev/dl/" target="_blank">https://go.dev/dl/</a> 查看最新版并替换下载即可，记得后面的版本号都要改</p><ul>
<li>配置环境变量</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>export PATH=$PATH:/usr/local/go/bin</span></div></div><div><div><div>2</div></div><div><span>go version</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>echo "export PATH=$PATH:/usr/local/go/bin" &gt;&gt; /etc/profile</span></div></div><div><div><div>2</div></div><div><span>source /etc/profile</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>go env -w GO111MODULE=on</span></div></div><div><div><div>2</div></div><div><span>go env -w GOPROXY=https://goproxy.cn,direct</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>安装最新版Derper<a href="#安装最新版derper"><span>#</span></a></h3><ul>
<li>安装</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>go install tailscale.com/cmd/derper@latest</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>拷贝二进制文件</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>mkdir /etc/derp/</span></div></div><div><div><div>2</div></div><div><span>cp ~/go/bin/derper /etc/derp/</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>查看是否拷贝成功</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ls /etc/derp</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>derper 域名部署（宝塔反代模式,国内服务器域名需要备案）<a href="#derper-域名部署宝塔反代模式国内服务器域名需要备案"><span>#</span></a></h3><p>⚠️ <strong>关键原则：DERP 不自己处理 TLS</strong></p><ul>
<li>TLS 由宝塔 / Nginx 负责</li>
<li>derper 只监听本地 HTTP</li>
</ul><section><h4>正确启动参数（手动验证用）：<a href="#正确启动参数手动验证用"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/etc/derp/derper \</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>-a :13445 \</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>--hostname derp.muxui.com \</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>--certmode=off \</span></div></div><div><div><div>5</div></div><div><span><span>  </span></span><span>--stun \</span></div></div><div><div><div>6</div></div><div><span><span>  </span></span><span>--stun-port 3478 \</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>--verify-clients</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>看到以下日志即为成功：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>STUN server listening on [::]:3478</span></div></div><div><div><div>2</div></div><div><span>derper: serving on :13445</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section></section><section><h3>systemd 服务（最终版）<a href="#systemd-服务最终版"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[Unit]</span></div></div><div><div><div>2</div></div><div><span>Description=Tailscale DERP Server</span></div></div><div><div><div>3</div></div><div><span>After=network-online.target</span></div></div><div><div><div>4</div></div><div><span>Wants=network-online.target</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>[Service]</span></div></div><div><div><div>7</div></div><div><span>Type=simple</span></div></div><div><div><div>8</div></div><div><span>User=root</span></div></div><div><div><div>9</div></div><div><span>ExecStart=/etc/derp/derper \</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>-a :13445 \</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>--hostname derp.muxui.com \</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>--certmode=off \</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>--stun \</span></div></div><div><div><div>14</div></div><div><span><span>  </span></span><span>--stun-port 3478 \</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>--verify-clients</span></div></div><div><div><div>16</div></div><div><span>Restart=always</span></div></div><div><div><div>17</div></div><div><span>RestartSec=5</span></div></div><div><div><div>18</div></div><div><span>LimitNOFILE=1048576</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>[Install]</span></div></div><div><div><div>21</div></div><div><span>WantedBy=multi-user.target</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>启用并启动：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>systemctl daemon-reload</span></div></div><div><div><div>2</div></div><div><span>systemctl enable derp --now</span></div></div><div><div><div>3</div></div><div><span>systemctl status derp</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>Derper IP部署(123.123.123.123改成自己服务器的IP)<a href="#derper-ip部署123123123123改成自己服务器的ip"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>DERP_IP="123.123.123.123"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout ${DERP_IP}.key -out ${DERP_IP}.crt -subj "/CN=${DERP_IP}" -addext "subjectAltName=IP:${DERP_IP}"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这时候<code>/root</code>文件夹下会有两个证书文件：<code>/root/123.123.123.123.crt</code>和<code>/root/123.123.123.123.key</code>，拷贝到<code>/etc/derp/</code>:</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>mv /root/123.123.123.123.crt /etc/derp</span></div></div><div><div><div>2</div></div><div><span>mv /root/123.123.123.123.key /etc/derp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>启动Derper服务器<a href="#启动derper服务器"><span>#</span></a></h3><ul>
<li>写入后台</li>
</ul><p>新建文件<code>derp.service</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>touch /etc/systemd/system/derp.service</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>写入以下内容</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[Unit]</span></div></div><div><div><div>2</div></div><div><span>Description=TS Derper</span></div></div><div><div><div>3</div></div><div><span>After=network.target</span></div></div><div><div><div>4</div></div><div><span>Wants=network.target</span></div></div><div><div><div>5</div></div><div><span>[Service]</span></div></div><div><div><div>6</div></div><div><span>User=root</span></div></div><div><div><div>7</div></div><div><span>Restart=always</span></div></div><div><div><div>8</div></div><div><span>ExecStart=/etc/derp/derper -hostname 123.123.123.123 -a :13445 -http-port 13446 -certmode manual -certdir /etc/derp</span></div></div><div><div><div>9</div></div><div><span>RestartPreventExitStatus=1</span></div></div><div><div><div>10</div></div><div><span>[Install]</span></div></div><div><div><div>11</div></div><div><span>WantedBy=multi-user.target</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><ul>
<li>启动</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>systemctl enable derp</span></div></div><div><div><div>2</div></div><div><span>systemctl restart derp</span></div></div><div><div><div>3</div></div><div><span>systemctl status derp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>验证Derper是否搭建成功<a href="#验证derper是否搭建成功"><span>#</span></a></h3><p>浏览器打开<code>https://123.123.123.123:13445</code>，忽略不安全，看看是不是显示下面的内容：</p><blockquote><p>DERP<br />
This is a Tailscale DERP server.</p><p>It provides STUN, interactive connectivity establishment, and relaying of end-to-end encrypted traffic for Tailscale clients.</p><p>Documentation:</p><p>About DERP<br />
Protocol &amp; Go docs<br />
How to run a DERP server</p></blockquote></section></section>
<section><h2>DERP 防白嫖（verify-clients）<a href="#derp-防白嫖verify-clients"><span>#</span></a></h2><section><h3>为什么必须开启？<a href="#为什么必须开启"><span>#</span></a></h3><p>不开启 <code>--verify-clients</code>：</p><ul>
<li>任何人只要知道你的 DERP 地址</li>
<li>都可以把你当 TCP 中继刷流量</li>
</ul></section><section><h3>verify-clients 的工作方式<a href="#verify-clients-的工作方式"><span>#</span></a></h3><ul>
<li>DERP 通过 <strong>本机的 tailscaled</strong></li>
<li>向 <strong>Headscale</strong> 校验客户端身份</li>
<li>只允许本 Tailnet 成员使用</li>
</ul></section></section>
<section><h2>搭建Headscale和Headscale-ui<a href="#搭建headscale和headscale-ui"><span>#</span></a></h2><section><h3> 安装最新版Headscale<a href="#安装最新版headscale"><span>#</span></a></h3><a href="https://github.com/juanfont/headscale" target="_blank"><div><div><div><div></div><div>juanfont</div></div><div>/</div><div>headscale</div></div><div></div></div><div>An open source, self-hosted implementation of the Tailscale control server</div><div><div>—</div><div>—</div><div>BSD-3-Clause</div><span>Go</span></div></a><ul>
<li>下载</li>
</ul><div><div>HeadscaleHeadscale UI</div><div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>wget</span><span> </span><span>--output-document=headscale.deb</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>https://github.com/juanfont/headscale/releases/download/v0.27.1/headscale_0.27.1_linux_amd64.deb</span></div></div></code></pre><div><div></div><div></div></div></figure></div></div><div><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>wget</span><span> </span><span>https://github.com/gurucomputing/headscale-ui/releases/download/2025.08.23/headscale-ui.zip</span></div></div></code></pre><div><div></div><div></div></div></figure></div></div></div><p>目前最新版为 0.27.1，后续若有更新可前往 <a href="https://github.com/juanfont/headscale/releases" target="_blank">Headscale Releases</a> 替换下载链接中的版本号；Headscale UI 见 <a href="https://github.com/gurucomputing/headscale-ui/releases" target="_blank">Headscale UI Releases</a>。</p><hr /><ul>
<li>安装</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>mv headscale_0.27.1_linux_amd64.deb headscale.deb</span></div></div><div><div><div>2</div></div><div><span>dpkg --install headscale.deb</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><ul>
<li>修改配置文件（<code>/etc/headscale/config.yaml</code>）</li>
</ul><p>仅列出需要修改的地方：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>server_url</span><span>: </span><span>https://headscale.muxui.com</span></div></div><div><div><div>2</div></div><div><span>listen_addr</span><span>: </span><span>0.0.0.0:8080</span></div></div><div><div><div>3</div></div><div><span>metrics_listen_addr</span><span>: </span><span>""</span></div></div><div><div><div>4</div></div><div><span>prefixes</span><span>:</span></div></div><div><div><div>5</div></div><div><span>  </span><span>v4</span><span>: </span><span>100.64.0.0/10</span></div></div><div><div><div>6</div></div><div><span>#  v6: fd7a:115c:a1e0::/48</span></div></div><div><div><div>7</div></div><div><span>derp</span><span>:</span></div></div><div><div><div>8</div></div><div><span>  </span><span>urls</span><span>:</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>- </span><span>https://derp.muxui.com/derp.json</span></div></div><div><div><div>10</div></div><div><span>dns</span><span>:</span></div></div><div><div><div>11</div></div><div><span>  </span><span>base_domain</span><span>: </span><span>tailnet.muxui.com</span></div></div><div><div><div>12</div></div><div><span>  </span><span>nameservers</span><span>:</span></div></div><div><div><div>13</div></div><div><span>    </span><span>global</span><span>:</span></div></div><div><div><div>14</div></div><div><span><span>      </span></span><span>- </span><span>114.114.114.114</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><ul>
<li>启动</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>systemctl enable headscale</span></div></div><div><div><div>2</div></div><div><span>systemctl restart headscale</span></div></div><div><div><div>3</div></div><div><span>systemctl status headscale</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>3.3 安装最新版Headscale-ui<a href="#33-安装最新版headscale-ui"><span>#</span></a></h3><a href="https://github.com/gurucomputing/headscale-ui" target="_blank"><div><div><div><div></div><div>gurucomputing</div></div><div>/</div><div>headscale-ui</div></div><div></div></div><div>A web frontend for the headscale Tailscale-compatible coordination server</div><div><div>—</div><div>—</div><div>BSD-3-Clause</div><span>Svelte</span></div></a><ul>
<li>安装</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>apt-get install unzip</span></div></div><div><div><div>2</div></div><div><span>unzip -d /var/www headscale-ui.zip</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>配置Derper服务器<a href="#配置derper服务器"><span>#</span></a></h3><ul>
<li>配置<code>/etc/headscale/derp.json</code>文件</li>
</ul><p>新建</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>touch /etc/headscale/derp.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>写入以下内容</p><p>ip部署</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>"Regions": {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>"901": {</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>"RegionID":   901,</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>"RegionCode": "Myself",</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>"RegionName": "Myself Derper",</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>"Nodes": [</span></div></div><div><div><div>8</div></div><div><span><span>          </span></span><span>{</span></div></div><div><div><div>9</div></div><div><span><span>          </span></span><span>"Name":             "901a",</span></div></div><div><div><div>10</div></div><div><span><span>          </span></span><span>"RegionID":         901,</span></div></div><div><div><div>11</div></div><div><span><span>          </span></span><span>"DERPPort":         13445,</span></div></div><div><div><div>12</div></div><div><span><span>          </span></span><span>"IPv4":   "123.123.123.123",</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>"InsecureForTests": true</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>]</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section><section><h3>使用域名（推荐做法）<a href="#使用域名推荐做法"><span>#</span></a></h3><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>"Regions": {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>"901": {</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>"RegionID": 901,</span></div></div><div><div><div>5</div></div><div><span><span>      </span></span><span>"RegionCode": "MUXUI",</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>"RegionName": "Muxui DERP",</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>"Nodes": [</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>9</div></div><div><span><span>          </span></span><span>"Name": "901a",</span></div></div><div><div><div>10</div></div><div><span><span>          </span></span><span>"RegionID": 901,</span></div></div><div><div><div>11</div></div><div><span><span>          </span></span><span>"HostName": "derp.muxui.com",</span></div></div><div><div><div>12</div></div><div><span><span>          </span></span><span>"DERPPort": 443,</span></div></div><div><div><div>13</div></div><div><span><span>          </span></span><span>"InsecureForTests": true</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>]</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section><section><h3>Nginx / 宝塔暴露 derp.json<a href="#nginx--宝塔暴露-derpjson"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>location /d/derp.json {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>alias /etc/headscale/derp.json;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>default_type application/json;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>验证：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>https://derp.muxui.com/derp.json</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>能直接返回 JSON。</p><p>如果不是宝塔,就下载安装并配置Nginx</p><ul>
<li>下载</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>apt install -y nginx</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><ul>
<li>打开<code>/etc/nginx/sites-available/default</code>并添加以下内容：</li>
</ul><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>map $http_upgrade $connection_upgrade {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>default      upgrade;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>''           close;</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div><span>server {</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>listen 80;</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>listen [::]:80;</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>listen 443      ssl http2;</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>listen [::]:443 ssl http2;</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>server_name headscale.example.com;</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>ssl_certificate /root/cert.crt;</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>ssl_certificate_key /root/private.key;</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>ssl_protocols TLSv1.2 TLSv1.3;</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>location / {</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>proxy_pass http://127.0.0.1:8080;</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>proxy_http_version 1.1;</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>proxy_set_header Upgrade $http_upgrade;</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>proxy_set_header Connection $connection_upgrade;</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>proxy_set_header Host $server_name;</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>proxy_redirect http:// https://;</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>proxy_buffering off;</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>proxy_set_header X-Real-IP $remote_addr;</span></div></div><div><div><div>23</div></div><div><span><span>        </span></span><span>proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>proxy_set_header X-Forwarded-Proto $scheme;</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>location /web {</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>index  index.html;</span></div></div><div><div><div>29</div></div><div><span><span>          </span></span><span>alias  /var/www/web;</span></div></div><div><div><div>30</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>31</div></div><div><span>}</span></div></div><div><div><div>32</div></div><div><span>server {</span></div></div><div><div><div>33</div></div><div><span><span>  </span></span><span>listen 80;</span></div></div><div><div><div>34</div></div><div><span><span>  </span></span><span>listen [::]:80;</span></div></div><div><div><div>35</div></div><div>
</div></div><div><div><div>36</div></div><div><span><span>  </span></span><span>server_name 127.0.0.1;</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span><span>  </span></span><span>root /var/www;</span></div></div><div><div><div>39</div></div><div><span><span>  </span></span><span>index index.html index.htm index.nginx-debian.html;</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>location /d {</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>alias   /var/www;</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>autoindex on;</span></div></div><div><div><div>43</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>location / {</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>try_files $uri $uri/ =404;</span></div></div><div><div><div>46</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>47</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /><ul>
<li>启动</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>systemctl enable nginx</span></div></div><div><div><div>2</div></div><div><span>systemctl restart nginx</span></div></div><div><div><div>3</div></div><div><span>systemctl status nginx</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>验证安装<a href="#验证安装"><span>#</span></a></h3><ul>
<li>重启Derper，Headscale和Nginx服务</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>systemctl restart derp</span></div></div><div><div><div>2</div></div><div><span>systemctl status derp</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>systemctl restart headscale</span></div></div><div><div><div>2</div></div><div><span>systemctl status headscale</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>systemctl restart nginx</span></div></div><div><div><div>2</div></div><div><span>systemctl status nginx</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><ul>
<li>打开网站</li>
</ul><p><a href="https://headscale.example.com/web" target="_blank">https://headscale.example.com/web</a></p><p>如无意外应该是三个服务的状态应该是全绿的，网页也能正常打开，有报错的话建议按教程自查或在帖子底下留言</p></section><section><h3>配置Headscale-ui<a href="#配置headscale-ui"><span>#</span></a></h3><ul>
<li>生成API Key</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>headscale apikeys create --expiration 9999d</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>并将其写入到<code>https://headscale.muxui.com/web/settings.html</code>的<code>Headscale API Key</code>里面，点击<code>Save API Key</code>提交，见到右侧有小对勾即可</p><hr /><ul>
<li>新建用户<code>Default</code></li>
</ul><p>打开<code>https://headscale.`muxui`.com/web/users.html</code>，点击<code>New User</code>，输入<code>Default</code>并提交</p></section></section>
<section><h2>最终验证（最关键）<a href="#最终验证最关键"><span>#</span></a></h2><section><h3>Windows 客户端 netcheck<a href="#windows-客户端-netcheck"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tailscale netcheck</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>结果示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Nearest DERP: Muxui DERP</span></div></div><div><div><div>2</div></div><div><span>DERP latency:</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>- MUXUI: 6.9ms</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>下载最新客户端<a href="#下载最新客户端"><span>#</span></a></h3><p><a href="https://tailscale.com/download" target="_blank">https://tailscale.com/download</a></p><hr /></section><section><h3>启动指令解析<a href="#启动指令解析"><span>#</span></a></h3><blockquote><p>—login-server: 指定使用的Headscale服务器地址，即<code>https://headscale.example.com</code><br />
—advertise-routes: 向Headscale服务器报告当前客户端处于哪个内网网段下, 便于Headscale服务器让同内网设备直接内网直连(可选的)或者将其他设备指定流量路由到当前内网(可选)，多条路由英文逗号隔开<br />
—accept-routes: 是否接受Headscale服务器下发的用于路由到其他客户端内网的路由规则(可选)<br />
—accept-dns: 是否使用Headscale服务器下发的 DNS 相关配置(可选, 推荐关闭)<br />
—force-reauth：强制重新认证<br />
—advertise-exit-node：作为出口节点</p></blockquote><hr /></section><section><h3>不同客户端的启动方法<a href="#不同客户端的启动方法"><span>#</span></a></h3><ul>
<li>Windows</li>
</ul><p>以管理员身份启动PowerShell，输入以下代码：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tailscale login --login-server https://headscale.example.com</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><ul>
<li>iOS</li>
</ul><p>用非国区Apple ID下载，配置V P N文件</p><p>点击<code>右上角头像</code>，点击<code>Log In...</code>，点击<code>右上角三个点</code>,点击<code>Use a custom coordination server</code>，输入<code>https://headscale.example.com</code>,点击<code>Login in</code></p><hr /><ul>
<li>Linux</li>
</ul><p>安装</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>curl -fsSL https://tailscale.com/install.sh | sh</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>运行</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>tailscale up --reset --advertise-routes=192.168.x.0/24 --accept-routes=true --login-server=https://headscale.example.com</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>到Headscale-ui中允许设备加入局域网<a href="#到headscale-ui中允许设备加入局域网"><span>#</span></a></h3><p>上面在客户端执行了<code>tailscale up</code>之后，在客户都会显示一个带Device Key的代码，格式类似于：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>headscale nodes register --user USERNAME --key mkey:ed8f19e22f51c9c231c8bc8ccbxxxxxxxxxxxxxxf86c8211e4ad32b6c6e</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>拷贝<code>mkey:ed8f19e22f51c9c231c8bc8ccbxxxxxxxxxxxxxxf86c8211e4ad32b6c6e</code>，去 <code>https://headscale.example.com/web/devices.html</code>中导入Device Key</p></section></section>
<section><h2> 参考教程<a href="#参考教程"><span>#</span></a></h2><ul>
<li><a href="https://www.cnblogs.com/yafengabc/p/18591222" target="_blank">Tailscale 搭建derp中继节点，不需要域名，不需要备案，不需要申请证书（最新）</a></li>
<li><a href="https://www.youtube.com/watch?v=gEmmhOdtLbA&amp;t=188s" target="_blank">Tailscale实现内网穿透、异地组网、远程访问，纯IP搭建DERP服务器，点对点传输，更好的使用体验！轻松打通内外网！群晖、威联通NAS，替代zerotier#一瓶奶油</a></li>
<li><a href="https://www.youtube.com/watch?v=6K0QOo3owkE" target="_blank">域名搭建Tailscale开源版本Headscale，配合headscale-ui使用，全方面提升使用体验，实现内网穿透、远程访问，点对点传输，轻松打通内外网！更适合国内的方案</a></li>
<li><a href="https://blog.csdn.net/weixin_36114752/article/details/144129664" target="_blank">自建Headscale后windows登录login无反应的问题解决方法</a></li>
<li><a href="https://www.youtube.com/watch?v=mgDpJX3oNvI" target="_blank">Tailscale玩法之内网穿透、异地组网、全隧道模式、纯IP的双栈DERP搭建、Headscale协调服务器搭建，用一期搞定，看一看不亏吧？</a></li>
<li><a href="https://linux.do/t/topic/171651" target="_blank">Tailscale+Headscale+自建Derp踩坑记录</a></li>
<li><a href="https://www.nodeseek.com/post-253641-1" target="_blank">Headscale+Derper的最新教程</a></li>
</ul></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/cloudflare-workers-telegram-d1-anonymous-chat/</id>
      <title type="text">基于 Cloudflare Workers + Telegram Bot + D1 的双向匿名聊天系统完整实现</title>
      <published>2025-11-16T00:00:00.000Z</published>
      <updated>2025-11-17T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/cloudflare-workers-telegram-d1-anonymous-chat/"/>
      <summary type="text">在本篇文章中，我将分享一个基于 Cloudflare Workers + Telegram Bot + D1 数据库 的完全无服务器（Serverless）聊天系统。该机器人支持用户匿名与管理员双向通信，具备首访验证、自动诈骗检测、屏蔽管理</summary>
      <content type="html"><![CDATA[<p>在本篇文章中，我将分享一个基于 <strong>Cloudflare Workers + Telegram Bot + D1 数据库</strong> 的完全无服务器（Serverless）聊天系统。该机器人支持用户匿名与管理员双向通信，具备首访验证、自动诈骗检测、屏蔽管理、消息映射等功能，是一个非常适合作为客服、匿名反馈、小型 IM 系统的解决方案。</p>
<hr />
<section><h2>⭐ <strong>系统亮点</strong><a href="#-系统亮点"><span>#</span></a></h2><ul>
<li>🚀 <strong>100% Serverless 无需服务器</strong></li>
<li>🔐 <strong>首访用户自动加减法验证（防骚扰）</strong></li>
<li>🔁 <strong>双向匿名对话（用户看不到管理员 ID）</strong></li>
<li>🛡 <strong>管理员可屏蔽 / 解封任何用户</strong></li>
<li>🧠 <strong>自动诈骗账号检测（基于 D1 fraud 表）</strong></li>
<li>💾 <strong>D1 数据库存储用户状态与消息映射</strong></li>
<li>📌 <strong>管理员收到用户消息时自动生成“用户资料卡”</strong></li>
<li>🔔 <strong>可控频率的通知提醒机制</strong></li>
</ul><hr /></section>
<section><h2>🧩 系统架构<a href="#-系统架构"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[Telegram User]</span></div></div><div><div><div>2</div></div><div><span><span>      </span></span><span>↓ 发送消息</span></div></div><div><div><div>3</div></div><div><span>[Cloudflare Workers /endpoint]</span></div></div><div><div><div>4</div></div><div><span><span>      </span></span><span>↓</span></div></div><div><div><div>5</div></div><div><span>[验证系统（加减法 1～10）]</span></div></div><div><div><div>6</div></div><div><span><span>      </span></span><span>↓ 通过后</span></div></div><div><div><div>7</div></div><div><span>[反诈检测(fraud)]</span></div></div><div><div><div>8</div></div><div><span><span>      </span></span><span>↓</span></div></div><div><div><div>9</div></div><div><span>[自动转发消息 → 管理员]</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>管理员 ←→ 用户 （双向聊天）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section>
<section><h2>📦 使用技术<a href="#-使用技术"><span>#</span></a></h2><table><thead><tr><th>技术</th><th>用途</th></tr></thead><tbody><tr><td><strong>Cloudflare Workers</strong></td><td>核心逻辑执行环境</td></tr><tr><td><strong>Telegram Bot API</strong></td><td>双向聊天接口</td></tr><tr><td><strong>Cloudflare D1</strong></td><td>用户资料、屏蔽状态、验证码、消息映射存储</td></tr><tr><td><strong>Webhooks</strong></td><td>实时接收 Telegram 更新事件</td></tr></tbody></table><hr /></section>
<section><h1>🗄 数据库结构（D1）<a href="#-数据库结构d1"><span>#</span></a></h1><p>为保证机器人功能完整，需要创建以下表：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>CREATE TABLE blocks (</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>user_id TEXT PRIMARY KEY,</span></div></div><div><div><div>3</div></div><div><span><span>  </span></span><span>is_blocked INTEGER</span></div></div><div><div><div>4</div></div><div><span>);</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>CREATE TABLE msg_map (</span></div></div><div><div><div>7</div></div><div><span><span>  </span></span><span>admin_msg_id TEXT PRIMARY KEY,</span></div></div><div><div><div>8</div></div><div><span><span>  </span></span><span>user_id TEXT</span></div></div><div><div><div>9</div></div><div><span>);</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>CREATE TABLE lastmsg (</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>user_id TEXT PRIMARY KEY,</span></div></div><div><div><div>13</div></div><div><span><span>  </span></span><span>ts INTEGER</span></div></div><div><div><div>14</div></div><div><span>);</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span>CREATE TABLE fraud (</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>user_id TEXT PRIMARY KEY</span></div></div><div><div><div>18</div></div><div><span>);</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span>CREATE TABLE verify (</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>user_id TEXT PRIMARY KEY,</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>question TEXT,</span></div></div><div><div><div>23</div></div><div><span><span>  </span></span><span>answer INTEGER,</span></div></div><div><div><div>24</div></div><div><span><span>  </span></span><span>passed INTEGER</span></div></div><div><div><div>25</div></div><div><span>);</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>CREATE TABLE user_first (</span></div></div><div><div><div>28</div></div><div><span><span>  </span></span><span>user_id TEXT PRIMARY KEY</span></div></div><div><div><div>29</div></div><div><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><hr /></section>
<section><h1>🔢 首次验证系统（加减法 1～10）<a href="#-首次验证系统加减法-110"><span>#</span></a></h1><p>为了防止广告机器人、骚扰用户，系统会对第一次聊天的用户进行简单的数学验证：</p><section><h3>✨ 验证示例<a href="#-验证示例"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>为了防止骚扰，请先完成验证：</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>7 + 3 = ?</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>请直接回复数字答案。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>答对后自动进入聊天流程；答错则重新生成题目。</p><hr /></section></section>
<section><h1>🎯 核心功能说明<a href="#-核心功能说明"><span>#</span></a></h1><section><h2>1️⃣ <strong>匿名聊天（Forward/Copy）</strong><a href="#1️⃣-匿名聊天forwardcopy"><span>#</span></a></h2><p>用户发送消息 → 机器人自动转发给管理员<br />
管理员「回复这条转发消息」→ 机器人将回复复制给用户</p><p>整个过程：</p><ul>
<li>用户不会看到管理员的 UID</li>
<li>管理员同样看不到用户昵称（除资料卡）</li>
</ul><p>实现完整“匿名双向通信”。</p><hr /></section><section><h2>2️⃣ <strong>用户资料卡（首次出现）</strong><a href="#2️⃣-用户资料卡首次出现"><span>#</span></a></h2><p>管理员第一次收到用户消息时，自动生成用户信息卡：</p><p><img src="https://s.muxui.com:8015/images/2026/09/atyzJj1Glh51.webp" alt="" /></p><p>管理员点按钮后 → 自动屏蔽</p><hr /></section><section><h2>3️⃣ <strong>屏蔽 / 解除屏蔽</strong><a href="#3️⃣-屏蔽--解除屏蔽"><span>#</span></a></h2><p>管理员可直接通过命令控制：</p><table><thead><tr><th>指令</th><th>说明</th></tr></thead><tbody><tr><td><code>/block</code></td><td>屏蔽此用户</td></tr><tr><td><code>/unblock</code></td><td>解除屏蔽</td></tr><tr><td><code>/checkblock</code></td><td>查看用户是否被屏蔽</td></tr></tbody></table><hr /></section><section><h2>4️⃣ <strong>诈骗检测（D1 fraud 表）</strong><a href="#4️⃣-诈骗检测d1-fraud-表"><span>#</span></a></h2><p>只需点击按钮</p><p><img src="https://s.muxui.com:8015/images/2026/09/1hj7rHykrhdC.webp" alt="" /></p><p>或者</p><p><img src="https://s.muxui.com:8015/images/2026/09/OVmJwCkhRoUM.webp" alt="" /></p><p>回复的是输入/block 也可以拉黑用户</p><hr /></section><section><h2>5️⃣ <strong>可控间隔的通知提醒系统</strong><a href="#5️⃣-可控间隔的通知提醒系统"><span>#</span></a></h2><p>为了避免管理员被刷屏，可设置提醒间隔（默认 1 小时）：</p><p>作用：</p><ul>
<li>用户第一次联系 → 提醒</li>
<li>用户长时间没聊天，再次联系 → 再提醒一次</li>
<li>保证管理员不会被Spam</li>
</ul><hr /></section></section>
<section><h1>🛠 Worker 代码结构说明<a href="#-worker-代码结构说明"><span>#</span></a></h1><p>代码整体分为：</p><ul>
<li>Webhook 接收器</li>
<li>Telegram API 封装</li>
<li>核心消息处理</li>
<li>加减法验证模块</li>
<li>管理员控制命令</li>
<li>D1 数据访问模块</li>
<li>首次资料卡发送模块（可扩展）</li>
</ul><p>该架构清晰、可维护性高、支持未来扩展。</p><hr /></section>
<section><h1>📚 使用教程<a href="#-使用教程"><span>#</span></a></h1><section><h2>1. 创建 Telegram Bot<a href="#1-创建-telegram-bot"><span>#</span></a></h2><p>在 @BotFather 创建一个机器人，并获得：</p><ul>
<li>Bot Token</li>
</ul><p><img src="https://s.muxui.com:8015/images/2026/09/yRrI8v2v6wpO.webp" alt="" /></p></section><section><h2>2. 部署到 Cloudflare Workers<a href="#2-部署到-cloudflare-workers"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// ================== 常量区 ==================</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>const START_MESSAGE = `</span></div></div><div><div><div>4</div></div><div><span>🤖 你好，欢迎使用 @guaguawa_bot 双向聊天机器人！</span></div></div><div><div><div>5</div></div><div><span>请通过此机器人发送您的消息与我联系，我会立即为您传达。</span></div></div><div><div><div>6</div></div><div><span>`;</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>const START_MESSAGE_ADMIN = `</span></div></div><div><div><div>9</div></div><div><span>🤖 &lt;b&gt;关于本机器人&lt;/b&gt;</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>本机器人用于 &lt;b&gt;双向匿名聊天&lt;/b&gt;，旨在提高用户的沟通体验，并防止骚扰。</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>📌 &lt;b&gt;功能介绍：&lt;/b&gt;</span></div></div><div><div><div>14</div></div><div><span>- 匿名聊天：用户可以自由交流，管理员可管理消息流转。</span></div></div><div><div><div>15</div></div><div><span>- 防骚扰功能：管理员可使用 /block 指令屏蔽不良用户。</span></div></div><div><div><div>16</div></div><div><span>- 诈骗检测：自动检测是否属于诈骗用户，并提醒管理员。</span></div></div><div><div><div>17</div></div><div>
</div></div><div><div><div>18</div></div><div><span>📜 &lt;b&gt;可用命令：&lt;/b&gt;</span></div></div><div><div><div>19</div></div><div><span>- /start 获取欢迎信息</span></div></div><div><div><div>20</div></div><div><span>- /block 屏蔽用户（管理员）</span></div></div><div><div><div>21</div></div><div><span>- /unblock 解除屏蔽（管理员）</span></div></div><div><div><div>22</div></div><div><span>- /checkblock 查询用户状态（管理员）</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>🔹 机器人由 &lt;b&gt;匿名聊天管理系统&lt;/b&gt; 提供支持。</span></div></div><div><div><div>25</div></div><div><span>`;</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div>
</div></div><div><div><div>28</div></div><div><span>// ================== Worker 入口（ESM 格式） ==================</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>export default {</span></div></div><div><div><div>31</div></div><div><span><span>  </span></span><span>async fetch(request, env, ctx) {</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>const url = new URL(request.url);</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>if (url.pathname === "/endpoint") {</span></div></div><div><div><div>35</div></div><div><span><span>      </span></span><span>return handleWebhook(request, env, ctx);</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>if (url.pathname === "/registerWebhook") {</span></div></div><div><div><div>38</div></div><div><span><span>      </span></span><span>return registerWebhook(request, env);</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>if (url.pathname === "/unRegisterWebhook") {</span></div></div><div><div><div>41</div></div><div><span><span>      </span></span><span>return unRegisterWebhook(request, env);</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>43</div></div><div>
</div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>return new Response("OK");</span></div></div><div><div><div>45</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>46</div></div><div><span>};</span></div></div><div><div><div>47</div></div><div>
</div></div><div><div><div>48</div></div><div>
</div></div><div><div><div>49</div></div><div><span>// ===================== Telegram API 封装 ========================</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>function apiUrl(methodName, params = null, env) {</span></div></div><div><div><div>52</div></div><div><span><span>  </span></span><span>let query = params ? "?" + new URLSearchParams(params).toString() : "";</span></div></div><div><div><div>53</div></div><div><span><span>  </span></span><span>return `https://api.telegram.org/bot${env.ENV_BOT_TOKEN}/${methodName}${query}`;</span></div></div><div><div><div>54</div></div><div><span>}</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div><span>function requestTelegram(method, body, env, params = null) {</span></div></div><div><div><div>57</div></div><div><span><span>  </span></span><span>return fetch(apiUrl(method, params, env), body).then(r =&gt; r.json());</span></div></div><div><div><div>58</div></div><div><span>}</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span>const makeReq = (body) =&gt; ({</span></div></div><div><div><div>61</div></div><div><span><span>  </span></span><span>method: "POST",</span></div></div><div><div><div>62</div></div><div><span><span>  </span></span><span>headers: { "content-type": "application/json" },</span></div></div><div><div><div>63</div></div><div><span><span>  </span></span><span>body: JSON.stringify(body)</span></div></div><div><div><div>64</div></div><div><span>});</span></div></div><div><div><div>65</div></div><div>
</div></div><div><div><div>66</div></div><div><span>const sendMessage = (msg, env) =&gt;</span></div></div><div><div><div>67</div></div><div><span><span>  </span></span><span>requestTelegram("sendMessage", makeReq(msg), env);</span></div></div><div><div><div>68</div></div><div>
</div></div><div><div><div>69</div></div><div><span>const forwardMessage = (msg, env) =&gt;</span></div></div><div><div><div>70</div></div><div><span><span>  </span></span><span>requestTelegram("forwardMessage", makeReq(msg), env);</span></div></div><div><div><div>71</div></div><div>
</div></div><div><div><div>72</div></div><div><span>const copyMessage = (msg, env) =&gt;</span></div></div><div><div><div>73</div></div><div><span><span>  </span></span><span>requestTelegram("copyMessage", makeReq(msg), env);</span></div></div><div><div><div>74</div></div><div>
</div></div><div><div><div>75</div></div><div>
</div></div><div><div><div>76</div></div><div><span>// ===================== Webhook ========================</span></div></div><div><div><div>77</div></div><div>
</div></div><div><div><div>78</div></div><div><span>async function handleWebhook(request, env, ctx) {</span></div></div><div><div><div>79</div></div><div><span><span>  </span></span><span>if (request.headers.get("X-Telegram-Bot-Api-Secret-Token") !== env.ENV_BOT_SECRET) {</span></div></div><div><div><div>80</div></div><div><span><span>    </span></span><span>return new Response("Unauthorized", { status: 403 });</span></div></div><div><div><div>81</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>82</div></div><div>
</div></div><div><div><div>83</div></div><div><span><span>  </span></span><span>const update = await request.json();</span></div></div><div><div><div>84</div></div><div><span><span>  </span></span><span>ctx.waitUntil(onUpdate(update, env));</span></div></div><div><div><div>85</div></div><div><span><span>  </span></span><span>return new Response("OK");</span></div></div><div><div><div>86</div></div><div><span>}</span></div></div><div><div><div>87</div></div><div>
</div></div><div><div><div>88</div></div><div>
</div></div><div><div><div>89</div></div><div><span>// ===================== Update Routing ========================</span></div></div><div><div><div>90</div></div><div>
</div></div><div><div><div>91</div></div><div><span>async function onUpdate(update, env) {</span></div></div><div><div><div>92</div></div><div><span><span>  </span></span><span>if (update.message) {</span></div></div><div><div><div>93</div></div><div><span><span>    </span></span><span>await onMessage(update.message, env);</span></div></div><div><div><div>94</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>95</div></div><div><span><span>  </span></span><span>if (update.callback_query) {</span></div></div><div><div><div>96</div></div><div><span><span>    </span></span><span>await onCallback(update.callback_query, env);</span></div></div><div><div><div>97</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>98</div></div><div><span>}</span></div></div><div><div><div>99</div></div><div>
</div></div><div><div><div>100</div></div><div>
</div></div><div><div><div>101</div></div><div><span>// ===================== D1 数据库封装 ========================</span></div></div><div><div><div>102</div></div><div>
</div></div><div><div><div>103</div></div><div><span>// 拉黑</span></div></div><div><div><div>104</div></div><div><span>async function getBlock(env, userId) {</span></div></div><div><div><div>105</div></div><div><span><span>  </span></span><span>const row = await env.TG_BOT_DB.prepare("SELECT is_blocked FROM blocks WHERE user_id=?").bind(userId).first();</span></div></div><div><div><div>106</div></div><div><span><span>  </span></span><span>return row ? row.is_blocked === 1 : false;</span></div></div><div><div><div>107</div></div><div><span>}</span></div></div><div><div><div>108</div></div><div>
</div></div><div><div><div>109</div></div><div><span>async function setBlock(env, userId, val) {</span></div></div><div><div><div>110</div></div><div><span><span>  </span></span><span>await env.TG_BOT_DB.prepare("INSERT OR REPLACE INTO blocks (user_id, is_blocked) VALUES (?, ?)").bind(userId, val ? 1 : 0).run();</span></div></div><div><div><div>111</div></div><div><span>}</span></div></div><div><div><div>112</div></div><div>
</div></div><div><div><div>113</div></div><div><span>// 消息映射</span></div></div><div><div><div>114</div></div><div><span>async function getMsgMap(env, adminMsgId) {</span></div></div><div><div><div>115</div></div><div><span><span>  </span></span><span>const row = await env.TG_BOT_DB.prepare("SELECT user_id FROM msg_map WHERE admin_msg_id=?").bind(adminMsgId).first();</span></div></div><div><div><div>116</div></div><div><span><span>  </span></span><span>return row ? row.user_id : null;</span></div></div><div><div><div>117</div></div><div><span>}</span></div></div><div><div><div>118</div></div><div>
</div></div><div><div><div>119</div></div><div><span>async function setMsgMap(env, adminMsgId, userId) {</span></div></div><div><div><div>120</div></div><div><span><span>  </span></span><span>await env.TG_BOT_DB.prepare("INSERT OR REPLACE INTO msg_map (admin_msg_id, user_id) VALUES (?, ?)").bind(adminMsgId, userId).run();</span></div></div><div><div><div>121</div></div><div><span>}</span></div></div><div><div><div>122</div></div><div>
</div></div><div><div><div>123</div></div><div><span>// 通知节流</span></div></div><div><div><div>124</div></div><div><span>async function getLastMsg(env, userId) {</span></div></div><div><div><div>125</div></div><div><span><span>  </span></span><span>const row = await env.TG_BOT_DB.prepare("SELECT ts FROM lastmsg WHERE user_id=?").bind(userId).first();</span></div></div><div><div><div>126</div></div><div><span><span>  </span></span><span>return row ? row.ts : null;</span></div></div><div><div><div>127</div></div><div><span>}</span></div></div><div><div><div>128</div></div><div>
</div></div><div><div><div>129</div></div><div><span>async function setLastMsg(env, userId, ts) {</span></div></div><div><div><div>130</div></div><div><span><span>  </span></span><span>await env.TG_BOT_DB.prepare("INSERT OR REPLACE INTO lastmsg (user_id, ts) VALUES (?, ?)").bind(userId, ts).run();</span></div></div><div><div><div>131</div></div><div><span>}</span></div></div><div><div><div>132</div></div><div>
</div></div><div><div><div>133</div></div><div><span>// 诈骗</span></div></div><div><div><div>134</div></div><div><span>async function isFraud(env, userId) {</span></div></div><div><div><div>135</div></div><div><span><span>  </span></span><span>const row = await env.TG_BOT_DB.prepare("SELECT user_id FROM fraud WHERE user_id=?").bind(userId).first();</span></div></div><div><div><div>136</div></div><div><span><span>  </span></span><span>return !!row;</span></div></div><div><div><div>137</div></div><div><span>}</span></div></div><div><div><div>138</div></div><div>
</div></div><div><div><div>139</div></div><div>
</div></div><div><div><div>140</div></div><div><span>// ===================== 首次加减验证 ========================</span></div></div><div><div><div>141</div></div><div>
</div></div><div><div><div>142</div></div><div><span>function genVerifyQuestion() {</span></div></div><div><div><div>143</div></div><div><span><span>  </span></span><span>let a = Math.floor(Math.random() * 10) + 1;</span></div></div><div><div><div>144</div></div><div><span><span>  </span></span><span>let b = Math.floor(Math.random() * 10) + 1;</span></div></div><div><div><div>145</div></div><div>
</div></div><div><div><div>146</div></div><div><span><span>  </span></span><span>if (Math.random() &lt; 0.5) {</span></div></div><div><div><div>147</div></div><div><span><span>    </span></span><span>return { question: `${a} + ${b} = ?`, answer: a + b };</span></div></div><div><div><div>148</div></div><div><span><span>  </span></span><span>} else {</span></div></div><div><div><div>149</div></div><div><span><span>    </span></span><span>if (a &lt; b) [a, b] = [b, a];</span></div></div><div><div><div>150</div></div><div><span><span>    </span></span><span>return { question: `${a} - ${b} = ?`, answer: a - b };</span></div></div><div><div><div>151</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>152</div></div><div><span>}</span></div></div><div><div><div>153</div></div><div>
</div></div><div><div><div>154</div></div><div><span>async function getVerify(env, userId) {</span></div></div><div><div><div>155</div></div><div><span><span>  </span></span><span>return await env.TG_BOT_DB.prepare("SELECT * FROM verify WHERE user_id=?").bind(userId).first();</span></div></div><div><div><div>156</div></div><div><span>}</span></div></div><div><div><div>157</div></div><div>
</div></div><div><div><div>158</div></div><div><span>async function setVerify(env, userId, question, answer) {</span></div></div><div><div><div>159</div></div><div><span><span>  </span></span><span>await env.TG_BOT_DB.prepare("INSERT OR REPLACE INTO verify (user_id, question, answer, passed) VALUES (?, ?, ?, 0)").bind(userId, question, answer).run();</span></div></div><div><div><div>160</div></div><div><span>}</span></div></div><div><div><div>161</div></div><div>
</div></div><div><div><div>162</div></div><div><span>async function passVerify(env, userId) {</span></div></div><div><div><div>163</div></div><div><span><span>  </span></span><span>await env.TG_BOT_DB.prepare("UPDATE verify SET passed=1 WHERE user_id=?").bind(userId).run();</span></div></div><div><div><div>164</div></div><div><span>}</span></div></div><div><div><div>165</div></div><div>
</div></div><div><div><div>166</div></div><div>
</div></div><div><div><div>167</div></div><div><span>// ===================== 消息主处理 ========================</span></div></div><div><div><div>168</div></div><div>
</div></div><div><div><div>169</div></div><div><span>async function onMessage(message, env) {</span></div></div><div><div><div>170</div></div><div><span><span>  </span></span><span>const chatId = message.chat.id.toString();</span></div></div><div><div><div>171</div></div><div><span><span>  </span></span><span>const ADMIN_UID = env.ENV_ADMIN_UID;</span></div></div><div><div><div>172</div></div><div>
</div></div><div><div><div>173</div></div><div><span><span>  </span></span><span>// /start</span></div></div><div><div><div>174</div></div><div><span><span>  </span></span><span>if (message.text === "/start") {</span></div></div><div><div><div>175</div></div><div><span><span>    </span></span><span>return sendMessage({ chat_id: chatId, text: chatId === ADMIN_UID ? START_MESSAGE_ADMIN : START_MESSAGE, parse_mode: "HTML" }, env);</span></div></div><div><div><div>176</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>177</div></div><div>
</div></div><div><div><div>178</div></div><div><span><span>  </span></span><span>// -------- 管理员 --------</span></div></div><div><div><div>179</div></div><div><span><span>  </span></span><span>if (chatId === ADMIN_UID) {</span></div></div><div><div><div>180</div></div><div><span><span>    </span></span><span>return handleAdminMessage(message, env);</span></div></div><div><div><div>181</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>182</div></div><div>
</div></div><div><div><div>183</div></div><div><span><span>  </span></span><span>// -------- 用户 --------</span></div></div><div><div><div>184</div></div><div><span><span>  </span></span><span>return handleGuestMessage(message, env);</span></div></div><div><div><div>185</div></div><div><span>}</span></div></div><div><div><div>186</div></div><div>
</div></div><div><div><div>187</div></div><div>
</div></div><div><div><div>188</div></div><div><span>// ===================== 管理员逻辑 ========================</span></div></div><div><div><div>189</div></div><div>
</div></div><div><div><div>190</div></div><div><span>async function handleAdminMessage(message, env) {</span></div></div><div><div><div>191</div></div><div><span><span>  </span></span><span>const ADMIN_UID = env.ENV_ADMIN_UID;</span></div></div><div><div><div>192</div></div><div>
</div></div><div><div><div>193</div></div><div><span><span>  </span></span><span>if (!message.reply_to_message?.message_id) {</span></div></div><div><div><div>194</div></div><div><span><span>    </span></span><span>return sendMessage({ chat_id: ADMIN_UID, text: START_MESSAGE_ADMIN, parse_mode: "HTML" }, env);</span></div></div><div><div><div>195</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>196</div></div><div>
</div></div><div><div><div>197</div></div><div><span><span>  </span></span><span>const guestId = await getMsgMap(env, message.reply_to_message.message_id.toString());</span></div></div><div><div><div>198</div></div><div>
</div></div><div><div><div>199</div></div><div><span><span>  </span></span><span>if (!guestId) {</span></div></div><div><div><div>200</div></div><div><span><span>    </span></span><span>return sendMessage({ chat_id: ADMIN_UID, text: "无法找到用户映射" }, env);</span></div></div><div><div><div>201</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>202</div></div><div>
</div></div><div><div><div>203</div></div><div><span><span>  </span></span><span>// 管理员命令</span></div></div><div><div><div>204</div></div><div><span><span>  </span></span><span>if (message.text === "/block") return handleBlock(message, env);</span></div></div><div><div><div>205</div></div><div><span><span>  </span></span><span>if (message.text === "/unblock") return handleUnblock(message, env);</span></div></div><div><div><div>206</div></div><div><span><span>  </span></span><span>if (message.text === "/checkblock") return handleCheckBlock(message, env);</span></div></div><div><div><div>207</div></div><div>
</div></div><div><div><div>208</div></div><div><span><span>  </span></span><span>// 正常回复</span></div></div><div><div><div>209</div></div><div><span><span>  </span></span><span>return copyMessage({</span></div></div><div><div><div>210</div></div><div><span><span>    </span></span><span>chat_id: guestId,</span></div></div><div><div><div>211</div></div><div><span><span>    </span></span><span>from_chat_id: ADMIN_UID,</span></div></div><div><div><div>212</div></div><div><span><span>    </span></span><span>message_id: message.message_id</span></div></div><div><div><div>213</div></div><div><span><span>  </span></span><span>}, env);</span></div></div><div><div><div>214</div></div><div><span>}</span></div></div><div><div><div>215</div></div><div>
</div></div><div><div><div>216</div></div><div>
</div></div><div><div><div>217</div></div><div><span>// ===================== 用户逻辑（含验证 + 资料卡） ========================</span></div></div><div><div><div>218</div></div><div>
</div></div><div><div><div>219</div></div><div><span>async function handleGuestMessage(message, env) {</span></div></div><div><div><div>220</div></div><div><span><span>  </span></span><span>const chatId = message.chat.id.toString();</span></div></div><div><div><div>221</div></div><div><span><span>  </span></span><span>const ADMIN_UID = env.ENV_ADMIN_UID;</span></div></div><div><div><div>222</div></div><div>
</div></div><div><div><div>223</div></div><div><span><span>  </span></span><span>// --- 1. 验证 ---</span></div></div><div><div><div>224</div></div><div><span><span>  </span></span><span>const verify = await getVerify(env, chatId);</span></div></div><div><div><div>225</div></div><div>
</div></div><div><div><div>226</div></div><div><span><span>  </span></span><span>if (!verify) {</span></div></div><div><div><div>227</div></div><div><span><span>    </span></span><span>const q = genVerifyQuestion();</span></div></div><div><div><div>228</div></div><div><span><span>    </span></span><span>await setVerify(env, chatId, q.question, q.answer);</span></div></div><div><div><div>229</div></div><div>
</div></div><div><div><div>230</div></div><div><span><span>    </span></span><span>return sendMessage({</span></div></div><div><div><div>231</div></div><div><span><span>      </span></span><span>chat_id: chatId,</span></div></div><div><div><div>232</div></div><div><span><span>      </span></span><span>text: `为了防止骚扰，请先完成验证：\n\n&lt;b&gt;${q.question}&lt;/b&gt;`,</span></div></div><div><div><div>233</div></div><div><span><span>      </span></span><span>parse_mode: "HTML"</span></div></div><div><div><div>234</div></div><div><span><span>    </span></span><span>}, env);</span></div></div><div><div><div>235</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>236</div></div><div>
</div></div><div><div><div>237</div></div><div><span><span>  </span></span><span>if (verify.passed !== 1) {</span></div></div><div><div><div>238</div></div><div><span><span>    </span></span><span>const ans = parseInt(message.text);</span></div></div><div><div><div>239</div></div><div>
</div></div><div><div><div>240</div></div><div><span><span>    </span></span><span>if (ans === verify.answer) {</span></div></div><div><div><div>241</div></div><div><span><span>      </span></span><span>await passVerify(env, chatId);</span></div></div><div><div><div>242</div></div><div>
</div></div><div><div><div>243</div></div><div><span><span>      </span></span><span>return sendMessage({</span></div></div><div><div><div>244</div></div><div><span><span>        </span></span><span>chat_id: chatId,</span></div></div><div><div><div>245</div></div><div><span><span>        </span></span><span>text: "验证通过！你现在可以给管理员发送消息了 🎉"</span></div></div><div><div><div>246</div></div><div><span><span>      </span></span><span>}, env);</span></div></div><div><div><div>247</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>248</div></div><div>
</div></div><div><div><div>249</div></div><div><span><span>    </span></span><span>const q = genVerifyQuestion();</span></div></div><div><div><div>250</div></div><div><span><span>    </span></span><span>await setVerify(env, chatId, q.question, q.answer);</span></div></div><div><div><div>251</div></div><div>
</div></div><div><div><div>252</div></div><div><span><span>    </span></span><span>return sendMessage({</span></div></div><div><div><div>253</div></div><div><span><span>      </span></span><span>chat_id: chatId,</span></div></div><div><div><div>254</div></div><div><span><span>      </span></span><span>text: `❌ 错误，请重新验证：\n\n&lt;b&gt;${q.question}&lt;/b&gt;`,</span></div></div><div><div><div>255</div></div><div><span><span>      </span></span><span>parse_mode: "HTML"</span></div></div><div><div><div>256</div></div><div><span><span>    </span></span><span>}, env);</span></div></div><div><div><div>257</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>258</div></div><div>
</div></div><div><div><div>259</div></div><div><span><span>  </span></span><span>// --- 2. 是否被屏蔽 ---</span></div></div><div><div><div>260</div></div><div><span><span>  </span></span><span>if (await getBlock(env, chatId)) {</span></div></div><div><div><div>261</div></div><div><span><span>    </span></span><span>return sendMessage({ chat_id: chatId, text: "🚫 你被拉黑了" }, env);</span></div></div><div><div><div>262</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>263</div></div><div>
</div></div><div><div><div>264</div></div><div><span><span>  </span></span><span>// --- 3. 首次与管理员建立联系 → 发送资料卡 ---</span></div></div><div><div><div>265</div></div><div><span><span>  </span></span><span>const firstRow = await env.TG_BOT_DB.prepare("SELECT user_id FROM user_first WHERE user_id=?").bind(chatId).first();</span></div></div><div><div><div>266</div></div><div>
</div></div><div><div><div>267</div></div><div><span><span>  </span></span><span>if (!firstRow) {</span></div></div><div><div><div>268</div></div><div><span><span>    </span></span><span>await env.TG_BOT_DB.prepare("INSERT INTO user_first (user_id) VALUES (?)").bind(chatId).run();</span></div></div><div><div><div>269</div></div><div>
</div></div><div><div><div>270</div></div><div><span><span>    </span></span><span>const fullname = (message.from.first_name || "") + (message.from.last_name ? " " + message.from.last_name : "");</span></div></div><div><div><div>271</div></div><div><span><span>    </span></span><span>const username = message.from.username ? "@" + message.from.username : "无";</span></div></div><div><div><div>272</div></div><div>
</div></div><div><div><div>273</div></div><div><span><span>    </span></span><span>const now = new Date();</span></div></div><div><div><div>274</div></div><div><span><span>    </span></span><span>const timeDisplay = now.toLocaleString("zh-CN", { hour12: false });</span></div></div><div><div><div>275</div></div><div>
</div></div><div><div><div>276</div></div><div><span><span>    </span></span><span>const userCard = `</span></div></div><div><div><div>277</div></div><div><span>&lt;b&gt;用户资料卡&lt;/b&gt;</span></div></div><div><div><div>278</div></div><div><span>---</span></div></div><div><div><div>279</div></div><div><span>• 昵称名称: &lt;b&gt;${fullname}&lt;/b&gt;</span></div></div><div><div><div>280</div></div><div><span>• 用户名: &lt;b&gt;${username}&lt;/b&gt;</span></div></div><div><div><div>281</div></div><div><span>• ID: &lt;code&gt;${chatId}&lt;/code&gt;</span></div></div><div><div><div>282</div></div><div><span>• 首次连接时间: &lt;code&gt;${timeDisplay}&lt;/code&gt;</span></div></div><div><div><div>283</div></div><div><span>`;</span></div></div><div><div><div>284</div></div><div>
</div></div><div><div><div>285</div></div><div><span><span>    </span></span><span>const blockKeyboard = {</span></div></div><div><div><div>286</div></div><div><span><span>      </span></span><span>inline_keyboard: [</span></div></div><div><div><div>287</div></div><div><span><span>        </span></span><span>[{ text: "🚫 屏蔽此人 (Block)", callback_data: `block:${chatId}` }]</span></div></div><div><div><div>288</div></div><div><span><span>      </span></span><span>]</span></div></div><div><div><div>289</div></div><div><span><span>    </span></span><span>};</span></div></div><div><div><div>290</div></div><div>
</div></div><div><div><div>291</div></div><div><span><span>    </span></span><span>await sendMessage({</span></div></div><div><div><div>292</div></div><div><span><span>      </span></span><span>chat_id: ADMIN_UID,</span></div></div><div><div><div>293</div></div><div><span><span>      </span></span><span>text: userCard,</span></div></div><div><div><div>294</div></div><div><span><span>      </span></span><span>parse_mode: "HTML",</span></div></div><div><div><div>295</div></div><div><span><span>      </span></span><span>reply_markup: blockKeyboard</span></div></div><div><div><div>296</div></div><div><span><span>    </span></span><span>}, env);</span></div></div><div><div><div>297</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>298</div></div><div>
</div></div><div><div><div>299</div></div><div><span><span>  </span></span><span>// --- 4. 转发消息 ---</span></div></div><div><div><div>300</div></div><div><span><span>  </span></span><span>const fw = await forwardMessage({</span></div></div><div><div><div>301</div></div><div><span><span>    </span></span><span>chat_id: ADMIN_UID,</span></div></div><div><div><div>302</div></div><div><span><span>    </span></span><span>from_chat_id: chatId,</span></div></div><div><div><div>303</div></div><div><span><span>    </span></span><span>message_id: message.message_id</span></div></div><div><div><div>304</div></div><div><span><span>  </span></span><span>}, env);</span></div></div><div><div><div>305</div></div><div>
</div></div><div><div><div>306</div></div><div><span><span>  </span></span><span>if (fw.ok) {</span></div></div><div><div><div>307</div></div><div><span><span>    </span></span><span>await setMsgMap(env, fw.result.message_id.toString(), chatId);</span></div></div><div><div><div>308</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>309</div></div><div><span>}</span></div></div><div><div><div>310</div></div><div>
</div></div><div><div><div>311</div></div><div>
</div></div><div><div><div>312</div></div><div><span>// ===================== 回调按钮（屏蔽） ========================</span></div></div><div><div><div>313</div></div><div>
</div></div><div><div><div>314</div></div><div><span>async function onCallback(query, env) {</span></div></div><div><div><div>315</div></div><div><span><span>  </span></span><span>const ADMIN_UID = env.ENV_ADMIN_UID;</span></div></div><div><div><div>316</div></div><div>
</div></div><div><div><div>317</div></div><div><span><span>  </span></span><span>if (query.from.id.toString() !== ADMIN_UID) {</span></div></div><div><div><div>318</div></div><div><span><span>    </span></span><span>return;</span></div></div><div><div><div>319</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>320</div></div><div>
</div></div><div><div><div>321</div></div><div><span><span>  </span></span><span>const data = query.data;</span></div></div><div><div><div>322</div></div><div>
</div></div><div><div><div>323</div></div><div><span><span>  </span></span><span>if (data.startsWith("block:")) {</span></div></div><div><div><div>324</div></div><div><span><span>    </span></span><span>const userId = data.split(":")[1];</span></div></div><div><div><div>325</div></div><div>
</div></div><div><div><div>326</div></div><div><span><span>    </span></span><span>await setBlock(env, userId, true);</span></div></div><div><div><div>327</div></div><div>
</div></div><div><div><div>328</div></div><div><span><span>    </span></span><span>await requestTelegram(</span></div></div><div><div><div>329</div></div><div><span><span>      </span></span><span>"answerCallbackQuery",</span></div></div><div><div><div>330</div></div><div><span><span>      </span></span><span>makeReq({ callback_query_id: query.id, text: "已拉黑该用户" }),</span></div></div><div><div><div>331</div></div><div><span><span>      </span></span><span>env</span></div></div><div><div><div>332</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>333</div></div><div>
</div></div><div><div><div>334</div></div><div><span><span>    </span></span><span>await requestTelegram(</span></div></div><div><div><div>335</div></div><div><span><span>      </span></span><span>"editMessageReplyMarkup",</span></div></div><div><div><div>336</div></div><div><span><span>      </span></span><span>makeReq({</span></div></div><div><div><div>337</div></div><div><span><span>        </span></span><span>chat_id: query.message.chat.id,</span></div></div><div><div><div>338</div></div><div><span><span>        </span></span><span>message_id: query.message.message_id,</span></div></div><div><div><div>339</div></div><div><span><span>        </span></span><span>reply_markup: {</span></div></div><div><div><div>340</div></div><div><span><span>          </span></span><span>inline_keyboard: [[{ text: "✔ 已拉黑", callback_data: "noop" }]]</span></div></div><div><div><div>341</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>342</div></div><div><span><span>      </span></span><span>}),</span></div></div><div><div><div>343</div></div><div><span><span>      </span></span><span>env</span></div></div><div><div><div>344</div></div><div><span><span>    </span></span><span>);</span></div></div><div><div><div>345</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>346</div></div><div><span>}</span></div></div><div><div><div>347</div></div><div>
</div></div><div><div><div>348</div></div><div>
</div></div><div><div><div>349</div></div><div><span>// ===================== 管理员命令 ========================</span></div></div><div><div><div>350</div></div><div>
</div></div><div><div><div>351</div></div><div><span>async function handleBlock(message, env) {</span></div></div><div><div><div>352</div></div><div><span><span>  </span></span><span>const guest = await getMsgMap(env, message.reply_to_message.message_id.toString());</span></div></div><div><div><div>353</div></div><div>
</div></div><div><div><div>354</div></div><div><span><span>  </span></span><span>await setBlock(env, guest, true);</span></div></div><div><div><div>355</div></div><div><span><span>  </span></span><span>return sendMessage({ chat_id: env.ENV_ADMIN_UID, text: `UID:${guest} 已屏蔽` }, env);</span></div></div><div><div><div>356</div></div><div><span>}</span></div></div><div><div><div>357</div></div><div>
</div></div><div><div><div>358</div></div><div><span>async function handleUnblock(message, env) {</span></div></div><div><div><div>359</div></div><div><span><span>  </span></span><span>const guest = await getMsgMap(env, message.reply_to_message.message_id.toString());</span></div></div><div><div><div>360</div></div><div>
</div></div><div><div><div>361</div></div><div><span><span>  </span></span><span>await setBlock(env, guest, false);</span></div></div><div><div><div>362</div></div><div><span><span>  </span></span><span>return sendMessage({ chat_id: env.ENV_ADMIN_UID, text: `UID:${guest} 已解除屏蔽` }, env);</span></div></div><div><div><div>363</div></div><div><span>}</span></div></div><div><div><div>364</div></div><div>
</div></div><div><div><div>365</div></div><div><span>async function handleCheckBlock(message, env) {</span></div></div><div><div><div>366</div></div><div><span><span>  </span></span><span>const guest = await getMsgMap(env, message.reply_to_message.message_id.toString());</span></div></div><div><div><div>367</div></div><div><span><span>  </span></span><span>const blocked = await getBlock(env, guest);</span></div></div><div><div><div>368</div></div><div>
</div></div><div><div><div>369</div></div><div><span><span>  </span></span><span>return sendMessage({</span></div></div><div><div><div>370</div></div><div><span><span>    </span></span><span>chat_id: env.ENV_ADMIN_UID,</span></div></div><div><div><div>371</div></div><div><span><span>    </span></span><span>text: `UID:${guest} → ${blocked ? "已屏蔽" : "未屏蔽"}`</span></div></div><div><div><div>372</div></div><div><span><span>  </span></span><span>}, env);</span></div></div><div><div><div>373</div></div><div><span>}</span></div></div><div><div><div>374</div></div><div>
</div></div><div><div><div>375</div></div><div>
</div></div><div><div><div>376</div></div><div><span>// ===================== Webhook 控制 ========================</span></div></div><div><div><div>377</div></div><div>
</div></div><div><div><div>378</div></div><div><span>async function registerWebhook(request, env) {</span></div></div><div><div><div>379</div></div><div><span><span>  </span></span><span>const url = new URL(request.url);</span></div></div><div><div><div>380</div></div><div><span><span>  </span></span><span>const webhookUrl = `${url.protocol}//${url.hostname}/endpoint`;</span></div></div><div><div><div>381</div></div><div>
</div></div><div><div><div>382</div></div><div><span><span>  </span></span><span>const res = await fetch(apiUrl("setWebhook", { url: webhookUrl, secret_token: env.ENV_BOT_SECRET }, env)).then(r =&gt; r.json());</span></div></div><div><div><div>383</div></div><div><span><span>  </span></span><span>return new Response(res.ok ? "OK" : JSON.stringify(res));</span></div></div><div><div><div>384</div></div><div><span>}</span></div></div><div><div><div>385</div></div><div>
</div></div><div><div><div>386</div></div><div><span>async function unRegisterWebhook(request, env) {</span></div></div><div><div><div>387</div></div><div><span><span>  </span></span><span>const res = await fetch(apiUrl("setWebhook", { url: "" }, env)).then(r =&gt; r.json());</span></div></div><div><div><div>388</div></div><div><span><span>  </span></span><span>return new Response(res.ok ? "OK" : JSON.stringify(res));</span></div></div><div><div><div>389</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><img src="https://s.muxui.com:8015/images/2026/09/LBRzi6dDrfmd.webp" alt="" /></p><p><img src="https://s.muxui.com:8015/images/2026/09/b7tGwXUlHmpK.webp" alt="" /></p><p><img src="https://s.muxui.com:8015/images/2026/09/Qw0DC6MlU9co.webp" alt="" /></p><p>然后把上面的代码全部复制到里面去.</p></section><section><h2>3. 绑定 D1 数据库<a href="#3-绑定-d1-数据库"><span>#</span></a></h2><p><img src="https://s.muxui.com:8015/images/2026/09/aQI55gnsNUk9.webp" alt="" /></p><p><img src="https://s.muxui.com:8015/images/2026/09/0Vvabkzf9QY6.webp" alt="" /></p><p>D1的key为</p></section><section><h2>4. 设置环境变量<a href="#4-设置环境变量"><span>#</span></a></h2><table><thead><tr><th>变量</th><th>说明</th></tr></thead><tbody><tr><td><code>ENV_BOT_TOKEN</code></td><td>Telegram Bot Token</td></tr><tr><td><code>ENV_ADMIN_UID</code></td><td>管理员 UID</td></tr><tr><td><code>ENV_BOT_SECRET</code></td><td>Webhook secret（可随意生成）</td></tr></tbody></table><p><img src="https://s.muxui.com:8015/images/2026/09/Wpf5cwuAZ7o6.webp" alt="" /></p><ol>
<li>从<a href="https://t.me/BotFather" target="_blank">@BotFather</a>获取token，并且可以发送<code>/setjoingroups</code>来禁止此Bot被添加到群组</li>
<li>从<a href="https://www.uuidgenerator.net/" target="_blank">uuidgenerator</a>获取一个随机uuid作为secret</li>
<li>从<a href="https://t.me/username_to_id_bot" target="_blank">@username_to_id_bot</a>获取你的用户id</li>
<li>配置worker的变量
<ul>
<li>增加一个<code>ENV_BOT_TOKEN</code>变量，数值为从步骤1中获得的token</li>
<li>增加一个<code>ENV_BOT_SECRET</code>变量，数值为从步骤2中获得的secret</li>
<li>增加一个<code>ENV_ADMIN_UID</code>变量，数值为从步骤3中获得的用户id</li>
</ul>
</li>
</ol></section><section><h2>5. 注册 Webhook<a href="#5-注册-webhook"><span>#</span></a></h2><p>访问：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>https://你的worker域名/registerWebhook</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h2>6. 测试<a href="#6-测试"><span>#</span></a></h2><p>测试Bot: <a href="https://t.me/guaguawa_bot" target="_blank">@guaguawa_bot</a></p></section></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/mac-disk-eject-fix/</id>
      <title type="text">Mac中磁盘没有正常推出的解决方案</title>
      <published>2025-10-23T00:00:00.000Z</published>
      <updated>2026-01-07T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/mac-disk-eject-fix/"/>
      <summary type="text">由于mac电脑关机或者没有实现正常推出硬盘导致的磁盘没有正常推出。 解决办法 ： 1）打开终端，输入如下命令查看一下硬盘的信息，确定一下自己的磁盘是否可以显示： diskutil list 在终端的最后一行显示了我硬盘的名字，因此 /dev</summary>
      <content type="html"><![CDATA[<blockquote><p>由于mac电脑关机或者没有实现正常推出硬盘导致的磁盘没有正常推出。</p></blockquote>
<p><strong>解决办法</strong>：</p>
<p>1）打开终端，输入如下命令查看一下硬盘的信息，确定一下自己的磁盘是否可以显示：</p>
<p>diskutil list</p>
<p>在终端的最后一行显示了我硬盘的名字，因此<code>/dev/disk2</code>所在目录就是我的移动硬盘。</p>
<p>2）杀死对应进程</p>
<p>sudo fsck_hfs -fy /dev/disk2</p>
<p>3） 挂载移动硬盘数据卷</p>
<p>sudo diskutil mount /dev/disk2</p>
<p>4）去磁盘工具可以看到外置里面自己的硬盘了，但是显示是灰色，我们等待一段时间就可以看到我们硬盘了</p>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/ai-summary-wordpress-plugin/</id>
      <title type="text">[开源] AI Summary - WordPress智能摘要生成插件</title>
      <published>2025-09-19T00:00:00.000Z</published>
      <updated>2025-09-19T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/ai-summary-wordpress-plugin/"/>
      <summary type="text">AI Summary是一款专为WordPress设计的智能摘要生成插件，它集成了五大主流AI服务：百度文心一言、OpenAI ChatGPT、Google Gemini、字节豆包和阿里通义千问。无论您是个人博客作者还是企业网站运营者，这款插</summary>
      <content type="html"><![CDATA[<p>AI Summary是一款专为WordPress设计的智能摘要生成插件，它集成了五大主流AI服务：百度文心一言、OpenAI ChatGPT、Google Gemini、字节豆包和阿里通义千问。无论您是个人博客作者还是企业网站运营者，这款插件都能帮您轻松提升内容质量和搜索引擎优化效果。</p>
<section><h2>🚀 核心功能特性<a href="#-核心功能特性"><span>#</span></a></h2><p><strong>多AI服务支持</strong></p><ul>
<li>🌟 ChatGPT - OpenAI官方API，支持自定义API地址和模型名称</li>
<li>✨ Google Gemini - 谷歌最新AI模型，支持自定义API地址和模型名称</li>
<li>🔥 文心一言 - 百度千帆大模型平台，支持最新API Key认证</li>
<li>🔥 字节豆包 - 火山方舟平台，支持自定义API地址和模型名称</li>
<li>⚡ 通义千问 - 阿里云百炼平台，支持多种模型规格</li>
</ul><p><strong>智能内容生成</strong></p><ul>
<li>📝 <strong>AI摘要生成</strong> - 自动提取文章核心内容，生成吸引人的摘要</li>
<li>🔍 <strong>SEO自动优化</strong> - 智能生成SEO描述和关键词</li>
<li>🎯 <strong>内容精准提取</strong> - 500字关键内容提取，降低API成本</li>
</ul><p><strong>完美主题集成</strong></p><ul>
<li>🎨 <strong>B2主题完美适配</strong> - 自动填充B2主题SEO字段</li>
<li>🔄 <strong>双向数据同步</strong> - AI字段与主题字段实时同步</li>
<li>📱 <strong>响应式设计</strong> - 支持所有WordPress编辑器</li>
</ul></section>
<section><h2>💡 使用方法<a href="#-使用方法"><span>#</span></a></h2><p><strong>编辑页面生成</strong></p><p>在文章编辑页面右侧找到 <strong>“AI Summary设置”</strong> 元框：</p><ol>
<li><strong>生成摘要</strong>: 点击”生成摘要”按钮，AI自动分析文章内容</li>
<li><strong>生成SEO</strong>: 点击”生成SEO”按钮，自动生成描述和关键词</li>
<li><strong>自动保存</strong>: 生成成功后自动保存，无需手动操作</li>
<li><strong>一键同步</strong>: 自动填充到B2主题SEO字段</li>
</ol><p><strong>批量管理</strong></p><p>在 <strong>AI Summary</strong> → <strong>文章管理</strong> 页面：</p><ul>
<li>✅ <strong>批量生成摘要</strong>: 选择多篇文章，一键生成摘要</li>
<li>✅ <strong>批量生成SEO</strong>: 智能跳过已生成的内容</li>
<li>✅ <strong>状态监控</strong>: 清晰显示每篇文章的生成状态</li>
<li>✅ <strong>进度反馈</strong>: 详细的操作进度和结果提示</li>
</ul></section>
<section><h2>🎨 前端展示效果<a href="#-前端展示效果"><span>#</span></a></h2><p><img src="https://s.muxui.com:8015/images/2026/09/hTO164DeMzxX.webp" alt="" /></p><p>插件在文章页面自动添加精美的AI摘要展示区域：</p><ul>
<li>🎭 <strong>打字机动画效果</strong> - 吸引用户注意力</li>
<li>🎯 <strong>“看其他文章”按钮</strong> - 增加页面停留时间</li>
<li>📱 <strong>响应式设计</strong> - 完美适配移动端</li>
<li>🎨 <strong>可自定义样式</strong> - 与网站主题完美融合</li>
</ul><p><img src="https://s.muxui.com:8015/images/2026/09/imlfylfbx2q0.webp" alt="" /></p></section>
<section><h2>🎨 后端展示效果<a href="#-后端展示效果"><span>#</span></a></h2><p><img src="https://s.muxui.com:8015/images/2026/09/RtwkBlMsgm7T.webp" alt="" /></p><p><img src="https://s.muxui.com:8015/images/2026/09/vWtld8dGz1XL.webp" alt="" /></p><p><img src="https://s.muxui.com:8015/images/2026/09/gUQML9Xvq3Qk.webp" alt="" /></p><p><img src="https://s.muxui.com:8015/images/2026/09/nzaFfs0wxegM.webp" alt="" /></p><p><img src="https://s.muxui.com:8015/images/2026/09/tH4jYj00m5V5.webp" alt="" /></p><p><img src="https://s.muxui.com:8015/images/2026/09/FLsxLjDGiATl.webp" alt="" /></p><p><img src="https://s.muxui.com:8015/images/2026/09/fuCR6EzJ5Zg6.webp" alt="" /></p></section>
<section><h2>⚡ 性能优化特性<a href="#-性能优化特性"><span>#</span></a></h2><p><strong>智能内容提取</strong></p><ul>
<li>🔹 <strong>关键内容识别</strong>: 自动提取标题、首段、关键段落</li>
<li>🔹 <strong>500字限制</strong>: 大幅降低API调用成本</li>
<li>🔹 <strong>提取算法</strong>: 智能识别文章核心信息</li>
</ul><p><strong>数据缓存机制</strong></p><ul>
<li>🔹 <strong>生成结果缓存</strong>: 避免重复生成相同内容</li>
<li>🔹 <strong>本地数据存储</strong>: WordPress原生meta机制</li>
<li>🔹 <strong>增量更新</strong>: 只有内容变化时才重新生成</li>
</ul></section>
<section><h2>🛠️ 技术实现细节<a href="#️-技术实现细节"><span>#</span></a></h2><p><strong>架构设计</strong></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 统一AI调用接口</span></div></div><div><div><div>2</div></div><div><span>class AI {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>static function getSummary($content) {</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>// 智能摘要生成</span></div></div><div><div><div>5</div></div><div><span><span>   </span></span><span>}</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>static function getSeo($content) {</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>// SEO信息生成</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div><span>​</span></div></div><div><div><div>13</div></div><div><span>// 多AI服务适配</span></div></div><div><div><div>14</div></div><div><span>switch ($provider) {</span></div></div><div><div><div>15</div></div><div><span><span>   </span></span><span>case 'wenxin': return WenXin::chat();</span></div></div><div><div><div>16</div></div><div><span><span> </span></span><span>case 'chatgpt': return ChatGPT::chat();</span></div></div><div><div><div>17</div></div><div><span><span> </span></span><span>case 'gemini': return Gemini::chat();</span></div></div><div><div><div>18</div></div><div><span><span> </span></span><span>case 'doubao': return Doubao::chat();</span></div></div><div><div><div>19</div></div><div><span><span> </span></span><span>case 'tongyi': return Tongyi::chat();</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>📊 功能对比表<a href="#-功能对比表"><span>#</span></a></h2><table><thead><tr><th>功能特性</th><th>AI Summary</th><th>其他插件</th></tr></thead><tbody><tr><td>多AI服务支持</td><td>✅ 5个主流服务</td><td>❌ 单一服务</td></tr><tr><td>B2主题集成</td><td>✅ 完美适配</td><td>❌ 需手动配置</td></tr><tr><td>智能内容提取</td><td>✅ 500字优化</td><td>❌ 全文发送</td></tr><tr><td>实时双向同步</td><td>✅ 自动同步</td><td>❌ 单向导出</td></tr><tr><td>批量处理</td><td>✅ 智能跳过</td><td>❌ 重复生成</td></tr><tr><td>用户体验</td><td>✅ 一键操作</td><td>❌ 复杂配置</td></tr></tbody></table></section>
<section><h2>🎯 适用场景<a href="#-适用场景"><span>#</span></a></h2><p><strong>个人博客</strong></p><ul>
<li>📝 技术分享博客 - 自动生成技术要点摘要</li>
<li>📚 知识分享站点 - 提炼核心知识点</li>
<li>🎨 创作类网站 - 吸引读者阅读兴趣</li>
</ul><p><strong>企业网站</strong></p><ul>
<li>🏢 公司新闻站 - 快速生成新闻摘要</li>
<li>📈 产品介绍页 - 突出产品核心卖点</li>
<li>🎓 在线教育 - 课程内容精华提取</li>
</ul><p><strong>内容平台</strong></p><ul>
<li>📖 资讯聚合站 - 批量生成文章摘要</li>
<li>🌐 多语言网站 - 统一内容生成标准</li>
<li>📱 移动优先站 - 简洁摘要提升体验</li>
</ul></section>
<section><h2>🔧 高级配置技巧<a href="#-高级配置技巧"><span>#</span></a></h2><p><strong>自定义API配置</strong></p><p><strong>ChatGPT配置:</strong></p><ul>
<li>🔹 API地址 (可选): 支持第三方代理和镜像站</li>
<li>🔹 自定义模型: 支持最新模型如gpt-4o、gpt-4o-mini等</li>
<li>🔹 优先级: 自定义模型名称优先于下拉选择</li>
</ul><p><strong>Gemini配置:</strong></p><ul>
<li>🔹 API地址 (可选): 支持代理服务和第三方兼容接口</li>
<li>🔹 自定义模型: 支持最新模型如gemini-1.5-flash等</li>
<li>🔹 优先级: 自定义模型名称优先于下拉选择</li>
</ul><p><strong>API成本优化</strong></p><ol>
<li><strong>选择合适模型</strong>:
<ul>
<li>日常使用: ernie-3.5-8k / gpt-3.5-turbo</li>
<li>高质量需求: ernie-4.0-8k / gpt-4</li>
</ul>
</li>
<li><strong>内容长度控制</strong>:
<ul>
<li>摘要字数: 100-200字 (平衡质量与成本)</li>
<li>SEO描述: 120-150字符 (搜索引擎友好)</li>
</ul>
</li>
<li><strong>批量处理策略</strong>:
<ul>
<li>选择性生成: 只为重要文章生成</li>
<li>定期更新: 根据流量数据优化</li>
</ul>
</li>
</ol></section>
<section><h2>📈 SEO优化效果<a href="#-seo优化效果"><span>#</span></a></h2><p>使用AI Summary插件后，您的网站将获得：</p><p><strong>搜索引擎优化</strong></p><ul>
<li>🎯 <strong>精准关键词</strong>: AI智能识别文章核心关键词</li>
<li>📝 <strong>吸引人描述</strong>: 生成符合搜索引擎要求的描述</li>
<li>🔍 <strong>提升点击率</strong>: 优质摘要吸引更多用户点击</li>
</ul><p><strong>用户体验提升</strong></p><ul>
<li>⚡ <strong>快速了解</strong>: 用户可快速了解文章内容</li>
<li>📱 <strong>移动友好</strong>: 简洁摘要适合移动端阅读</li>
<li>🎨 <strong>视觉效果</strong>: 打字机动画增强用户体验</li>
</ul></section>
<section><h2>🆚 版本更新记录<a href="#-版本更新记录"><span>#</span></a></h2><p><strong>v1.0.0 (2025-06-28)</strong></p><ul>
<li>🎉 <strong>首次发布</strong>: 完整功能实现</li>
<li>✅ <strong>多AI支持</strong>: 文心一言、ChatGPT、Gemini</li>
<li>🔧 <strong>B2主题集成</strong>: 完美适配B2主题</li>
<li>🚀 <strong>智能优化</strong>: 内容提取和成本控制</li>
</ul><p><strong>v1.1.0 (<strong>2025</strong>-06-30)</strong></p><ul>
<li>🎉 <strong>新增模型</strong>: 豆包、通义千问</li>
<li>🚀 <strong>优化代码</strong>: 优化代码逻辑及界面优化</li>
</ul><p><strong>v1.1.2 (2025-07-02)</strong></p><ul>
<li>🚀 功能增强: ChatGPT和Gemini配置升级</li>
<li>🔧 ChatGPT新增: 自定义模型名称配置</li>
<li>🌐 Gemini新增: 自定义API地址和模型名称配置</li>
<li>⚡ 优先级逻辑: 自定义模型优先于下拉选择</li>
<li>🎯 兼容性: 完全向后兼容，不影响现有配置</li>
</ul><p><strong>未来规划</strong></p><ul>
<li>📊 <strong>数据分析</strong>: 生成效果统计和优化建议</li>
<li>🎨 <strong>主题扩展</strong>: 支持更多WordPress主题</li>
</ul></section>
<section><h2>🎁 送一些Gemini的key<a href="#-送一些gemini的key"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>AIzaSyA7Br2V-gqJ-SdIpKU4jHZX4bDsbQLtWFY,</span></div></div><div><div><div>2</div></div><div><span>AIzaSyCuQA8YMmF3nQACiNUvpH7eUBiJc2UG65w,</span></div></div><div><div><div>3</div></div><div><span>AIzaSyAWfB2vi0x-yM2pnOPxeu1HXe4MC0ObJUU,</span></div></div><div><div><div>4</div></div><div><span>AIzaSyDnjFUaqySz3ju_8kEUuyJ8JE6AIc47Lkw,</span></div></div><div><div><div>5</div></div><div><span>AIzaSyDzfJuNKrKpzai82YgqRg6ksLcpIhvGS7A,</span></div></div><div><div><div>6</div></div><div><span>AIzaSyAmML4tOnwjo202n60Ue_kU2mY2SWor3oA,</span></div></div><div><div><div>7</div></div><div><span>AIzaSyCcHl0Ge0qgbJD6KAqNw7ulxuCkFF-rg6Q,</span></div></div><div><div><div>8</div></div><div><span>AIzaSyDzOr21jeAoJMHlj-bihsOvg5uB4HOBM40,</span></div></div><div><div><div>9</div></div><div><span>AIzaSyDjZ0SnKsXje34Mn09HCTPDO0Cb7FaTflc,</span></div></div><div><div><div>10</div></div><div><span>AIzaSyC5kBkGmo2GClIHLbDhELQowpwadXwf9iY,</span></div></div><div><div><div>11</div></div><div><span>AIzaSyCglOiXDxSp7MnNNLKBmwIphGD4efphAjc,</span></div></div><div><div><div>12</div></div><div><span>AIzaSyAPK7Z43CjDFkGjU4aP-OBnzIipyL-KVws,</span></div></div><div><div><div>13</div></div><div><span>AIzaSyAXPQswBkbAHHhOhGK_UWQpxAjdhYbTis4,</span></div></div><div><div><div>14</div></div><div><span>AIzaSyCbbndNEN44tBrDnv7rOsMXWCiILx7-jbY,</span></div></div><div><div><div>15</div></div><div><span>AIzaSyBafWhRUWKkPg5k6M_O57Mz74V7IP5PWVs,</span></div></div><div><div><div>16</div></div><div><span>AIzaSyDRvXuYRBaZ-AlBw8GNDJKSyTHQmp3rQUU,</span></div></div><div><div><div>17</div></div><div><span>AIzaSyAvoAgK8wIqjediRfXCY0MmMbShN_VYlJI,</span></div></div><div><div><div>18</div></div><div><span>AIzaSyDMqE0spVwOOdBkXmgKdXIY1JqMk8LiQWI,</span></div></div><div><div><div>19</div></div><div><span>AIzaSyAiQEeAeozmBi5HTk6XkG2fomYdUFQxDxg,</span></div></div><div><div><div>20</div></div><div><span>AIzaSyBfFOs3ffu4oF4K5HkU6h6CY99XQnlFM3o,</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>🌕想说的话<a href="#想说的话"><span>#</span></a></h2><p>本插件的开发灵感来源于 <strong><a href="https://www.lovestu.com/core-ai-power.html" target="_blank">Core AI Power</a></strong>。当初偶然间发现这款插件，原本只是想尝试一下，结果发现无法正常使用——主要有两个原因：一是我的博客所用的主题不同，二是该插件仅支持 <strong>文心一言 1.0</strong> 版本，而目前官网已经不再开放 1.0 的申请接口。</p><p>因此，我决定自己动手开发了一个兼容 <strong>文心一言 2.0 接口</strong> 的插件，并对 <strong>B2 主题</strong> 进行了适配测试，运行效果良好。其他主题暂未测试，如遇兼容性问题，建议可以尝试使用原版的 Core AI Power 插件。</p><p>另外需要说明的是，本插件默认带有一个跳转至我博客的引流链接。如果你不希望显示此链接，可以在你的主题样式中添加以下 CSS 代码进行隐藏：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>.ai-summary-brand {  display: none;}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果你的博客部署在海外服务器（外网环境），我推荐优先使用 <strong>Gemini</strong> 模型。上文中提到的 API Key，你可以直接拿来用，开箱即用。</p><p>其实最初我就是打算接入 Gemini 的。为此测试了很久，每次都连不上，搞得我一度崩溃 —— 明明本地测试一切正常。最终才发现，问题根源在于我用的是 <strong>国内的阿里云服务器</strong>。我淦……😩</p></section>
<section><h2>开源地址<a href="#开源地址"><span>#</span></a></h2><a href="https://github.com/O96u/ai-summary" target="_blank"><div><div><div><div></div><div>O96u</div></div><div>/</div><div>ai-summary</div></div><div></div></div><div>AI Summary是一款专为WordPress设计的智能摘要生成插件</div><div><div>—</div><div>—</div><div>no-license</div><span>PHP</span></div></a></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/kms-activation-tool/</id>
      <title type="text">KMS系统激活工具</title>
      <published>2025-08-19T00:00:00.000Z</published>
      <updated>2025-08-20T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/kms-activation-tool/"/>
      <summary type="text">几乎完美的系统激活工具，功能丰富而强大 功能： 1、基本的各种激活，从Windows XP到Windows 11 24H2（含Server）. 2、激活Microsoft Office 2010到2024，包括365和UWP版. 3、可配合</summary>
      <content type="html"><![CDATA[<p>几乎完美的系统激活工具，功能丰富而强大</p>
<p><img src="https://s.muxui.com:8015/images/2026/09/wnJFcsOIsnRl.webp" alt="" /></p>
<section><h2>功能：<a href="#功能"><span>#</span></a></h2><p>1、基本的各种激活，从Windows XP到Windows 11 24H2（含Server）.<br />
2、激活Microsoft Office 2010到2024，包括365和UWP版.<br />
3、可配合搭建本地内网激活专用服务器.<br />
4、更改任意Windows版本（如从家庭版切换为专业版）</p></section>
<section><h2>系统激活方式有：<a href="#系统激活方式有"><span>#</span></a></h2><p>1、数字许可证永久激活<br />
2、KMS38离线激活<br />
3、OEM激活（如惠普联想整机）<br />
4、上古电话激活</p></section>
<section><h2>更新日志：<a href="#更新日志"><span>#</span></a></h2><p>**v63.2.0 更新说明：<br />
**1.增加对Windows评估版的支持（重置90天）。<br />
2.修复智能激活误启动KMS服务器的bug。</p><p>文件: HEU_KMS_Activator_v63.2.0.exe<br />
MD5: 8F331BCBF428291F36EA59467A4E080B<br />
SHA1: 5DB1793734E2A7577614BCE7E07067AA9CD08F65<br />
SHA256: 85D199B41482B4D0C341F817ADE60EDF789CCEDDC2845F4B1938C597D5F54300</p><p>**v63.1.0 更新说明：<br />
**1.修复智能激活选项中单独勾选Office KMS选项执行失败的bug。</p><p>文件: HEU_KMS_Activator_v63.1.0.exe<br />
MD5: 8C52CBADE34FEA1FB1B53135B97F4CB8<br />
SHA1: 7E86EF8E396E6D4CBC961045B02EF0C1C70240D2<br />
SHA256: FE4B65E02DA1A6BC38383C13D1010A94941C3E13668B6120D936D72941473D4A</p><p>**v63.0.0 更新说明：<br />
**1.优化了激活后提示信息过长的问题。<br />
2.修复了OHook创建符号链接因为权限导致失败的问题。<br />
3.修复了某些情况下软件闪退问题。<br />
4.新增TForge StaticCID激活方式(需联网)，基于开源项目<a href="https://github.com/massgravel/Microsoft-Activation-Scripts%E3%80%82" target="_blank">https://github.com/massgravel/Microsoft-Activation-Scripts。</a><br />
5.新增静默参数/wzc /ozc /wsc /osc 使用说明使用/?查看。<br />
6.新增在线获取CID的功能，基于开源项目<a href="https://github.com/dadorner-msft/activationws%E3%80%82" target="_blank">https://github.com/dadorner-msft/activationws。</a></p><p>文件: HEU_KMS_Activator_v63.0.0.exe<br />
MD5: D6C8EC6CF38176865BFA1F1CF4FB467D<br />
SHA1: 3A5AF309E88C23CB4396E316A20BF654CAB07E37<br />
SHA256: 954A91CC3D0CB045D8E44A4DAC7C349CA330CBD84F57853785F9754BBF4772F4</p><p>**v62.0.0 更新说明：<br />
**1.OHook新增对OSPP的支持（支持Window XP及以上系统的所有Office版本）（OSPP方法基于开源程序MAS3.2）<br />
2.调整经典界面布局。<br />
3.修复了检测Office永久激活状态错误的bug。<br />
4.修复了一些已知问题。</p><p>文件: HEU_KMS_Activator_v62.0.0.exe<br />
MD5: 3F6573A37D373123D2014A09E4FDB509<br />
SHA1: 9F28201A03A5CA363936720C780D466292E78934<br />
SHA256: 930A816F6B78A667981B282E46DB7D0CDCE1AF7D8E20C3910DAF997031F32987</p></section>
<section><h2>参考开源项目<a href="#参考开源项目"><span>#</span></a></h2><a href="https://github.com/massgravel/Microsoft-Activation-Scripts" target="_blank"><div><div><div><div></div><div>massgravel</div></div><div>/</div><div>Microsoft-Activation-Scripts</div></div><div></div></div><div>Open-source Windows and Office activator featuring HWID, Ohook, TSforge, and Online KMS activation methods, along with advanced troubleshooting.</div><div><div>—</div><div>—</div><div>GPL-3.0</div><span>Batchfile</span></div></a><a href="https://github.com/dadorner-msft/activationws" target="_blank"><div><div><div><div></div><div>dadorner-msft</div></div><div>/</div><div>activationws</div></div><div></div></div><div>Robust and practical MAK activation solution for Windows.</div><div><div>—</div><div>—</div><div>MIT</div><span>C#</span></div></a></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/mysql-knowledge-notes/</id>
      <title type="text">Mysql 知识整理</title>
      <published>2025-07-16T00:00:00.000Z</published>
      <updated>2025-07-16T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/mysql-knowledge-notes/"/>
      <summary type="text">数据库三大范式 第一范式：每个列都不可以再拆分。 第二范式：在第一范式的基础上，非主键列完全依赖于主键，而不能是依赖于主键的一部分。 第三范式：在第二范式的基础上，非主键列只依赖于主键，不依赖于其他非主键。 SQL语言共分为四大类 1. 数</summary>
      <content type="html"><![CDATA[<section><h2>数据库三大范式<a href="#数据库三大范式"><span>#</span></a></h2><p><strong>第一范式：每个列都不可以再拆分。</strong></p><p><img src="https://s.muxui.com:8015/images/2026/09/sCResVIf7RG8.webp" alt="" /></p><p><strong>第二范式：在第一范式的基础上，非主键列完全依赖于主键，而不能是依赖于主键的一部分。</strong></p><p><img src="https://s.muxui.com:8015/images/2026/09/RdScLRGQSAzj.webp" alt="" /></p><p><strong>第三范式：在第二范式的基础上，非主键列只依赖于主键，不依赖于其他非主键。</strong></p><p><img src="https://s.muxui.com:8015/images/2026/09/qDy7IwP1LEUl.webp" alt="" /></p></section>
<section><h2>SQL语言共分为四大类<a href="#sql语言共分为四大类"><span>#</span></a></h2><ol>
<li>数据查询语言（DQL）：用于从数据库中查询数据。常用的 DQL 命令是 select，它允许用户指定要从数据库中检索哪些数据。通过 select 语句，你可以选择特定的列、过滤数据、排序结果等。</li>
<li>数据定义语言（DDL）：用于定义和管理数据库中的结构。DDL 命令用于创建、修改和删除数据库、表、索引、视图等对象。常见的 DDL 命令包括 create、alter 和 drop。</li>
<li>数据操纵语言（DML）：用于操作数据库中的数据。DML 命令允许你插入、更新和删除数据。常用的 DML 命令包括 insert、update 和 delete。</li>
<li>数据控制语言（DCL）：用于控制数据库用户的权限和访问级别。DCL 命令允许管理员授权或撤销用户对数据库对象的访问权限。常见的 DCL 命令包括 grant 和 revoke。</li>
</ol><table><tbody><tr><td>类别</td><td>英文缩写</td><td>中文含义</td><td>作用关键词</td><td>常见语句</td></tr><tr><td>DDL</td><td>Data Definition</td><td>数据定义</td><td>定义结构</td><td><code>CREATE</code><code>ALTER</code><code>DROP</code></td></tr><tr><td>DML</td><td>Data Manipulation</td><td>数据操作</td><td>增删改数据</td><td><code>INSERT</code><code>UPDATE</code><code>DELETE</code></td></tr><tr><td>DQL</td><td>Data Query</td><td>数据查询</td><td>查找数据</td><td><code>SELECT</code></td></tr><tr><td>DCL</td><td>Data Control</td><td>权限控制</td><td>分配/撤销权限</td><td><code>GRANT</code><code>REVOKE</code></td></tr></tbody></table></section>
<section><h1>索引<a href="#索引"><span>#</span></a></h1><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>MySQL 索引</span></div></div><div><div><div>2</div></div><div><span><span> </span></span><span>├── 功能角度（逻辑类型）</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>│   ├── 主键索引（Primary Key）</span></div></div><div><div><div>4</div></div><div><span><span> </span></span><span>│   ├── 唯一索引（Unique Index）</span></div></div><div><div><div>5</div></div><div><span><span> </span></span><span>│   ├── 普通索引（Normal Index）</span></div></div><div><div><div>6</div></div><div><span><span> </span></span><span>│   ├── 联合索引（Composite Index）</span></div></div><div><div><div>7</div></div><div><span><span> </span></span><span>│   ├── 全文索引（Fulltext Index）</span></div></div><div><div><div>8</div></div><div><span><span> </span></span><span>│   └── 空间索引（Spatial Index）</span></div></div><div><div><div>9</div></div><div><span><span> </span></span><span>│</span></div></div><div><div><div>10</div></div><div><span><span> </span></span><span>└── 存储结构角度（物理结构）</span></div></div><div><div><div>11</div></div><div><span><span>     </span></span><span>├── 聚簇索引（Clustered Index）= 主键索引</span></div></div><div><div><div>12</div></div><div><span><span>     </span></span><span>├── 二级索引（Secondary Index）= 辅助索引</span></div></div></code></pre><div><div></div><div></div></div></figure></div><section><h2>创建索引的原则<a href="#创建索引的原则"><span>#</span></a></h2><table><tbody><tr><td>原则总结</td><td>是否推荐建索引？</td></tr><tr><td>查询条件字段</td><td>✅</td></tr><tr><td>区分度高的字段</td><td>✅</td></tr><tr><td>联合索引按顺序使用</td><td>✅</td></tr><tr><td>尽量实现覆盖查询</td><td>✅</td></tr><tr><td>控制索引数量（别太多）</td><td>⚠️ 慎重</td></tr><tr><td>频繁更新字段</td><td>❌ 尽量避免</td></tr><tr><td>长文本字段避免建索引</td><td>❌ 避免</td></tr><tr><td>函数、运算字段索引失效</td><td>❌ 避免</td></tr><tr><td>重复率高字段不推荐索引</td><td>❌ 避免</td></tr><tr><td>外键字段明确加索引</td><td>✅</td></tr></tbody></table></section><section><h2>MySQL为什么使用B+树作为索引的数据结构<a href="#mysql为什么使用b树作为索引的数据结构"><span>#</span></a></h2><p><strong>B+Tree vs B Tree:</strong></p><ul>
<li>存储相同数据量级别的情况下，B+Tree树高比 B Tree低,磁盘IO次数更少。</li>
<li>B+Tree叶子节点用双向链表串起来,适合范围查询，BTree无法做到这点</li>
</ul><p><strong>B+Tree vs二叉树:</strong></p><ul>
<li>随着数据量的增加，二叉树的树高会越来越高，磁盘IO次数也会更多,B+Tree在千万级别的数据量下，高度依然维持在34层左右，也就是说一次数据查询操作只需要做34次的磁盘O操作就能查询到目标数据。</li>
</ul><p><strong>B+Tree vs Hash:</strong></p><ul>
<li>虽然Hash的等值查询效率很高，但是无法做范围查询</li>
</ul></section><section><h2>索引失效有哪些？<a href="#索引失效有哪些"><span>#</span></a></h2><table><tbody><tr><td>场景/错误写法</td><td>原因</td><td>是否失效</td></tr><tr><td><code>WHERE UPPER(name) = 'XX'</code></td><td>函数操作</td><td>✅</td></tr><tr><td><code>WHERE age != 30</code></td><td>使用不等于</td><td>✅</td></tr><tr><td><code>WHERE name LIKE '%三'</code></td><td>通配符在前</td><td>✅</td></tr><tr><td><code>WHERE name = '张三' OR age = 20</code></td><td>OR 条件中部分无索引</td><td>✅</td></tr><tr><td><code>WHERE age = 20</code> 联合索引跳过最左字段</td><td>最左前缀原则不符合</td><td>✅</td></tr><tr><td><code>WHERE phone = 1234567890</code>（字段是字符串）</td><td>类型不一致，触发隐式转换</td><td>✅</td></tr><tr><td>小表查索引</td><td>优化器判断索引没必要</td><td>✅（可能）</td></tr></tbody></table></section><section><h2>什么时候需要 / 不需要创建索引？<a href="#什么时候需要--不需要创建索引"><span>#</span></a></h2><table><tbody><tr><td>是否建索引？</td><td>场景 / 条件</td></tr><tr><td>✅ 需要</td><td>频繁 <code>WHERE</code> 查询、<code>JOIN</code>、<code>排序</code>、<code>分组</code>、<code>唯一字段</code></td></tr><tr><td>✅ 需要</td><td>大表中的范围查询字段、外键字段、联合索引中最左字段</td></tr><tr><td>❌ 不需要</td><td>数据重复度高、字段频繁更新、字段很少被查询</td></tr><tr><td>❌ 不需要</td><td>极小的表、临时表、字段本身参与计算或函数</td></tr></tbody></table><ul>
<li><strong>读多写少 → 建索引优先（查询系统）</strong></li>
<li><strong>写多读少 → 谨慎建索引（日志、消息系统）</strong></li>
<li><strong>建索引之后要配合</strong> <code>**EXPLAIN**</code> <strong>分析是否命中</strong></li>
</ul></section><section><h2>有什么优化索引的方法？<a href="#有什么优化索引的方法"><span>#</span></a></h2><ul>
<li>前缀索引优化；</li>
<li>覆盖索引优化；</li>
<li>主键索引最好是自增的；</li>
<li>防止索引失效</li>
</ul><table><tbody><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>仅限字符串类型（CHAR/VARCHAR等）</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></section><section><h2>EXPLAIN 常见字段汇总（重点关注这几个）<a href="#explain-常见字段汇总重点关注这几个"><span>#</span></a></h2><p><img src="https://s.muxui.com:8015/images/2026/09/GnadPZBvaEYA.webp" alt="" /></p><section><h3><code>type</code>（连接类型）🌟最重要的性能指标之一<a href="#type连接类型最重要的性能指标之一"><span>#</span></a></h3><table><tbody><tr><td><strong>type</strong></td><td><strong>含义</strong></td><td><strong>效率</strong></td></tr><tr><td><code><strong>system</strong></code></td><td><strong>表仅有一行（系统表）</strong></td><td><strong>极高</strong></td></tr><tr><td><code><strong>const</strong></code></td><td><strong>一次命中，结果恒定</strong></td><td>✅<strong> 极高</strong></td></tr><tr><td><code><strong>eq_ref</strong></code></td><td><strong>主键/唯一索引的等值查询</strong></td><td>✅<strong> 非常好</strong></td></tr><tr><td><code><strong>ref</strong></code></td><td><strong>非唯一索引的等值查询</strong></td><td>✅<strong> 好</strong></td></tr><tr><td><code><strong>range</strong></code></td><td><strong>范围查询，如 </strong><code><strong>BETWEEN</strong></code><strong>、</strong><code><strong>LIKE 'xx%'</strong></code></td><td>✅<strong> 不错</strong></td></tr><tr><td><code><strong>index</strong></code></td><td><strong>全索引扫描（不回表）</strong></td><td>⚠️<strong> 中等</strong></td></tr><tr><td><code><strong>ALL</strong></code></td><td><strong>全表扫描</strong></td><td>❌<strong> 慢</strong></td></tr></tbody></table></section><section><h3><code>key</code>（使用的索引名称）<a href="#key使用的索引名称"><span>#</span></a></h3><p>显示 MySQL 实际使用的索引名称。</p><ul>
<li>如果是 <code>NULL</code> → 没有使用索引 ❌</li>
<li>如果是索引名 → 说明命中索引 ✅</li>
</ul></section><section><h3><code>rows</code>（扫描的行数）<a href="#rows扫描的行数"><span>#</span></a></h3><p>表示 MySQL 预估要读取的行数（越少越好）：</p><ul>
<li>小于 100：查询很快</li>
<li>上万以上：说明可能需要优化</li>
</ul><p>✅ 注意：这里是估算值，并非真实执行统计。</p></section><section><h3><code>Extra</code>（额外信息，能看出是否回表）<a href="#extra额外信息能看出是否回表"><span>#</span></a></h3><table><tbody><tr><td>Extra 内容</td><td>含义说明</td></tr><tr><td><code>Using index</code></td><td>✅ 覆盖索引，无需回表，性能最佳</td></tr><tr><td><code>Using index condition</code></td><td>✅ 使用了索引条件下推（ICP）</td></tr><tr><td><code>Using where</code></td><td>有使用 WHERE 条件（不是坏事）</td></tr><tr><td><code>Using temporary</code></td><td>❌ 用了临时表（常见于复杂排序或分组）</td></tr><tr><td><code>Using filesort</code></td><td>❌ 用了文件排序，可能意味着索引未用于 <code>ORDER BY</code></td></tr></tbody></table></section></section></section>
<section><h1>事务<a href="#事务"><span>#</span></a></h1><section><h2>事务(ACID)有哪些特性？<a href="#事务acid有哪些特性"><span>#</span></a></h2><table><tbody><tr><td>特性</td><td>说明（通俗理解）</td></tr><tr><td><strong>A 原子性</strong></td><td>要么都做，要么一个都不做（不可拆分）</td></tr><tr><td><strong>C 一致性</strong></td><td>数据操作前后都保持规则/约束一致</td></tr><tr><td><strong>I 隔离性</strong></td><td>多个事务之间互不干扰</td></tr><tr><td><strong>D 持久性</strong></td><td>提交成功后数据永不丢失</td></tr></tbody></table><p>事务必须要遵守 4 个特性，分别如下：</p><ul>
<li><strong>原子性（</strong><em><strong>Atomicity</strong></em><strong>）</strong>：一个事务中的所有操作，要么全部完成，要么全部不完成，不会结束在中间某个环节，而且事务在执行过程中发生错误，会被回滚到事务开始前的状态。</li>
<li><strong>一致性（</strong><em><strong>Consistency</strong></em><strong>）</strong>：是指事务操作前和操作后，数据满足完整性约束，数据库保持一致性状态。比如，用户 A 和用户 B 在银行分别有 800 元和 600 元，总共 1400 元，用户 A 给用户 B 不论如何转账，最后的结果是1400 元。</li>
<li><strong>隔离性（</strong><em><strong>Isolation</strong></em><strong>）</strong>：数据库允许多个并发事务同时对其数据进行读写和修改的能力，隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致，因为多个事务同时使用相同的数据时，不会相互干扰，每个事务都有一个完整的数据空间，对其他并发事务是隔离的。</li>
<li><strong>持久性（</strong><em><strong>Durability</strong></em><strong>）</strong>：事务处理结束后，对数据的修改就是永久的，即便系统故障也不会丢失。</li>
</ul><section><h3>InnoDB 引擎通过什么技术来保证事务的这四个特性的呢？<a href="#innodb-引擎通过什么技术来保证事务的这四个特性的呢"><span>#</span></a></h3><ul>
<li>持久性是通过 redo log （重做日志）来保证的；</li>
<li>原子性是通过 undo log（回滚日志） 来保证的；</li>
<li>隔离性是通过 MVCC（多版本并发控制） 或锁机制来保证的；</li>
<li>一致性则是通过持久性+原子性+隔离性来保证；</li>
</ul></section></section><section><h2>什么是脏读？幻读？不可重复读？<a href="#什么是脏读幻读不可重复读"><span>#</span></a></h2><table><tbody><tr><td>异常类型</td><td>描述</td></tr><tr><td>脏读</td><td>读到了别的事务未提交的数据</td></tr><tr><td>不可重复读</td><td>同一事务两次读到的结果不同（被别人修改了）</td></tr><tr><td>幻读</td><td>同一事务两次查询，新增或删除了行数</td></tr></tbody></table><ul>
<li><strong>脏读(</strong><em><strong>Drity Read</strong></em><strong>)</strong>：一个事务读取另一个事务未提交的数据</li>
<li><strong>不可重复读(</strong><em><strong>Non-repeatable read</strong></em><strong>)</strong>：一个事务对同一数据进行读取，前后数据内容不一致</li>
<li><strong>幻读(</strong><em><strong>Phantom Read</strong></em><strong>)</strong>：一个事务内进行了多次读取，数据总量不一致。</li>
</ul><p><img src="https://s.muxui.com:8015/images/2026/09/h7lN4BTApc3d.webp" alt="" /></p></section><section><h2>事务的隔离级别有哪些？<a href="#事务的隔离级别有哪些"><span>#</span></a></h2><table><tbody><tr><td>隔离级别</td><td>脏读</td><td>不可重复读</td><td>幻读</td></tr><tr><td>未提交读（READ-UNCOMMITTED）</td><td>√</td><td>√</td><td>√</td></tr><tr><td>已提交读（READ-COMMITTED）</td><td>×</td><td>√</td><td>√</td></tr><tr><td>可重复读（REPEATABLE-READ）</td><td>×</td><td>×</td><td>√</td></tr><tr><td>可串行（SERIALIZABLE）</td><td>×</td><td>×</td><td>×</td></tr></tbody></table><ul>
<li><strong>读未提交（</strong><em><strong>read uncommitted</strong></em><strong>）</strong>，指一个事务还没提交时，它做的变更就能被其他事务看到；</li>
<li><strong>读提交（</strong><em><strong>read committed</strong></em><strong>）</strong>，指一个事务提交之后，它做的变更才能被其他事务看到；</li>
<li><strong>可重复读（</strong><em><strong>repeatable read</strong></em><strong>）</strong>，指一个事务执行过程中看到的数据，一直跟这个事务启动时看到的数据是一致的，<strong>MySQL InnoDB 引擎的默认隔离级别</strong>；</li>
<li><strong>串行化（</strong><em><strong>serializable</strong></em> <strong>）</strong>；会对记录加上读写锁，在多个事务对这条记录进行读写操作时，如果发生了读写冲突的时候，后访问的事务必须等前一个事务执行完成，才能继续执行；</li>
</ul><p>按隔离水平高低排序如下：</p><p><img src="https://s.muxui.com:8015/images/2026/09/1ORRyQHcDVhi.webp" alt="" /></p></section><section><h2>MVCC<a href="#mvcc"><span>#</span></a></h2><p>它的实现原理主要是版本链，undo日志 ，Read View来实现的。即多版本并发控制，主要是为了提高数据库的并发性能。</p><section><h3>版本链<a href="#版本链"><span>#</span></a></h3><p>除了我们数据库每行的数据，还有几个隐藏字段，分别是事务ID（trx_id），一个是回滚指针(roll_pointer)。</p><p>trx_id ：每次一个事务对某条聚簇索引记录改动时，都会把自己的事务id重新写到 trx_id 隐藏列。</p><p>roll_pointer ：每次对某条聚簇索引记录进行改动时，都会把旧版本记录写入到 undo日志中，roll_pointer这个隐藏列是一个地址指针，通过它可以找到该聚簇索引记录历史修改信息。</p></section><section><h3>undo日志<a href="#undo日志"><span>#</span></a></h3><p>Undo log 主要用于记录数据被修改之前的日志，在表信息修改之前先会把数据拷贝到undo log里。当事务进行回滚时可以通过undo log 里的日志进行数据还原。</p><p>undo log主要分为两种：</p><p>insert undo log：顾名思义，此代表执行insert语句时产生的undo log, 它只在事务回滚时需要，因为这种log只是对本事务可见，其他事务不可见，所以当事务提交后，这种类型的undo log就会被系统直接删除回收（也就是该undo log占用的undo页面链表被释放）。</p><p>update undo log（主要）：事务在进行update或者delete时产生的undo log; 不仅在事务回滚时需要，在快照读时也需要（也就是MVCC），所以不能在事务提交后马上删除，只在提交后放入undo log的链表，等待purge线程进行最后的删除。</p></section><section><h3>Read View读视图<a href="#read-view读视图"><span>#</span></a></h3><section><h4>什么是Read View？<a href="#什么是read-view"><span>#</span></a></h4><p><strong>Read View 是当前事务在执行“快照读”时生成的一份“可见数据版本清单”</strong>，用于判断当前能“看见哪些事务提交的数据”。</p><p><strong>通俗地说：</strong></p><p>Read View 就像是当前事务“拍下的一张快照”，它决定了你在事务中看到的“历史数据版本”。</p><table><tbody><tr><td>元素</td><td>说明</td></tr><tr><td><code>m_ids</code></td><td>当前系统中<strong>活跃事务的 ID 列表</strong>（未提交）</td></tr><tr><td><code>min_trx_id</code></td><td>活跃事务中最小的事务 ID，用于加速判断</td></tr><tr><td><code>max_trx_id</code></td><td>当前系统中尚未分配的下一个事务 ID（即分配到的最大事务 ID + 1）</td></tr><tr><td><code>creator_trx_id</code></td><td>当前事务自己的 ID，用于标识“自己是否能看到自己”</td></tr></tbody></table><p>***creator_trx_id ：***指创建该 Read View 的事务的事务 id。</p><p>***m_ids ：***指创建该 Read View 时数据库中所有活跃事务的事务 id 列表，这是一个列表，活跃事务则代表是已启动但未提交的事务。</p><p>***min_trx_id ：***指创建 Read View 时数据库中活跃事务中最小的事务 id，也就是 m_ids 中的最小值。</p><p>***max_trx_id ：***这里并不是m_ids中的最大值，而是创建 Read View 时当前数据中应该给下一个事务的id值，也就是全局事务中最大的事务id值+ 1。</p><p>在可重复读隔离级别下，Read View是在事务开始（begin）之后且执行第一条sql时创建，创建Read View的同时也就生成了一个新的事务id（直到commit结束），事务会依赖该 Read View保证查询结果保持不变直到该事务结束。</p></section><section><h4>Read View 的判断规则（事务可见性判断）<a href="#read-view-的判断规则事务可见性判断"><span>#</span></a></h4><table><tbody><tr><td>条件</td><td>是否可见</td><td>原因说明</td></tr><tr><td><code>trx_id &lt; min_trx_id</code></td><td>✅ 可见</td><td>已经提交了，早于其他活跃事务</td></tr><tr><td><code>trx_id = creator_trx_id</code></td><td>✅ 可见</td><td>自己修改的当然能看到</td></tr><tr><td><code>trx_id &gt; max_trx_id</code></td><td>❌ 不可见</td><td>是后面新开的事务，还未开始 → 不可见</td></tr><tr><td><code>trx_id ∈ [min_trx_id, max_trx_id) 且 trx_id ∈ m_ids</code></td><td>❌ 不可见</td><td>活跃事务依然活跃着（还没提交事务）</td></tr><tr><td><code>trx_id ∈ [min_trx_id, max_trx_id) 且 trx_id ∉ m_ids</code></td><td>✅ 可见</td><td>事务虽启动时已存在，但已提交，可见</td></tr></tbody></table><p>一个事务去访问记录的时候，除了自己的更新记录总是可见之外，还有这几种情况：</p><ul>
<li>
<p>如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值，表示这个版本的记录是在创建 Read View <strong>前</strong>已经提交的事务生成的，所以该版本的记录对当前事务<strong>可见</strong>。</p>
</li>
<li>
<p>如果记录的 trx_id 值等于当前事务id,就是自己的事务自己处理,所以总是<strong>可见</strong></p>
</li>
<li>
<p>如果记录的 trx_id 值大于 Read View 中的 max_trx_id 值，表示这个版本的记录是在创建 Read View <strong>后</strong>才启动的事务生成的，所以该版本的记录对当前事务<strong>不可见</strong>。</p>
</li>
<li>
<p>如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间，需要判断 trx_id 是否在m_ids列表中：</p>
</li>
<li>
<p>如果记录的 trx_id <strong>在</strong> m_ids 列表中，表示生成该版本记录的活跃事务依然活跃着（还没提交事务），所以该版本的记录对当前事务<strong>不可见</strong>。</p>
</li>
<li>
<p>如果记录的 trx_id <strong>不在</strong> m_ids列表中，表示生成该版本记录的活跃事务已经被提交，所以该版本的记录对当前事务<strong>可见</strong>。</p>
</li>
</ul><p><strong>可重复读(RR)隔离级别是启动事务时生成一个 Read View，然后整个事务期间都在用这个 Read View</strong></p><p><strong>读提交(RC)隔离级别是在每次读取数据时，都会生成一个新的 Read View</strong>。</p></section><section><h4>Read View 何时生成？<a href="#read-view-何时生成"><span>#</span></a></h4><table><tbody><tr><td>读方式</td><td>是否创建 Read View</td><td>是否加锁</td><td>说明</td></tr><tr><td><code>SELECT * FROM table</code></td><td>✅ 是</td><td>❌ 否</td><td>快照读，用 Read View 构建数据版本</td></tr><tr><td><code>SELECT ... FOR UPDATE</code></td><td>❌ 否</td><td>✅ 是</td><td>当前读，直接加锁</td></tr><tr><td><code>UPDATE</code> / <code>DELETE</code></td><td>❌ 否</td><td>✅ 是</td><td>当前读</td></tr></tbody></table></section></section></section></section>
<section><h1>锁<a href="#锁"><span>#</span></a></h1><section><h2>MySQL有那些锁?<a href="#mysql有那些锁"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>MySQL 锁</span></div></div><div><div><div>2</div></div><div><span>├── 全局锁（Global Lock）</span></div></div><div><div><div>3</div></div><div><span>│   └── FTWRL（Flush Tables with Read Lock）</span></div></div><div><div><div>4</div></div><div><span>│       ├─ 锁住整个实例，所有库所有表 → 只读维护操作</span></div></div><div><div><div>5</div></div><div><span>│       └─ 常用于备份/主从复制初始化</span></div></div><div><div><div>6</div></div><div><span>│</span></div></div><div><div><div>7</div></div><div><span>├── 表级锁（Table Lock）</span></div></div><div><div><div>8</div></div><div><span>│   ├── 表锁（Table Lock）              -- `LOCK TABLES`</span></div></div><div><div><div>9</div></div><div><span>│   ├── 元数据锁（Meta Lock）           -- 执行 DDL 时自动加（如 `ALTER TABLE`）</span></div></div><div><div><div>10</div></div><div><span>│   ├── 意向锁（Intention Lock）        -- 行锁前提 → 意向共享(IS) / 意向排它(IX)</span></div></div><div><div><div>11</div></div><div><span>│   └── AUTO-INC 锁（自增锁）           -- 旧版使用表级锁，8.0 后为轻量级互斥锁</span></div></div><div><div><div>12</div></div><div><span>│</span></div></div><div><div><div>13</div></div><div><span>└── 行级锁（Row Lock） ← InnoDB 专属</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>├── Record Lock（记录锁）</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>│   └─ 锁住某一具体数据行，精确控制并发</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>├── Gap Lock（间隙锁）</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>│   └─ 锁住“记录之间的空隙”，防止插入 → 用于防止幻读</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>└── Next-Key Lock（临键锁）</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>└─ Record Lock + Gap Lock，锁住行 + 间隙，最常见（默认加锁方式）</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><section><h3>全局锁（Global Lock）<a href="#全局锁global-lock"><span>#</span></a></h3><p>▶ 类型：FTWRL（Flush Tables With Read Lock）</p><p>📌 特点：</p><ul>
<li>会锁住<strong>整个 MySQL 实例</strong>：所有数据库、所有表</li>
<li>所有对数据的读写都将被阻塞（DML + DDL）</li>
<li>常用于<strong>逻辑备份（mysqldump）</strong>，保证备份期间数据一致</li>
</ul><p>✅ 示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>FLUSH TABLES WITH READ LOCK;</span></div></div><div><div><div>2</div></div><div><span>-- 所有库所有表只读，其他事务无法写入</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>🔓 解锁：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>UNLOCK TABLES;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>表级锁（Table Lock）<a href="#表级锁table-lock"><span>#</span></a></h3><section><h4>1. 表锁（Table Lock）<a href="#1-表锁table-lock"><span>#</span></a></h4><p>📌 特点：</p><ul>
<li>显式锁定整张表，<strong>读或写时阻塞其他用户</strong></li>
<li>常用于 MyISAM 引擎（无行锁）</li>
</ul><p>✅ 示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>//表级别的共享锁，也就是读锁；</span></div></div><div><div><div>2</div></div><div><span>//允许当前会话读取被锁定的表，但阻止其他会话对这些表进行写操作。</span></div></div><div><div><div>3</div></div><div><span>lock tables t_student read;</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>//表级别的独占锁，也就是写锁；</span></div></div><div><div><div>6</div></div><div><span>//允许当前会话对表进行读写操作，但阻止其他会话对这些表进行任何操作（读或写）。</span></div></div><div><div><div>7</div></div><div><span>lock tables t_stuent write;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>需要注意的是，表锁除了会限制别的线程的读写外，也会限制本线程接下来的读写操作。比如我在当前线程执行了select * from t_student; 我就不能执行update及其他语句,也不能执行其他表查询.</p></section><section><h4>2. 元数据锁（Meta Data Lock, MDL）<a href="#2-元数据锁meta-data-lock-mdl"><span>#</span></a></h4><p>📌 特点：</p><ul>
<li><strong>自动加锁</strong>，用于防止 DDL / DML 冲突</li>
<li>执行 <code>SELECT</code> 时也会加共享 MDL 锁，防止别人同时改表结构</li>
</ul><p>✅ 示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>-- 会话 A</span></div></div><div><div><div>2</div></div><div><span>SELECT * FROM user;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>-- 会话 B</span></div></div><div><div><div>5</div></div><div><span>ALTER TABLE user ADD COLUMN email VARCHAR(100);</span></div></div><div><div><div>6</div></div><div><span>-- ❌ 阻塞！直到 A 释放锁</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>3. 意向锁（Intention Lock）<a href="#3-意向锁intention-lock"><span>#</span></a></h4><p>📌 特点：</p><ul>
<li>
<p>InnoDB 自动加的表级锁，不需要显式处理</p>
</li>
<li>
<p>用于表示“即将”对某一行加锁</p>
</li>
<li>
<p>有两种：</p>
</li>
<li>
<p>IS：<strong>意向共享锁</strong>（用于 SELECT … LOCK IN SHARE MODE）</p>
</li>
<li>
<p>IX：<strong>意向排它锁</strong>（用于 SELECT … FOR UPDATE、UPDATE 等）</p>
</li>
</ul><p>✅ 示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>-- 先在表上加上意向共享锁，然后对读取的记录加共享锁</span></div></div><div><div><div>2</div></div><div><span>select ... lock in share mode;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>-- 你执行 FOR UPDATE 时，InnoDB 自动加 IX 意向锁</span></div></div><div><div><div>5</div></div><div><span>SELECT * FROM user WHERE id=1 FOR UPDATE;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>执行插入、更新、删除操作，需要先对表加上「意向独占锁」，然后对该记录加独占锁。而普通的 select 是不会加行级锁的，普通的 select 语句是利用 MVCC 实现一致性读，是无锁的。</p></section><section><h4>4. AUTO-INC 锁（自增锁）<a href="#4-auto-inc-锁自增锁"><span>#</span></a></h4><p>📌 特点：</p><ul>
<li>控制自增列的生成</li>
<li>MySQL 5.x 是<strong>表级锁</strong>，多个事务串行插入</li>
<li>MySQL 8.0 优化为<strong>轻量级互斥锁</strong>，并发更高效</li>
</ul><p>✅ 示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>INSERT INTO order (user_id, product_id) VALUES (1, 100);</span></div></div><div><div><div>2</div></div><div><span>-- 自动生成主键 id，需要加 AUTO-INC 锁来顺序保证</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>行级锁（Row Lock）（InnoDB 专属）<a href="#行级锁row-lockinnodb-专属"><span>#</span></a></h3><section><h4>1. Record Lock（记录锁）<a href="#1-record-lock记录锁"><span>#</span></a></h4><p>📌 特点：</p><ul>
<li>精确锁住某一行</li>
<li>用于排它性访问（<code>FOR UPDATE</code>、<code>UPDATE</code>）</li>
</ul><p>✅ 示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>SELECT * FROM user WHERE id = 1 FOR UPDATE;</span></div></div><div><div><div>2</div></div><div><span>-- 精确加锁，仅锁住 id=1 那一行</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>2. Gap Lock（间隙锁）<a href="#2-gap-lock间隙锁"><span>#</span></a></h4><p>📌 特点：</p><ul>
<li>锁住“某两个记录之间的间隙”，阻止插入操作</li>
<li><strong>解决幻读问题</strong>（比如防止插入张三）</li>
</ul><p>✅ 示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>SELECT * FROM user WHERE age &gt; 30 FOR UPDATE;</span></div></div><div><div><div>2</div></div><div><span>-- age=40 存在，那么 (30, 40) 的间隙被锁住，不能插入 age=35 的行</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>3. Next-Key Lock（临键锁）<a href="#3-next-key-lock临键锁"><span>#</span></a></h4><p>📌 特点：</p><ul>
<li>InnoDB 默认使用的锁类型</li>
<li>等于 <strong>Record Lock + Gap Lock</strong></li>
<li>能锁住已有行及其前间隙，全面防止幻读</li>
</ul><p>✅ 示例：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>UPDATE user SET age=age+1 WHERE age BETWEEN 30 AND 40;</span></div></div><div><div><div>2</div></div><div><span>-- 会锁住 30 到 40 范围内的所有记录 + 相关间隙（Next-Key Lock）</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section><section><h3>总结表格（核心对照）<a href="#总结表格核心对照"><span>#</span></a></h3><table><tbody><tr><td>类别</td><td>锁类型</td><td>场景</td><td>阻塞其他事务</td><td>是否防幻读</td></tr><tr><td>全局锁</td><td>FTWRL</td><td>备份、主从</td><td>✅ 是</td><td>✅ 是</td></tr><tr><td>表级锁</td><td>表锁</td><td><code>LOCK TABLES</code></td><td>✅ 是</td><td>❌ 否</td></tr><tr><td>表级锁</td><td>元数据锁</td><td><code>ALTER TABLE</code>, <code>SELECT</code></td><td>✅ 是</td><td>❌ 否</td></tr><tr><td>表级锁</td><td>意向锁</td><td>自动加，用于协同行锁</td><td>❌ 否</td><td>❌ 否</td></tr><tr><td>表级锁</td><td>AUTO-INC 锁</td><td>插入自增主键时</td><td>部分情况会</td><td>❌ 否</td></tr><tr><td>行级锁</td><td>Record Lock</td><td>精确主键查询 + <code>FOR UPDATE</code></td><td>✅ 是</td><td>❌ 否</td></tr><tr><td>行级锁</td><td>Gap Lock</td><td>范围查询</td><td>✅ 是</td><td>✅ 是</td></tr><tr><td>行级锁</td><td>Next-Key Lock</td><td>范围更新/查询 + 防幻读</td><td>✅ 是</td><td>✅ 是</td></tr></tbody></table></section><section><h3>按锁的粒度划分：<a href="#按锁的粒度划分"><span>#</span></a></h3><table><tbody><tr><td>锁粒度</td><td>说明</td><td>引擎</td></tr><tr><td><strong>表级锁</strong></td><td>锁定整张表，开销小、并发差</td><td>MyISAM、InnoDB（如 <code>LOCK TABLES</code>）</td></tr><tr><td><strong>行级锁</strong></td><td>锁定一行记录，开销大、并发高</td><td>✅ InnoDB</td></tr><tr><td><strong>页级锁</strong></td><td>锁定一页（多个记录），介于表和行之间</td><td>BDB（较少使用）</td></tr></tbody></table></section><section><h3>按锁的类型划分：<a href="#按锁的类型划分"><span>#</span></a></h3><table><tbody><tr><td>锁类型</td><td>说明</td><td>示例语句</td></tr><tr><td><strong>共享锁（S锁）</strong></td><td>允许多个事务并发读取，但不能写入</td><td><code>SELECT ... LOCK IN SHARE MODE</code></td></tr><tr><td><strong>排它锁（X锁）</strong></td><td>只允许一个事务读写，其他事务被阻塞</td><td><code>SELECT ... FOR UPDATE</code>、<code>UPDATE</code></td></tr><tr><td><strong>意向锁</strong></td><td>事务声明自己将对某行加锁的意图</td><td>自动加，用户不可见</td></tr></tbody></table></section><section><h3>按加锁方式划分：<a href="#按加锁方式划分"><span>#</span></a></h3><table><tbody><tr><td>加锁方式</td><td>说明</td><td>示例</td></tr><tr><td><strong>显式加锁</strong></td><td>手动加锁</td><td><code>LOCK TABLE</code>, <code>FOR UPDATE</code></td></tr><tr><td><strong>隐式加锁</strong></td><td>InnoDB 自动加锁</td><td>执行 <code>UPDATE</code> / <code>DELETE</code> 时自动</td></tr></tbody></table></section></section><section><h2>各类锁的使用/触发举例<a href="#各类锁的使用触发举例"><span>#</span></a></h2><table><tbody><tr><td>SQL 操作</td><td>加锁方式</td><td>涉及锁类型</td></tr><tr><td><code>LOCK TABLE users READ</code></td><td>显式表锁</td><td>表级锁</td></tr><tr><td><code>SELECT * FROM users</code></td><td>快照读（无锁）</td><td>使用 MVCC</td></tr><tr><td><code>SELECT * FROM users FOR UPDATE</code></td><td>当前读（加锁）</td><td>行锁 / Gap Lock / Next-Key Lock</td></tr><tr><td><code>UPDATE users SET name='a' WHERE id=1</code></td><td>当前读（加锁）</td><td>行锁 / Next-Key Lock</td></tr><tr><td><code>ALTER TABLE</code></td><td>系统自动加锁</td><td>元数据锁</td></tr><tr><td><code>INSERT INTO auto_table VALUES (...)</code></td><td>自动</td><td>自增锁（AUTO-INC 锁）</td></tr></tbody></table></section><section><h2>特别注意的“锁误区”：<a href="#特别注意的锁误区"><span>#</span></a></h2><table><tbody><tr><td>易混点</td><td>正确解释</td></tr><tr><td>“SELECT 会不会加锁？”</td><td>普通 <code>SELECT</code> 是快照读 → 不加锁（MVCC）</td></tr><tr><td>“意向锁是你手动加的吗？”</td><td>❌ 不是，InnoDB 自动加，用于辅助判断是否能加行锁</td></tr><tr><td>“Gap Lock 也会锁住已有记录吗？”</td><td>❌ 不会，Gap Lock 只锁间隙，不锁记录本身</td></tr><tr><td>“Next-Key Lock 是什么意思？”</td><td>记录锁 + 间隙锁一起加，防止幻读的默认策略</td></tr><tr><td>“AUTO-INC 锁会阻塞插入吗？”</td><td>✅ 会，在老版本中（表级锁），8.0 起是轻量级</td></tr></tbody></table></section><section><h2>隔离级别与锁的关系<a href="#隔离级别与锁的关系"><span>#</span></a></h2><p>在未提交度级别下，读取数据不需要加共享锁，这样就不会跟被修改的数据上的排他锁冲突</p><p>在已提交度级别下，读操作需要加共享锁，但是在语句执行完以后释放共享锁；</p><p>在可重复度级别下，读操作需要加共享锁，但是在事务提交之前并不释放共享锁，也就是必须等待事务执行完毕以后才释放共享锁。</p><p>可串行化是限制性最强的隔离级别，因为该级别锁定整个范围的键，并一直持有锁，直到事务完成。</p></section><section><h2>什么是死锁？怎么解决？<a href="#什么是死锁怎么解决"><span>#</span></a></h2><p>死锁是指两个或多个事务在同一资源上相互占用，并请求锁定对方的资源，从而导致恶性循环的现象。</p><p>常见的解决死锁的方法</p><ol>
<li>如果不同程序会并发存取多个表，尽量约定以相同的顺序访问表，可以大大降低死锁机会。</li>
<li>在同一个事务中，尽可能做到一次锁定所需要的所有资源，减少死锁产生概率；</li>
<li>对于非常容易产生死锁的业务部分，可以尝试使用升级锁定颗粒度，通过表级锁定来减少死锁产生的概率；</li>
</ol><p>如果业务处理不好可以用分布式事务锁或者使用乐观锁</p></section></section>
<section><h1>日志<a href="#日志"><span>#</span></a></h1><p><img src="https://s.muxui.com:8015/images/2026/09/GCFdfjD3hsDV.webp" alt="" /></p><section><h2>undo log、redo log、binlog 有什么用？<a href="#undo-logredo-logbinlog-有什么用"><span>#</span></a></h2><p>undo log（回滚日志）：是 Innodb 存储引擎层生成的日志，实现了事务中的原子性，主要用于事务回滚和 MVCC。</p><p>redo log（重做日志）：是 Innodb 存储引擎层生成的日志，实现了事务中的持久性，主要用于掉电等故障恢复；</p><p>binlog （归档日志）：是 Server 层生成的日志，主要用于数据备份和主从复制；</p></section><section><h2>为什么需要 undo log？<a href="#为什么需要-undo-log"><span>#</span></a></h2><ul>
<li>实现事务回滚，保障事务的原子性。事务处理过程中，如果出现了错误或者用户执 行了 ROLLBACK 语句，MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。</li>
<li>实现 MVCC（多版本并发控制）关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据，MySQL 在执行快照读（普通 select 语句）的时候，会根据事务的 Read View 里的信息，顺着 undo log 的版本链找到满足其可见性的记录。</li>
</ul></section><section><h2>主从复制是怎么实现？<a href="#主从复制是怎么实现"><span>#</span></a></h2><p>MySQL 的主从复制依赖于 binlog ，也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。</p><p>这个过程一般是异步的，也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。</p><p><img src="https://s.muxui.com:8015/images/2026/09/ZEC9c3SjT6oO.webp" alt="" /></p><p>MySQL 集群的主从复制过程梳理成 3 个阶段：</p><ul>
<li><strong>写入 Binlog</strong>：主库写 binlog 日志，提交事务，并更新本地存储数据。</li>
<li><strong>同步 Binlog</strong>：把 binlog 复制到所有从库上，每个从库把 binlog 写到暂存日志中。</li>
<li><strong>回放 Binlog</strong>：回放 binlog，并更新存储引擎中的数据。</li>
</ul></section><section><h2>为什么需要 Buffer Pool？<a href="#为什么需要-buffer-pool"><span>#</span></a></h2><ul>
<li>当读取数据时，如果数据存在于 Buffer Pool 中，客户端就会直接读取 Buffer Pool 中的数据，否则再去磁盘中读取。</li>
<li>当修改数据时，如果数据存在于 Buffer Pool 中，那直接修改 Buffer Pool 中数据所在的页，然后将其页设置为脏页（该页的内存数据和磁盘上的数据已经不一致），为了减少磁盘I/O，不会立即将脏页写入磁盘，后续由后台线程选择一个合适的时机将脏页写入到磁盘。</li>
</ul></section><section><h2>Innodb引擎SQL执行的BufferPool缓存机制<a href="#innodb引擎sql执行的bufferpool缓存机制"><span>#</span></a></h2><p><img src="https://s.muxui.com:8015/images/2026/09/76zBZFbAABbz.webp" alt="" /></p></section><section><h2>为什么需要 redo log ？<a href="#为什么需要-redo-log-"><span>#</span></a></h2><p>redo log 是物理日志，记录了某个数据页做了什么修改，每当执行一个事务就会产生这样的一条或者多条物理日志。</p><p>在事务提交时，只要先将 redo log 持久化到磁盘即可，可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。</p><p>当系统崩溃时，虽然脏页数据没有持久化，但是 redo log 已经持久化，接着 MySQL 重启后，可以根据 redo log 的内容，将所有数据恢复到最新的状态。</p></section><section><h2>redo log 和 undo log 区别在哪？<a href="#redo-log-和-undo-log-区别在哪"><span>#</span></a></h2><p>这两种日志是属于 InnoDB 存储引擎的日志，它们的区别在于：</p><ul>
<li>redo log 记录了此次事务「<strong>完成后</strong>」的数据状态，记录的是更新<strong>之后</strong>的值；</li>
<li>undo log 记录了此次事务「<strong>开始前</strong>」的数据状态，记录的是更新<strong>之前</strong>的值；</li>
</ul></section><section><h2>redo log 什么时候刷盘？<a href="#redo-log-什么时候刷盘"><span>#</span></a></h2><p>主要有下面几个时机：</p><ul>
<li>MySQL 正常关闭时；</li>
<li>当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时，会触发落盘；</li>
<li>InnoDB 的后台线程每隔 1 秒，将 redo log buffer 持久化到磁盘。</li>
</ul></section><section><h2>为什么需要 binlog ？<a href="#为什么需要-binlog-"><span>#</span></a></h2><p>MySQL 在完成一条更新操作后，Server 层还会生成一条 binlog，等之后事务提交的时候，会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。</p><p>binlog 文件是记录了所有数据库表结构变更和表数据修改的日志，不会记录查询类的操作，比如 SELECT 和 SHOW 操作。</p></section><section><h2>binlog有几种录入格式？分别有什么区别？<a href="#binlog有几种录入格式分别有什么区别"><span>#</span></a></h2><p>有三种格式，statement，row和mixed。</p><ul>
<li>statement模式下，每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化，减少了binlog日志量，节约了IO，提高性能。由于sql的执行是有上下文的，因此在保存的时候需要保存相关的信息，同时还有一些使用了函数之类的语句无法被记录复制。</li>
<li>row级别下，不记录sql语句上下文相关信息，仅保存哪条记录被修改。记录单元为每一行的改动，基本是可以全部记下来但是由于很多操作，会导致大量行的改动(比如alter table)，因此这种模式的文件保存的信息太多，日志量太大。</li>
<li>mixed，一种折中的方案，普通操作使用statement记录，当无法使用statement的时候使用row。</li>
</ul></section></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/finalshell-pro-activation/</id>
      <title type="text"> FinalShell专业版激活</title>
      <published>2025-07-11T00:00:00.000Z</published>
      <updated>2025-07-11T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/finalshell-pro-activation/"/>
      <summary type="text">源码 from Crypto.Hash import keccak from Crypto.Hash import MD5 def md5 msg : hash obj = MD5.new msg return hash obj.hexdi</summary>
      <content type="html"><![CDATA[<section><h2>源码<a href="#源码"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>from</span><span> Crypto.Hash </span><span>import</span><span> keccak</span></div></div><div><div><div>2</div></div><div><span>from</span><span> Crypto.Hash </span><span>import</span><span><span> </span><span>MD5</span></span></div></div><div><div><div>3</div></div><div><span>def</span><span> </span><span>md5</span><span>(</span><span>msg</span><span>):</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>hash_obj </span><span>=</span><span> </span><span>MD5</span><span>.</span><span>new</span><span>(msg)</span></div></div><div><div><div>5</div></div><div><span>    </span><span>return</span><span><span> hash_obj.</span><span>hexdigest</span><span>()</span></span></div></div><div><div><div>6</div></div><div><span>def</span><span> </span><span>keccak384</span><span>(</span><span>msg</span><span>):</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>hash_obj </span><span>=</span><span> keccak.</span><span>new</span><span>(</span><span>data</span><span><span>=</span><span>msg, </span></span><span>digest_bits</span><span>=</span><span>384</span><span>)</span></div></div><div><div><div>8</div></div><div><span>    </span><span>return</span><span><span> hash_obj.</span><span>hexdigest</span><span>()</span></span></div></div><div><div><div>9</div></div><div><span>if</span><span> </span><span>__name__</span><span><span> </span><span>==</span><span> </span></span><span>'__main__'</span><span>:</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>code </span><span>=</span><span> </span><span>input</span><span>(</span><span>'输入机器码: '</span><span>)</span></div></div><div><div><div>11</div></div><div><span>    </span><span>print</span><span>(</span><span>'版本号 &lt; 3.9.6 (旧版)'</span><span>)</span></div></div><div><div><div>12</div></div><div><span>    </span><span>print</span><span>(</span><span>'高级版:'</span><span><span>, </span><span>md5</span><span>(</span></span><span>f</span><span>'61305</span><span>{</span><span>code</span><span>}</span><span>8552'</span><span><span>.</span><span>encode</span><span>())[</span></span><span>8</span><span>:</span><span>24</span><span>])</span></div></div><div><div><div>13</div></div><div><span>    </span><span>print</span><span>(</span><span>'专业版:'</span><span><span>, </span><span>md5</span><span>(</span></span><span>f</span><span>'2356</span><span>{</span><span>code</span><span>}</span><span>13593'</span><span><span>.</span><span>encode</span><span>())[</span></span><span>8</span><span>:</span><span>24</span><span>])</span></div></div><div><div><div>14</div></div><div><span>    </span><span>print</span><span>(</span><span>'版本号 &gt;= 3.9.6 (新版)'</span><span>)</span></div></div><div><div><div>15</div></div><div><span>    </span><span>print</span><span>(</span><span>'高级版:'</span><span><span>, </span><span>keccak384</span><span>(</span></span><span>f</span><span>'</span><span>{</span><span>code</span><span>}</span><span>hSf(78cvVlS5E'</span><span><span>.</span><span>encode</span><span>())[</span></span><span>12</span><span>:</span><span>28</span><span>])</span></div></div><div><div><div>16</div></div><div><span>    </span><span>print</span><span>(</span><span>'专业版:'</span><span><span>, </span><span>keccak384</span><span>(</span></span><span>f</span><span>'</span><span>{</span><span>code</span><span>}</span><span>FF3Go(*Xvbb5s2'</span><span><span>.</span><span>encode</span><span>())[</span></span><span>12</span><span>:</span><span>28</span><span>])</span></div></div><div><div><div>17</div></div><div><span>    </span><span>print</span><span>(</span><span>'版本号 (4.5)'</span><span>)</span></div></div><div><div><div>18</div></div><div><span>    </span><span>print</span><span>(</span><span>'高级版:'</span><span><span>, </span><span>keccak384</span><span>(</span></span><span>f</span><span>'</span><span>{</span><span>code</span><span>}</span><span>wcegS3gzA$'</span><span><span>.</span><span>encode</span><span>())[</span></span><span>12</span><span>:</span><span>28</span><span>])</span></div></div><div><div><div>19</div></div><div><span>    </span><span>print</span><span>(</span><span>'专业版:'</span><span><span>, </span><span>keccak384</span><span>(</span></span><span>f</span><span>'</span><span>{</span><span>code</span><span>}</span><span>b(xxkHn%z);x'</span><span><span>.</span><span>encode</span><span>())[</span></span><span>12</span><span>:</span><span>28</span><span>])</span></div></div><div><div><div>20</div></div><div><span>    </span><span>print</span><span>(</span><span>'版本号 (4.6)'</span><span>)</span></div></div><div><div><div>21</div></div><div><span>    </span><span>print</span><span>(</span><span>'高级版:'</span><span><span>, </span><span>keccak384</span><span>(</span></span><span>f</span><span>'</span><span>{</span><span>code</span><span>}</span><span>csSf5*xlkgYSX,y'</span><span><span>.</span><span>encode</span><span>())[</span></span><span>12</span><span>:</span><span>28</span><span>])</span></div></div><div><div><div>22</div></div><div><span>    </span><span>print</span><span>(</span><span>'专业版:'</span><span><span>, </span><span>keccak384</span><span>(</span></span><span>f</span><span>'</span><span>{</span><span>code</span><span>}</span><span>Scfg*ZkvJZc,s,Y'</span><span><span>.</span><span>encode</span><span>())[</span></span><span>12</span><span>:</span><span>28</span><span>])</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>在线激活<a href="#在线激活"><span>#</span></a></h2><p>输入机器码，点击计算即可生成各版本激活码（与上方 Python 脚本逻辑一致）。</p><div><div>机器码计算</div><div></div></div></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/mybatis-code-helper-pro/</id>
      <title type="text">MyBatisCodeHelper-Pro破解[V3.4.4]</title>
      <published>2025-07-04T00:00:00.000Z</published>
      <updated>2025-07-11T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/mybatis-code-helper-pro/"/>
      <summary type="text">提前申明：这篇文章只用于作者个人记录，任何人不得转载、分享。不转发不扩散，如因此造成的任何损失或其他影响，本人概不承担。MyBatisCodeHelper Pro 是一款很不错的 IDEA 插件，有能力的还是购买正版授权。如官方侵权或产生其</summary>
      <content type="html"><![CDATA[<p>提前申明：这篇文章只用于作者个人记录，任何人不得转载、分享。不转发不扩散，如因此造成的任何损失或其他影响，本人概不承担。MyBatisCodeHelper-Pro 是一款很不错的 IDEA 插件，有能力的还是购买正版授权。如官方侵权或产生其他影响请联系我立即删除。</p>
<section><h2>下载 MyBatisCodeHelper-Pro<a href="#下载-mybatiscodehelper-pro"><span>#</span></a></h2><p>在 idea 插件市场下载所需版本。目前分两个版本，一个是idea2023.2+版本使用，一个是idea2019.1 — 2023.1.7版本使用。更新时间：2025年07月04日</p><p>下载目前最新版：<a href="https://plugins.jetbrains.com/plugin/9837-mybatiscodehelperpro/versions/stable" target="_blank">MyBatisCodeHelper-Pro</a>。解压，找到 jar 包：instrumented-MyBatisCodeHelper-Pro241-3.4.4+2321</p></section>
<section><h2>下载 cfr<a href="#下载-cfr"><span>#</span></a></h2><p>一个 Java 反编译器，官网：<a href="https://www.benf.org/other/cfr" target="_blank">https://www.benf.org/other/cfr</a></p><p>下载好后，将 cfr-0.152.jar 和 instrumented-MyBatisCodeHelper-Pro241-3.4.4+2321.jar 放在同一目录下，执行命令：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>java -jar cfr-0.152.jar instrumented-MyBatisCodeHelper-Pro241-3.4.4+2321.jar --renamedupmembers true --hideutf false </span><span>&gt;&gt;</span><span> a.txt</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>等待数秒钟命令执行完毕，分析 a.txt 文件：</p><p>最终定位到：搜索 <code>@SerializedName(value="validTo")</code></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>f</span><span> </span></span><span>{</span></div></div><div><div><div>2</div></div><div><span>    </span><span>// 付费密钥</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>@</span><span>SerializedName</span><span>(</span><span>value</span><span>=</span><span>"paidKey"</span><span>)</span></div></div><div><div><div>4</div></div><div><span>    </span><span>private</span><span> </span><span>String</span><span> var_java_lang_String_a</span><span>;</span></div></div><div><div><div>5</div></div><div><span>    </span><span>// 校验是否通过</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>@</span><span>SerializedName</span><span>(</span><span>value</span><span>=</span><span>"valid"</span><span>)</span></div></div><div><div><div>7</div></div><div><span>    </span><span>private</span><span> </span><span>Boolean</span><span> var_java_lang_Boolean_a</span><span>;</span></div></div><div><div><div>8</div></div><div><span>    </span><span>// 机器码</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>@</span><span>SerializedName</span><span>(</span><span>value</span><span>=</span><span>"userMac"</span><span>)</span></div></div><div><div><div>10</div></div><div><span>    </span><span>private</span><span> </span><span>String</span><span> b</span><span>;</span></div></div><div><div><div>11</div></div><div><span>    </span><span>// 激活截止日期</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>@</span><span>SerializedName</span><span>(</span><span>value</span><span>=</span><span>"validTo"</span><span>)</span></div></div><div><div><div>13</div></div><div><span>    </span><span>private</span><span> </span><span>Long</span><span> var_java_lang_Long_a</span><span>;</span></div></div><div><div><div>14</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>我加了注释，然后我们使用 Java 字节码技术修改这个类的各个属性 get 方法，使之直接返回正确的值即可。</p><p>通过上述方法随便输入激活码激活时，正常都是成功的，但有的可能会发现激活是吧，报错：base64解密异常，分析报错信息</p><p>最终定位到：打开 a.txt，搜索 <code>private static Gson</code></p><p>找到如下所在的包及类：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public</span><span> </span><span>class</span><span><span> </span><span>e</span><span> </span></span><span>{</span></div></div><div><div><div>2</div></div><div><span>    </span><span>private</span><span> </span><span>static</span><span> </span><span>Gson</span><span> a </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>Gson</span><span><span>()</span><span>;</span></span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>    </span><span>public</span><span> </span><span>static</span><span> f </span><span>a</span><span>(</span><span>String</span><span><span> </span><span>string</span><span>)</span><span> </span><span>{</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>String</span><span><span> </span><span>string2</span></span><span>;</span></div></div><div><div><div>6</div></div><div><span>        </span><span>byte</span><span><span>[] </span><span>byArray</span><span> </span></span><span><span>=</span><span> </span></span><span>null</span><span>;</span></div></div><div><div><div>7</div></div><div><span>        </span><span>try</span><span> {</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>byArray </span><span>=</span><span> </span><span>d</span><span>.</span><span>b</span><span>(</span><span>d</span><span>.</span><span>java_security_interfaces_RSAPublicKey_a</span><span>(</span><span>new</span><span> </span><span>String</span><span>(</span><span>Base64</span><span>.</span><span>getDecoder</span><span>().</span><span>decode</span><span>(</span><span>"TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FDZzUyUjExV0h1MysvNUV2WnhkS0l2a3ovekpnS2VNUUhNLytMVkxSZS9zWUpFQlUxbUUrODc3MmJJckk4UThscldqSHc5cmVjQ1RWVVhXUnhWYXBndk1HYTZ3KzU4STZwYXdSaFhwZDBrRkhUY2xxeUZGWFpoS3ZiQUtoblphRGNuZkJtSkhObTQwR0JFTGpCTmx5MXpha2FIblFmUzF0QlhaSGQwOUV0c2VRSURBUUFC"</span><span>), </span><span>Charsets</span><span>.</span><span>UTF_8</span><span>)), </span><span>Base64</span><span>.</span><span>getDecoder</span><span>().</span><span>decode</span><span>(string));</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>        </span><span>catch</span><span> (</span><span>b</span><span><span> </span><span>b2</span><span>) {</span></span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>byArray </span><span>=</span><span> </span><span>d</span><span>.</span><span>b</span><span>(</span><span>new</span><span> </span><span>c</span><span>(), </span><span>Base64</span><span>.</span><span>getDecoder</span><span>().</span><span>decode</span><span>(string));</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>f f2 </span><span>=</span><span> </span><span>null</span><span>;</span></div></div><div><div><div>14</div></div><div><span>        </span><span>try</span><span> {</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>string2 </span><span>=</span><span> </span><span>new</span><span> </span><span>String</span><span>(byArray, </span><span>"UTF-8"</span><span>);</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>        </span><span>catch</span><span> (</span><span>UnsupportedEncodingException</span><span><span> </span><span>unsupportedEncodingException</span><span>) {</span></span></div></div><div><div><div>18</div></div><div><span>            </span><span>throw</span><span> </span><span>new</span><span> </span><span>RuntimeException</span><span>(unsupportedEncodingException);</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>        </span><span>try</span><span> {</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>f2 </span><span>=</span><span> (f)</span><span>a</span><span>.</span><span>fromJson</span><span>(string2, </span><span>f</span><span>.</span><span>class</span><span>);</span></div></div><div><div><div>22</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span>        </span><span>catch</span><span> (</span><span>Exception</span><span><span> </span><span>exception</span><span>) {</span></span></div></div><div><div><div>24</div></div><div><span>            </span><span>throw</span><span> </span><span>new</span><span> </span><span>RuntimeException</span><span>(</span><span>"gson catch exception, the json string is"</span><span><span> </span><span>+</span><span> string2, exception);</span></span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span>        </span><span>return</span><span> f2;</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>28</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>看他里面逻辑无非就是 base64 解析，然后 gson 转换。直接修改这个类即可。</p></section>
<section><h2>使用 Javassist 修改类文件<a href="#使用-javassist-修改类文件"><span>#</span></a></h2><p>添加 maven 依赖：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&lt;</span><span>dependency</span><span>&gt;</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>&lt;</span><span>groupId</span><span>&gt;org.javassist&lt;/</span><span>groupId</span><span>&gt;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>&lt;</span><span>artifactId</span><span>&gt;javassist&lt;/</span><span>artifactId</span><span>&gt;</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>&lt;</span><span>version</span><span>&gt;3.25.0-GA&lt;/</span><span>version</span><span>&gt;</span></div></div><div><div><div>5</div></div><div><span>&lt;/</span><span>dependency</span><span>&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如上分析，主要修改的就是 <code>com.ccnode.codegenerator.af.f.e</code> 的 <code>e</code> 方法。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>       </span><span>CtClass</span><span> ctClass </span><span><span>=</span><span> </span></span><span>classPool</span><span>.</span><span>get</span><span>(</span><span>"com.ccnode.codegenerator.af.f.e"</span><span>);</span></div></div><div><div><div>2</div></div><div><span>        </span><span>CtMethod</span><span> parseLicenseMethod </span><span><span>=</span><span> </span></span><span>ctClass</span><span>.</span><span>getDeclaredMethod</span><span>(</span><span>"a"</span><span>, </span><span>new</span><span> </span><span>CtClass</span><span>[] { </span><span>classPool</span><span>.</span><span>get</span><span>(</span><span>"java.lang.String"</span><span>) });</span></div></div><div><div><div>3</div></div><div><span>        </span><span>StringBuffer</span><span> bodyBuff </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>StringBuffer</span><span><span>()</span><span>;</span></span></div></div><div><div><div>4</div></div><div><span>        </span><span>bodyBuff</span><span>.</span><span>append</span><span>(</span><span>"{"</span><span>)</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>.</span><span>append</span><span>(</span><span>" com.google.gson.Gson gson = new com.google.gson.Gson();"</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>                </span></span><span>.</span><span>append</span><span>(</span><span>" com.ccnode.codegenerator.af.d.f e = (com.ccnode.codegenerator.af.d.f)gson.fromJson($1,com.ccnode.codegenerator.af.d.f.class);"</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>.</span><span>append</span><span>(</span><span>" return e;"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>.</span><span>append</span><span>(</span><span>"}"</span><span>);</span></div></div><div><div><div>9</div></div><div><span>        </span><span>parseLicenseMethod</span><span>.</span><span>setBody</span><span>(</span><span>bodyBuff</span><span>.</span><span>toString</span><span>());</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>之前直接这么修改就完成了，但是后面作者加了验证，所以每一行我们都要去修改</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span> </span><span>try</span><span> {</span></div></div><div><div><div>2</div></div><div><span>                </span><span>ApplicationManager</span><span>.</span><span>getApplication</span><span>().</span><span>invokeLater</span><span>(</span><span>new</span><span> </span><span>Runnable</span><span>(){</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>                    </span></span><span>@</span><span>Override</span></div></div><div><div><div>5</div></div><div><span>                    </span><span>public</span><span> </span><span>void</span><span> </span><span>run</span><span><span>()</span><span> </span><span>{</span></span></div></div><div><div><div>6</div></div><div><span>                        </span><span>Messages</span><span>.</span><span>showErrorDialog</span><span>((String)</span><span>"你正在使用破解版的MybatisCodeHelperPro插件，将会生成错误的代码和造成电脑卡慢请到 https://brucege.com/pay/view 购买正版"</span><span>, (String)</span><span>"MybatisCodeHelperPro插件非正版"</span><span>);</span></div></div><div><div><div>7</div></div><div><span>                        </span><span>com</span><span>.</span><span>ccnode</span><span>.</span><span>codegenerator</span><span>.</span><span>j</span><span>.</span><span>a</span><span>.</span><span>com_ccnode_codegenerator_myconfigurable_Profile_a</span><span>().</span><span>setValid</span><span>(</span><span>false</span><span>);</span></div></div><div><div><div>8</div></div><div><span>                        </span><span>ApplicationImpl</span><span><span> </span><span>applicationImpl</span><span> </span></span><span><span>=</span><span> (ApplicationImpl)</span></span><span>ApplicationManager</span><span>.</span><span>getApplication</span><span>();</span></div></div><div><div><div>9</div></div><div><span>                        </span><span>applicationImpl</span><span>.</span><span>exit</span><span>(</span><span>true</span><span>, </span><span>true</span><span>);</span></div></div><div><div><div>10</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>});</span></div></div><div><div><div>12</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>            </span><span>catch</span><span> (</span><span>Exception</span><span><span> </span><span>exception</span><span>) {</span></span></div></div><div><div><div>14</div></div><div><span>                </span><span>// empty catch block</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>我现在主要做的就是在进行验证的时候，我们直接<code>return</code>掉，代码如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>  </span><span>// 修改第一个方法</span></div></div><div><div><div>2</div></div><div><span>        </span><span>CtClass</span><span> ctClass </span><span><span>=</span><span> </span></span><span>classPool</span><span>.</span><span>get</span><span>(</span><span>"com.ccnode.codegenerator.af.f.e"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>        </span><span>CtMethod</span><span> parseLicenseMethod </span><span><span>=</span><span> </span></span><span>ctClass</span><span>.</span><span>getDeclaredMethod</span><span>(</span><span>"a"</span><span>, </span><span>new</span><span> </span><span>CtClass</span><span>[] { </span><span>classPool</span><span>.</span><span>get</span><span>(</span><span>"java.lang.String"</span><span>) });</span></div></div><div><div><div>4</div></div><div><span>        </span><span>StringBuffer</span><span> bodyBuff </span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>StringBuffer</span><span><span>()</span><span>;</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>bodyBuff</span><span>.</span><span>append</span><span>(</span><span>"{"</span><span>)</span></div></div><div><div><div>6</div></div><div><span><span>                </span></span><span>.</span><span>append</span><span>(</span><span>" com.google.gson.Gson gson = new com.google.gson.Gson();"</span><span>)</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>.</span><span>append</span><span>(</span><span>" com.ccnode.codegenerator.af.d.f e = (com.ccnode.codegenerator.af.d.f)gson.fromJson($1,com.ccnode.codegenerator.af.d.f.class);"</span><span>)</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>.</span><span>append</span><span>(</span><span>" return e;"</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>.</span><span>append</span><span>(</span><span>"}"</span><span>);</span></div></div><div><div><div>10</div></div><div><span>        </span><span>parseLicenseMethod</span><span>.</span><span>setBody</span><span>(</span><span>bodyBuff</span><span>.</span><span>toString</span><span>());</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>        </span><span>//修改第二个方法</span></div></div><div><div><div>13</div></div><div><span>        </span><span>CtClass</span><span> ctClass2 </span><span><span>=</span><span> </span></span><span>classPool</span><span>.</span><span>get</span><span>(</span><span>"com.ccnode.codegenerator.n.e"</span><span>);</span></div></div><div><div><div>14</div></div><div><span>        </span><span>CtMethod</span><span> methodToModify2 </span><span><span>=</span><span> </span></span><span>ctClass2</span><span>.</span><span>getDeclaredMethod</span><span>(</span><span>"a"</span><span>);</span></div></div><div><div><div>15</div></div><div><span>        </span><span>methodToModify2</span><span>.</span><span>setBody</span><span>(</span><span>"{ return; }"</span><span>);</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span>        </span><span>//修改第三个方法</span></div></div><div><div><div>18</div></div><div><span>        </span><span>CtClass</span><span> ctClass3 </span><span><span>=</span><span> </span></span><span>classPool</span><span>.</span><span>get</span><span>(</span><span>"com.ccnode.codegenerator.w.a"</span><span>);</span></div></div><div><div><div>19</div></div><div><span>        </span><span>CtMethod</span><span> methodToModify3 </span><span><span>=</span><span> </span></span><span>ctClass3</span><span>.</span><span>getDeclaredMethod</span><span>(</span><span>"a"</span><span>);</span></div></div><div><div><div>20</div></div><div><span>        </span><span>methodToModify3</span><span>.</span><span>setBody</span><span>(</span><span>"{ return; }"</span><span>);</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span>        </span><span>//修改第四个方法</span></div></div><div><div><div>23</div></div><div><span>        </span><span>CtClass</span><span> ctClass4 </span><span><span>=</span><span> </span></span><span>classPool</span><span>.</span><span>get</span><span>(</span><span>"com.ccnode.codegenerator.G.e.d"</span><span>);</span></div></div><div><div><div>24</div></div><div><span>        </span><span>CtMethod</span><span> methodToModify4 </span><span><span>=</span><span> </span></span><span>ctClass4</span><span>.</span><span>getDeclaredMethod</span><span>(</span><span>"a"</span><span>, </span><span>new</span><span> </span><span>CtClass</span><span>[]{});</span></div></div><div><div><div>25</div></div><div><span>        </span><span>methodToModify4</span><span>.</span><span>setBody</span><span>(</span><span>"{ return; }"</span><span>);</span></div></div><div><div><div>26</div></div><div>
</div></div><div><div><div>27</div></div><div><span>        </span><span>//修改第五个方法</span></div></div><div><div><div>28</div></div><div><span>        </span><span>CtClass</span><span> ctClass5 </span><span><span>=</span><span> </span></span><span>classPool</span><span>.</span><span>get</span><span>(</span><span>"com.ccnode.codegenerator.ad.ab"</span><span>);</span></div></div><div><div><div>29</div></div><div><span>        </span><span>CtMethod</span><span> methodToModify5 </span><span><span>=</span><span> </span></span><span>ctClass5</span><span>.</span><span>getDeclaredMethod</span><span>(</span><span>"a"</span><span>);</span></div></div><div><div><div>30</div></div><div><span>        </span><span>methodToModify5</span><span>.</span><span>setBody</span><span>(</span><span>"{ return; }"</span><span>);</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span>        </span><span>//修改第六个方法</span></div></div><div><div><div>33</div></div><div><span>        </span><span>CtClass</span><span> ctClass6 </span><span><span>=</span><span> </span></span><span>classPool</span><span>.</span><span>get</span><span>(</span><span>"com.ccnode.codegenerator.ad.a.b"</span><span>);</span></div></div><div><div><div>34</div></div><div><span>        </span><span>CtMethod</span><span> methodToModify6 </span><span><span>=</span><span> </span></span><span>ctClass6</span><span>.</span><span>getDeclaredMethod</span><span>(</span><span>"a"</span><span>);</span></div></div><div><div><div>35</div></div><div><span>        </span><span>methodToModify6</span><span>.</span><span>setBody</span><span>(</span><span>"{ return; }"</span><span>);</span></div></div><div><div><div>36</div></div><div>
</div></div><div><div><div>37</div></div><div><span>        </span><span>//修改第七个方法</span></div></div><div><div><div>38</div></div><div><span>        </span><span>CtClass</span><span> ctClass7 </span><span><span>=</span><span> </span></span><span>classPool</span><span>.</span><span>get</span><span>(</span><span>"com.ccnode.codegenerator.ae.a"</span><span>);</span></div></div><div><div><div>39</div></div><div><span>        </span><span>CtMethod</span><span> methodToModify7 </span><span><span>=</span><span> </span></span><span>ctClass7</span><span>.</span><span>getDeclaredMethod</span><span>(</span><span>"a"</span><span>, </span><span>new</span><span> </span><span>CtClass</span><span>[] {});</span></div></div><div><div><div>40</div></div><div><span>        </span><span>methodToModify7</span><span>.</span><span>setBody</span><span>(</span><span>"{ return; }"</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>运行完成后，会将修改的 class 文件生成到指定目录下。</p></section>
<section><h2>替换 jar 包的对应 class 文件<a href="#替换-jar-包的对应-class-文件"><span>#</span></a></h2><p><img src="https://s.muxui.com:8015/images/2026/09/FfXwvVqEM9qC.webp" alt="" /></p><p>使用压缩工具 Bandzip 或 7zip 替换都有问题。推荐使用360压缩，这里值得注意的是要和 jar 里的的路径对应起来。</p></section>
<section><h2>查看结果<a href="#查看结果"><span>#</span></a></h2><p>然后将更新后的这个 jar 包，替换原始 jar 包，重启 idea，按如下格式输入离线激活码激活，大功告成。</p><p><img src="https://s.muxui.com:8015/images/2026/09/ac4My4EZLhjt.webp" alt="" /></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span>  </span><span>"paidKey"</span><span>: </span><span>"muxui"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>"valid"</span><span>: </span><span>true</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>"userMac"</span><span>: </span><span>"离线激活时显示的唯一码"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>"validTo"</span><span>: </span><span>4859711999000</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><img src="https://s.muxui.com:8015/images/2026/09/LexqIBbW1rjc.webp" alt="" /></p></section>
<section><h2>参考文章：<a href="#参考文章"><span>#</span></a></h2><p><a href="https://www.52pojie.cn/thread-1759478-1-1.html" target="_blank">https://www.52pojie.cn/thread-1759478-1-1.html</a></p></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/typora-1-10-8-portable-edition/</id>
      <title type="text">Typora 1.10.8 学习版直装包</title>
      <published>2025-07-01T00:00:00.000Z</published>
      <updated>2025-07-02T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/typora-1-10-8-portable-edition/"/>
      <summary type="text">Typora 是一款轻量级、所见即所得（WYSIWYG）的 Markdown 编辑器 。与传统 Markdown 编辑器不同，Typora 实时渲染 Markdown 语法，无需左右分屏，编辑与预览完全合一，极大提升了写作专注度与效率。 官</summary>
      <content type="html"><![CDATA[<p><strong>Typora</strong> 是一款轻量级、所见即所得（WYSIWYG）的 <strong>Markdown 编辑器</strong>。与传统 Markdown 编辑器不同，Typora 实时渲染 Markdown 语法，无需左右分屏，编辑与预览完全合一，极大提升了写作专注度与效率。</p>
<p>官网地址：<a href="https://typora.io" target="_blank">https://typora.io</a></p>
<section><h2>🌟 核心特性<a href="#-核心特性"><span>#</span></a></h2><section><h3>✅ 所见即所得<a href="#-所见即所得"><span>#</span></a></h3><ul>
<li>不再需要左右两栏的“编辑+预览”界面</li>
<li>所见即所得，边写边看效果</li>
</ul></section><section><h3>✅ 完整支持 Markdown 语法<a href="#-完整支持-markdown-语法"><span>#</span></a></h3><ul>
<li>标题、段落、加粗、斜体</li>
<li>有序/无序列表、任务列表</li>
<li>图片、链接、引用块</li>
<li>代码块（支持语法高亮）</li>
<li>表格、脚注、数学公式（LaTeX）</li>
</ul></section><section><h3>✅ 强大的导出能力<a href="#-强大的导出能力"><span>#</span></a></h3><ul>
<li>支持导出为：<code>PDF</code>、<code>HTML</code>、<code>Word (.docx)</code>、<code>OpenOffice (.odt)</code>、<code>EPUB</code> 等</li>
<li>支持自定义样式（CSS），自由控制排版美感</li>
</ul></section><section><h3>✅ 插件与扩展性<a href="#-插件与扩展性"><span>#</span></a></h3><ul>
<li>集成图床上传（如：PicGo）</li>
<li>自动保存与恢复</li>
<li>目录大纲导航</li>
<li>支持 Vim 模式</li>
</ul></section><section><h3>✅ 多平台支持<a href="#-多平台支持"><span>#</span></a></h3><ul>
<li>macOS / Windows / Linux 均有原生客户端</li>
<li>可跨平台同步使用（搭配 Git、云盘等）</li>
</ul><hr /></section></section>
<section><h2>✍️ 基本使用方法<a href="#️-基本使用方法"><span>#</span></a></h2><section><h3>🧱 常见 Markdown 语法示例<a href="#-常见-markdown-语法示例"><span>#</span></a></h3><table><thead><tr><th>功能</th><th>Markdown 语法</th><th>显示效果</th></tr></thead><tbody><tr><td>标题</td><td><code>## 二级标题</code></td><td>二级标题</td></tr><tr><td>加粗</td><td><code>**加粗文字**</code></td><td><strong>加粗文字</strong></td></tr><tr><td>斜体</td><td><code>*斜体文字*</code></td><td><em>斜体文字</em></td></tr><tr><td>引用</td><td><code>&gt; 这是引用内容</code></td><td>&gt; 这是引用内容</td></tr><tr><td>无序列表</td><td><code>- 项目1</code></td><td>- 项目1</td></tr><tr><td>有序列表</td><td><code>1. 项目1</code></td><td>1. 项目1</td></tr><tr><td>插入图片</td><td><code>!(url)</code></td><td>插入图片</td></tr><tr><td>插入链接</td><td><code>(链接)</code></td><td>超链接</td></tr><tr><td>表格</td><td>`</td><td>列1</td></tr><tr><td>代码块</td><td>```java\n代码\n```</td><td>代码块（高亮）</td></tr></tbody></table><hr /></section></section>
<section><h2>🧩 WordPress + Typora 写作流程<a href="#-wordpress--typora-写作流程"><span>#</span></a></h2><ol>
<li>在 Typora 中撰写完整 Markdown 文档</li>
<li>使用图片链接或上传至图床（Typora 支持自定义图床）</li>
<li>导出为 HTML 或直接复制 Markdown</li>
<li>在 WordPress 中粘贴（建议配合插件如 “Markdown Block”）</li>
<li>调整排版样式并发布</li>
</ol><p>👉 推荐使用插件：</p><ul>
<li><strong>WP Githuber MD</strong>：支持 Markdown 直接在 WordPress 编辑器中渲染</li>
<li><strong>Markdown Block</strong>：支持区块编辑器中的 Markdown 格式粘贴</li>
</ul><hr /></section>
<section><h2>💡 高级技巧推荐<a href="#-高级技巧推荐"><span>#</span></a></h2><ul>
<li>使用 <code>侧边栏大纲</code> 快速跳转标题结构</li>
<li>自定义主题 CSS（Typora → 偏好设置 → 外观 → 打开主题文件夹）</li>
<li>启用 <code>自动保存</code>，防止数据丢失</li>
<li>与 Git 配合进行版本控制与协作写作</li>
<li>搭配语雀、Obsidian、Notion 做笔记整合</li>
</ul><hr /></section>
<section><h2>📦 安装方法<a href="#-安装方法"><span>#</span></a></h2><section><h3>Windows 学习版：<a href="#windows-学习版"><span>#</span></a></h3><ol>
<li>学习版直装包，一路安装即可，点击“<strong>试用</strong>”即可永久学习。</li>
<li>好用就完事儿咯，多的咱也不介绍😎</li>
<li>个人是替代Windows自带笔记本使用的。</li>
<li>当然，写代码还是请另找高明，你别告诉我你用记事本写代码。</li>
<li>有能力建议入正</li>
</ol><hr /></section></section>
<section><h2>🎯 适合人群<a href="#-适合人群"><span>#</span></a></h2><table><thead><tr><th>用户类型</th><th>适用原因</th></tr></thead><tbody><tr><td>博客写作者</td><td>轻松排版、导出方便、专注写作</td></tr><tr><td>技术开发者</td><td>支持代码块、高亮与 LaTeX</td></tr><tr><td>内容编辑 / 运营</td><td>快速组织内容结构、导出发布</td></tr><tr><td>学术研究人员</td><td>数学公式支持好，适合写笔记</td></tr></tbody></table></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/java-thread-pool-guide/</id>
      <title type="text">Java线程池</title>
      <published>2025-06-23T00:00:00.000Z</published>
      <updated>2025-06-24T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/java-thread-pool-guide/"/>
      <summary type="text">总体设计 ThreadPoolExecutor实现的顶层接口是Executor，顶层接口Executor提供了一种思想：将任务提交和任务执行进行解耦。用户无需关注如何创建线程，如何调度线程来执行任务，用户只需提供Runnable对象，将任务</summary>
      <content type="html"><![CDATA[<section><h2>总体设计<a href="#总体设计"><span>#</span></a></h2><p><img src="https://s.muxui.com:8015/images/2026/09/sigvVuLkZevq.webp" alt="" /></p><p>ThreadPoolExecutor实现的顶层接口是Executor，顶层接口Executor提供了一种思想：将任务提交和任务执行进行解耦。用户无需关注如何创建线程，如何调度线程来执行任务，用户只需提供Runnable对象，将任务的运行逻辑提交到执行器(Executor)中，由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增加了一些能力：（1）扩充执行任务的能力，补充可以为一个或一批异步任务生成Future的方法；（2）提供了管控线程池的方法，比如停止线程池的运行。AbstractExecutorService则是上层的抽象类，将执行任务的流程串联了起来，保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类ThreadPoolExecutor实现最复杂的运行部分，ThreadPoolExecutor将会一方面维护自身的生命周期，另一方面同时管理线程和任务，使两者良好的结合从而执行并行任务。</p></section>
<section><h2>线程池的核心类：<code>ThreadPoolExecutor</code><a href="#线程池的核心类threadpoolexecutor"><span>#</span></a></h2><p>Java 提供的线程池主要通过 <code>java.util.concurrent.ThreadPoolExecutor</code> 实现。</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public ThreadPoolExecutor(    int corePoolSize,        // 核心线程数（常驻）    int maximumPoolSize,     // 最大线程数    long keepAliveTime,      // 非核心线程的存活时间    TimeUnit unit,           // 上面参数的单位    BlockingQueue&lt;Runnable&gt; workQueue,  // 任务队列    ThreadFactory threadFactory,        // 线程工厂（创建线程的方式）    RejectedExecutionHandler handler    // 拒绝策略)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section>
<section><h2>线程池参数详解<a href="#线程池参数详解"><span>#</span></a></h2><table><thead><tr><th>参数</th><th>说明</th></tr></thead><tbody><tr><td>corePoolSize</td><td>核心线程数，线程池中始终保留的线程数量。</td></tr><tr><td>maximumPoolSize</td><td>最大线程数。线程池能创建的最大线程数（包括核心线程）。</td></tr><tr><td>keepAliveTime</td><td>非核心线程的空闲存活时间。超过这个时间会被销毁。</td></tr><tr><td>unit</td><td>存活时间单位，如 TimeUnit.SECONDS。</td></tr><tr><td>workQueue</td><td>任务缓存队列（如 ArrayBlockingQueue、LinkedBlockingQueue）。</td></tr><tr><td>threadFactory</td><td>自定义线程创建方式，可设置线程名、守护线程等。</td></tr><tr><td>handler</td><td>拒绝策略，线程池已满时的处理方式。</td></tr></tbody></table><hr /></section>
<section><h2>任务执行流程<a href="#任务执行流程"><span>#</span></a></h2><p><img src="https://s.muxui.com:8015/images/2026/09/bx02hI5u1kt0.webp" alt="" /></p><ol>
<li>若线程数 &lt; corePoolSize → <strong>创建新线程执行任务</strong></li>
<li>若线程数 ≥ corePoolSize → <strong>放入队列</strong></li>
<li>若队列满且线程数 &lt; maximumPoolSize → <strong>创建新线程执行任务</strong></li>
<li>若线程数 ≥ maximumPoolSize 且队列也满 → <strong>触发拒绝策略</strong></li>
</ol><hr /></section>
<section><h2>常见线程池拒绝策略（<code>RejectedExecutionHandler</code>）<a href="#常见线程池拒绝策略rejectedexecutionhandler"><span>#</span></a></h2><table><thead><tr><th>策略</th><th>含义</th></tr></thead><tbody><tr><td>AbortPolicy</td><td>默认策略，抛出 RejectedExecutionException 异常。</td></tr><tr><td>CallerRunsPolicy</td><td>谁提交谁执行（任务回退给主线程）。</td></tr><tr><td>DiscardPolicy</td><td>直接丢弃任务，不抛异常。</td></tr><tr><td>DiscardOldestPolicy</td><td>丢弃队列最旧的任务，尝试执行当前任务。</td></tr></tbody></table><hr /></section>
<section><h2>常用线程池队列（<code>BlockingQueue</code>）<a href="#常用线程池队列blockingqueue"><span>#</span></a></h2><table><thead><tr><th>队列类型</th><th>特点</th></tr></thead><tbody><tr><td>ArrayBlockingQueue</td><td>有界，数组实现，必须设置容量，<strong>适合固定大小任务量</strong></td></tr><tr><td>LinkedBlockingQueue</td><td>可设容量（默认无限），链表实现，<strong>适合任务突发</strong></td></tr><tr><td>SynchronousQueue</td><td>不缓存任务，直接传递给线程，<strong>适合高并发、立即处理任务</strong></td></tr></tbody></table><hr /><p><strong>如何创建线程池？</strong></p><p><strong>推荐：使用 <code>ThreadPoolExecutor</code>，避免 <code>Executors</code> 工厂方法（不推荐）</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ExecutorService threadPool = new ThreadPoolExecutor(    2, 5,    10, TimeUnit.SECONDS,    new LinkedBlockingQueue&lt;&gt;(100),    Executors.defaultThreadFactory(),    new ThreadPoolExecutor.AbortPolicy());</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>不推荐的 <code>Executors</code>（会导致OOM）：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Executors.newFixedThreadPool(10); // 队列无界Executors.newCachedThreadPool();  // 线程无限Executors.newSingleThreadExecutor(); // 只有一个线程</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section>
<section><h2>线程池使用示例<a href="#线程池使用示例"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public class ThreadPoolDemo {    public static void main(String args) {        ExecutorService pool = new ThreadPoolExecutor(            2, 4,            10, TimeUnit.SECONDS,            new ArrayBlockingQueue&lt;&gt;(5),            Executors.defaultThreadFactory(),            new ThreadPoolExecutor.CallerRunsPolicy()        );        for (int i = 0; i &lt; 10; i++) {            final int taskId = i;            pool.execute(() -&gt; {                System.out.println("任务 " + taskId + " 执行线程: " + Thread.currentThread().getName());            });        }        pool.shutdown(); // 关闭线程池    }}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section>
<section><h2>阿里巴巴线程池规范（强烈建议背熟）<a href="#阿里巴巴线程池规范强烈建议背熟"><span>#</span></a></h2><p>使用 <strong>ThreadPoolExecutor</strong> 自定义线程池，<strong>禁止</strong>使用 <strong>Executors</strong> 提供的默认线程池，原因是：</p><ul>
<li><code>FixedThreadPool</code> 和 <code>SingleThreadPool</code> 使用的是<strong>无界队列</strong>，可能导致 OOM；</li>
<li><code>CachedThreadPool</code> 使用的是<strong>无界线程数量</strong>，也可能 OOM；</li>
<li>推荐设置合理 <code>队列大小</code> 和 <code>最大线程数</code>；</li>
</ul><hr /></section>
<section><h2>Executors<a href="#executors"><span>#</span></a></h2><p><code>Executors</code> 是 Java 提供的线程池工厂工具类，位于 <code>java.util.concurrent.Executors</code> 包中，它提供了一些<strong>快速创建线程池、定时任务线程池、单线程执行器</strong>的方法，使用简单，但也有一些坑（如无界队列、OOM 等问题）。</p><p><strong>Executors 常用方法总览</strong></p><table><thead><tr><th>方法名</th><th>描述</th><th>推荐程度</th></tr></thead><tbody><tr><td>newFixedThreadPool(int nThreads)</td><td>固定线程数线程池</td><td>❌ 不推荐（使用无界队列）</td></tr><tr><td>newCachedThreadPool()</td><td>缓存线程池，线程数不限制</td><td>❌ 不推荐（线程数无界）</td></tr><tr><td>newSingleThreadExecutor()</td><td>单线程线程池</td><td>❌ 不推荐（无界队列）</td></tr><tr><td>newScheduledThreadPool(int corePoolSize)</td><td>支持定时/周期执行的线程池</td><td>✅ 合理使用</td></tr><tr><td>newSingleThreadScheduledExecutor()</td><td>单线程定时线程池</td><td>✅ 可用于简单定时任务</td></tr><tr><td>newWorkStealingPool() （Java 8+）</td><td>ForkJoinPool 支持的多任务窃取线程池</td><td>✅ 特定并行任务</td></tr><tr><td>callable(Runnable task, V result)</td><td>把 Runnable 转为 Callable</td><td>✅ 工具方法</td></tr><tr><td>unconfigurableExecutorService(...)</td><td>包装不可配置的线程池</td><td>✅ 安全场景用</td></tr><tr><td>privilegedThreadFactory()</td><td>获取当前线程上下文权限的 ThreadFactory</td><td>✅ 高安全场景</td></tr><tr><td>defaultThreadFactory()</td><td>默认线程工厂（可用于手动 ThreadPoolExecutor）</td><td>✅ 常用</td></tr></tbody></table><hr /><p><strong>重点方法说明</strong></p><p><strong>1. newFixedThreadPool(int nThreads)</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ExecutorService pool = Executors.newFixedThreadPool(5);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>特点：线程数固定，超出部分任务排队执行</li>
<li>使用：适合任务量固定、不会突发堆积的场景</li>
<li>⚠️ <strong>问题：使用的是无界队列</strong>，易造成内存堆积 ➜ ❌不推荐</li>
</ul><hr /><p><strong>2. newCachedThreadPool()</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ExecutorService pool = Executors.newCachedThreadPool();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>特点：线程数量无限制（Integer.MAX_VALUE）</li>
<li>使用：适合大量、短周期的轻量任务</li>
<li>⚠️ <strong>问题：线程无限增长</strong>，易 OOM ➜ ❌不推荐</li>
</ul><hr /><p><strong>3. newSingleThreadExecutor()</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ExecutorService pool = Executors.newSingleThreadExecutor();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>特点：只有一个线程，串行执行任务</li>
<li>使用：任务必须顺序执行场景</li>
<li>⚠️ <strong>问题：队列无界，线程挂了会自动创建新线程</strong>，可能隐藏问题 ➜ ❌不推荐用于生产高并发</li>
</ul><hr /><p><strong>4. newScheduledThreadPool(int corePoolSize)</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>特点：支持定时、周期性任务，基于 <code>ScheduledThreadPoolExecutor</code></li>
<li>使用：定时任务（如日志归档、缓存刷新）</li>
<li>✅ 推荐使用，但建议配合 <code>ThreadFactory</code> + 拒绝策略显式构造更安全</li>
</ul><hr /><p><strong>5. newWorkStealingPool()</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ExecutorService pool = Executors.newWorkStealingPool(); // Java 8+</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>✅ 高性能，但不适合高并发请求处理，且无法精细控制线程池参数</p><p>特点：基于 <code>ForkJoinPool</code>，自动按 CPU 核心数创建工作线程</p><p>使用：适合并行分治任务（如大数据分片处理）</p><p><strong>推荐做法（线程池最佳实践）</strong></p><p><strong>阿里巴巴 Java 开发</strong>手册强烈建议不要使用 <strong>Executors</strong>，而是手动构造：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ThreadPoolExecutor pool = new ThreadPoolExecutor(    10, 20,    60L, TimeUnit.SECONDS,    new ArrayBlockingQueue&lt;&gt;(200),    Executors.defaultThreadFactory(), // 可替换为自定义命名工厂    new ThreadPoolExecutor.CallerRunsPolicy());</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><p><strong>拓展方法</strong></p><table><thead><tr><th>方法</th><th>说明</th></tr></thead><tbody><tr><td>Executors.callable(Runnable task)</td><td>把 Runnable 包装成 Callable（返回 null）</td></tr><tr><td>Executors.defaultThreadFactory()</td><td>返回默认线程工厂，线程名如：pool-1-thread-1</td></tr><tr><td>Executors.privilegedThreadFactory()</td><td>创建带当前 AccessControlContext 的线程</td></tr><tr><td>Executors.unconfigurableExecutorService()</td><td>包装线程池，不允许调用配置方法（如 shutdownNow）</td></tr></tbody></table><hr /><p><strong>总结：Executors 方法使用建议</strong></p><table><thead><tr><th>方法</th><th>是否推荐</th><th>说明</th></tr></thead><tbody><tr><td>newFixedThreadPool</td><td>❌</td><td>无界队列风险</td></tr><tr><td>newCachedThreadPool</td><td>❌</td><td>无界线程数风险</td></tr><tr><td>newSingleThreadExecutor</td><td>❌</td><td>无界队列、线程替换隐藏问题</td></tr><tr><td>newScheduledThreadPool</td><td>✅</td><td>合理控制线程数</td></tr><tr><td>newWorkStealingPool</td><td>✅</td><td>高吞吐并行计算</td></tr><tr><td>自定义 ThreadPoolExecutor</td><td>✅强烈推荐</td><td>可精细控制每个参数，安全、灵活</td></tr></tbody></table><hr /></section>
<section><h2>ScheduledThreadPoolExecutor<a href="#scheduledthreadpoolexecutor"><span>#</span></a></h2><p><code>**ScheduledThreadPoolExecutor**</code> 是 Java 中专门用于执行<strong>定时任务</strong>和<strong>周期任务</strong>的线程池，它比 <code>Timer</code> 更强大、更安全，是企业级定时任务的首选方案。</p><p><strong><code>ScheduledThreadPoolExecutor</code> 是什么？</strong></p><p><code>ScheduledThreadPoolExecutor</code> 是一个线程池，支持以下几种定时任务调度：</p><table><thead><tr><th>调度类型</th><th>描述</th></tr></thead><tbody><tr><td><strong>延迟执行</strong></td><td>延迟固定时间后执行一次任务</td></tr><tr><td><strong>周期执行（固定速率）</strong></td><td>每隔固定时间开始执行（忽略任务执行时长）</td></tr><tr><td><strong>周期执行（固定延迟）</strong></td><td>每次任务执行完后，延迟固定时间再开始下一次</td></tr></tbody></table><p>它是 <code>Executors.newScheduledThreadPool(int corePoolSize)</code> 的底层实现类，继承自 <code>ThreadPoolExecutor</code>。</p><p><img src="https://s.muxui.com:8015/OXhATjkxIZ6w.webp" alt="" /></p><hr /><p><strong>创建线程池的方式</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>等价于：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(    2,    Executors.defaultThreadFactory(),    new ThreadPoolExecutor.AbortPolicy());</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><p><strong>常用方法介绍</strong></p><p>1. schedule(Runnable command, long delay, TimeUnit unit)</p><p>延迟固定时间后执行一次</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>scheduler.schedule(() -&gt; {    System.out.println("延迟 3 秒后执行一次任务");}, 3, TimeUnit.SECONDS);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><p><strong>2. scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)</strong></p><p>周期执行（固定速率）：每隔 period 时间触发一次任务，不管任务执行多久</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>scheduler.scheduleAtFixedRate(() -&gt; {    System.out.println("每 5 秒执行一次，固定速率");}, 1, 5, TimeUnit.SECONDS);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>✅ 适合：定时轮询任务（比如定时监控、健康检查）</p><p>⚠️ 注意：如果任务执行超过周期，会出现<strong>任务并发执行重叠</strong></p><hr /><p><strong>3. scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)</strong></p><p>周期执行（固定延迟）：每次任务执行完之后，等 delay 时间再执行下一次</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>scheduler.scheduleWithFixedDelay(() -&gt; {    System.out.println("每次执行完后再等 5 秒执行");}, 1, 5, TimeUnit.SECONDS);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>✅ 适合：任务耗时不固定、希望每次之间有固定“间隔”</p><hr /><p><strong>含义和区别：</strong></p><table><thead><tr><th>方法</th><th>重复？</th><th>延迟/周期点</th><th>执行时机</th><th>并发执行</th></tr></thead><tbody><tr><td>schedule(Runnable, delay)</td><td>否（一次性）</td><td>延迟 delay 后执行一次</td><td>不管任务执行多长时间，到达延迟点就执行一次</td><td>不会重复，也不存在并发问题</td></tr><tr><td>scheduleAtFixedRate(Runnable, initialDelay, period)</td><td>是（固定速率）</td><td>以调度开始时间为基准，每隔 period 安排一次</td><td>上一次<strong>开始</strong>时间 + period 到点就触发</td><td>不会并发执行（内部串行），但如果执行慢于 period，会“立即”补跑</td></tr><tr><td>scheduleWithFixedDelay(Runnable, initialDelay, delay)</td><td>是（固定延迟）</td><td>以上一次<strong>结束</strong>时间为基准，结束后再延迟 delay</td><td>上一次<strong>完成</strong>时间 + delay 到点才触发</td><td>同样不并发执行</td></tr></tbody></table><hr /><p><strong>与 Timer 的区别（为什么用 ScheduledThreadPoolExecutor）</strong></p><table><thead><tr><th>比较点</th><th><code>Timer</code></th><th><code>ScheduledThreadPoolExecutor</code></th></tr></thead><tbody><tr><td>错误隔离</td><td>❌ 一个任务出异常会终止所有任务</td><td>✅ 一个任务异常不影响其他任务</td></tr><tr><td>并发支持</td><td>❌ 单线程</td><td>✅ 多线程</td></tr><tr><td>精度</td><td>一般</td><td>更精确</td></tr><tr><td>线程管理</td><td>❌ 不支持</td><td>✅ 支持线程回收、拒绝策略等</td></tr></tbody></table><p>✅ 所以：<strong>推荐使用 <code>ScheduledThreadPoolExecutor</code> 替代 <code>Timer</code></strong></p><hr /><p><strong>推荐封装方式（更安全、更灵活）</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(    4,    new BasicThreadFactory.Builder()        .namingPattern("scheduled-task-%d")        .daemon(true)        .build(),    new ThreadPoolExecutor.AbortPolicy());</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><code>BasicThreadFactory</code> 是来自 <code>commons-lang3</code> 的工具类，可设置线程名、是否守护线程</li>
<li>可结合监控系统收集执行时间、失败次数等</li>
</ul><hr /><p><strong>注意事项</strong></p><table><thead><tr><th>问题</th><th>建议</th></tr></thead><tbody><tr><td>任务执行时间过长</td><td>建议使用 scheduleWithFixedDelay 避免任务重叠</td></tr><tr><td>出现异常未捕获</td><td>用 try-catch 包裹任务逻辑，避免线程退出</td></tr><tr><td>执行慢造成堆积</td><td>可使用 ScheduledExecutorService.shutdownNow() 停止并清理</td></tr><tr><td>定时任务过多</td><td>拆分多个线程池，按业务划分执行优先级</td></tr></tbody></table><hr /><p><strong>实战示例：每晚 2 点定时执行</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public class DailyTask {    public static void main(String args) {        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);        Runnable task = () -&gt; {            System.out.println("执行每日任务：" + LocalDateTime.now());        };        long initialDelay = computeInitialDelay(); // 计算当前时间到 2 点的间隔        long period = 24 * 60 * 60; // 每 24 小时执行一次        scheduler.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.SECONDS);    }    private static long computeInitialDelay() {        LocalDateTime now = LocalDateTime.now();        LocalDateTime nextRun = now.withHour(2).withMinute(0).withSecond(0);        if (now.compareTo(nextRun) &gt; 0) {            nextRun = nextRun.plusDays(1);        }        return Duration.between(now, nextRun).getSeconds();    }}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section>
<section><h2>相关阅读<a href="#相关阅读"><span>#</span></a></h2><a href="/posts/java-thread-fundamentals/"><div><div>Java 线程基础知识</div><div>线程基础 Java线程是什么？ 线程 （Thread）是操作系统能够进行运算调度的最小单位。它被包含在进程之中，是进程中的实际运作单位。 进程 （Process）：程序运行的实例，每个进程有独立的内存空间。 线程 ：进程内部的执行单元，可以</div><div><span>2025-06-19</span><span>原创</span><span><span>#Java</span></span></div></div><div><img src="https://s.muxui.com:8015/images/2026/09/8e8t7ONN0FKz.webp" alt="" loading="lazy" /></div></a></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/java-thread-fundamentals/</id>
      <title type="text">Java线程知识</title>
      <published>2025-06-19T00:00:00.000Z</published>
      <updated>2025-06-24T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/java-thread-fundamentals/"/>
      <summary type="text">线程基础 Java线程是什么？ 线程 （Thread）是操作系统能够进行运算调度的最小单位。它被包含在进程之中，是进程中的实际运作单位。 进程 （Process）：程序运行的实例，每个进程有独立的内存空间。 线程 ：进程内部的执行单元，可以</summary>
      <content type="html"><![CDATA[<section><h2>线程基础<a href="#线程基础"><span>#</span></a></h2><p><strong>Java线程是什么？</strong></p><p><strong>线程</strong>（Thread）是操作系统能够进行运算调度的最小单位。它被包含在进程之中，是进程中的实际运作单位。</p><ul>
<li><strong>进程</strong>（Process）：程序运行的实例，每个进程有独立的内存空间。</li>
<li><strong>线程</strong>：进程内部的执行单元，可以多个线程共享同一个进程的内存资源。</li>
</ul><p><strong>Java线程</strong>指的是Java程序中用<code>Thread</code>类或实现<code>Runnable</code>接口创建的轻量级执行单元。一个Java应用至少有一个线程（main线程）。</p><hr /><p><strong>为什么要用线程？</strong></p><ul>
<li><strong>并发执行</strong>：让程序可以同时做多件事，比如下载、界面响应、计算等互不阻塞。</li>
<li><strong>资源共享</strong>：同一个进程内的多个线程可以共享数据，减少资源消耗，通信高效。</li>
<li><strong>提高性能</strong>：多核CPU上利用多线程能让程序跑得更快（合理使用的话）。</li>
</ul><hr /><section><h3><strong>创建线程的方式</strong><a href="#创建线程的方式"><span>#</span></a></h3><p><strong>继承 Thread 类</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class MyThread extends Thread {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>public void run() {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>System.out.println("MyThread is running");</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div><span>new MyThread().start();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>实现 Runnable 接口</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class MyRunnable implements Runnable {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>public void run() {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>System.out.println("MyRunnable is running");</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div><div><div><div>6</div></div><div><span>new Thread(new MyRunnable()).start();</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>实现 Callable 接口 + Future</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Callable&lt;Integer&gt; task = () -&gt; {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>return 123;</span></div></div><div><div><div>3</div></div><div><span>};</span></div></div><div><div><div>4</div></div><div><span>FutureTask&lt;Integer&gt; future = new FutureTask&lt;&gt;(task);</span></div></div><div><div><div>5</div></div><div><span>new Thread(future).start();</span></div></div><div><div><div>6</div></div><div><span>Integer result = future.get(); // 等待执行完</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>区别：</strong></p><ul>
<li><code>Thread</code>直接继承，不适合多继承。</li>
<li><code>Runnable</code>/<code>Callable</code>推荐（面向接口，更灵活）。</li>
</ul><hr /></section><section><h3>线程的生命周期<a href="#线程的生命周期"><span>#</span></a></h3><p>Java 线程状态分为几个阶段：</p><table><thead><tr><th>状态</th><th>说明</th></tr></thead><tbody><tr><td>NEW</td><td>新建，刚创建，还没启动（new Thread时）</td></tr><tr><td>RUNNABLE</td><td>可运行，包括正在运行和就绪等待CPU</td></tr><tr><td>BLOCKED</td><td>阻塞，等待锁</td></tr><tr><td>WAITING</td><td>等待，等待其他线程唤醒（如wait()/join()）</td></tr><tr><td>TIMED_WAITING</td><td>超时等待（如sleep(n)、wait(n)、join(n)）</td></tr><tr><td>TERMINATED</td><td>终止，线程执行完毕</td></tr></tbody></table><p><strong>状态切换典型例子</strong></p><ul>
<li><code>start()</code>：NEW -&gt; RUNNABLE</li>
<li>等待 CPU 分配：RUNNABLE -&gt; RUNNING</li>
<li><code>sleep()/wait()/join()</code>：RUNNING -&gt; TIMED_WAITING/WAITING</li>
<li>获取不到锁：BLOCKED</li>
<li><code>notify()/notifyAll()</code>：WAITING -&gt; RUNNABLE</li>
<li>执行完 <code>run()</code>：RUNNING -&gt; TERMINATED</li>
</ul><hr /></section><section><h3>常用线程操作方法<a href="#常用线程操作方法"><span>#</span></a></h3><ul>
<li><code>start()</code> ：启动线程</li>
<li><code>run()</code> ：线程要执行的代码</li>
<li><code>sleep(ms)</code> ：让当前线程休眠指定时间（不释放锁）</li>
<li><code>join()</code> ：等待另一个线程执行完毕</li>
<li><code>interrupt()</code> ：中断线程（设置中断标志，具体还要自己响应）</li>
<li><code>isAlive()</code> ：判断线程是否还在运行</li>
</ul><hr /><p><strong>例子：多线程打印</strong></p><p><strong>让两个线程交替打印数字：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public class PrintDemo {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>public static void main(String[] args) {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>Runnable task = () -&gt; {</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>for (int i = 0; i &lt; 5; i++) {</span></div></div><div><div><div>5</div></div><div><span><span>                </span></span><span>System.out.println(Thread.currentThread().getName() + ": " + i);</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>Thread t1 = new Thread(task, "A");</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>Thread t2 = new Thread(task, "B");</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>t1.start();</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>t2.start();</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>输出样例：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>A: 0</span></div></div><div><div><div>2</div></div><div><span>B: 0</span></div></div><div><div><div>3</div></div><div><span>A: 1</span></div></div><div><div><div>4</div></div><div><span>B: 1</span></div></div><div><div><div>5</div></div><div><span>...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>面试题举例<a href="#面试题举例"><span>#</span></a></h3><p><strong>1. Thread 和 Runnable 的区别？哪种更推荐？</strong></p><p><strong>答：</strong></p><ul>
<li>Thread 是类，Runnable 是接口。</li>
<li>Thread 本身也实现了 Runnable。</li>
<li>如果继承 Thread，不能再继承其他类（Java 单继承）。</li>
<li>实现 Runnable 更灵活，便于资源共享、可配合线程池等。</li>
<li>推荐使用 Runnable。</li>
</ul><p><strong>解析：</strong></p><ul>
<li>面试官想考你“类和接口的区别”、Java 的单继承特性和面向接口编程思想。</li>
</ul><hr /><p><strong>2. 直接调用 run() 和 start() 有什么区别？</strong></p><p><strong>答：</strong></p><ul>
<li><code>start()</code> 会新建一个线程，让线程异步并发执行 <code>run()</code>。</li>
<li><code>run()</code> 只是一个普通方法调用，会在当前线程（通常是 main 线程）里顺序执行，不会新建线程。</li>
</ul><p><strong>代码举例：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Thread t = new Thread(() -&gt; System.out.println(Thread.currentThread().getName()));t.run();   // maint.start(); // Thread-0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>解析：</strong></p><ul>
<li>主要考你是否理解线程启动的机制，很多初学者会混淆这两者。</li>
</ul><hr /><p><strong>3. Java 线程的生命周期有哪些状态？各状态之间如何转换？</strong></p><p><strong>答：</strong></p><ul>
<li>状态有：NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。</li>
<li>NEW —(start)—&gt; RUNNABLE —(获取CPU)—&gt; RUNNING</li>
<li>RUNNING —(wait/sleep/join/lock)—&gt; WAITING/BLOCKED/TIMED_WAITING</li>
<li>WAITING —(notify/join结束)—&gt; RUNNABLE</li>
<li>RUNNING —(run结束)—&gt; TERMINATED</li>
</ul><p><strong>解析：</strong></p><ul>
<li>考察你是否了解多线程的本质和线程调度机制。</li>
</ul><hr /><p><strong>4. 为什么建议用 Runnable 而不是 Thread？</strong></p><p><strong>答：</strong></p><ul>
<li>Runnable是接口，可实现多继承，Thread是类只可单继承。</li>
<li>Runnable代码和线程对象分离，便于解耦、代码复用。</li>
<li>Runnable更适合资源共享。</li>
<li>线程池只能提交 Runnable/Callable，不接受 Thread。</li>
</ul><p><strong>解析：</strong></p><ul>
<li>考察你面向接口编程思想和并发框架适配能力。</li>
</ul><hr /><p><strong>5. 线程在什么情况下会进入 BLOCKED/WAITING/TIMED_WAITING 状态？</strong></p><p><strong>答：</strong></p><ul>
<li>BLOCKED：等待获取 synchronized 锁时进入（比如两个线程抢一个锁）。</li>
<li>WAITING：调用 Object.wait()、Thread.join()、LockSupport.park() 等，线程无限等待。</li>
<li>TIMED_WAITING：调用 Thread.sleep(time)、wait(time)、join(time)、parkNanos() 等，超时等待。</li>
</ul><p><strong>解析：</strong></p><ul>
<li>你要清楚每种等待的应用场景和 API。</li>
</ul><hr /><p><strong>6. 线程池为什么只能提交 Runnable/Callable，不能直接提交 Thread？</strong></p><p><strong>答：</strong></p><ul>
<li>线程池的任务提交方法参数是 Runnable 或 Callable，底层线程池会把它包装到自己的工作线程（Worker）去执行，而不是直接管理 Thread 对象。</li>
<li>Thread 是线程实例，不适合作为任务的“逻辑单元”，Runnable/Callable 更轻便、复用性高。</li>
</ul><p><strong>解析：</strong></p><ul>
<li>体现面向接口编程、线程池架构设计思想。</li>
</ul><hr /><p><strong>7. 常见代码题：下面这段代码输出什么？</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public class Demo {    public static void main(String args) {        Thread t = new Thread(() -&gt; System.out.println(Thread.currentThread().getName()));        t.run();        t.start();    }}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>答：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>mainThread-0</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>解析：</strong></p><ul>
<li><code>t.run()</code> 只是普通方法调用，输出 <code>main</code></li>
<li><code>t.start()</code> 启动新线程，输出 <code>Thread-0</code></li>
</ul><hr /><p><strong>8. 说出你知道的线程的常用方法，并简要说明作用</strong></p><p><strong>答：</strong></p><ul>
<li>start()：启动线程</li>
<li>run()：线程体方法</li>
<li>sleep(ms)：让当前线程休眠指定毫秒数</li>
<li>join()：让当前线程等待目标线程执行完毕</li>
<li>interrupt()：中断线程（设置中断标志位）</li>
<li>isAlive()：判断线程是否存活</li>
</ul><p><strong>解析：</strong></p><ul>
<li>体现你实际开发经验和 API 熟悉度。</li>
</ul><hr /><p><strong>9. 线程安全问题是怎么产生的？举例说明</strong></p><p><strong>答：</strong></p><ul>
<li>多个线程同时访问同一共享资源（如变量、集合）且有写操作时，线程切换可能导致数据不一致，即线程安全问题。</li>
</ul><p><strong>代码举例：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class Counter {    public int count = 0;    public void incr() { count++; }}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个线程同时执行 incr()，结果可能小于2，说明数据被覆盖了。</p><hr /><p><strong>10. 线程用完后为什么不能复用？如何提高线程复用效率？</strong></p><p><strong>答：</strong></p><ul>
<li>Thread 对象只能启动一次（start()），否则会抛出 IllegalThreadStateException。</li>
<li>为提高效率，推荐使用线程池（如 Executors），线程池中的线程可以复用，避免频繁创建销毁。</li>
</ul></section><section><h3>在哪些场景会用到线程？<a href="#在哪些场景会用到线程"><span>#</span></a></h3><p><strong>1. 提高程序性能、充分利用多核CPU</strong></p><p><strong>场景举例：</strong></p><ul>
<li><strong>批量数据处理</strong><br />
比如：对 100 万条数据做运算，把数据分成 10 份，开 10 个线程，每个线程处理一份，最后合并结果，大大加快速度。</li>
<li><strong>大文件处理</strong><br />
文件分块多线程上传或下载。</li>
</ul><hr /><p><strong>2. 避免阻塞主线程，提高用户体验</strong></p><p><strong>场景举例：</strong></p><ul>
<li><strong>GUI 应用/移动端开发</strong><br />
比如：Swing/Android 开发中，如果网络请求、文件读写都在主线程，界面会卡死。必须放到子线程里处理，主线程负责界面响应。</li>
<li><strong>Web服务中的异步任务</strong><br />
用户下单后订单信息入库是主流程，但发短信、邮件等操作可以用新线程或线程池异步处理，提升接口响应速度。</li>
</ul><hr /><p><strong>3. 处理需要并发访问的场景</strong></p><p><strong>场景举例：</strong></p><ul>
<li><strong>高并发 Web 服务器</strong><br />
Tomcat、Jetty等Web服务器为每个请求分配线程（或线程池中的线程），可以同时处理成百上千的请求。</li>
<li><strong>聊天室/游戏服务器</strong><br />
每个用户一个线程，实时通信。</li>
</ul><hr /><p><strong>4. 定时、周期性任务</strong></p><p><strong>场景举例：</strong></p><ul>
<li><strong>定时任务调度</strong><br />
通过 ScheduledExecutorService，每隔一段时间自动执行某项任务（如每5分钟同步数据）。</li>
<li><strong>日志异步落盘</strong><br />
日志框架往往会用线程异步写磁盘，避免主业务阻塞。</li>
</ul><hr /><p><strong>5. 多线程爬虫/数据采集</strong></p><p><strong>场景举例：</strong></p><ul>
<li><strong>多线程爬虫</strong><br />
爬取多个网站页面，每个页面一个线程，同时采集，提高抓取效率。</li>
<li><strong>多线程文件上传/下载</strong><br />
分块并发上传大文件，比如网盘、断点续传。</li>
</ul><hr /><p><strong>6. 并行计算/任务分片</strong></p><p><strong>场景举例：</strong></p><ul>
<li><strong>矩阵运算/科学计算</strong><br />
大规模计算任务分解为多个线程，并行处理。</li>
<li><strong>视频转码</strong><br />
视频按片段切分，每段用一个线程转码，最后拼接结果。</li>
</ul><hr /><p><strong>7. 线程池场景（资源有限、任务繁多）</strong></p><p><strong>场景举例：</strong></p><ul>
<li><strong>消息队列消费者</strong><br />
多线程从消息队列（如RabbitMQ、Kafka）取消息，提升吞吐量。</li>
<li><strong>Web爬虫任务池</strong><br />
多线程不断处理任务队列中的爬虫任务。</li>
</ul><hr /><p><strong>8. 其他场景</strong></p><ul>
<li><strong>异步事件驱动编程</strong>（比如Netty，虽然底层不完全用传统线程，但线程池是基础）</li>
<li><strong>并发缓存刷新</strong></li>
<li><strong>文件系统监听与同步</strong></li>
</ul></section></section>
<section><h2>线程同步（synchronized、Lock）<a href="#线程同步synchronizedlock"><span>#</span></a></h2><section><h3>一、为什么需要线程同步？<a href="#一为什么需要线程同步"><span>#</span></a></h3><ul>
<li><strong>线程安全问题</strong>：多个线程同时访问/修改同一个资源（如变量、集合），可能导致数据混乱（脏读、丢数据等）。</li>
<li><strong>举例：</strong></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class Counter {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>int count = 0;</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>void incr() { count++; }</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div><div><div><div>5</div></div><div><span>// 多线程执行incr()，count结果可能不是你预期的</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>结论</strong>：要保证共享数据的正确性，需要<strong>线程同步</strong>，让同一时刻只能有一个线程修改资源。</p><hr /></section><section><h3>二、synchronized 同步机制<a href="#二synchronized-同步机制"><span>#</span></a></h3><p><strong>1. 基本语法</strong></p><ul>
<li><strong>修饰实例方法</strong>：锁住当前实例对象</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public synchronized void incr() { count++; }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>修饰静态方法</strong>：锁住类对象（Class对象）</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public static synchronized void incr() { ... }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>修饰代码块</strong>：指定锁对象</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public void incr() {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>synchronized(this) { count++; }</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>// 或 synchronized(某个对象) { ... }</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>2. 作用范围</strong></p><ul>
<li><strong>实例方法</strong>：锁住当前对象（this），同一个对象同一时刻只有一个线程进入该方法。</li>
<li><strong>静态方法</strong>：锁住Class对象（全类唯一）。</li>
<li><strong>代码块</strong>：可以灵活指定锁粒度（如锁某个共享变量、类对象等）。</li>
</ul><p><strong>3. synchronized 的原理</strong></p><ul>
<li>每个对象有一个“内置锁”（Monitor），synchronized 本质就是拿这个锁。</li>
<li>获取不到锁的线程会阻塞，直到获得锁。</li>
</ul><p><strong>4. synchronized 代码演示</strong></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class SafeCounter {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>private int count = 0;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>public synchronized void incr() {</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>count++;</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>public int getCount() {</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>return count;</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>public class Demo {</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>public static void main(String[] args) throws InterruptedException {</span></div></div><div><div><div>15</div></div><div><span><span>        </span></span><span>SafeCounter counter = new SafeCounter();</span></div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>Thread t1 = new Thread(() -&gt; { for (int i=0; i&lt;10000; i++) counter.incr(); });</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>Thread t2 = new Thread(() -&gt; { for (int i=0; i&lt;10000; i++) counter.incr(); });</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>t1.start(); t2.start();</span></div></div><div><div><div>19</div></div><div><span><span>        </span></span><span>t1.join(); t2.join();</span></div></div><div><div><div>20</div></div><div><span><span>        </span></span><span>System.out.println(counter.getCount()); // 20000</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>没有同步，结果可能小于 20000；加了 synchronized，一定是 20000。</strong></p><hr /></section><section><h3>三、Lock 接口及其实现<a href="#三lock-接口及其实现"><span>#</span></a></h3><p><strong>1. Lock 概述</strong></p><ul>
<li><code>Lock</code> 是 <code>java.util.concurrent.locks</code> 包下的接口，比 <code>synchronized</code> 更灵活。</li>
<li>常用实现：<code>ReentrantLock</code>（可重入锁）</li>
</ul><p><strong>2. 基本用法</strong></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import java.util.concurrent.locks.Lock;</span></div></div><div><div><div>2</div></div><div><span>import java.util.concurrent.locks.ReentrantLock;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>class SafeCounter2 {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>private int count = 0;</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>private final Lock lock = new ReentrantLock();</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>public void incr() {</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>lock.lock();         // 加锁</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>try {</span></div></div><div><div><div>11</div></div><div><span><span>            </span></span><span>count++;</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>} finally {</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>lock.unlock();   // 解锁，必须写在finally防止死锁</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div>
</div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>public int getCount() {</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>return count;</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>20</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>3. Lock 的优势</strong></p><ul>
<li>能响应中断（<code>lockInterruptibly()</code>）</li>
<li>可以<strong>尝试获取锁</strong>（<code>tryLock()</code>）</li>
<li>支持公平/非公平锁</li>
<li>支持条件变量（<code>Condition</code>，可实现更灵活的线程通信）</li>
</ul><hr /></section><section><h3>四、synchronized VS Lock<a href="#四synchronized-vs-lock"><span>#</span></a></h3><table><thead><tr><th>方面</th><th>synchronized</th><th>Lock (如ReentrantLock)</th></tr></thead><tbody><tr><td>JDK版本</td><td>关键字，JVM层实现</td><td>类库，JDK 1.5后</td></tr><tr><td>解锁方式</td><td>自动（方法/代码块执行完自动释放）</td><td>手动（需要unlock，容易死锁）</td></tr><tr><td>是否可重入</td><td>是</td><td>是</td></tr><tr><td>是否可中断</td><td>否</td><td>是（lockInterruptibly）</td></tr><tr><td>是否公平锁</td><td>否</td><td>可选（构造时指定公平性）</td></tr><tr><td>条件变量</td><td>无</td><td>有（Condition）</td></tr><tr><td>性能</td><td>JDK1.6以后，差别很小</td><td>灵活场景更多</td></tr></tbody></table><p><strong>总结：</strong></p><ul>
<li><strong>简单用法优先synchronized</strong>（易读，易维护）。</li>
<li><strong>复杂并发控制、可中断、公平锁、条件队列等用Lock</strong>。</li>
</ul><hr /></section><section><h3>五、常见线程同步场景举例<a href="#五常见线程同步场景举例"><span>#</span></a></h3><p><strong>1. 银行账户转账（防止两个人同时转账导致余额出错）</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>class Account {    private int balance = 1000;    public synchronized void withdraw(int money) {        if (balance &gt;= money) balance -= money;    }}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>2. 多线程安全队列</strong></p><ul>
<li>用 <code>Lock</code> 实现队列的线程安全入队出队</li>
<li>JDK 自带 <code>ConcurrentLinkedQueue</code> 等并发容器（底层就是 CAS/锁）</li>
</ul><hr /></section><section><h3>六、常见面试/笔试陷阱<a href="#六常见面试笔试陷阱"><span>#</span></a></h3><ul>
<li><strong>忘记用 unlock()，导致死锁</strong></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>lock.lock();</span></div></div><div><div><div>2</div></div><div><span>// 代码异常没有finally unlock，锁永远不释放</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>锁对象不同导致同步失败</strong></li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>synchronized(new Object()) { ... } // 每次都是新对象，没有同步效果</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>误以为 static synchronized 锁住所有实例，其实只锁类对象</strong></li>
</ul><hr /></section><section><h3>七、最佳实践建议<a href="#七最佳实践建议"><span>#</span></a></h3><ul>
<li>多线程只锁<strong>真正需要同步的最小代码块</strong>，避免扩大锁粒度，防止性能下降。</li>
<li>如果锁住的是<strong>静态资源</strong>，一定要锁 Class 对象（如<code>synchronized(ClassName.class)</code>）。</li>
<li>用 Lock 的时候一定要 unlock() 写在 finally 里。</li>
<li>面试时会被问：<strong>如何避免死锁？怎么用Lock写出更灵活的同步控制？</strong></li>
</ul><hr /></section><section><h3>八、死锁问题复现<a href="#八死锁问题复现"><span>#</span></a></h3><p><strong>1. 什么是死锁？</strong></p><ul>
<li>死锁（Deadlock）：多个线程因互相持有对方需要的锁资源，导致都永远等待下去，程序卡住。</li>
<li>直观理解：
<ul>
<li>线程A拿着资源1，要等资源2；线程B拿着资源2，要等资源1；谁都不肯放手，谁都等不到。</li>
</ul>
</li>
</ul><p><strong>2. 死锁经典代码复现</strong></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public class DeadLockDemo {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>private static final Object lockA = new Object();</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>private static final Object lockB = new Object();</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>public static void main(String[] args) {</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>Thread t1 = new Thread(() -&gt; {</span></div></div><div><div><div>7</div></div><div><span><span>            </span></span><span>synchronized (lockA) {</span></div></div><div><div><div>8</div></div><div><span><span>                </span></span><span>System.out.println("Thread1 got lockA");</span></div></div><div><div><div>9</div></div><div><span><span>                </span></span><span>try { Thread.sleep(100); } catch (InterruptedException e) {}</span></div></div><div><div><div>10</div></div><div><span><span>                </span></span><span>synchronized (lockB) {</span></div></div><div><div><div>11</div></div><div><span><span>                    </span></span><span>System.out.println("Thread1 got lockB");</span></div></div><div><div><div>12</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span><span>        </span></span><span>Thread t2 = new Thread(() -&gt; {</span></div></div><div><div><div>17</div></div><div><span><span>            </span></span><span>synchronized (lockB) {</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>System.out.println("Thread2 got lockB");</span></div></div><div><div><div>19</div></div><div><span><span>                </span></span><span>try { Thread.sleep(100); } catch (InterruptedException e) {}</span></div></div><div><div><div>20</div></div><div><span><span>                </span></span><span>synchronized (lockA) {</span></div></div><div><div><div>21</div></div><div><span><span>                    </span></span><span>System.out.println("Thread2 got lockA");</span></div></div><div><div><div>22</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>25</div></div><div>
</div></div><div><div><div>26</div></div><div><span><span>        </span></span><span>t1.start();</span></div></div><div><div><div>27</div></div><div><span><span>        </span></span><span>t2.start();</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>29</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>运行结果：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Thread1 got lockA</span></div></div><div><div><div>2</div></div><div><span>Thread2 got lockB</span></div></div><div><div><div>3</div></div><div><span>// 程序一直卡住，不再输出</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>原因：</strong></p><ul>
<li>t1 获得 lockA，t2 获得 lockB，之后双方都在等待对方释放锁，永远卡住。</li>
</ul><hr /><p><strong>如何避免死锁？</strong></p><p><strong>1. 尽量保证获取多个锁时顺序一致</strong></p><ul>
<li><strong>方案</strong>：统一规定加锁顺序，比如永远先拿lockA再拿lockB。</li>
<li><strong>改造代码</strong>：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 两个线程都先拿lockA再拿lockB，不会死锁</span></div></div><div><div><div>2</div></div><div><span>Runnable task = () -&gt; {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>synchronized (lockA) {</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>try { Thread.sleep(100); } catch (InterruptedException e) {}</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>synchronized (lockB) {</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>System.out.println(Thread.currentThread().getName() + " got both locks");</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span>};</span></div></div><div><div><div>10</div></div><div><span>// t1和t2都用同一个顺序</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>2. 尽量减少锁的持有时间、减少同步范围</strong></p><ul>
<li>锁只包裹最小、最关键的代码块，避免不必要的锁嵌套。</li>
</ul><p><strong>3. 避免嵌套锁（能不多层加锁就不多层）</strong></p><ul>
<li>有些场景通过业务设计避免多重锁定。</li>
</ul><p><strong>4. 使用 tryLock 尝试获取锁，失败立即释放已持有的锁</strong></p><ul>
<li>利用 Lock 的 tryLock 特性，可以让线程在拿不到全部锁时“主动放弃”，避免死锁。</li>
</ul><hr /><p><strong>用Lock写更灵活的同步控制</strong></p><p><strong>1. ReentrantLock 支持 tryLock（核心功能！）</strong></p><ul>
<li><strong>基本思想</strong>：尝试同时获取所有锁，失败则释放已获得的锁并稍后重试，防止死锁。</li>
</ul><p><strong>例子：避免死锁的 tryLock 实现</strong></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import java.util.concurrent.locks.Lock;</span></div></div><div><div><div>2</div></div><div><span>import java.util.concurrent.locks.ReentrantLock;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>public class TryLockAvoidDeadlock {</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>private static final Lock lockA = new ReentrantLock();</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>private static final Lock lockB = new ReentrantLock();</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>public static void main(String[] args) {</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>Runnable task1 = () -&gt; {</span></div></div><div><div><div>10</div></div><div><span><span>            </span></span><span>while (true) {</span></div></div><div><div><div>11</div></div><div><span><span>                </span></span><span>if (lockA.tryLock()) {</span></div></div><div><div><div>12</div></div><div><span><span>                    </span></span><span>try {</span></div></div><div><div><div>13</div></div><div><span><span>                        </span></span><span>Thread.sleep(100); // 模拟业务</span></div></div><div><div><div>14</div></div><div><span><span>                        </span></span><span>if (lockB.tryLock()) {</span></div></div><div><div><div>15</div></div><div><span><span>                            </span></span><span>try {</span></div></div><div><div><div>16</div></div><div><span><span>                                </span></span><span>System.out.println("Task1 got both locks!");</span></div></div><div><div><div>17</div></div><div><span><span>                                </span></span><span>break; // 成功获得所有锁，跳出</span></div></div><div><div><div>18</div></div><div><span><span>                            </span></span><span>} finally {</span></div></div><div><div><div>19</div></div><div><span><span>                                </span></span><span>lockB.unlock();</span></div></div><div><div><div>20</div></div><div><span><span>                            </span></span><span>}</span></div></div><div><div><div>21</div></div><div><span><span>                        </span></span><span>}</span></div></div><div><div><div>22</div></div><div><span><span>                    </span></span><span>} catch (InterruptedException e) {}</span></div></div><div><div><div>23</div></div><div><span><span>                    </span></span><span>finally {</span></div></div><div><div><div>24</div></div><div><span><span>                        </span></span><span>lockA.unlock();</span></div></div><div><div><div>25</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>26</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>27</div></div><div><span><span>                </span></span><span>// 失败时稍微等下再试</span></div></div><div><div><div>28</div></div><div><span><span>                </span></span><span>try { Thread.sleep(10); } catch (InterruptedException e) {}</span></div></div><div><div><div>29</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>30</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span><span>        </span></span><span>Runnable task2 = () -&gt; {</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>while (true) {</span></div></div><div><div><div>34</div></div><div><span><span>                </span></span><span>if (lockB.tryLock()) {</span></div></div><div><div><div>35</div></div><div><span><span>                    </span></span><span>try {</span></div></div><div><div><div>36</div></div><div><span><span>                        </span></span><span>Thread.sleep(100);</span></div></div><div><div><div>37</div></div><div><span><span>                        </span></span><span>if (lockA.tryLock()) {</span></div></div><div><div><div>38</div></div><div><span><span>                            </span></span><span>try {</span></div></div><div><div><div>39</div></div><div><span><span>                                </span></span><span>System.out.println("Task2 got both locks!");</span></div></div><div><div><div>40</div></div><div><span><span>                                </span></span><span>break;</span></div></div><div><div><div>41</div></div><div><span><span>                            </span></span><span>} finally {</span></div></div><div><div><div>42</div></div><div><span><span>                                </span></span><span>lockA.unlock();</span></div></div><div><div><div>43</div></div><div><span><span>                            </span></span><span>}</span></div></div><div><div><div>44</div></div><div><span><span>                        </span></span><span>}</span></div></div><div><div><div>45</div></div><div><span><span>                    </span></span><span>} catch (InterruptedException e) {}</span></div></div><div><div><div>46</div></div><div><span><span>                    </span></span><span>finally {</span></div></div><div><div><div>47</div></div><div><span><span>                        </span></span><span>lockB.unlock();</span></div></div><div><div><div>48</div></div><div><span><span>                    </span></span><span>}</span></div></div><div><div><div>49</div></div><div><span><span>                </span></span><span>}</span></div></div><div><div><div>50</div></div><div><span><span>                </span></span><span>try { Thread.sleep(10); } catch (InterruptedException e) {}</span></div></div><div><div><div>51</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>52</div></div><div><span><span>        </span></span><span>};</span></div></div><div><div><div>53</div></div><div>
</div></div><div><div><div>54</div></div><div><span><span>        </span></span><span>new Thread(task1).start();</span></div></div><div><div><div>55</div></div><div><span><span>        </span></span><span>new Thread(task2).start();</span></div></div><div><div><div>56</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>57</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>说明：</strong></p><ul>
<li>tryLock 如果拿不到锁不会阻塞，直接返回false，这样线程能主动释放已持有的锁和重试，避免僵死等待。</li>
</ul><hr /></section><section><h3>问：如果两个线程中,程序挂了,会当如何？<a href="#问如果两个线程中程序挂了会当如何"><span>#</span></a></h3><p>这个“如果两个线程中，程序挂了，会当如何”——这个问题我拆分一下理解：</p><ul>
<li>如果你的意思是<strong>线程死锁卡住了</strong>，两个线程都在等对方，不会继续往下走，但整个Java进程（JVM）不会自动退出，进程会一直挂着（CPU/内存不会释放），直到你手动kill进程或用工具分析。</li>
<li>如果你的意思是<strong>线程执行代码抛出了异常</strong>（比如空指针、数组越界），这时候分几种情况，下面详细说说：</li>
</ul><hr /><p><strong>1. 线程发生死锁（Deadlock）</strong></p><ul>
<li>两个线程都在等待对方释放锁，谁也不让步，<strong>不会抛出异常</strong>，只是这两个线程永远卡住。</li>
<li>只要主线程没退出（比如main方法还在等），<strong>整个Java程序会一直处于“卡死”状态</strong>，你只能用 jstack/jconsole 查看线程堆栈，或者重启程序。</li>
</ul><hr /><p><strong>2. 线程内部抛出异常</strong></p><p><strong>2.1 主线程抛出异常</strong></p><ul>
<li>主线程（main）如果抛出未捕获异常，主线程会退出，<strong>但进程不会马上终止</strong>，因为还有其他子线程可能还在跑。</li>
</ul><p><strong>2.2 子线程抛出异常</strong></p><ul>
<li>子线程如果抛出异常（没catch），该线程会退出，但不会影响其他线程。</li>
<li><strong>不会导致整个Java程序（JVM进程）挂掉或退出</strong>，其他线程继续跑。</li>
<li>如果这个子线程很重要（比如线程池里的worker），那它挂了后，相关任务会失败，但程序总体还活着。</li>
</ul><p><strong>例子：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public class Demo {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>public static void main(String[] args) {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>Thread t = new Thread(() -&gt; {</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>System.out.println("子线程开始");</span></div></div><div><div><div>5</div></div><div><span><span>            </span></span><span>int x = 1/0; // 这里异常</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>System.out.println("子线程结束");</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>});</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>t.start();</span></div></div><div><div><div>9</div></div><div><span><span>        </span></span><span>System.out.println("main线程结束");</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>11</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>输出类似：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>子线程开始</span></div></div><div><div><div>2</div></div><div><span>main线程结束</span></div></div><div><div><div>3</div></div><div><span>Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>at ...</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>线程 Thread-0 挂了，main线程没受影响，JVM进程还会活着直到所有线程都退出。</li>
</ul><hr /><p><strong>3. 线程组/线程池中线程异常</strong></p><ul>
<li>在线程池中，如果任务抛出异常，只影响当前任务，线程池会自动回收、继续接收新任务。</li>
<li>但如果<strong>线程异常没人捕获且主线程已经退出，且没有非守护线程存活，JVM会自动退出</strong>。</li>
</ul><hr /><p><strong>4. 怎样防止线程因异常挂掉影响业务？</strong></p><ul>
<li>用 <code>try-catch</code> 包裹线程体，日志输出异常，确保即使出错也能优雅处理：</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Runnable task = () -&gt; {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>try {</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>// 业务代码</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>} catch (Exception e) {</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>e.printStackTrace();</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>7</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>对于线程池，可以用<code>ThreadPoolExecutor</code>的<code>afterExecute</code>回调，统一日志处理。</li>
</ul><hr /><p><strong>5. 死锁挂住如何处理？</strong></p><ul>
<li><strong>定位死锁</strong>：jstack/jconsole 工具可以看到“waiting to lock…”的线程栈，能发现死锁。</li>
<li><strong>手动干预</strong>：一般只能重启服务或优化代码。</li>
<li><strong>预防</strong>：如上面说的 tryLock、锁顺序统一等。</li>
</ul><hr /><p><strong>6. 其他特殊情况</strong></p><ul>
<li>如果你的主线程是守护线程（daemon thread），所有非守护线程退出后，JVM 会自动退出。</li>
<li>但只要有<strong>非守护线程（比如死锁、死循环）活着，JVM就不会自动退出</strong>。</li>
</ul><hr /><p><strong>小结</strong></p><ul>
<li>单个线程挂了不会影响整个程序，除非它是最后一个非守护线程。</li>
<li>死锁会让程序卡住不动，但不会自动报错退出。</li>
<li>要健壮，多用 try-catch 保证线程异常能被日志记录，不影响主流程。</li>
<li>生产环境最好有线程存活/心跳监控，死锁检测和自动报警。</li>
</ul></section></section>
<section><h2>理解JMM和可见性问题<a href="#理解jmm和可见性问题"><span>#</span></a></h2><section><h3>一、什么是 JMM（Java 内存模型）？<a href="#一什么是-jmmjava-内存模型"><span>#</span></a></h3><ul>
<li>JMM是Java语言中规定的<strong>多线程并发时，变量如何在内存中存储、读取和传递</strong>的一套规范。</li>
<li>目的是：<strong>屏蔽不同硬件和操作系统的内存访问差异，让Java程序员写出的多线程代码能在所有平台下表现一致。</strong></li>
</ul><hr /></section><section><h3>二、为什么需要 JMM？<a href="#二为什么需要-jmm"><span>#</span></a></h3><ol>
<li><strong>多核CPU下的高速缓存导致“可见性”问题</strong>
<ul>
<li>每个CPU都有自己的缓存，各个线程可以同时运行在不同CPU上。</li>
<li>线程A在自己的CPU缓存里修改了变量，线程B在另一个CPU的缓存里还没看到这个最新值。</li>
</ul>
</li>
<li><strong>指令重排序</strong>
<ul>
<li>为了提升性能，JVM和CPU都可以对代码执行顺序做优化。</li>
<li>可能导致代码实际运行顺序和我们写出来的顺序不同，从而出错。</li>
</ul>
</li>
</ol><hr /></section><section><h3>三、JMM的三大核心问题<a href="#三jmm的三大核心问题"><span>#</span></a></h3><ol>
<li><strong>原子性</strong>：操作不可被线程切割（一次完成）。比如++不是原子操作。</li>
<li><strong>可见性</strong>：一个线程修改了变量，其他线程能否立即看到这个变化？</li>
<li><strong>有序性</strong>：程序执行顺序是否和代码顺序一致？</li>
</ol><hr /></section><section><h3>四、可见性问题的现象和举例<a href="#四可见性问题的现象和举例"><span>#</span></a></h3><p><strong>1. 代码示例：可见性问题</strong></p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>public class VisibilityDemo {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>private static boolean flag = false;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>public static void main(String[] args) throws Exception {</span></div></div><div><div><div>5</div></div><div><span><span>        </span></span><span>new Thread(() -&gt; {</span></div></div><div><div><div>6</div></div><div><span><span>            </span></span><span>while (!flag) {</span></div></div><div><div><div>7</div></div><div><span><span>                </span></span><span>// do nothing</span></div></div><div><div><div>8</div></div><div><span><span>            </span></span><span>}</span></div></div><div><div><div>9</div></div><div><span><span>            </span></span><span>System.out.println("线程检测到flag变为true");</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>}).start();</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>Thread.sleep(1000);</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>flag = true;</span></div></div><div><div><div>14</div></div><div><span><span>        </span></span><span>System.out.println("main线程已修改flag=true");</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>16</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>你可能期望输出：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>main线程已修改flag=true</span></div></div><div><div><div>2</div></div><div><span>线程检测到flag变为true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>但实际运行时输出：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>main线程已修改flag=true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>而子线程里的<code>System.out.println("线程检测到flag变为true");</code><strong>永远不会执行！</strong></p><p><strong>这是为什么？</strong></p><p><strong>2.1 JMM的内存模型决定了这种情况</strong></p><ul>
<li>Java 的每个线程都有自己的<strong>工作内存</strong>（可以理解为“线程私有变量的副本”）。</li>
<li>当子线程开始时，会把 <code>flag</code> 的值从主内存<strong>拷贝到自己的工作内存</strong>。</li>
<li>子线程在 while 循环里，一直判断自己的“副本flag”，如果主线程把主内存的 flag 改为 true，但<strong>子线程不会自动刷新自己的flag副本</strong>。</li>
<li>所以<strong>子线程一直看到的都是原来的false</strong>，while循环永远不退出。</li>
</ul><p><strong>2.2 不是每次都出现！</strong></p><ul>
<li>有时候你加一条输出（比如在while里打印），或者改成debug模式，JVM的“优化”行为会变，副本会被刷新，表现出正确结果。这就是**“偶现/不稳定”问题**，更难排查。</li>
</ul><p><strong>结论：这不是Java“Bug”，而是JMM特意的规定</strong></p><ul>
<li>Java这样设计，是为了<strong>多线程下的性能</strong>（缓存机制、减少主内存的读写压力）。</li>
<li>只有当你用<strong>synchronized/volatile</strong>等方式，才强制各线程刷新变量。</li>
</ul><hr /></section><section><h3>五、JMM对可见性问题的规定<a href="#五jmm对可见性问题的规定"><span>#</span></a></h3><ul>
<li>JMM规定了<strong>主内存（Main Memory）和每个线程的工作内存（Working Memory）</strong>。</li>
<li>变量必须先拷贝到线程工作内存，再从工作内存写回主内存。</li>
<li>线程间通信，<strong>必须通过主内存</strong>。</li>
<li>线程自己工作区变量改了，不保证立刻刷回主内存。</li>
</ul><hr /></section><section><h3>六、解决可见性问题的常用方法<a href="#六解决可见性问题的常用方法"><span>#</span></a></h3><p><strong>1. 使用 <code>volatile</code> 关键字</strong></p><ul>
<li><strong>作用</strong>：保证变量对所有线程<strong>立即可见</strong>，每次读写都强制从主内存读/写。</li>
<li><strong>缺点</strong>：只能保证可见性和有序性，不保证原子性。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private static volatile boolean flag = false;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>2. 使用 <code>synchronized</code> 关键字</strong></p><ul>
<li>进入/退出同步块时，JMM会保证<strong>变量的刷新</strong>（解锁前会把本地工作内存的变量写回主内存，加锁后清空本地缓存重新读取主内存）。</li>
<li>因此，synchronized 既保证了<strong>可见性</strong>，又能保证<strong>原子性</strong>。</li>
</ul><p><strong>3. 使用 <code>Lock</code>（如ReentrantLock）</strong></p><ul>
<li>lock() 和 unlock() 同步也会带来可见性保证。</li>
</ul><hr /></section><section><h3>七、指令重排序与 volatile 的“内存屏障”作用<a href="#七指令重排序与-volatile-的内存屏障作用"><span>#</span></a></h3><ul>
<li>volatile 原理其实就是加了“内存屏障”，禁止 JVM/CPU 优化时乱序执行，保证前后的代码顺序对其他线程可见。</li>
<li>synchronized 也有相似效果（加锁/解锁时会有内存屏障）。</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 仅保证可见性</span></div></div><div><div><div>2</div></div><div><span>private static volatile boolean flag = false;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>// 既保证可见性又保证原子性</span></div></div><div><div><div>5</div></div><div><span>public synchronized void setFlag(boolean value) { flag = value; }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /><p><strong>volatile能保证什么？不能保证什么？</strong></p><p><strong>volatile能保证：</strong></p><ul>
<li><strong>可见性</strong>：一个线程修改变量，其他线程立刻可见。</li>
<li><strong>有序性</strong>：禁止指令重排（保证前后的操作不会被JVM/CPU优化错乱顺序）。</li>
</ul><p><strong>volatile不能保证：</strong></p><ul>
<li><strong>原子性</strong>：一个操作“不可再分”，volatile不保证复合操作的原子性。</li>
</ul><p><strong>举例（不保证原子性）：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private static volatile int count = 0;</span></div></div><div><div><div>2</div></div><div><span>public static void main(String[] args) {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>new Thread(() -&gt; { for(int i=0;i&lt;10000;i++) count++; }).start();</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>new Thread(() -&gt; { for(int i=0;i&lt;10000;i++) count++; }).start();</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>// 结果可能小于20000，说明++不是原子操作</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>synchronized和volatile的区别？<a href="#synchronized和volatile的区别"><span>#</span></a></h3><table><thead><tr><th>对比点</th><th>volatile</th><th>synchronized</th></tr></thead><tbody><tr><td>关键字类型</td><td>修饰变量</td><td>修饰方法/代码块</td></tr><tr><td>可见性</td><td>保证</td><td>保证</td></tr><tr><td>原子性</td><td>不保证</td><td>保证</td></tr><tr><td>有序性</td><td>保证（禁止重排）</td><td>保证（进入/退出内存屏障）</td></tr><tr><td>性能</td><td>性能高，适合状态标志</td><td>性能低，涉及线程调度</td></tr><tr><td>适用场景</td><td>状态标志、单例双检锁等</td><td>复杂逻辑同步、复合操作</td></tr></tbody></table><hr /><p><strong>原子性、可见性、有序性各举一个代码例子</strong></p><p><strong>原子性举例：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private static int count = 0;</span></div></div><div><div><div>2</div></div><div><span>public static void main(String[] args) throws Exception {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>Thread t1 = new Thread(() -&gt; { for(int i=0;i&lt;10000;i++) count++; });</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>Thread t2 = new Thread(() -&gt; { for(int i=0;i&lt;10000;i++) count++; });</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>t1.start(); t2.start(); t1.join(); t2.join();</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>System.out.println(count); // 结果可能小于20000，++不是原子操作</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>可见性举例：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>private static boolean running = true;</span></div></div><div><div><div>2</div></div><div><span>Thread t = new Thread(() -&gt; {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>while (running) {} // 没有volatile或synchronized修饰，主线程改了，子线程看不到</span></div></div><div><div><div>4</div></div><div><span>});</span></div></div><div><div><div>5</div></div><div><span>t.start();</span></div></div><div><div><div>6</div></div><div><span>Thread.sleep(1000);</span></div></div><div><div><div>7</div></div><div><span>running = false; // 子线程可能永远跳不出循环</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>有序性举例（指令重排序）：</strong></p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>int a = 0, b = 0, x = 0, y = 0;</span></div></div><div><div><div>2</div></div><div><span>Thread t1 = new Thread(() -&gt; { a = 1; x = b; });</span></div></div><div><div><div>3</div></div><div><span>Thread t2 = new Thread(() -&gt; { b = 1; y = a; });</span></div></div><div><div><div>4</div></div><div><span>// 理论上x、y都为1，但由于指令重排，有可能x=0或y=0</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>
<section><h2>相关阅读<a href="#相关阅读"><span>#</span></a></h2><a href="/posts/java-thread-pool-guide/"><div><div>Java 线程池实战</div><div>总体设计 ThreadPoolExecutor实现的顶层接口是Executor，顶层接口Executor提供了一种思想：将任务提交和任务执行进行解耦。用户无需关注如何创建线程，如何调度线程来执行任务，用户只需提供Runnable对象，将任务</div><div><span>2025-06-23</span><span>原创</span><span><span>#Java</span></span></div></div><div><img src="https://s.muxui.com:8015/images/2026/09/8e8t7ONN0FKz.webp" alt="" loading="lazy" /></div></a></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/mihomo-personal-config/</id>
      <title type="text">Mihomo (原Clash) 自用配置</title>
      <published>2025-05-20T00:00:00.000Z</published>
      <updated>2025-05-20T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/mihomo-personal-config/"/>
      <summary type="text">DNS防泄漏查询 查询地址: $1 $1 其实吧,我感觉DNS泄露这东西,咋个说呢,感觉并没啥大用 仁者见仁吧 DNS解析流程 节点配置 全局配置 mixed port: 7890 HTTP S + SOCKS 混合代理端口，Clash 主</summary>
      <content type="html"><![CDATA[<section><h2>DNS防泄漏查询<a href="#dns防泄漏查询"><span>#</span></a></h2><p><img src="https://s.muxui.com:8015/images/2026/09/hcN1EuB8HLHA.webp" alt="" /></p><p><img src="https://s.muxui.com:8015/images/2026/09/hSDJOg8jD6GY.webp" alt="" /></p><p>查询地址:</p><ul>
<li><a href="https://ipcheck.ing/#/" target="_blank">https://ipcheck.ing/#/</a></li>
<li><a href="https://browserleaks.com/dns" target="_blank">https://browserleaks.com/dns</a></li>
</ul><blockquote><p>其实吧,我感觉DNS泄露这东西,咋个说呢,感觉并没啥大用!仁者见仁吧</p></blockquote></section>
<section><h2><strong>DNS解析流程</strong><a href="#dns解析流程"><span>#</span></a></h2><p><img src="https://s.muxui.com:8015/images/2026/09/KxFeag0fVhnP.webp" alt="" /></p></section>
<section><h2>节点配置<a href="#节点配置"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>### 全局配置</span></div></div><div><div><div>2</div></div><div><span>mixed-port: 7890 # HTTP(S) + SOCKS 混合代理端口，Clash 主代理入口</span></div></div><div><div><div>3</div></div><div><span>allow-lan: true # 允许局域网设备连接本机代理</span></div></div><div><div><div>4</div></div><div><span>mode: rule # 代理模式，使用规则分流</span></div></div><div><div><div>5</div></div><div><span>bind-address: "*" # 绑定所有网卡地址，使局域网内其他设备可访问</span></div></div><div><div><div>6</div></div><div><span>ipv6: false # 禁用 IPv6，防止 DNS 泄露及访问异常</span></div></div><div><div><div>7</div></div><div><span>log-level: debug # 日志等级，调试时推荐设置为 debug</span></div></div><div><div><div>8</div></div><div><span>profile:</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>store-selected: true # 记住上次选择的节点或策略</span></div></div><div><div><div>10</div></div><div><span><span>  </span></span><span>store-fake-ip: true # 记住 fake-ip 映射关系，提升命中率</span></div></div><div><div><div>11</div></div><div><span>unified-delay: true # 启用统一延迟计算</span></div></div><div><div><div>12</div></div><div><span>tcp-concurrent: true # TCP 并发连接，提升连接速度</span></div></div><div><div><div>13</div></div><div><span>global-client-fingerprint: chrome # 全局默认浏览器指纹，用于模拟真实浏览器行为</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span>### DNS 配置</span></div></div><div><div><div>16</div></div><div><span>dns:</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>enable: true # 启用内置 DNS 服务器</span></div></div><div><div><div>18</div></div><div><span><span>  </span></span><span>prefer-h3: true # 优先使用 HTTP/3（QUIC）协议解析 DNS 请求</span></div></div><div><div><div>19</div></div><div><span><span>  </span></span><span>ipv6: false # 不解析 AAAA 记录，避免走 IPv6</span></div></div><div><div><div>20</div></div><div><span><span>  </span></span><span>enhanced-mode: fake-ip # 使用 fake-ip 模式，防止 DNS 泄露</span></div></div><div><div><div>21</div></div><div><span><span>  </span></span><span>fake-ip-range: 198.18.0.1/16 # 用于 fake-ip 映射的 IP 段（保留地址段）</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>fake-ip-filter: # fake-ip 排除列表，这些域名将使用真实 IP 解析，防止功能异常</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span># 本地主机/设备</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>- +.lan</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>- +.local</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span># Windows网络出现小地球图标</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>- +.msftconnecttest.com</span></div></div><div><div><div>28</div></div><div><span><span>    </span></span><span>- +.msftncsi.com</span></div></div><div><div><div>29</div></div><div><span><span>    </span></span><span># QQ快速登录检测失败</span></div></div><div><div><div>30</div></div><div><span><span>    </span></span><span>- localhost.ptlogin2.qq.com</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>- localhost.sec.qq.com</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span># 追加以下条目</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>- +.in-addr.arpa</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>- +.ip6.arpa</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>- time.*.com</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>- time.*.gov</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>- pool.ntp.org</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span># 微信快速登录检测失败</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>- localhost.work.weixin.qq.com</span></div></div><div><div><div>40</div></div><div><span><span>  </span></span><span>respect-rules: true # DNS 查询也按 Clash 规则处理</span></div></div><div><div><div>41</div></div><div><span><span>  </span></span><span>nameserver: # 直连使用的 DNS 服务器（DoH）</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>- https://223.5.5.5/dns-query # 阿里公共 DNS</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>- https://doh.pub/dns-query # 腾讯公共 DNS</span></div></div><div><div><div>44</div></div><div><span><span>  </span></span><span>proxy-server-nameserver: # 被规则判断为走代理的域名使用此处 DNS</span></div></div><div><div><div>45</div></div><div><span><span>    </span></span><span>- https://cloudflare-dns.com/dns-query # CloudflareDNS</span></div></div><div><div><div>46</div></div><div><span><span>    </span></span><span>- https://77.88.8.8/dns-query # YandexDNS</span></div></div><div><div><div>47</div></div><div><span><span>    </span></span><span>- https://8.8.4.4/dns-query#ecs=1.1.1.1/24&amp;ecs-override=true  #GoogleDNS</span></div></div><div><div><div>48</div></div><div><span><span>    </span></span><span>- https://208.67.222.222/dns-query#ecs=1.1.1.1/24&amp;ecs-override=true  #OpenDNS</span></div></div><div><div><div>49</div></div><div><span><span>    </span></span><span>- https://9.9.9.9/dns-query  #Quad9DNS</span></div></div><div><div><div>50</div></div><div>
</div></div><div><div><div>51</div></div><div><span>## 这里替换你的自建节点</span></div></div><div><div><div>52</div></div><div><span>proxies:</span></div></div><div><div><div>53</div></div><div><span><span>  </span></span><span>- { name: "🇯🇵日本claw"}</span></div></div><div><div><div>54</div></div><div><span><span>  </span></span><span>- { name: "🇺🇸美国dmit"}</span></div></div><div><div><div>55</div></div><div>
</div></div><div><div><div>56</div></div><div>
</div></div><div><div><div>57</div></div><div><span>### 出站策略锚点（用于复用）</span></div></div><div><div><div>58</div></div><div><span>pr: &amp;pr { type: select, proxies: [ 节点选择, 自动选择, 🇯🇵日本claw, 🇺🇸美国dmit, 全局直连 ] }</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span>### 代理组定义</span></div></div><div><div><div>61</div></div><div><span>proxy-groups:</span></div></div><div><div><div>62</div></div><div><span><span>  </span></span><span>- { name: 节点选择, type: select, proxies: [ 自动选择, 🇯🇵日本claw, 🇺🇸美国dmit ] ,icon: https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/adjust.svg }</span></div></div><div><div><div>63</div></div><div><span><span>  </span></span><span>- { name: 自动选择, type: url-test, include-all: true, tolerance: 20, interval: 300, filter: "^((?!(全局直连)).)*$" ,icon: https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/speed.svg }</span></div></div><div><div><div>64</div></div><div><span><span>  </span></span><span>- { name: Google, &lt;&lt;: *pr ,icon: https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/google.svg }</span></div></div><div><div><div>65</div></div><div><span><span>  </span></span><span>- { name: Stream, &lt;&lt;: *pr , icon: https://cdn.jsdelivr.net/gh/O96u/O96u@refs/heads/master/image/stream.svg }</span></div></div><div><div><div>66</div></div><div><span><span>  </span></span><span>- { name: ChatGPT, &lt;&lt;: *pr , icon: https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/chatgpt.svg }</span></div></div><div><div><div>67</div></div><div><span><span>  </span></span><span>- { name: Microsoft, &lt;&lt;: *pr , icon: https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/microsoft.svg }</span></div></div><div><div><div>68</div></div><div><span><span>  </span></span><span>- { name: Telegram, &lt;&lt;: *pr ,icon: https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/telegram.svg }</span></div></div><div><div><div>69</div></div><div><span><span>  </span></span><span>- { name: Apple, &lt;&lt;: *pr , icon: https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/apple.svg }</span></div></div><div><div><div>70</div></div><div><span><span>  </span></span><span>- { name: 漏网之鱼, &lt;&lt;: *pr , icon: https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/fish.svg }</span></div></div><div><div><div>71</div></div><div><span><span>  </span></span><span>- { name: 全局直连, type: select, proxies: [ DIRECT ], icon: https://fastly.jsdelivr.net/gh/clash-verge-rev/clash-verge-rev.github.io@main/docs/assets/icons/link.svg }</span></div></div><div><div><div>72</div></div><div>
</div></div><div><div><div>73</div></div><div><span># 规则配置</span></div></div><div><div><div>74</div></div><div><span>RuleSet_classical: &amp;RuleSet_classical { type: http, behavior: classical, interval: 86400, format: text}</span></div></div><div><div><div>75</div></div><div><span>RuleSet_domain: &amp;RuleSet_domain { type: http, behavior: domain, interval: 86400, format: text}</span></div></div><div><div><div>76</div></div><div><span>RuleSet_ipcidr: &amp;RuleSet_ipcidr { type: http, behavior: ipcidr, interval: 86400, format: text}</span></div></div><div><div><div>77</div></div><div>
</div></div><div><div><div>78</div></div><div><span># 订阅规则</span></div></div><div><div><div>79</div></div><div><span>rule-providers:</span></div></div><div><div><div>80</div></div><div><span><span>  </span></span><span>reject_non_ip_no_drop:</span></div></div><div><div><div>81</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>82</div></div><div><span><span>    </span></span><span>url: 'https://ruleset.skk.moe/Clash/non_ip/reject-no-drop.txt'</span></div></div><div><div><div>83</div></div><div><span><span>    </span></span><span>path: './rule_set/sukkaw_ruleset/reject_non_ip_no_drop.txt'</span></div></div><div><div><div>84</div></div><div>
</div></div><div><div><div>85</div></div><div><span><span>  </span></span><span>reject_non_ip_drop:</span></div></div><div><div><div>86</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>87</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/reject-drop.txt</span></div></div><div><div><div>88</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/reject_non_ip_drop.txt</span></div></div><div><div><div>89</div></div><div>
</div></div><div><div><div>90</div></div><div><span><span>  </span></span><span>reject_non_ip:</span></div></div><div><div><div>91</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>92</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/reject.txt</span></div></div><div><div><div>93</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/reject_non_ip.txt</span></div></div><div><div><div>94</div></div><div>
</div></div><div><div><div>95</div></div><div><span><span>  </span></span><span>reject_domainset:</span></div></div><div><div><div>96</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_domain</span></div></div><div><div><div>97</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/domainset/reject.txt</span></div></div><div><div><div>98</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/reject_domainset.txt</span></div></div><div><div><div>99</div></div><div>
</div></div><div><div><div>100</div></div><div><span><span>  </span></span><span>reject_ip:</span></div></div><div><div><div>101</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>102</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/ip/reject.txt</span></div></div><div><div><div>103</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/reject_ip.txt</span></div></div><div><div><div>104</div></div><div>
</div></div><div><div><div>105</div></div><div><span><span>  </span></span><span>cdn_domainset:</span></div></div><div><div><div>106</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_domain</span></div></div><div><div><div>107</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/domainset/cdn.txt</span></div></div><div><div><div>108</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/cdn_domainset.txt</span></div></div><div><div><div>109</div></div><div>
</div></div><div><div><div>110</div></div><div><span><span>  </span></span><span>cdn_non_ip:</span></div></div><div><div><div>111</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_domain</span></div></div><div><div><div>112</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/cdn.txt</span></div></div><div><div><div>113</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/cdn_non_ip.txt</span></div></div><div><div><div>114</div></div><div>
</div></div><div><div><div>115</div></div><div><span><span>  </span></span><span># 所有流媒体（包括上述所有流媒体）</span></div></div><div><div><div>116</div></div><div><span><span>  </span></span><span>stream_non_ip:</span></div></div><div><div><div>117</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>118</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/stream.txt</span></div></div><div><div><div>119</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/stream_non_ip.txt</span></div></div><div><div><div>120</div></div><div>
</div></div><div><div><div>121</div></div><div><span><span>  </span></span><span>stream_ip:</span></div></div><div><div><div>122</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>123</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/ip/stream.txt</span></div></div><div><div><div>124</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/stream_ip.txt</span></div></div><div><div><div>125</div></div><div>
</div></div><div><div><div>126</div></div><div><span><span>  </span></span><span>ai_non_ip:</span></div></div><div><div><div>127</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>128</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/ai.txt</span></div></div><div><div><div>129</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/ai_non_ip.txt</span></div></div><div><div><div>130</div></div><div>
</div></div><div><div><div>131</div></div><div><span><span>  </span></span><span>telegram_non_ip:</span></div></div><div><div><div>132</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>133</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/telegram.txt</span></div></div><div><div><div>134</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/telegram_non_ip.txt</span></div></div><div><div><div>135</div></div><div>
</div></div><div><div><div>136</div></div><div><span><span>  </span></span><span>telegram_ip:</span></div></div><div><div><div>137</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>138</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/ip/telegram.txt</span></div></div><div><div><div>139</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/telegram_ip.txt</span></div></div><div><div><div>140</div></div><div>
</div></div><div><div><div>141</div></div><div><span><span>  </span></span><span>apple_cdn:</span></div></div><div><div><div>142</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_domain</span></div></div><div><div><div>143</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/domainset/apple_cdn.txt</span></div></div><div><div><div>144</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/apple_cdn.txt</span></div></div><div><div><div>145</div></div><div>
</div></div><div><div><div>146</div></div><div><span><span>  </span></span><span>apple_services:</span></div></div><div><div><div>147</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>148</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/apple_services.txt</span></div></div><div><div><div>149</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/apple_services.txt</span></div></div><div><div><div>150</div></div><div>
</div></div><div><div><div>151</div></div><div><span><span>  </span></span><span>apple_cn_non_ip:</span></div></div><div><div><div>152</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>153</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/apple_cn.txt</span></div></div><div><div><div>154</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/apple_cn_non_ip.txt</span></div></div><div><div><div>155</div></div><div>
</div></div><div><div><div>156</div></div><div><span><span>  </span></span><span>microsoft_cdn_non_ip:</span></div></div><div><div><div>157</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>158</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/microsoft_cdn.txt</span></div></div><div><div><div>159</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/microsoft_cdn_non_ip.txt</span></div></div><div><div><div>160</div></div><div>
</div></div><div><div><div>161</div></div><div><span><span>  </span></span><span>microsoft_non_ip:</span></div></div><div><div><div>162</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>163</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/microsoft.txt</span></div></div><div><div><div>164</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/microsoft_non_ip.txt</span></div></div><div><div><div>165</div></div><div>
</div></div><div><div><div>166</div></div><div><span><span>  </span></span><span># 软件更新、操作系统等大文件下载</span></div></div><div><div><div>167</div></div><div><span><span>  </span></span><span>download_domainset:</span></div></div><div><div><div>168</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_domain</span></div></div><div><div><div>169</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/domainset/download.txt</span></div></div><div><div><div>170</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/download_domainset.txt</span></div></div><div><div><div>171</div></div><div>
</div></div><div><div><div>172</div></div><div><span><span>  </span></span><span>download_non_ip:</span></div></div><div><div><div>173</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_domain</span></div></div><div><div><div>174</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/download.txt</span></div></div><div><div><div>175</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/download_non_ip.txt</span></div></div><div><div><div>176</div></div><div>
</div></div><div><div><div>177</div></div><div><span><span>  </span></span><span># 内网域名和局域网 IP</span></div></div><div><div><div>178</div></div><div><span><span>  </span></span><span>lan_non_ip:</span></div></div><div><div><div>179</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>180</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/lan.txt</span></div></div><div><div><div>181</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/lan_non_ip.txt</span></div></div><div><div><div>182</div></div><div>
</div></div><div><div><div>183</div></div><div><span><span>  </span></span><span>lan_ip:</span></div></div><div><div><div>184</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>185</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/ip/lan.txt</span></div></div><div><div><div>186</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/lan_ip.txt</span></div></div><div><div><div>187</div></div><div>
</div></div><div><div><div>188</div></div><div><span><span>  </span></span><span>domestic_non_ip:</span></div></div><div><div><div>189</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>190</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/domestic.txt</span></div></div><div><div><div>191</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/domestic_non_ip.txt</span></div></div><div><div><div>192</div></div><div>
</div></div><div><div><div>193</div></div><div><span><span>  </span></span><span>direct_non_ip:</span></div></div><div><div><div>194</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>195</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/direct.txt</span></div></div><div><div><div>196</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/direct_non_ip.txt</span></div></div><div><div><div>197</div></div><div>
</div></div><div><div><div>198</div></div><div><span><span>  </span></span><span>global_non_ip:</span></div></div><div><div><div>199</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>200</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/non_ip/global.txt</span></div></div><div><div><div>201</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/global_non_ip.txt</span></div></div><div><div><div>202</div></div><div>
</div></div><div><div><div>203</div></div><div><span><span>  </span></span><span>domestic_ip:</span></div></div><div><div><div>204</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_classical</span></div></div><div><div><div>205</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/ip/domestic.txt</span></div></div><div><div><div>206</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/domestic_ip.txt</span></div></div><div><div><div>207</div></div><div>
</div></div><div><div><div>208</div></div><div><span><span>  </span></span><span>china_ip:</span></div></div><div><div><div>209</div></div><div><span><span>    </span></span><span>&lt;&lt;: *RuleSet_ipcidr</span></div></div><div><div><div>210</div></div><div><span><span>    </span></span><span>url: https://ruleset.skk.moe/Clash/ip/china_ip.txt</span></div></div><div><div><div>211</div></div><div><span><span>    </span></span><span>path: ./rule_set/sukkaw_ruleset/china_ip.txt</span></div></div><div><div><div>212</div></div><div>
</div></div><div><div><div>213</div></div><div><span># 分流规则</span></div></div><div><div><div>214</div></div><div><span>rules:</span></div></div><div><div><div>215</div></div><div><span><span>  </span></span><span>### 自定义类规则</span></div></div><div><div><div>216</div></div><div><span><span>  </span></span><span>- DOMAIN-SUFFIX,infini.money,🇯🇵日本claw</span></div></div><div><div><div>217</div></div><div><span><span>  </span></span><span>- DOMAIN-SUFFIX,piggo.me,DIRECT</span></div></div><div><div><div>218</div></div><div><span><span>  </span></span><span>### 非 IP 类规则</span></div></div><div><div><div>219</div></div><div><span><span>  </span></span><span>- RULE-SET,reject_non_ip,REJECT</span></div></div><div><div><div>220</div></div><div><span><span>  </span></span><span>- RULE-SET,reject_domainset,REJECT</span></div></div><div><div><div>221</div></div><div><span><span>  </span></span><span>- RULE-SET,reject_non_ip_drop,REJECT-DROP</span></div></div><div><div><div>222</div></div><div><span><span>  </span></span><span>- RULE-SET,reject_non_ip_no_drop,REJECT</span></div></div><div><div><div>223</div></div><div><span><span>  </span></span><span>- RULE-SET,cdn_domainset,节点选择</span></div></div><div><div><div>224</div></div><div><span><span>  </span></span><span>- RULE-SET,cdn_non_ip,节点选择</span></div></div><div><div><div>225</div></div><div><span><span>  </span></span><span>- RULE-SET,stream_non_ip,Stream</span></div></div><div><div><div>226</div></div><div><span><span>  </span></span><span>- RULE-SET,telegram_non_ip,Telegram</span></div></div><div><div><div>227</div></div><div><span><span>  </span></span><span>- RULE-SET,download_domainset,节点选择</span></div></div><div><div><div>228</div></div><div><span><span>  </span></span><span>- RULE-SET,download_non_ip,节点选择</span></div></div><div><div><div>229</div></div><div><span><span>  </span></span><span>- RULE-SET,apple_cdn,Apple</span></div></div><div><div><div>230</div></div><div><span><span>  </span></span><span>- RULE-SET,apple_cn_non_ip,Apple</span></div></div><div><div><div>231</div></div><div><span><span>  </span></span><span>- RULE-SET,apple_services,Apple</span></div></div><div><div><div>232</div></div><div><span><span>  </span></span><span>- RULE-SET,microsoft_cdn_non_ip,Microsoft</span></div></div><div><div><div>233</div></div><div><span><span>  </span></span><span>- RULE-SET,microsoft_non_ip,Microsoft</span></div></div><div><div><div>234</div></div><div><span><span>  </span></span><span>- RULE-SET,ai_non_ip,ChatGPT</span></div></div><div><div><div>235</div></div><div><span><span>  </span></span><span>- RULE-SET,global_non_ip,节点选择</span></div></div><div><div><div>236</div></div><div><span><span>  </span></span><span>- RULE-SET,domestic_non_ip,全局直连</span></div></div><div><div><div>237</div></div><div><span><span>  </span></span><span>- RULE-SET,direct_non_ip,全局直连</span></div></div><div><div><div>238</div></div><div><span><span>  </span></span><span>- RULE-SET,lan_non_ip,全局直连</span></div></div><div><div><div>239</div></div><div>
</div></div><div><div><div>240</div></div><div><span><span>  </span></span><span>### IP 类规则</span></div></div><div><div><div>241</div></div><div><span><span>  </span></span><span>- RULE-SET,reject_ip,REJECT</span></div></div><div><div><div>242</div></div><div><span><span>  </span></span><span>- RULE-SET,telegram_ip,Telegram</span></div></div><div><div><div>243</div></div><div><span><span>  </span></span><span>- RULE-SET,stream_ip,Stream</span></div></div><div><div><div>244</div></div><div><span><span>  </span></span><span>- RULE-SET,lan_ip,全局直连</span></div></div><div><div><div>245</div></div><div><span><span>  </span></span><span>- RULE-SET,domestic_ip,全局直连</span></div></div><div><div><div>246</div></div><div><span><span>  </span></span><span>- RULE-SET,china_ip,全局直连</span></div></div><div><div><div>247</div></div><div><span><span>  </span></span><span>- GEOIP,CN,全局直连</span></div></div><div><div><div>248</div></div><div><span><span>  </span></span><span>- MATCH,漏网之鱼</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><img src="https://s.muxui.com:8015/images/2026/09/xWe5pCOyuvTJ.webp" alt="" /></p></section>
<section><h2>软件下载(Win/Mac/Linux通用)<a href="#软件下载winmaclinux通用"><span>#</span></a></h2><a href="https://github.com/mihomo-party-org/mihomo-party" target="_blank"><div><div><div><div></div><div>mihomo-party-org</div></div><div>/</div><div>mihomo-party</div></div><div></div></div><div> Another Mihomo GUI. </div><div><div>—</div><div>—</div><div>GPL-3.0</div><span>TypeScript</span></div></a><a href="https://github.com/clash-verge-rev/clash-verge-rev" target="_blank"><div><div><div><div></div><div>clash-verge-rev</div></div><div>/</div><div>clash-verge-rev</div></div><div></div></div><div>A modern GUI client based on Tauri, designed to run in Windows, macOS and Linux for tailored proxy experience</div><div><div>—</div><div>—</div><div>GPL-3.0</div><span>Rust</span></div></a></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/windows-update-blocker/</id>
      <title type="text">Windows 更新阻止程序 v1.8</title>
      <published>2025-05-13T00:00:00.000Z</published>
      <updated>2025-05-13T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/windows-update-blocker/"/>
      <summary type="text">与以前版本的 Windows 相比，Windows 11 和 10 对操作系统更新行为的 UI 控制较少，没有使用控制面板或设置关闭 Windows 更新的选项Windows 11 和 Windows 10 中的应用程序会自动检查更新并安装</summary>
      <content type="html"><![CDATA[<p><img src="https://s.muxui.com:8015/images/2026/09/wR1CAtK37UP6.webp" alt="" /></p>
<p>与以前版本的 Windows 相比，Windows 11 和 10 对操作系统更新行为的 UI 控制较少，没有使用控制面板或设置关闭 Windows 更新的选项Windows 11 和 Windows 10 中的应用程序会自动检查更新并安装任何找到的更新，无论您是否喜欢。这项新功能实际上对大多数用户来说非常方便。如果您想控制更新过程，可以禁用 Windows 更新服务。这需要您打开服务管理器，找到该服务并更改其启动参数和状态。之后，每当您希望操作系统检查更新时，您都必须再次启用该服务（这看起来并不那么容易）。</p>
<p></p><figure><img src="https://www.sordum.org/wp-content/uploads/2023/06/windos_update_blocker_main.png" alt="Windows 更新阻止程序主要" title="Windows 更新阻止程序主要" /><figcaption>Windows 更新阻止程序主要</figcaption></figure><p></p>
<p>Windows Update Blocker 是一款免费软件，只需单击按钮即可帮助您彻底禁用或启用 Windows 系统上的自动更新。此实用程序是便携式的，这意味着您无需再经历安装过程，并且卸载后硬盘上不会留下任何残留物。如果您将程序文件放置到外部数据设备中，就可以随身携带 Windows Update Blocker，并在您接触的任何计算机上使用它，只要您的计算机配置正确，并且不想冒险让更新搞乱系统。此实用程序就是为这样的人准备的。</p>
<p><strong>工作原理</strong></p>
<p>下载后，将 Windows Update Blocker 解压到所需位置，然后运行它</p>
<p></p><figure><img src="https://www.sordum.org/wp-content/uploads/2023/06/windos_update_blocker_blocked_status.png" alt="windows 更新阻止程序被阻止" title="windows 更新阻止程序被阻止" /><figcaption>windows 更新阻止程序被阻止</figcaption></figure><p></p>
<p>服务状态图标将显示您的 Windows 更新服务状态，如果您想阻止自动 Windows 更新，请选择“禁用服务”并单击“立即应用”按钮，保护服务设置可以防止不受控制的更改（推荐）。</p>
<p><strong>如何阻止其他服务并保护设置</strong></p>
<p>Windows 更新阻止程序新版本允许用户将任何其他服务添加到 Wub.ini 文件中以阻止它，首先请编辑 Wub.ini 文件并在“dosvc=2,4”行下添加所需的服务名称，例如，如果您想阻止“Windows 更新医疗服务”，请打开服务并找到它</p>
<p></p><figure><img src="https://www.sordum.org/wp-content/uploads/2023/06/services.png" alt="Windows 服务" title="Windows 服务" /><figcaption>Windows 服务</figcaption></figure><p></p>
<p>然后单击它并复制服务名称</p>
<p></p><figure><img src="https://www.sordum.org/wp-content/uploads/2023/06/windows_update_medic_service.png" alt="Windows 更新医疗服务" title="Windows 更新医疗服务" /><figcaption>Windows 更新医疗服务</figcaption></figure><p></p>
<p>在 dosvc=2,4 行下写入或粘贴服务名称</p>
<p></p><figure><img src="https://www.sordum.org/wp-content/uploads/2023/06/wub_ini_file.png" alt="wub.ini 文件" title="wub.ini 文件" /><figcaption>wub.ini 文件</figcaption></figure><p></p>
<p>最后打开 Windows 更新阻止程序，然后点击“立即应用”按钮。如果您想在一段时间后删除最近添加的服务，请按照以下步骤操作：</p>
<p>1. 打开 Windows Update Blocker，选择“启用服务”选项，然后点击“立即应用”按钮。</p>
<p>2. 从 Wub.ini 文件中删除所需的服务行。</p>
<p>3. 现在，您可以选择“禁用服务”和“保护服务设置”选项。</p>
<p><strong>服务列表选项</strong></p>
<p>在“菜单”按钮下有一个“服务列表选项”选项，点击后会弹出“服务列表选项”窗口。通过此图形用户界面，您可以轻松控制已添加到 Wub.ini 部分的服务。您最多可以向 Wub.ini 添加 25 个服务。Wausrv 服务仅供参考，因此您无法对其进行编辑。</p>
<p></p><figure><img src="https://www.sordum.org/wp-content/uploads/2023/06/wub_service_list_options.png" alt="Wub 服务列表选项" title="Wub 服务列表选项" /><figcaption>Wub 服务列表选项</figcaption></figure><p></p>
<p><strong>注意：<strong>如果 Windows 更新服务无法启动，或者即使您已启用 Windows 更新，仍然出现问题，请</strong>重启系统</strong>并重试。<strong>关闭</strong>Windows 更新后，某些 Windows 进程或程序可能会尝试打开并修复 Windows 更新，**从而导致 Windows 更新损坏。**如果重启系统后更新仍未打开，<a href="https://support.microsoft.com/en-us/windows/windows-update-troubleshooter-19bc41ca-ad72-ae67-af3c-89ce169755dd" target="_blank">请使用Microsoft</a>提供的 Windows 更新疑难解答</p>
<p>Windows Update Blocker 支持命令行选项来自动执行 Windows Update Blocker 任务：</p>
<p></p><figure><img src="https://www.sordum.org/wp-content/uploads/2023/06/wub_cmd_parameters.png" alt="Windows 更新阻止程序 Cmd 参数" title="Windows 更新阻止程序 Cmd 参数" /><figcaption>Windows 更新阻止程序 Cmd 参数</figcaption></figure><p></p>
<p><strong>注意：</strong> Wub 软件不会阻止 Microsoft Defender 更新。</p>
<p><strong>支持的操作系统：</strong> Windows 11、Windows 10、Windows 8.1、Windows 8、Windows 7、Windows Vista、Windows XP（32\64 位）。</p>
<p>**支持的语言：**英语、土耳其语、德语、希伯来语、斯洛文尼亚语、法语、俄语、葡萄牙语、意大利语、波兰语、西班牙语、捷克语、乌克兰语、希腊语、匈牙利语、简体中文、繁体中文、葡萄牙语（巴西）、泰语、阿拉伯语、韩语、越南语、日语、印尼语、波斯语、瑞典语、泰语、格鲁吉亚语、丹麦语（丹麦语）、阿尔巴尼亚语、阿塞拜疆语、保加利亚语、塞尔维亚语、乌兹别克语、塔吉克语、阿布哈兹语、罗马尼亚语、斯洛伐克语、僧伽罗语、马其顿语、克罗地亚语、立陶宛语、土库曼语、荷兰语（荷兰）、荷兰语（迪迪埃）</p>
<p>文件：Wub_x64.exe │ <a href="https://www.virustotal.com/gui/file/187eeee9e518011de1b87cfb0ed03e12ea551e9011f0c8defdd0e4535e672da2/detection" target="_blank">Virustotal</a><br />
MD5：9d6778f7f274f7ecd4e7e875a7268b64<br />
SHA1：452fa439f1cc0b9fcc37cf4b8cfff96e8cc348aa</p>
<p>文件：Wub.exe │ <a href="https://www.virustotal.com/gui/file/aac1123f17f8569a36bf93876cea30e15103fd2379b401a79129a2a6e7285ac2/detection" target="_blank">Virustotal</a><br />
MD5：82aff8883099cf75462057c4e47e88ac<br />
SHA1：68e2939f59b3869e9bd3ecc4aca3947649631bf8</p>
<p>原文链接: <a href="https://www.sordum.org/9470/windows-update-blocker-v1-8/" target="_blank">https://www.sordum.org</a></p>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/windows-permanent-activation-guide/</id>
      <title type="text">[自用] win永久激活,非工具</title>
      <published>2025-05-12T00:00:00.000Z</published>
      <updated>2025-05-12T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/windows-permanent-activation-guide/"/>
      <summary type="text">近日，微软终于发布公告，正式宣布关闭 Windows 免费升级通道，并对服务器进行了限制，阻止此前常用的激活工具发起请求。这意味着，那些依赖“旧方法”进行激活的用户将不再能够绕过验证。 massgrave.dev 推出新激活方案⚙️🆕 虽</summary>
      <content type="html"><![CDATA[<p>近日，微软终于发布公告，正式宣布关闭 Windows 免费升级通道，并对服务器进行了限制，阻止此前常用的激活工具发起请求。这意味着，那些依赖“旧方法”进行激活的用户将不再能够绕过验证。</p>
<section><h2>massgrave.dev 推出新激活方案⚙️🆕<a href="#massgravedev-推出新激活方案️"><span>#</span></a></h2><p>虽然官方路径被封堵，但来自 massgrave.dev 团队的开发者们迅速研究出了一种全新的激活方法，并且经过多位用户实测（包括笔者在虚拟机中的 Windows 11 专业工作站版），<strong>激活成功！</strong></p><p>该方法无需繁琐操作，只需下载并运行一个脚本，即可获得<strong>永久的数字许可证激活</strong>。不仅适用于 Windows，连 Office 也能轻松搞定。</p><hr /></section>
<section><h2>使用方法 📝🔧<a href="#使用方法-"><span>#</span></a></h2><ol>
<li><strong>右键点击「开始菜单」 → 选择「PowerShell」或「终端」</strong>（<strong>不是 CMD</strong>）</li>
<li>复制并粘贴下方命令，按下 <code>Enter</code>：</li>
<li><code>`irm https://massgrave.dev/get | iex`</code></li>
<li>若提示需要管理员权限，请点击「允许」。</li>
<li>屏幕会出现激活选项，按数字键操作即可：
<ul>
<li><code>1</code>：激活 Windows</li>
<li><code>2</code> → <code>1</code>：激活 Office</li>
</ul>
</li>
</ol><hr /></section>
<section><h2>功能一览 📦✨<a href="#功能一览-"><span>#</span></a></h2><ul>
<li>✅ <strong>HWID（数字许可证）激活法</strong> – 永久激活 Windows 系统</li>
<li>✅ <strong>Ohook 激活法</strong> – 永久激活 Office 软件</li>
<li>✅ <strong>KMS38 激活法</strong> – 激活 Windows/Server 至 2038 年</li>
<li>✅ <strong>Online KMS 激活法</strong> – 有效期 180 天，支持自动续订</li>
<li>✅ <strong>高级激活故障排查工具</strong></li>
<li>✅ <strong>支持预激活的 <span><span>OEMOEM</span><span><span><span></span><span>O</span><span>E</span><span>M</span></span></span></span> 文件夹</strong></li>
<li>✅ <strong>Windows 版本切换工具</strong></li>
<li>✅ <strong>激活状态检查工具</strong></li>
<li>✅ <strong>支持合一文件和单文件版本</strong></li>
<li>✅ <strong>完全开源，基于 Batch 脚本</strong></li>
<li>✅ <strong>更少的杀毒软件报毒率</strong></li>
</ul><p>🔗 使用文档与脚本主页：<a href="http://massgrave.dev/" target="_blank">massgrave.dev</a></p></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/python-lottery-qinglong-notification-script/</id>
      <title type="text">用 Python 写个双色球青龙通知脚本，再也不用下那些流氓 App</title>
      <published>2025-04-30T00:00:00.000Z</published>
      <updated>2025-04-30T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/python-lottery-qinglong-notification-script/"/>
      <summary type="text">在生活中我们常常遇到这种情况： 只想简单看看双色球开奖号码，却被强制要求下载各种 App 📲 。 这些 App 不是广告满天飞 💥，就是界面臃肿卡顿 🐢，甚至还可能偷偷获取隐私信息 😒。 于是我决定： 自己动手，用 Python 写</summary>
      <content type="html"><![CDATA[<p>在生活中我们常常遇到这种情况：<strong>只想简单看看双色球开奖号码，却被强制要求下载各种 App 📲</strong>。<br />
这些 App 不是广告满天飞 💥，就是界面臃肿卡顿 🐢，甚至还可能偷偷获取隐私信息 😒。</p>
<p>于是我决定：<strong>自己动手，用 Python 写一个脚本，自动获取开奖号码并通过青龙面板发送通知 📬</strong>！<br />
干净、快速、自由，想看就看，拒绝被绑架。</p>
<hr />
<section><h2>🎯 开发目标<a href="#-开发目标"><span>#</span></a></h2><p>✅ 自动获取每日双色球开奖信息<br />
✅ 通过青龙定时任务推送通知（支持 Bark / 钉钉 / Telegram 等）<br />
✅ 无需手动操作，全自动运行</p><hr /></section>
<section><h2>⚙️ 技术选型<a href="#️-技术选型"><span>#</span></a></h2><p>📝 <strong>Python 3</strong>：脚本首选语言，灵活高效<br />
📡 <strong>requests</strong>：拉取开奖信息<br />
🧹 <strong>json &amp; re</strong>：处理数据内容<br />
🧭 <strong>青龙面板</strong>：定时执行 &amp; 多渠道通知</p><hr /></section>
<section><h2>🔗 开奖数据来源<a href="#-开奖数据来源"><span>#</span></a></h2><p>为了确保数据准确可靠，我选用了<strong>中彩网</strong>或其他公开接口作为数据源 🧮。</p><p>示例接口返回内容如下：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>{</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>"issue": "2025047",</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>"openTime": "2025-04-29",</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>"frontWinningNum": "01 14 21 22 23 31",</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>"backWinningNum": "12",</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>"seqFrontWinningNum": "14 22 31 23 01 21",</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>"seqBackWinningNum": "12",</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>"saleMoney": "364051920",</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>"r9SaleMoney": "",</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>"prizePoolMoney": "2211042881",</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>"week": "星期二",</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>"winnerDetails": [</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>"awardEtc": "1",</span></div></div><div><div><div>15</div></div><div><span><span>            </span></span><span>"baseBetWinner": {</span></div></div><div><div><div>16</div></div><div><span><span>                </span></span><span>"remark": "一等奖",</span></div></div><div><div><div>17</div></div><div><span><span>                </span></span><span>"awardNum": "4",</span></div></div><div><div><div>18</div></div><div><span><span>                </span></span><span>"awardMoney": "10000000",</span></div></div><div><div><div>19</div></div><div><span><span>                </span></span><span>"totalMoney": ""</span></div></div><div><div><div>20</div></div><div><span><span>            </span></span><span>},</span></div></div><div><div><div>21</div></div><div><span><span>            </span></span><span>"addToBetWinner": "",</span></div></div><div><div><div>22</div></div><div><span><span>            </span></span><span>"addToBetWinner2": "",</span></div></div><div><div><div>23</div></div><div><span><span>            </span></span><span>"addToBetWinner3": ""</span></div></div><div><div><div>24</div></div><div><span><span>        </span></span><span>},</span></div></div><div><div><div>25</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>26</div></div><div><span><span>            </span></span><span>"awardEtc": "2",</span></div></div><div><div><div>27</div></div><div><span><span>            </span></span><span>"baseBetWinner": {</span></div></div><div><div><div>28</div></div><div><span><span>                </span></span><span>"remark": "二等奖",</span></div></div><div><div><div>29</div></div><div><span><span>                </span></span><span>"awardNum": "174",</span></div></div><div><div><div>30</div></div><div><span><span>                </span></span><span>"awardMoney": "152770",</span></div></div><div><div><div>31</div></div><div><span><span>                </span></span><span>"totalMoney": ""</span></div></div><div><div><div>32</div></div><div><span><span>            </span></span><span>},</span></div></div><div><div><div>33</div></div><div><span><span>            </span></span><span>"addToBetWinner": "",</span></div></div><div><div><div>34</div></div><div><span><span>            </span></span><span>"addToBetWinner2": "",</span></div></div><div><div><div>35</div></div><div><span><span>            </span></span><span>"addToBetWinner3": ""</span></div></div><div><div><div>36</div></div><div><span><span>        </span></span><span>},</span></div></div><div><div><div>37</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>38</div></div><div><span><span>            </span></span><span>"awardEtc": "3",</span></div></div><div><div><div>39</div></div><div><span><span>            </span></span><span>"baseBetWinner": {</span></div></div><div><div><div>40</div></div><div><span><span>                </span></span><span>"remark": "三等奖",</span></div></div><div><div><div>41</div></div><div><span><span>                </span></span><span>"awardNum": "929",</span></div></div><div><div><div>42</div></div><div><span><span>                </span></span><span>"awardMoney": "3000",</span></div></div><div><div><div>43</div></div><div><span><span>                </span></span><span>"totalMoney": ""</span></div></div><div><div><div>44</div></div><div><span><span>            </span></span><span>},</span></div></div><div><div><div>45</div></div><div><span><span>            </span></span><span>"addToBetWinner": "",</span></div></div><div><div><div>46</div></div><div><span><span>            </span></span><span>"addToBetWinner2": "",</span></div></div><div><div><div>47</div></div><div><span><span>            </span></span><span>"addToBetWinner3": ""</span></div></div><div><div><div>48</div></div><div><span><span>        </span></span><span>},</span></div></div><div><div><div>49</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>50</div></div><div><span><span>            </span></span><span>"awardEtc": "4",</span></div></div><div><div><div>51</div></div><div><span><span>            </span></span><span>"baseBetWinner": {</span></div></div><div><div><div>52</div></div><div><span><span>                </span></span><span>"remark": "四等奖",</span></div></div><div><div><div>53</div></div><div><span><span>                </span></span><span>"awardNum": "47377",</span></div></div><div><div><div>54</div></div><div><span><span>                </span></span><span>"awardMoney": "200",</span></div></div><div><div><div>55</div></div><div><span><span>                </span></span><span>"totalMoney": ""</span></div></div><div><div><div>56</div></div><div><span><span>            </span></span><span>},</span></div></div><div><div><div>57</div></div><div><span><span>            </span></span><span>"addToBetWinner": "",</span></div></div><div><div><div>58</div></div><div><span><span>            </span></span><span>"addToBetWinner2": "",</span></div></div><div><div><div>59</div></div><div><span><span>            </span></span><span>"addToBetWinner3": ""</span></div></div><div><div><div>60</div></div><div><span><span>        </span></span><span>},</span></div></div><div><div><div>61</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>62</div></div><div><span><span>            </span></span><span>"awardEtc": "5",</span></div></div><div><div><div>63</div></div><div><span><span>            </span></span><span>"baseBetWinner": {</span></div></div><div><div><div>64</div></div><div><span><span>                </span></span><span>"remark": "五等奖",</span></div></div><div><div><div>65</div></div><div><span><span>                </span></span><span>"awardNum": "1005016",</span></div></div><div><div><div>66</div></div><div><span><span>                </span></span><span>"awardMoney": "10",</span></div></div><div><div><div>67</div></div><div><span><span>                </span></span><span>"totalMoney": ""</span></div></div><div><div><div>68</div></div><div><span><span>            </span></span><span>},</span></div></div><div><div><div>69</div></div><div><span><span>            </span></span><span>"addToBetWinner": "",</span></div></div><div><div><div>70</div></div><div><span><span>            </span></span><span>"addToBetWinner2": "",</span></div></div><div><div><div>71</div></div><div><span><span>            </span></span><span>"addToBetWinner3": ""</span></div></div><div><div><div>72</div></div><div><span><span>        </span></span><span>},</span></div></div><div><div><div>73</div></div><div><span><span>        </span></span><span>{</span></div></div><div><div><div>74</div></div><div><span><span>            </span></span><span>"awardEtc": "6",</span></div></div><div><div><div>75</div></div><div><span><span>            </span></span><span>"baseBetWinner": {</span></div></div><div><div><div>76</div></div><div><span><span>                </span></span><span>"remark": "六等奖",</span></div></div><div><div><div>77</div></div><div><span><span>                </span></span><span>"awardNum": "9948912",</span></div></div><div><div><div>78</div></div><div><span><span>                </span></span><span>"awardMoney": "5",</span></div></div><div><div><div>79</div></div><div><span><span>                </span></span><span>"totalMoney": ""</span></div></div><div><div><div>80</div></div><div><span><span>            </span></span><span>},</span></div></div><div><div><div>81</div></div><div><span><span>            </span></span><span>"addToBetWinner": "",</span></div></div><div><div><div>82</div></div><div><span><span>            </span></span><span>"addToBetWinner2": "",</span></div></div><div><div><div>83</div></div><div><span><span>            </span></span><span>"addToBetWinner3": ""</span></div></div><div><div><div>84</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>85</div></div><div><span><span>    </span></span><span>]</span></div></div><div><div><div>86</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>💻 代码示例<a href="#-代码示例"><span>#</span></a></h2><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>import requests</span></div></div><div><div><div>2</div></div><div><span>import time</span></div></div><div><div><div>3</div></div><div><span>import random</span></div></div><div><div><div>4</div></div><div><span>import json</span></div></div><div><div><div>5</div></div><div><span>import re</span></div></div><div><div><div>6</div></div><div><span>import notify</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># 生成动态参数以避免缓存</span></div></div><div><div><div>9</div></div><div><span>current_timestamp = int(time.time() * 1000)</span></div></div><div><div><div>10</div></div><div><span>tt_value = random.random()</span></div></div><div><div><div>11</div></div><div><span>callback_timestamp = current_timestamp - 1</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>url = (</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>"https://jc.zhcw.com/port/client_json.php?"</span></div></div><div><div><div>15</div></div><div><span><span>    </span></span><span>f"callback=jQuery1122029918790071673307_{callback_timestamp}&amp;"</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>"transactionType=10001001&amp;"</span></div></div><div><div><div>17</div></div><div><span><span>    </span></span><span>"lotteryId=1&amp;"</span></div></div><div><div><div>18</div></div><div><span><span>    </span></span><span>"issueCount=30&amp;"</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>"startIssue=&amp;"</span></div></div><div><div><div>20</div></div><div><span><span>    </span></span><span>"endIssue=&amp;"</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>"startDate=&amp;"</span></div></div><div><div><div>22</div></div><div><span><span>    </span></span><span>"endDate=&amp;"</span></div></div><div><div><div>23</div></div><div><span><span>    </span></span><span>"type=0&amp;"</span></div></div><div><div><div>24</div></div><div><span><span>    </span></span><span>"pageNum=1&amp;"</span></div></div><div><div><div>25</div></div><div><span><span>    </span></span><span>"pageSize=1&amp;"</span></div></div><div><div><div>26</div></div><div><span><span>    </span></span><span>f"tt={tt_value}&amp;"</span></div></div><div><div><div>27</div></div><div><span><span>    </span></span><span>f"_={current_timestamp}"</span></div></div><div><div><div>28</div></div><div><span>)</span></div></div><div><div><div>29</div></div><div>
</div></div><div><div><div>30</div></div><div><span>headers = {</span></div></div><div><div><div>31</div></div><div><span><span>    </span></span><span>"Accept": "*/*",</span></div></div><div><div><div>32</div></div><div><span><span>    </span></span><span>"Accept-Language": "zh-CN,zh-TW;q=0.9,zh;q=0.8,en;q=0.7",</span></div></div><div><div><div>33</div></div><div><span><span>    </span></span><span>"Cache-Control": "no-cache",</span></div></div><div><div><div>34</div></div><div><span><span>    </span></span><span>"Connection": "keep-alive",</span></div></div><div><div><div>35</div></div><div><span><span>    </span></span><span>"Cookie": "PHPSESSID=bi2vu7nn8gdqvg79ggal0bmvq0",</span></div></div><div><div><div>36</div></div><div><span><span>    </span></span><span>"Pragma": "no-cache",</span></div></div><div><div><div>37</div></div><div><span><span>    </span></span><span>"Referer": "https://www.zhcw.com/",</span></div></div><div><div><div>38</div></div><div><span><span>    </span></span><span>"Sec-Fetch-Dest": "script",</span></div></div><div><div><div>39</div></div><div><span><span>    </span></span><span>"Sec-Fetch-Mode": "no-cors",</span></div></div><div><div><div>40</div></div><div><span><span>    </span></span><span>"Sec-Fetch-Site": "same-site",</span></div></div><div><div><div>41</div></div><div><span><span>    </span></span><span>"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",</span></div></div><div><div><div>42</div></div><div><span><span>    </span></span><span>"sec-ch-ua": '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',</span></div></div><div><div><div>43</div></div><div><span><span>    </span></span><span>"sec-ch-ua-mobile": "?0",</span></div></div><div><div><div>44</div></div><div><span><span>    </span></span><span>"sec-ch-ua-platform": '"Windows"',</span></div></div><div><div><div>45</div></div><div><span>}</span></div></div><div><div><div>46</div></div><div>
</div></div><div><div><div>47</div></div><div><span>response = requests.get(url, headers=headers)</span></div></div><div><div><div>48</div></div><div><span>response_text = response.text</span></div></div><div><div><div>49</div></div><div><span># 清理字符串以获取有效的JSON对象</span></div></div><div><div><div>50</div></div><div><span>start_index = response_text.find('(') + 1</span></div></div><div><div><div>51</div></div><div><span>end_index = response_text.rfind(')')</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>json_str = response_text[start_index:end_index]</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span># 解析JSON字符串</span></div></div><div><div><div>56</div></div><div><span>parsed_data = json.loads(json_str)</span></div></div><div><div><div>57</div></div><div>
</div></div><div><div><div>58</div></div><div><span># 提取"data"中的信息</span></div></div><div><div><div>59</div></div><div><span>data_info = parsed_data.get('data', [])</span></div></div><div><div><div>60</div></div><div>
</div></div><div><div><div>61</div></div><div><span># 遍历每个数据项（在这个例子中只有一个）</span></div></div><div><div><div>62</div></div><div><span>for data in data_info:</span></div></div><div><div><div>63</div></div><div><span><span>    </span></span><span># 数据解析（建议添加类型校验）</span></div></div><div><div><div>64</div></div><div><span><span>    </span></span><span>red_numbers = data.get('frontWinningNum', '').split()  # 前区红球</span></div></div><div><div><div>65</div></div><div><span><span>    </span></span><span>blue_number = data.get('backWinningNum', '')           # 后区蓝球</span></div></div><div><div><div>66</div></div><div><span><span>    </span></span><span>open_time = data.get('openTime', '未知时间')</span></div></div><div><div><div>67</div></div><div><span><span>    </span></span><span>week = data.get('week', '未知星期')</span></div></div><div><div><div>68</div></div><div><span><span>    </span></span><span>issue = data.get('issue', '未知期号')</span></div></div><div><div><div>69</div></div><div>
</div></div><div><div><div>70</div></div><div><span><span>    </span></span><span># 动态生成红球HTML</span></div></div><div><div><div>71</div></div><div><span><span>    </span></span><span>red_balls = '\n'.join([</span></div></div><div><div><div>72</div></div><div><span><span>        </span></span><span>f'&lt;div style="width:40px;height:40px;background:#e74c3c;border-radius:50%;color:white;'</span></div></div><div><div><div>73</div></div><div><span><span>        </span></span><span>f'display:flex;align-items:center;justify-content:center;font-weight:bold;"&gt;{num}&lt;/div&gt;'</span></div></div><div><div><div>74</div></div><div><span><span>        </span></span><span>for num in red_numbers</span></div></div><div><div><div>75</div></div><div><span><span>    </span></span><span>])</span></div></div><div><div><div>76</div></div><div>
</div></div><div><div><div>77</div></div><div><span><span>    </span></span><span># 构建HTML模板</span></div></div><div><div><div>78</div></div><div><span><span>    </span></span><span>message = f'''</span></div></div><div><div><div>79</div></div><div><span><span>    </span></span><span>&lt;div style="background:#f8f9fa;padding:20px;border-radius:12px;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;"&gt;</span></div></div><div><div><div>80</div></div><div><span><span>        </span></span><span>&lt;h2 style="color:#2d3436;margin:0 0;border-bottom:2px solid #e74c3c;display:flex;"&gt;</span></div></div><div><div><div>81</div></div><div><span><span>            </span></span><span>&lt;span style="font-size:1.2em;margin-right:8px;"&gt;🎉&lt;/span&gt;双色球开奖公告</span></div></div><div><div><div>82</div></div><div><span><span>        </span></span><span>&lt;/h2&gt;</span></div></div><div><div><div>83</div></div><div>
</div></div><div><div><div>84</div></div><div><span><span>        </span></span><span>&lt;div style="color:#636e72;display: flex"&gt;</span></div></div><div><div><div>85</div></div><div><span><span>            </span></span><span>&lt;span style="color:#e74c3c;"&gt;📅 {open_time} {week}&lt;/span&gt;</span></div></div><div><div><div>86</div></div><div><span><span>            </span></span><span>&lt;span style="margin:0 8px"&gt;|&lt;/span&gt;</span></div></div><div><div><div>87</div></div><div><span><span>            </span></span><span>&lt;span&gt;第{issue}期&lt;/span&gt;</span></div></div><div><div><div>88</div></div><div><span><span>        </span></span><span>&lt;/div&gt;</span></div></div><div><div><div>89</div></div><div>
</div></div><div><div><div>90</div></div><div><span><span>        </span></span><span>&lt;div style="display:flex;flex-wrap:wrap;gap:8px;"&gt;</span></div></div><div><div><div>91</div></div><div><span><span>            </span></span><span>&lt;!-- 红球 --&gt;</span></div></div><div><div><div>92</div></div><div><span><span>            </span></span><span>{red_balls}</span></div></div><div><div><div>93</div></div><div>
</div></div><div><div><div>94</div></div><div><span><span>            </span></span><span>&lt;!-- 蓝球 --&gt;</span></div></div><div><div><div>95</div></div><div><span><span>            </span></span><span>&lt;div style="width:40px;height:40px;background:#3498db;border-radius:50%;color:white;</span></div></div><div><div><div>96</div></div><div><span><span>                 </span></span><span>display:flex;align-items:center;justify-content:center;font-weight:bold;"&gt;{blue_number}&lt;/div&gt;</span></div></div><div><div><div>97</div></div><div><span><span>        </span></span><span>&lt;/div&gt;</span></div></div><div><div><div>98</div></div><div>
</div></div><div><div><div>99</div></div><div><span><span>        </span></span><span>&lt;div style="color:#636e72;display: flex"&gt;</span></div></div><div><div><div>100</div></div><div><span><span>            </span></span><span>&lt;span&gt;⏰ 开奖时间：20:30（每周二、四、日）&lt;/span&gt;</span></div></div><div><div><div>101</div></div><div><span><span>            </span></span><span>&lt;span style="margin:0 8px"&gt;|&lt;/span&gt;</span></div></div><div><div><div>102</div></div><div><span><span>            </span></span><span>&lt;span&gt;📞 客服电话：95086&lt;/span&gt;</span></div></div><div><div><div>103</div></div><div><span><span>        </span></span><span>&lt;/div&gt;</span></div></div><div><div><div>104</div></div><div><span><span>    </span></span><span>&lt;/div&gt;</span></div></div><div><div><div>105</div></div><div><span><span>    </span></span><span>'''</span></div></div><div><div><div>106</div></div><div>
</div></div><div><div><div>107</div></div><div><span><span>    </span></span><span>notify.send("双色球", message)</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div></section>
<section><h2>🕒 青龙定时任务配置<a href="#-青龙定时任务配置"><span>#</span></a></h2><p>把脚本放进青龙的脚本目录，然后设置每天晚上开奖时间后自动执行 🧠。</p><p>🗓 <strong>定时任务规则：</strong></p><p>0 22 * * 2,4,0</p><hr /></section>
<section><h2>📱 效果展示<a href="#-效果展示"><span>#</span></a></h2><p>每天开奖后，手机立即收到通知👇：</p><p><img src="https://s.muxui.com:8015/images/2026/09/R8kC8G7UyjeM.webp" alt="" /></p></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/alipay-face-to-face-donation-page/</id>
      <title type="text">支付宝面对面对接个人赞赏页</title>
      <published>2025-02-25T00:00:00.000Z</published>
      <updated>2025-02-26T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/alipay-face-to-face-donation-page/"/>
      <summary type="text">📌 项目背景 ​ 在个人博客/自媒体场景中，接收读者小额赞赏是常见的互动方式。基于支付宝的「面对面支付」接口，我开发了一个轻量级赞赏页，支持金额选择、动态二维码生成、捐赠记录展示等功能。核心代码基于PHP+MySQL实现，无需依赖复杂框架</summary>
      <content type="html"><![CDATA[<section><h2><strong>📌 项目背景</strong>​<a href="#-项目背景"><span>#</span></a></h2><p>在个人博客/自媒体场景中，接收读者小额赞赏是常见的互动方式。基于支付宝的「面对面支付」接口，我开发了一个轻量级赞赏页，支持金额选择、动态二维码生成、捐赠记录展示等功能。核心代码基于PHP+MySQL实现，无需依赖复杂框架，适合个人快速部署</p><hr /><section><h3>​<strong>🚀 核心功能拆解</strong>​<a href="#-核心功能拆解"><span>#</span></a></h3><ol>
<li>​<strong>动态支付二维码生成</strong>​</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 调用第三方API生成二维码（示例）</span></div></div><div><div><div>2</div></div><div><span>&lt;img src="https://api.qrtool.cn/?text=&lt;?php echo urlencode($qrCode); ?&gt;"&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>通过<code>alipays://</code>协议深度链接唤起支付宝APP</li>
<li>移动端自动识别设备类型，显示「点击跳转支付宝」按钮</li>
</ul><ol>
<li>​<strong>捐赠记录实时展示</strong>​</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 数据库查询最新66条记录</span></div></div><div><div><div>2</div></div><div><span>$sql = "SELECT * FROM donations ORDER BY created_at DESC LIMIT 66";</span></div></div><div><div><div>3</div></div><div><span>while($row = $result-&gt;fetch_assoc()) {</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>echo '&lt;li&gt;网友'.$row.'赠送：'.$row.'&lt;/li&gt;';</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>采用瀑布流式滚动设计</li>
<li>统计总金额和捐赠人次（SQL聚合查询）</li>
</ul><ol>
<li>​<strong>支付状态轮询机制</strong>​</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 每5秒轮询支付状态</span></div></div><div><div><div>2</div></div><div><span>setInterval(function() {</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>$.ajax({ url: "/pay/query.php?action=serve", data: { gid: "&lt;?php echo $orderId; ?&gt;" } });</span></div></div><div><div><div>4</div></div><div><span>}, 5000);</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>// 60秒自动刷新防重复支付</span></div></div><div><div><div>7</div></div><div><span>setTimeout(() =&gt; window.location.reload(), 60000);</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>参考支付宝官方推荐的轮询策略</li>
<li>前端使用Layer.js实现支付成功弹窗提示</li>
</ul><hr /></section><section><h3>​<strong>🔧 技术亮点</strong>​<a href="#-技术亮点"><span>#</span></a></h3><ol>
<li>​<strong>设备自适应设计</strong>​<br />
通过<code>isMobile()</code>函数检测设备类型，动态切换H5/PC布局：</li>
</ol><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>function isMobile() {</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>$ua = $_SERVER ?? '';</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>return (strpos($ua, 'Mobile') !== false) || (strpos($ua, 'MicroMessenger') !== false);</span></div></div><div><div><div>4</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ol>
<li>​<strong>防重复支付设计</strong>​</li>
</ol><ul>
<li>生成带时间戳的订单号<code>createNewOrderNo()</code></li>
<li>数据库记录支付请求参数和状态</li>
</ul><ol>
<li>​<strong>安全增强措施</strong>​</li>
</ol><ul>
<li>使用<code>htmlspecialchars()</code>过滤用户输入</li>
<li>限制二维码有效期为60秒</li>
<li>关键参数加密传输（示例未展示具体实现）</li>
</ul><hr /></section><section><h3>​<strong>📊 数据统计展示</strong>​<a href="#-数据统计展示"><span>#</span></a></h3><p>通过底部统计模块直观展示捐赠影响力：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>$sql_stats = "SELECT COUNT(*) as total_count, SUM(amount) as total_amount FROM donations";</span></div></div><div><div><div>2</div></div><div><span>echo "共有[{$stats['total_count']}]位暖心人捐赠，总计[{$stats['total_amount']}]元";</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>数字动态更新，增强用户参与感</li>
<li>采用「恰饭」「零食」等趣味化文案</li>
</ul><hr /></section><section><h3>​<strong>💡 开发心得</strong>​<a href="#-开发心得"><span>#</span></a></h3><ol>
<li>​<strong>支付宝接口选择</strong>​<br />
个人开发者推荐使用「当面付」或「电脑网站支付」接口，前者无需营业执照即可申请</li>
<li>​<strong>性能优化点</strong>​</li>
</ol><ul>
<li>使用CDN加载Bootstrap/jQuery（减少服务器压力）</li>
<li>捐赠记录分页加载（当前示例限制66条）</li>
</ul><ol>
<li>​<strong>扩展可能性</strong>​</li>
</ol><ul>
<li>接入微信支付实现双渠道收款</li>
<li>添加打赏留言功能（需注意内容审核）</li>
<li>数据可视化图表展示</li>
</ul><blockquote><p>​<strong>源码结构参考</strong>：</p><p>donate ├── config/<br />
│ └── db.php # 数据库配置文件<br />
│<br />
├── pay/<br />
│ ├── pay.php # 支付宝支付核心处理类<br />
│ ├── pay_template.php # 支付页面模板<br />
│ ├── query.php # 支付状态查询处理<br />
│ └── public/ # 支付页面静态资源<br />
│ ├── style.css<br />
│ └── jquery.min.js<br />
│<br />
├── style/<br />
│ ├── css/<br />
│ │ └── style.css # 主页样式文件<br />
│ └── js/<br />
│ ├── jquery-3.2.1.min.js<br />
│ ├── layer/ # layer弹窗组件<br />
│ │ └── layer.js<br />
│ └── style.js # 主页JS逻辑<br />
│<br />
├── sql/<br />
│ ├── donation.sql # 捐赠记录表结构<br />
│ └── orders.sql # 订单表结构<br />
│<br />
├── favicon.ico # 网站图标<br />
└── index.php</p></blockquote><hr /></section><section><h3>​✒️ 修改配置​<a href="#️-修改配置"><span>#</span></a></h3><ul>
<li>修改<code>pay/pay.php</code>中的支付宝参数</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>define('ALIPAY_APPID', '你的支付宝APPID');</span></div></div><div><div><div>2</div></div><div><span>define('ALIPAY_PRIVATE_KEY', "-----BEGIN RSA PRIVATE KEY-----\n" .</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>"你的支付宝应用私钥" .</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>"\n-----END RSA PRIVATE KEY-----");</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>define('ALIPAY_PUBLIC_KEY', "-----BEGIN PUBLIC KEY-----\n" .</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>"你的支付宝公钥" .</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>"\n-----END PUBLIC KEY-----");</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li>修改<code>pay/pay.php</code>中63行的默认值，创建订单的时候，f12查询元素然后复制链接即可</li>
</ul><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>  </span></span><span>return 'https://qr.alipay.com/你的默认收款标号';</span></div></div></code></pre><div><div></div><div></div></div></figure></div><hr /></section><section><h3>​<strong>🌐 效果预览</strong>​<a href="#-效果预览"><span>#</span></a></h3><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/Iq43bOfarAhu.webp" alt="演示图" /><figcaption>演示图</figcaption></figure><p></p><p>通过这个项目，不仅实现了支付功能的技术验证，更深刻体会到「细节决定体验」—— 从自动换菜名的趣味交互，到支付成功的暖心提示，每一个微小设计都在传递温度</p><p><strong>源码获取</strong>：</p><a href="https://github.com/O96u/donate" target="_blank"><div><div><div><div></div><div>O96u</div></div><div>/</div><div>donate</div></div><div></div></div><div>Repository details unavailable</div><div><div>—</div><div>—</div><div>—</div><span>Unavailable</span></div></a></section></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/proxmox-ve-setup/</id>
      <title type="text">PVE设置</title>
      <published>2025-02-22T00:00:00.000Z</published>
      <updated>2025-06-27T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/proxmox-ve-setup/"/>
      <summary type="text">&lt;video controls=&quot;&quot; src=&quot; &lt;/video 基础设置 1\. 合并硬盘分区 1.1 删除local lvm lvremove pve/data 1.2 扩容local lvextend l +100%FREE r pv</summary>
      <content type="html"><![CDATA[
<section><h2>基础设置<a href="#基础设置"><span>#</span></a></h2><section><h3>1. 合并硬盘分区<a href="#1-合并硬盘分区"><span>#</span></a></h3><section><h4>1.1 删除local-lvm<a href="#11-删除local-lvm"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>lvremove pve/data</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>1.2 扩容local<a href="#12-扩容local"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>lvextend -l +100%FREE -r pve/root</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>1.3 然后数据中心进行图形界面的local-lvm删除和local编辑。<a href="#13-然后数据中心进行图形界面的local-lvm删除和local编辑"><span>#</span></a></h4></section></section><section><h3>2. 确认网络是否正常<a href="#2-确认网络是否正常"><span>#</span></a></h3><p>ping <a href="http://www.baidu.com" target="_blank">www.baidu.com</a> 如果ping不通，修改网关和dns。</p></section><section><h3>3. 更换软件源<a href="#3-更换软件源"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>https://mirrors.tuna.tsinghua.edu.cn/help/debian</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>nano /etc/apt/sources.list</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>添加网页上的内容，保存退出。</p><p>建议全部修改成清华源：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>4. 修改pve源<a href="#4-修改pve源"><span>#</span></a></h3><section><h4>4.1 添加清华源<a href="#41-添加清华源"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>https://mirrors.tuna.tsinghua.edu.cn/help/proxmox</span></div></div></code></pre><div><div></div><div></div></div></figure></div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>nano /etc/apt/sources.list.d/pve-no-subscription.list</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>加入内容   </p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>deb https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian/pve bookworm pve-no-subscription</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h4>4.2 去掉企业源<a href="#42-去掉企业源"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>nano /etc/apt/sources.list.d/pve-enterprise.list</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前面加入#</p></section><section><h4>4.3 去掉ceph源（有需要的可以之后自行修改）<a href="#43-去掉ceph源有需要的可以之后自行修改"><span>#</span></a></h4><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>nano /etc/apt/sources.list.d/ceph.list</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>前面加入#</p><p>PVE更新—存储库里面添加pve-no-subscription</p></section></section><section><h3>5. 去掉登录弹窗：<a href="#5-去掉登录弹窗"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>sed -i.backup -z "s/res === null || res === undefined || \!res || res\n\t\t\t.data.status.toLowerCase() \!== 'active'/false/g" /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js &amp;&amp; systemctl restart pveproxy.service</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>6. 更新软件源，升级软件<a href="#6-更新软件源升级软件"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>apt update</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>apt upgrade -y</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>7. 开启IPV6（可选）<a href="#7-开启ipv6可选"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>nano /etc/sysctl.conf</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>添加如下内容：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>net.ipv6.conf.all.accept_ra=2</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>net.ipv6.conf.default.accept_ra=2</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>net.ipv6.conf.vmbr0.accept_ra=2</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>net.ipv6.conf.all.autoconf=1</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>net.ipv6.conf.default.autoconf=1</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>net.ipv6.conf.vmbr0.autoconf=1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>重启系统：reboot</p></section></section>
<section><h2></h2><p>硬盘直通</p><section><h3>1. 查看硬盘信息<a href="#1-查看硬盘信息"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>ls -l /dev/disk/by-id/</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>2. 设置硬盘直通<a href="#2-设置硬盘直通"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>qm set 100 -sata1 /dev/disk/by-id/ata-WDC_WD5000AAKX-753CA1_WD-WMAYU7283459</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>qm set 100 -sata2 /dev/disk/by-id/ata-WDC_WD5000AAKX-00ERMA0_WD-WCC2E6DHKRCX</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>qm set 100 -sata3 /dev/disk/by-id/ata-WDC_WD5000AAKX-08U6AA0_WD-WCC2E2LDAVCN</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>3. 解除硬盘直通<a href="#3-解除硬盘直通"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>qm set 100 -sata1 none</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>qm set 100 -sata2 none</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>qm set 100 -sata3 none</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section></section>]]></content>
    </entry>
    <entry>
      <id>https://muxui.com/posts/adobe-photoshop-2023/</id>
      <title type="text">Adobe Photoshop 2023</title>
      <published>2025-02-20T00:00:00.000Z</published>
      <updated>2025-02-22T00:00:00.000Z</updated>
      <author><name>Muxui</name></author>
      <link rel="alternate" href="https://muxui.com/posts/adobe-photoshop-2023/"/>
      <summary type="text">介绍 Adobe Photoshop（PS）是Adobe公司推出的一款顶尖图像编辑与设计软件，它集成了最先进的内容识别填充技术和Adobe Sensei AI技术，使用户能够精确选择填充像素，轻松进行图像的旋转、缩放和镜像处理。作为Adob</summary>
      <content type="html"><![CDATA[<section><h2>介绍<a href="#介绍"><span>#</span></a></h2><p>Adobe Photoshop（PS）是Adobe公司推出的一款顶尖图像编辑与设计软件，它集成了最先进的内容识别填充技术和Adobe Sensei AI技术，使用户能够精确选择填充像素，轻松进行图像的旋转、缩放和镜像处理。作为Adobe系列中最受欢迎的图像处理工具，Photoshop拥有强大的图像选择、修饰、逼真绘画以及广泛的工作流程和性能增强功能，几乎是每个创意项目的核心。Photoshop不仅能够帮助用户将想象力转化为现实，还彻底革新了日常摄影，提供了专业的摄影工具组合，支持剪切、移除物体、合成照片等编辑操作，让色彩和效果的处理游刃有余。它为设计师提供了强大的设计工具，无论是海报、包装、横幅还是网站设计，都能从Photoshop开始。此外，Photoshop提供了强大的画笔工具，包括超过1000款由知名插画家Kyle T. Webster设计的笔刷，让插画创作更加灵动。它还具备快速填充和自然真实的模糊效果，通过对象选择工具和强化的变形功能，以及改进的属性面板和新预设，为用户提供了更多的创造性控制和便利。</p></section>
<section><h2>截图<a href="#截图"><span>#</span></a></h2><p></p><figure><img src="https://s.muxui.com:8015/images/2026/09/cGigibX0V67U.webp" alt="Adobe Photoshop" title="Adobe Photoshop 2025 26.2.0 破解版 - NeuralFilters/顶尖图像编辑与设计软件 | 图像处理" /><figcaption>Adobe Photoshop</figcaption></figure><p></p><p>Adobe Photoshop</p><p><img src="https://s.muxui.com:8015/images/2026/09/Js8A9hvuQiRx.webp" alt="" title="Adobe Photoshop 2025 26.2.0 破解版 - NeuralFilters/顶尖图像编辑与设计软件 | 图像处理" /></p><p>Adobe Photoshop</p><p><img src="https://s.muxui.com:8015/images/2026/09/5JNs4cfeKYZ6.webp" alt="" title="Adobe Photoshop 2025 26.2.0 破解版 - NeuralFilters/顶尖图像编辑与设计软件 | 图像处理" /></p><p>Adobe Photoshop</p><p><img src="https://s.muxui.com:8015/images/2026/09/CmAQIoH1N0tF.webp" alt="" title="Adobe Photoshop 2025 26.2.0 破解版 - NeuralFilters/顶尖图像编辑与设计软件 | 图像处理" /></p><p>Adobe Photoshop</p></section>
<section><h2>功能<a href="#功能"><span>#</span></a></h2><section><h3>修图、替换、合成等等<a href="#修图替换合成等等"><span>#</span></a></h3><p>让肖像更生动，面部表情更具表现力。只需一次鼠标点击，就可以替换背景中的天空。删除物体、修图、改变颜色。借助强大的编辑工具和新的智能功能，可以创建出惊人的效果，将图形工作转化为真正的艺术。</p></section><section><h3>图形设计的全能工具<a href="#图形设计的全能工具"><span>#</span></a></h3><p>海报、包装、横幅、网站——图形设计从Photoshop开始。将照片、图表和文本组合成全新的图像。只需一次鼠标点击就可以选择物体。通过播放颜色和效果来改善任何项目。</p></section><section><h3>千款惊艳画笔<a href="#千款惊艳画笔"><span>#</span></a></h3><p>使用各种专用画笔进行绘画，或者创建自己的画笔——世界上最大的Photoshop画笔库始终在您手边。</p></section><section><h3>追随灵感<a href="#追随灵感"><span>#</span></a></h3><p>混合照片、图形、效果和神经滤镜，将任何照片转化为艺术作品。借助一键选择、自动遮罩和几乎无限的图层，您可以快速创建出相机无法实现的效果。</p></section><section><h3>生成式AI开创新时代<a href="#生成式ai开创新时代"><span>#</span></a></h3><p>借助有史以来最惊人的Photoshop版本，大步迈向未来。利用Adobe Firefly的强大力量，使用生成填充和生成扩展功能，在图像中添加、删除或扩展内容。欢迎试用，亲眼见证。</p></section><section><h3>为您的业务注入创意<a href="#为您的业务注入创意"><span>#</span></a></h3><p>陶瓷艺术家Chinzalée Sonami在网上和全国各地的商店销售她的彩色陶器。她可以在Adobe Photoshop中更换背景，以更新其宣传海报和广告的外观。</p></section><section><h3>打造精美品牌<a href="#打造精美品牌"><span>#</span></a></h3><p>Meg Lewis设计虚拟背景和图形，在社交媒体上展示其风格并扩展业务。Adobe Photoshop为她提供了无尽的方式来混合图像并创作独特的作品。</p></section><section><h3>打造完美肖像<a href="#打造完美肖像"><span>#</span></a></h3><p>人们的照片可以吸引客户。通过Adobe Photoshop，您可以自动调整主体的特征和姿势，无需重新拍摄或搜索新的库存图片。</p></section><section><h3>您制作精美Instagram图像的必备工具<a href="#您制作精美instagram图像的必备工具"><span>#</span></a></h3><p>Photoshop可以轻松移除瑕疵，使纽约的Nice Day Chinese Takeout餐厅能快速制作令人垂涎三尺的社交帖文和图形。</p></section><section><h3>制作签名作品并快速分享<a href="#制作签名作品并快速分享"><span>#</span></a></h3><p>设计师兼插画家Aries Moross使用Photoshop笔刷进行绘图和上色，将字母转变为艺术表现形式。借助此应用程序的协作工具，他们可以与客户顺畅地共享作品以供审查和批准。</p></section><section><h3>浏览新增功能的作用<a href="#浏览新增功能的作用"><span>#</span></a></h3><p>Adobe Photoshop一直在推出新功能，让任何人都能以更快速、更聪明的方式使用。欢迎查看一些最新更新。另外，您还可以使用Photoshop网页版在线编辑图像。</p><section><h4>一瞬间将最狂野的梦想转化为令人惊叹的图像<a href="#一瞬间将最狂野的梦想转化为令人惊叹的图像"><span>#</span></a></h4><p>生成填充是令人惊叹的生成式AI工具，可以提升您的创造力并加快工作流程。只需要使用简单的文字提示，就可以执行复杂的编辑操作，并获得高品质的成果。您可以采用不具破坏性的方式添加、扩展和删除图像内容，保持一切在您的掌控之下。</p></section><section><h4>快速扩展图像<a href="#快速扩展图像"><span>#</span></a></h4><p>使用生成扩展，只需几个步骤就可以扩展图像。只需使用“裁剪”工具点击并拖动到图像的边界之外，就可以使用与现有图像融合的新生成内容顺畅地填充扩展的画布。增加外观比例以及将人像照片变成风景照，并重新处理图像，使其符合网站、社交媒体等的尺寸。</p></section><section><h4>只需几个步骤，就可以改变图像的外观<a href="#只需几个步骤就可以改变图像的外观"><span>#</span></a></h4><p>使用30多款新的调整预设集，大幅改变图像的外观。让鼠标停留以预览、点击以应用您最喜欢的项目，然后随心所欲地调整结果，创作出完全独特的作品。</p></section><section><h4>使用后续步骤建议进行编辑<a href="#使用后续步骤建议进行编辑"><span>#</span></a></h4><p>快显工作栏是一个浮动菜单，可以根据您正在执行的操作显示相关功能，并加速遮色片和生成式AI工作流程。直接点击菜单中的建议，就可以简化您的流程。</p></section><section><h4>使大型物体消失<a href="#使大型物体消失"><span>#</span></a></h4><p>使用“移除工具”在大面积的形状上刷过或圈选，就可以自动填入与其周围环境相符的内容，同时保留复杂背景中的物体细节和深度，从而创造无缝的编辑内容。</p></section></section></section>]]></content>
    </entry>
</feed>
